{-# LANGUAGE ScopedTypeVariables, ExistentialQuantification, RankNTypes,
FlexibleContexts, BangPatterns, CPP, DeriveDataTypeable #-}
module Test.Tasty.Run
( Status(..)
, StatusMap
, launchTestTree
, DependencyException(..)
) where
import qualified Data.IntMap as IntMap
import qualified Data.Sequence as Seq
import qualified Data.Foldable as F
import Data.Maybe
import Data.Graph (SCC(..), stronglyConnComp)
import Data.Typeable
import Control.Monad.State
import Control.Monad.Writer
import Control.Monad.Reader
import Control.Concurrent
import Control.Concurrent.STM
import Control.Concurrent.Timeout (timeout)
import Control.Concurrent.Async
import Control.Exception as E
import Control.Applicative
import Control.Arrow
import GHC.Conc (labelThread)
import Prelude
import Test.Tasty.Core
import Test.Tasty.Parallel
import Test.Tasty.Patterns
import Test.Tasty.Patterns.Types
import Test.Tasty.Options
import Test.Tasty.Options.Core
import Test.Tasty.Runners.Reducers
import Test.Tasty.Runners.Utils (timed)
import Test.Tasty.Providers.ConsoleFormat (noResultDetails)
data Status
= NotStarted
| Executing Progress
| Done Result
deriving Show
type StatusMap = IntMap.IntMap (TVar Status)
data Resource r
= NotCreated
| BeingCreated
| FailedToCreate SomeException
| Created r
| BeingDestroyed
| Destroyed
instance Show (Resource r) where
show r = case r of
NotCreated -> "NotCreated"
BeingCreated -> "BeingCreated"
FailedToCreate exn -> "FailedToCreate " ++ show exn
Created {} -> "Created"
BeingDestroyed -> "BeingDestroyed"
Destroyed -> "Destroyed"
data Initializer
= forall res . Initializer
(IO res)
(TVar (Resource res))
data Finalizer
= forall res . Finalizer
(res -> IO ())
(TVar (Resource res))
(TVar Int)
executeTest
:: ((Progress -> IO ()) -> IO Result)
-> TVar Status
-> Timeout
-> Seq.Seq Initializer
-> Seq.Seq Finalizer
-> IO ()
executeTest action statusVar timeoutOpt inits fins = mask $ \restore -> do
resultOrExn <- try $ restore $ do
initResources
withAsync (action yieldProgress) $ \asy -> do
labelThread (asyncThreadId asy) "tasty_test_execution_thread"
timed $ applyTimeout timeoutOpt $ wait asy
mbExn <- destroyResources restore
atomically . writeTVar statusVar $ Done $
case resultOrExn <* maybe (Right ()) Left mbExn of
Left ex -> exceptionResult ex
Right (t,r) -> r { resultTime = t }
where
initResources :: IO ()
initResources =
F.forM_ inits $ \(Initializer doInit initVar) -> do
join $ atomically $ do
resStatus <- readTVar initVar
case resStatus of
NotCreated -> do
writeTVar initVar BeingCreated
return $
(do
res <- doInit
atomically $ writeTVar initVar $ Created res
) `E.catch` \exn -> do
atomically $ writeTVar initVar $ FailedToCreate exn
throwIO exn
BeingCreated -> retry
Created {} -> return $ return ()
FailedToCreate exn -> return $ throwIO exn
Destroyed -> return $ sleepIndefinitely
BeingDestroyed -> return $ sleepIndefinitely
applyTimeout :: Timeout -> IO Result -> IO Result
applyTimeout NoTimeout a = a
applyTimeout (Timeout t tstr) a = do
let
timeoutResult =
Result
{ resultOutcome = Failure $ TestTimedOut t
, resultDescription =
"Timed out after " ++ tstr
, resultShortDescription = "TIMEOUT"
, resultTime = fromIntegral t
, resultDetailsPrinter = noResultDetails
}
fromMaybe timeoutResult <$> timeout t a
destroyResources :: (forall a . IO a -> IO a) -> IO (Maybe SomeException)
destroyResources restore = do
liftM getFirst . execWriterT . getTraversal $
flip F.foldMap fins $ \fin@(Finalizer _ _ finishVar) ->
Traversal $ do
iAmLast <- liftIO $ atomically $ do
nUsers <- readTVar finishVar
let nUsers' = nUsers - 1
writeTVar finishVar nUsers'
return $ nUsers' == 0
mbExcn <- liftIO $
if iAmLast
then destroyResource restore fin
else return Nothing
tell $ First mbExcn
yieldProgress _ = return ()
type InitFinPair = (Seq.Seq Initializer, Seq.Seq Finalizer)
type Deps = [(DependencyType, Expr)]
type Tr = Traversal
(WriterT ([(InitFinPair -> IO (), (TVar Status, Path, Deps))], Seq.Seq Finalizer)
(ReaderT (Path, Deps)
IO))
data DependencyException
= DependencyLoop
deriving (Typeable)
instance Show DependencyException where
show DependencyLoop = "Test dependencies form a loop."
instance Exception DependencyException
createTestActions
:: OptionSet
-> TestTree
-> IO ([(Action, TVar Status)], Seq.Seq Finalizer)
createTestActions opts0 tree = do
let
traversal :: Tr
traversal =
foldTestTree
(trivialFold :: TreeFold Tr)
{ foldSingle = runSingleTest
, foldResource = addInitAndRelease
, foldGroup = \name (Traversal a) ->
Traversal $ local (first (Seq.|> name)) a
, foldAfter = \deptype pat (Traversal a) ->
Traversal $ local (second ((deptype, pat) :)) a
}
opts0 tree
(tests, fins) <- unwrap (mempty :: Path) (mempty :: Deps) traversal
let
mb_tests :: Maybe [(Action, TVar Status)]
mb_tests = resolveDeps $ map
(\(act, testInfo) ->
(act (Seq.empty, Seq.empty), testInfo))
tests
case mb_tests of
Just tests' -> return (tests', fins)
Nothing -> throwIO DependencyLoop
where
runSingleTest :: IsTest t => OptionSet -> TestName -> t -> Tr
runSingleTest opts name test = Traversal $ do
statusVar <- liftIO $ atomically $ newTVar NotStarted
(parentPath, deps) <- ask
let
path = parentPath Seq.|> name
act (inits, fins) =
executeTest (run opts test) statusVar (lookupOption opts) inits fins
tell ([(act, (statusVar, path, deps))], mempty)
addInitAndRelease :: ResourceSpec a -> (IO a -> Tr) -> Tr
addInitAndRelease (ResourceSpec doInit doRelease) a = wrap $ \path deps -> do
initVar <- atomically $ newTVar NotCreated
(tests, fins) <- unwrap path deps $ a (getResource initVar)
let ntests = length tests
finishVar <- atomically $ newTVar ntests
let
ini = Initializer doInit initVar
fin = Finalizer doRelease initVar finishVar
tests' = map (first $ local $ (Seq.|> ini) *** (fin Seq.<|)) tests
return (tests', fins Seq.|> fin)
wrap
:: (Path ->
Deps ->
IO ([(InitFinPair -> IO (), (TVar Status, Path, Deps))], Seq.Seq Finalizer))
-> Tr
wrap = Traversal . WriterT . fmap ((,) ()) . ReaderT . uncurry
unwrap
:: Path
-> Deps
-> Tr
-> IO ([(InitFinPair -> IO (), (TVar Status, Path, Deps))], Seq.Seq Finalizer)
unwrap path deps = flip runReaderT (path, deps) . execWriterT . getTraversal
resolveDeps :: [(IO (), (TVar Status, Path, Deps))] -> Maybe [(Action, TVar Status)]
resolveDeps tests = checkCycles $ do
(run_test, (statusVar, path0, deps)) <- tests
let
deps' :: [(DependencyType, TVar Status, Path)]
deps' = do
(deptype, depexpr) <- deps
(_, (statusVar1, path, _)) <- tests
guard $ exprMatches depexpr path
return (deptype, statusVar1, path)
getStatus :: STM ActionStatus
getStatus = foldr
(\(deptype, statusvar, _) k -> do
status <- readTVar statusvar
case status of
Done result
| deptype == AllFinish || resultSuccessful result -> k
| otherwise -> return ActionSkip
_ -> return ActionWait
)
(return ActionReady)
deps'
let
dep_paths = map (\(_, _, path) -> path) deps'
action = Action
{ actionStatus = getStatus
, actionRun = run_test
, actionSkip = writeTVar statusVar $ Done $ Result
{ resultOutcome = Failure TestDepFailed
, resultDescription = ""
, resultShortDescription = "SKIP"
, resultTime = 0
, resultDetailsPrinter = noResultDetails
}
}
return ((action, statusVar), (path0, dep_paths))
checkCycles :: Ord b => [(a, (b, [b]))] -> Maybe [a]
checkCycles tests = do
let
result = fst <$> tests
graph = [ ((), v, vs) | (v, vs) <- snd <$> tests ]
sccs = stronglyConnComp graph
not_cyclic = all (\scc -> case scc of
AcyclicSCC{} -> True
CyclicSCC{} -> False)
sccs
guard not_cyclic
return result
getResource :: TVar (Resource r) -> IO r
getResource var =
atomically $ do
rState <- readTVar var
case rState of
Created r -> return r
Destroyed -> throwSTM UseOutsideOfTest
_ -> throwSTM $ unexpectedState "getResource" rState
destroyResource :: (forall a . IO a -> IO a) -> Finalizer -> IO (Maybe SomeException)
destroyResource restore (Finalizer doRelease stateVar _) = join . atomically $ do
rState <- readTVar stateVar
case rState of
Created res -> do
writeTVar stateVar BeingDestroyed
return $
(either Just (const Nothing)
<$> try (restore $ doRelease res))
<* atomically (writeTVar stateVar Destroyed)
BeingCreated -> retry
BeingDestroyed -> retry
NotCreated -> do
writeTVar stateVar Destroyed
return $ return Nothing
FailedToCreate {} -> return $ return Nothing
Destroyed -> return $ return Nothing
launchTestTree
:: OptionSet
-> TestTree
-> (StatusMap -> IO (Time -> IO a))
-> IO a
launchTestTree opts tree k0 = do
(testActions, fins) <- createTestActions opts tree
let NumThreads numTheads = lookupOption opts
(t,k1) <- timed $ do
abortTests <- runInParallel numTheads (fst <$> testActions)
(do let smap = IntMap.fromList $ zip [0..] (snd <$> testActions)
k0 smap)
`finallyRestore` \restore -> do
abortTests
F.mapM_ (destroyResource restore) fins
restore $ waitForResources fins
k1 t
where
alive :: Resource r -> Bool
alive r = case r of
NotCreated -> False
BeingCreated -> True
FailedToCreate {} -> False
Created {} -> True
BeingDestroyed -> True
Destroyed -> False
waitForResources fins = atomically $
F.forM_ fins $ \(Finalizer _ rvar _) -> do
res <- readTVar rvar
check $ not $ alive res
unexpectedState :: String -> Resource r -> SomeException
unexpectedState where_ r = toException $ UnexpectedState where_ (show r)
sleepIndefinitely :: IO ()
sleepIndefinitely = forever $ threadDelay (10^(7::Int))
finallyRestore
:: IO a
-> ((forall c . IO c -> IO c) -> IO b)
-> IO a
a `finallyRestore` sequel =
mask $ \restore -> do
r <- restore a `onException` sequel restore
_ <- sequel restore
return r