sydtest-0.0.0.0: src/Test/Syd/Runner/Asynchronous.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
-- | This module defines how to run a test suite
module Test.Syd.Runner.Asynchronous where
import Control.Concurrent
import Control.Concurrent.Async
import Control.Exception
import Control.Monad.Reader
import qualified Data.ByteString as SB
import qualified Data.ByteString.Char8 as SB8
import Data.IORef
import Data.Maybe
import Data.Set (Set)
import qualified Data.Set as S
import qualified Data.Text as T
import Rainbow
import Test.QuickCheck.IO ()
import Test.Syd.HList
import Test.Syd.Output
import Test.Syd.Run
import Test.Syd.SpecDef
import Test.Syd.SpecForest
runSpecForestAsynchronously :: Bool -> Int -> TestForest '[] () -> IO ResultForest
runSpecForestAsynchronously failFast nbThreads testForest = do
handleForest <- makeHandleForest testForest
failFastVar <- newEmptyMVar
let runRunner = runner failFast nbThreads failFastVar handleForest
runPrinter = liftIO $ waiter failFastVar handleForest
((), resultForest) <- concurrently runRunner runPrinter
pure resultForest
runSpecForestInterleavedWithOutputAsynchronously :: Maybe Bool -> Bool -> Int -> TestForest '[] () -> IO (Timed ResultForest)
runSpecForestInterleavedWithOutputAsynchronously mColour failFast nbThreads testForest = do
handleForest <- makeHandleForest testForest
failFastVar <- newEmptyMVar
let runRunner = runner failFast nbThreads failFastVar handleForest
runPrinter = liftIO $ printer mColour failFastVar handleForest
((), resultForest) <- concurrently runRunner runPrinter
pure resultForest
type HandleForest a b = SpecDefForest a b (MVar (Timed TestRunResult))
type HandleTree a b = SpecDefTree a b (MVar (Timed TestRunResult))
makeHandleForest :: TestForest a b -> IO (HandleForest a b)
makeHandleForest = traverse $
traverse $ \() ->
newEmptyMVar
runner :: Bool -> Int -> MVar () -> HandleForest '[] () -> IO ()
runner failFast nbThreads failFastVar handleForest = do
sem <- liftIO $ newQSemN nbThreads
jobs <- newIORef (S.empty :: Set (Async ()))
-- This is used to make sure that the 'after' part of the resources actually happens after the tests are done, not just when they are started.
let waitForCurrentlyRunning :: IO ()
waitForCurrentlyRunning = do
as <- readIORef jobs
mapM_ wait as
writeIORef jobs S.empty
let goForest :: Parallelism -> HList a -> HandleForest a () -> IO ()
goForest p a = mapM_ (goTree p a)
goTree :: Parallelism -> HList a -> HandleTree a () -> IO ()
goTree p a = \case
DefSpecifyNode _ td var -> do
mDone <- tryReadMVar failFastVar
case mDone of
Nothing -> do
let runNow = timeItT $ testDefVal td (\f -> f a ())
-- Wait before spawning a thread so that we don't spawn too many threads
let quantity = case p of
-- When the test wants to be executed sequentially, we take n locks because we must make sure that
-- 1. no more other tests are still running.
-- 2. no other tests are started during execution.
Sequential -> nbThreads
Parallel -> 1
liftIO $ waitQSemN sem quantity
let job :: IO ()
job = do
result <- runNow
putMVar var result
when (failFast && testRunResultStatus (timedValue result) == TestFailed) $ do
putMVar failFastVar ()
as <- readIORef jobs
mapM_ cancel as
liftIO $ signalQSemN sem quantity
jobAsync <- async job
modifyIORef jobs (S.insert jobAsync)
link jobAsync
Just () -> pure ()
DefPendingNode _ _ -> pure ()
DefDescribeNode _ sdf -> goForest p a sdf
DefWrapNode func sdf -> func (goForest p a sdf >> waitForCurrentlyRunning)
DefBeforeAllNode func sdf -> do
b <- func
goForest p (HCons b a) sdf
DefAroundAllNode func sdf ->
func (\b -> goForest p (HCons b a) sdf >> waitForCurrentlyRunning)
DefAroundAllWithNode func sdf ->
let HCons x _ = a
in func (\b -> goForest p (HCons b a) sdf >> waitForCurrentlyRunning) x
DefAfterAllNode func sdf -> goForest p a sdf `finally` (waitForCurrentlyRunning >> func a)
DefParallelismNode p' sdf -> goForest p' a sdf
DefRandomisationNode _ sdf -> goForest p a sdf
goForest Parallel HNil handleForest
printer :: Maybe Bool -> MVar () -> HandleForest '[] () -> IO (Timed ResultForest)
printer mColour failFastVar handleForest = do
byteStringMaker <- case mColour of
Just False -> pure toByteStringsColors0
Just True -> pure toByteStringsColors256
Nothing -> liftIO byteStringMakerFromEnvironment
let outputLine :: [Chunk] -> IO ()
outputLine lineChunks = do
let bss = chunksToByteStrings byteStringMaker lineChunks
mapM_ SB.putStr bss
SB8.putStrLn ""
treeWidth :: Int
treeWidth = specForestWidth handleForest
let pad :: Int -> [Chunk] -> [Chunk]
pad level = (chunk (T.pack (replicate (paddingSize * level) ' ')) :)
let goForest :: Int -> HandleForest a b -> IO (Maybe ResultForest)
goForest level hts = do
rts <- catMaybes <$> mapM (goTree level) hts
pure $ if null rts then Nothing else Just rts
goTree :: Int -> HandleTree a b -> IO (Maybe ResultTree)
goTree level = \case
DefSpecifyNode t td var -> do
failFastOrResult <- race (readMVar failFastVar) (takeMVar var)
case failFastOrResult of
Left () -> pure Nothing
Right result -> do
let td' = td {testDefVal = result}
mapM_ (outputLine . pad level) $ outputSpecifyLines level treeWidth t td'
pure $ Just $ SpecifyNode t td'
DefPendingNode t mr -> do
mapM_ (outputLine . pad level) $ outputPendingLines t mr
pure $ Just $ PendingNode t mr
DefDescribeNode t sf -> do
mDone <- tryReadMVar failFastVar
case mDone of
Nothing -> do
outputLine $ pad level $ outputDescribeLine t
fmap (DescribeNode t) <$> goForest (succ level) sf
Just () -> pure Nothing
DefWrapNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefBeforeAllNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefAroundAllNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefAroundAllWithNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefAfterAllNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefParallelismNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefRandomisationNode _ sdf -> fmap SubForestNode <$> goForest level sdf
mapM_ outputLine outputTestsHeader
resultForest <- timeItT $ fromMaybe [] <$> goForest 0 handleForest
outputLine [chunk " "]
mapM_ outputLine $ outputFailuresWithHeading (timedValue resultForest)
outputLine [chunk " "]
mapM_ outputLine $ outputStats (computeTestSuiteStats <$> resultForest)
outputLine [chunk " "]
pure resultForest
waiter :: MVar () -> HandleForest '[] () -> IO ResultForest
waiter failFastVar handleForest = do
let goForest :: Int -> HandleForest a b -> IO (Maybe ResultForest)
goForest level hts = do
rts <- catMaybes <$> mapM (goTree level) hts
pure $ if null rts then Nothing else Just rts
goTree :: Int -> HandleTree a b -> IO (Maybe ResultTree)
goTree level = \case
DefSpecifyNode t td var -> do
failFastOrResult <- race (readMVar failFastVar) (takeMVar var)
case failFastOrResult of
Left () -> pure Nothing
Right result -> do
let td' = td {testDefVal = result}
pure $ Just $ SpecifyNode t td'
DefPendingNode t mr -> pure $ Just $ PendingNode t mr
DefDescribeNode t sf -> do
fmap (DescribeNode t) <$> goForest (succ level) sf
DefWrapNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefBeforeAllNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefAroundAllNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefAroundAllWithNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefAfterAllNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefParallelismNode _ sdf -> fmap SubForestNode <$> goForest level sdf
DefRandomisationNode _ sdf -> fmap SubForestNode <$> goForest level sdf
fromMaybe [] <$> goForest 0 handleForest