1-- | Running tests 2{-# LANGUAGE ScopedTypeVariables, ExistentialQuantification, RankNTypes, 3 FlexibleContexts, BangPatterns, CPP, DeriveDataTypeable #-} 4module Test.Tasty.Run 5 ( Status(..) 6 , StatusMap 7 , launchTestTree 8 , DependencyException(..) 9 ) where 10 11import qualified Data.IntMap as IntMap 12import qualified Data.Sequence as Seq 13import qualified Data.Foldable as F 14import Data.Maybe 15import Data.Graph (SCC(..), stronglyConnComp) 16import Data.Typeable 17import Control.Monad.State 18import Control.Monad.Writer 19import Control.Monad.Reader 20import Control.Concurrent 21import Control.Concurrent.STM 22import Control.Concurrent.Timeout (timeout) 23import Control.Concurrent.Async 24import Control.Exception as E 25import Control.Applicative 26import Control.Arrow 27import GHC.Conc (labelThread) 28import Prelude -- Silence AMP and FTP import warnings 29 30import Test.Tasty.Core 31import Test.Tasty.Parallel 32import Test.Tasty.Patterns 33import Test.Tasty.Patterns.Types 34import Test.Tasty.Options 35import Test.Tasty.Options.Core 36import Test.Tasty.Runners.Reducers 37import Test.Tasty.Runners.Utils (timed, forceElements) 38import Test.Tasty.Providers.ConsoleFormat (noResultDetails) 39 40-- | Current status of a test 41data Status 42 = NotStarted 43 -- ^ test has not started running yet 44 | Executing Progress 45 -- ^ test is being run 46 | Done Result 47 -- ^ test finished with a given result 48 deriving Show 49 50-- | Mapping from test numbers (starting from 0) to their status variables. 51-- 52-- This is what an ingredient uses to analyse and display progress, and to 53-- detect when tests finish. 54type StatusMap = IntMap.IntMap (TVar Status) 55 56data Resource r 57 = NotCreated 58 | BeingCreated 59 | FailedToCreate SomeException 60 | Created r 61 | BeingDestroyed 62 | Destroyed 63 64instance Show (Resource r) where 65 show r = case r of 66 NotCreated -> "NotCreated" 67 BeingCreated -> "BeingCreated" 68 FailedToCreate exn -> "FailedToCreate " ++ show exn 69 Created {} -> "Created" 70 BeingDestroyed -> "BeingDestroyed" 71 Destroyed -> "Destroyed" 72 73data Initializer 74 = forall res . Initializer 75 (IO res) 76 (TVar (Resource res)) 77data Finalizer 78 = forall res . Finalizer 79 (res -> IO ()) 80 (TVar (Resource res)) 81 (TVar Int) 82 83-- | Execute a test taking care of resources 84executeTest 85 :: ((Progress -> IO ()) -> IO Result) 86 -- ^ the action to execute the test, which takes a progress callback as 87 -- a parameter 88 -> TVar Status -- ^ variable to write status to 89 -> Timeout -- ^ optional timeout to apply 90 -> Seq.Seq Initializer -- ^ initializers (to be executed in this order) 91 -> Seq.Seq Finalizer -- ^ finalizers (to be executed in this order) 92 -> IO () 93executeTest action statusVar timeoutOpt inits fins = mask $ \restore -> do 94 resultOrExn <- try $ restore $ do 95 -- N.B. this can (re-)throw an exception. It's okay. By design, the 96 -- actual test will not be run, then. We still run all the 97 -- finalizers. 98 -- 99 -- There's no point to transform these exceptions to something like 100 -- EitherT, because an async exception (cancellation) can strike 101 -- anyway. 102 initResources 103 104 -- If all initializers ran successfully, actually run the test. 105 -- We run it in a separate thread, so that the test's exception 106 -- handler doesn't interfere with our timeout. 107 withAsync (action yieldProgress) $ \asy -> do 108 labelThread (asyncThreadId asy) "tasty_test_execution_thread" 109 timed $ applyTimeout timeoutOpt $ do 110 r <- wait asy 111 -- Not only wait for the result to be returned, but make sure to 112 -- evalute it inside applyTimeout; see #280. 113 evaluate $ 114 resultOutcome r `seq` 115 forceElements (resultDescription r) `seq` 116 forceElements (resultShortDescription r) 117 return r 118 119 -- no matter what, try to run each finalizer 120 mbExn <- destroyResources restore 121 122 atomically . writeTVar statusVar $ Done $ 123 case resultOrExn <* maybe (Right ()) Left mbExn of 124 Left ex -> exceptionResult ex 125 Right (t,r) -> r { resultTime = t } 126 127 where 128 initResources :: IO () 129 initResources = 130 F.forM_ inits $ \(Initializer doInit initVar) -> do 131 join $ atomically $ do 132 resStatus <- readTVar initVar 133 case resStatus of 134 NotCreated -> do 135 -- signal to others that we're taking care of the resource 136 -- initialization 137 writeTVar initVar BeingCreated 138 return $ 139 (do 140 res <- doInit 141 atomically $ writeTVar initVar $ Created res 142 ) `E.catch` \exn -> do 143 atomically $ writeTVar initVar $ FailedToCreate exn 144 throwIO exn 145 BeingCreated -> retry 146 Created {} -> return $ return () 147 FailedToCreate exn -> return $ throwIO exn 148 -- If the resource is destroyed or being destroyed 149 -- while we're starting a test, the test suite is probably 150 -- shutting down. We are about to be killed. 151 -- (In fact we are probably killed already, so these cases are 152 -- unlikely to occur.) 153 -- In any case, the most sensible thing to do is to go to 154 -- sleep, awaiting our fate. 155 Destroyed -> return $ sleepIndefinitely 156 BeingDestroyed -> return $ sleepIndefinitely 157 158 applyTimeout :: Timeout -> IO Result -> IO Result 159 applyTimeout NoTimeout a = a 160 applyTimeout (Timeout t tstr) a = do 161 let 162 timeoutResult = 163 Result 164 { resultOutcome = Failure $ TestTimedOut t 165 , resultDescription = 166 "Timed out after " ++ tstr 167 , resultShortDescription = "TIMEOUT" 168 , resultTime = fromIntegral t 169 , resultDetailsPrinter = noResultDetails 170 } 171 fromMaybe timeoutResult <$> timeout t a 172 173 -- destroyResources should not be interrupted by an exception 174 -- Here's how we ensure this: 175 -- 176 -- * the finalizer is wrapped in 'try' 177 -- * async exceptions are masked by the caller 178 -- * we don't use any interruptible operations here (outside of 'try') 179 destroyResources :: (forall a . IO a -> IO a) -> IO (Maybe SomeException) 180 destroyResources restore = do 181 -- remember the first exception that occurred 182 liftM getFirst . execWriterT . getTraversal $ 183 flip F.foldMap fins $ \fin@(Finalizer _ _ finishVar) -> 184 Traversal $ do 185 iAmLast <- liftIO $ atomically $ do 186 nUsers <- readTVar finishVar 187 let nUsers' = nUsers - 1 188 writeTVar finishVar nUsers' 189 return $ nUsers' == 0 190 191 mbExcn <- liftIO $ 192 if iAmLast 193 then destroyResource restore fin 194 else return Nothing 195 196 tell $ First mbExcn 197 198 -- The callback 199 -- Since this is not used yet anyway, disable for now. 200 -- I'm not sure whether we should get rid of this altogether. For most 201 -- providers this is either difficult to implement or doesn't make 202 -- sense at all. 203 -- See also https://github.com/feuerbach/tasty/issues/33 204 yieldProgress _ = return () 205 206type InitFinPair = (Seq.Seq Initializer, Seq.Seq Finalizer) 207 208-- | Dependencies of a test 209type Deps = [(DependencyType, Expr)] 210 211-- | Traversal type used in 'createTestActions' 212type Tr = Traversal 213 (WriterT ([(InitFinPair -> IO (), (TVar Status, Path, Deps))], Seq.Seq Finalizer) 214 (ReaderT (Path, Deps) 215 IO)) 216 217-- | Exceptions related to dependencies between tests. 218data DependencyException 219 = DependencyLoop 220 -- ^ Test dependencies form a loop. In other words, test A cannot start 221 -- until test B finishes, and test B cannot start until test 222 -- A finishes. 223 deriving (Typeable) 224 225instance Show DependencyException where 226 show DependencyLoop = "Test dependencies form a loop." 227 228instance Exception DependencyException 229 230-- | Turn a test tree into a list of actions to run tests coupled with 231-- variables to watch them. 232createTestActions 233 :: OptionSet 234 -> TestTree 235 -> IO ([(Action, TVar Status)], Seq.Seq Finalizer) 236createTestActions opts0 tree = do 237 let 238 traversal :: Tr 239 traversal = 240 foldTestTree 241 (trivialFold :: TreeFold Tr) 242 { foldSingle = runSingleTest 243 , foldResource = addInitAndRelease 244 , foldGroup = \_opts name (Traversal a) -> 245 Traversal $ local (first (Seq.|> name)) a 246 , foldAfter = \_opts deptype pat (Traversal a) -> 247 Traversal $ local (second ((deptype, pat) :)) a 248 } 249 opts0 tree 250 (tests, fins) <- unwrap (mempty :: Path) (mempty :: Deps) traversal 251 let 252 mb_tests :: Maybe [(Action, TVar Status)] 253 mb_tests = resolveDeps $ map 254 (\(act, testInfo) -> 255 (act (Seq.empty, Seq.empty), testInfo)) 256 tests 257 case mb_tests of 258 Just tests' -> return (tests', fins) 259 Nothing -> throwIO DependencyLoop 260 261 where 262 runSingleTest :: IsTest t => OptionSet -> TestName -> t -> Tr 263 runSingleTest opts name test = Traversal $ do 264 statusVar <- liftIO $ atomically $ newTVar NotStarted 265 (parentPath, deps) <- ask 266 let 267 path = parentPath Seq.|> name 268 act (inits, fins) = 269 executeTest (run opts test) statusVar (lookupOption opts) inits fins 270 tell ([(act, (statusVar, path, deps))], mempty) 271 addInitAndRelease :: OptionSet -> ResourceSpec a -> (IO a -> Tr) -> Tr 272 addInitAndRelease _opts (ResourceSpec doInit doRelease) a = wrap $ \path deps -> do 273 initVar <- atomically $ newTVar NotCreated 274 (tests, fins) <- unwrap path deps $ a (getResource initVar) 275 let ntests = length tests 276 finishVar <- atomically $ newTVar ntests 277 let 278 ini = Initializer doInit initVar 279 fin = Finalizer doRelease initVar finishVar 280 tests' = map (first $ local $ (Seq.|> ini) *** (fin Seq.<|)) tests 281 return (tests', fins Seq.|> fin) 282 wrap 283 :: (Path -> 284 Deps -> 285 IO ([(InitFinPair -> IO (), (TVar Status, Path, Deps))], Seq.Seq Finalizer)) 286 -> Tr 287 wrap = Traversal . WriterT . fmap ((,) ()) . ReaderT . uncurry 288 unwrap 289 :: Path 290 -> Deps 291 -> Tr 292 -> IO ([(InitFinPair -> IO (), (TVar Status, Path, Deps))], Seq.Seq Finalizer) 293 unwrap path deps = flip runReaderT (path, deps) . execWriterT . getTraversal 294 295-- | Take care of the dependencies. 296-- 297-- Return 'Nothing' if there is a dependency cycle. 298resolveDeps :: [(IO (), (TVar Status, Path, Deps))] -> Maybe [(Action, TVar Status)] 299resolveDeps tests = checkCycles $ do 300 (run_test, (statusVar, path0, deps)) <- tests 301 let 302 -- Note: Duplicate dependencies may arise if the same test name matches 303 -- multiple patterns. It's not clear that removing them is worth the 304 -- trouble; might consider this in the future. 305 deps' :: [(DependencyType, TVar Status, Path)] 306 deps' = do 307 (deptype, depexpr) <- deps 308 (_, (statusVar1, path, _)) <- tests 309 guard $ exprMatches depexpr path 310 return (deptype, statusVar1, path) 311 312 getStatus :: STM ActionStatus 313 getStatus = foldr 314 (\(deptype, statusvar, _) k -> do 315 status <- readTVar statusvar 316 case status of 317 Done result 318 | deptype == AllFinish || resultSuccessful result -> k 319 | otherwise -> return ActionSkip 320 _ -> return ActionWait 321 ) 322 (return ActionReady) 323 deps' 324 let 325 dep_paths = map (\(_, _, path) -> path) deps' 326 action = Action 327 { actionStatus = getStatus 328 , actionRun = run_test 329 , actionSkip = writeTVar statusVar $ Done $ Result 330 -- See Note [Skipped tests] 331 { resultOutcome = Failure TestDepFailed 332 , resultDescription = "" 333 , resultShortDescription = "SKIP" 334 , resultTime = 0 335 , resultDetailsPrinter = noResultDetails 336 } 337 } 338 return ((action, statusVar), (path0, dep_paths)) 339 340checkCycles :: Ord b => [(a, (b, [b]))] -> Maybe [a] 341checkCycles tests = do 342 let 343 result = fst <$> tests 344 graph = [ ((), v, vs) | (v, vs) <- snd <$> tests ] 345 sccs = stronglyConnComp graph 346 not_cyclic = all (\scc -> case scc of 347 AcyclicSCC{} -> True 348 CyclicSCC{} -> False) 349 sccs 350 guard not_cyclic 351 return result 352 353-- | Used to create the IO action which is passed in a WithResource node 354getResource :: TVar (Resource r) -> IO r 355getResource var = 356 atomically $ do 357 rState <- readTVar var 358 case rState of 359 Created r -> return r 360 Destroyed -> throwSTM UseOutsideOfTest 361 _ -> throwSTM $ unexpectedState "getResource" rState 362 363-- | Run a resource finalizer. 364-- 365-- This function is called from two different places: 366-- 367-- 1. A test thread, which is the last one to use the resource. 368-- 2. The main thread, if an exception (e.g. Ctrl-C) is received. 369-- 370-- Therefore, it is possible that this function is called multiple 371-- times concurrently on the same finalizer. 372-- 373-- This function should be run with async exceptions masked, 374-- and the restore function should be passed as an argument. 375destroyResource :: (forall a . IO a -> IO a) -> Finalizer -> IO (Maybe SomeException) 376destroyResource restore (Finalizer doRelease stateVar _) = join . atomically $ do 377 rState <- readTVar stateVar 378 case rState of 379 Created res -> do 380 writeTVar stateVar BeingDestroyed 381 return $ 382 (either Just (const Nothing) 383 <$> try (restore $ doRelease res)) 384 <* atomically (writeTVar stateVar Destroyed) 385 BeingCreated -> retry 386 -- If the resource is being destroyed, wait until it is destroyed. 387 -- This is so that we don't start destroying the next resource out of 388 -- order. 389 BeingDestroyed -> retry 390 NotCreated -> do 391 -- prevent the resource from being created by a competing thread 392 writeTVar stateVar Destroyed 393 return $ return Nothing 394 FailedToCreate {} -> return $ return Nothing 395 Destroyed -> return $ return Nothing 396 397-- | Start running the tests (in background, in parallel) and pass control 398-- to the callback. 399-- 400-- Once the callback returns, stop running the tests. 401-- 402-- The number of test running threads is determined by the 'NumThreads' 403-- option. 404launchTestTree 405 :: OptionSet 406 -> TestTree 407 -> (StatusMap -> IO (Time -> IO a)) 408 -- ^ A callback. First, it receives the 'StatusMap' through which it 409 -- can observe the execution of tests in real time. Typically (but not 410 -- necessarily), it waits until all the tests are finished. 411 -- 412 -- After this callback returns, the test-running threads (if any) are 413 -- terminated and all resources acquired by tests are released. 414 -- 415 -- The callback must return another callback (of type @'Time' -> 'IO' 416 -- a@) which additionally can report and/or record the total time 417 -- taken by the test suite. This time includes the time taken to run 418 -- all resource initializers and finalizers, which is why it is more 419 -- accurate than what could be measured from inside the first callback. 420 -> IO a 421launchTestTree opts tree k0 = do 422 (testActions, fins) <- createTestActions opts tree 423 let NumThreads numTheads = lookupOption opts 424 (t,k1) <- timed $ do 425 abortTests <- runInParallel numTheads (fst <$> testActions) 426 (do let smap = IntMap.fromList $ zip [0..] (snd <$> testActions) 427 k0 smap) 428 `finallyRestore` \restore -> do 429 -- Tell all running tests to wrap up. 430 abortTests 431 -- Destroy all allocated resources in the case they didn't get 432 -- destroyed by their tests. (See #75.) 433 F.mapM_ (destroyResource restore) fins 434 -- Wait until all resources are destroyed. (Specifically, those 435 -- that were being destroyed by their tests, not those that were 436 -- destroyed by destroyResource above.) 437 restore $ waitForResources fins 438 k1 t 439 where 440 alive :: Resource r -> Bool 441 alive r = case r of 442 NotCreated -> False 443 BeingCreated -> True 444 FailedToCreate {} -> False 445 Created {} -> True 446 BeingDestroyed -> True 447 Destroyed -> False 448 449 waitForResources fins = atomically $ 450 F.forM_ fins $ \(Finalizer _ rvar _) -> do 451 res <- readTVar rvar 452 check $ not $ alive res 453 454unexpectedState :: String -> Resource r -> SomeException 455unexpectedState where_ r = toException $ UnexpectedState where_ (show r) 456 457sleepIndefinitely :: IO () 458sleepIndefinitely = forever $ threadDelay (10^(7::Int)) 459 460-- | Like 'finally' (which also masks its finalizers), but pass the restore 461-- action to the finalizer. 462finallyRestore 463 :: IO a 464 -- ^ computation to run first 465 -> ((forall c . IO c -> IO c) -> IO b) 466 -- ^ computation to run afterward (even if an exception was raised) 467 -> IO a 468 -- ^ returns the value from the first computation 469a `finallyRestore` sequel = 470 mask $ \restore -> do 471 r <- restore a `onException` sequel restore 472 _ <- sequel restore 473 return r 474