packages feed

sydtest-0.13.0.3: src/Test/Syd/Runner/Asynchronous.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

-- | This module defines how to run a test suite
module Test.Syd.Runner.Asynchronous
  ( runSpecForestAsynchronously,
    runSpecForestInterleavedWithOutputAsynchronously,
  )
where

import Control.Concurrent
import Control.Concurrent.Async as Async
import Control.Exception
#if MIN_VERSION_mtl(2,3,0)
import Control.Monad (when)
#endif
import Control.Monad.Reader
import Data.Maybe
import Data.Set (Set)
import qualified Data.Set as S
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Test.QuickCheck.IO ()
import Test.Syd.HList
import Test.Syd.OptParse
import Test.Syd.Output
import Test.Syd.Run
import Test.Syd.Runner.Single
import Test.Syd.SpecDef
import Test.Syd.SpecForest
import Text.Colour

runSpecForestAsynchronously :: Settings -> Word -> TestForest '[] () -> IO ResultForest
runSpecForestAsynchronously settings nbThreads testForest = do
  handleForest <- makeHandleForest testForest
  failFastVar <- newEmptyMVar
  let runRunner = runner settings nbThreads failFastVar handleForest
      runPrinter = liftIO $ waiter failFastVar handleForest
  ((), resultForest) <- concurrently runRunner runPrinter
  pure resultForest

runSpecForestInterleavedWithOutputAsynchronously :: Settings -> Word -> TestForest '[] () -> IO (Timed ResultForest)
runSpecForestInterleavedWithOutputAsynchronously settings nbThreads testForest = do
  handleForest <- makeHandleForest testForest
  failFastVar <- newEmptyMVar
  let runRunner = runner settings nbThreads failFastVar handleForest
      runPrinter = liftIO $ printer settings failFastVar handleForest
  ((), resultForest) <- concurrently runRunner runPrinter
  pure resultForest

type HandleForest a b = SpecDefForest a b (MVar (Timed TestRunReport))

type HandleTree a b = SpecDefTree a b (MVar (Timed TestRunReport))

makeHandleForest :: TestForest a b -> IO (HandleForest a b)
makeHandleForest = traverse $ traverse $ \() -> newEmptyMVar

runner :: Settings -> Word -> MVar () -> HandleForest '[] () -> IO ()
runner settings nbThreads failFastVar handleForest = do
  sem <- liftIO $ newQSemN $ fromIntegral nbThreads
  jobsVar <- newMVar (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
        modifyMVar_ jobsVar $ \jobThreads -> do
          mapM_ Async.wait jobThreads
          pure S.empty

  let goForest :: forall a. HandleForest a () -> R a ()
      goForest = mapM_ goTree

      goTree :: forall a. HandleTree a () -> R a ()
      goTree = \case
        DefSpecifyNode _ td var -> do
          -- If the fail-fast var has been put, This will return 'Just ()', in
          -- which case we must stop.
          mDoneEarly <- liftIO $ tryReadMVar failFastVar
          case mDoneEarly of
            Just () -> pure ()
            Nothing -> do
              Env {..} <- ask

              liftIO $ do
                -- Wait before spawning a thread so that we don't spawn too many threads
                let quantity = case eParallelism 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
                waitQSemN sem $ fromIntegral quantity

                let runNow =
                      timeItT $
                        runSingleTestWithFlakinessMode
                          noProgressReporter
                          eExternalResources
                          td
                          eRetries
                          eFlakinessMode
                          eExpectationMode
                let job :: IO ()
                    job = do
                      -- Start the test
                      result <- runNow

                      -- Put the result in the mvar
                      putMVar var result

                      -- If we should fail fast, put the fail-fast var and cancel all other jobs.
                      when (settingFailFast settings && testRunReportFailed settings (timedValue result)) $ do
                        putMVar failFastVar ()
                        withMVar jobsVar $ \jobThreads ->
                          mapM_ cancel jobThreads
                      liftIO $ signalQSemN sem $ fromIntegral quantity

                modifyMVar_ jobsVar $ \jobThreads -> do
                  jobThread <- async job
                  link jobThread
                  pure (S.insert jobThread jobThreads)
        DefPendingNode _ _ -> pure ()
        DefDescribeNode _ sdf -> goForest sdf
        DefWrapNode func sdf -> do
          e <- ask
          liftIO $
            func $ do
              runReaderT (goForest sdf) e
              waitForCurrentlyRunning
        DefBeforeAllNode func sdf -> do
          b <- liftIO func
          withReaderT
            (\e -> e {eExternalResources = HCons b (eExternalResources e)})
            (goForest sdf)
        DefAroundAllNode func sdf -> do
          e <- ask
          liftIO $
            func
              ( \b -> do
                  runReaderT
                    (goForest sdf)
                    (e {eExternalResources = HCons b (eExternalResources e)})
                  waitForCurrentlyRunning
              )
        DefAroundAllWithNode func sdf -> do
          e <- ask
          let HCons x _ = eExternalResources e
          liftIO $
            func
              ( \b -> do
                  runReaderT
                    (goForest sdf)
                    (e {eExternalResources = HCons b (eExternalResources e)})
                  waitForCurrentlyRunning
              )
              x
        DefAfterAllNode func sdf -> do
          e <- ask
          liftIO $
            runReaderT (goForest sdf) e
              `finally` ( do
                            waitForCurrentlyRunning
                            func (eExternalResources e)
                        )
        DefParallelismNode p' sdf ->
          withReaderT
            (\e -> e {eParallelism = p'})
            (goForest sdf)
        DefRandomisationNode _ sdf -> goForest sdf -- Ignore, randomisation has already happened.
        DefRetriesNode modRetries sdf ->
          withReaderT
            (\e -> e {eRetries = modRetries (eRetries e)})
            (goForest sdf)
        DefFlakinessNode fm sdf ->
          withReaderT
            (\e -> e {eFlakinessMode = fm})
            (goForest sdf)
        DefExpectationNode em sdf ->
          withReaderT
            (\e -> e {eExpectationMode = em})
            (goForest sdf)

  runReaderT
    (goForest handleForest)
    Env
      { eParallelism = Parallel,
        eRetries = settingRetries settings,
        eFlakinessMode = MayNotBeFlaky,
        eExpectationMode = ExpectPassing,
        eExternalResources = HNil
      }

type R a = ReaderT (Env a) IO

-- Not exported, on purpose.
data Env externalResources = Env
  { eParallelism :: !Parallelism,
    eRetries :: !Word,
    eFlakinessMode :: !FlakinessMode,
    eExpectationMode :: !ExpectationMode,
    eExternalResources :: !(HList externalResources)
  }

printer :: Settings -> MVar () -> HandleForest '[] () -> IO (Timed ResultForest)
printer settings failFastVar handleForest = do
  tc <- deriveTerminalCapababilities settings

  let outputLine :: [Chunk] -> IO ()
      outputLine lineChunks = liftIO $ do
        putChunksLocaleWith tc lineChunks
        TIO.putStrLn ""

      treeWidth :: Int
      treeWidth = specForestWidth handleForest

  let pad :: Int -> [Chunk] -> [Chunk]
      pad level = (chunk (T.pack (replicate (paddingSize * level) ' ')) :)

  let outputLineP :: [Chunk] -> P ()
      outputLineP line = do
        level <- ask
        liftIO $ outputLine $ pad level line

      outputLinesP :: [[Chunk]] -> P ()
      outputLinesP = mapM_ outputLineP

  let goForest :: HandleForest a b -> P (Maybe ResultForest)
      goForest hts = do
        rts <- catMaybes <$> mapM goTree hts
        pure $ if null rts then Nothing else Just rts

      goTree :: HandleTree a b -> P (Maybe ResultTree)
      goTree = \case
        DefSpecifyNode t td var -> do
          failFastOrResult <-
            liftIO $
              race
                (readMVar failFastVar)
                (takeMVar var)
          case failFastOrResult of
            Left () -> pure Nothing
            Right result -> do
              let td' = td {testDefVal = result}
              level <- ask
              outputLinesP $ outputSpecifyLines settings level treeWidth t td'
              pure $ Just $ SpecifyNode t td'
        DefPendingNode t mr -> do
          outputLinesP $ outputPendingLines t mr
          pure $ Just $ PendingNode t mr
        DefDescribeNode t sf -> do
          mDoneEarly <- liftIO $ tryReadMVar failFastVar
          case mDoneEarly of
            Just () -> pure Nothing
            Nothing -> do
              outputLineP $ outputDescribeLine t
              fmap (DescribeNode t) <$> addLevel (goForest sf)
        DefWrapNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefBeforeAllNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefAroundAllNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefAroundAllWithNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefAfterAllNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefParallelismNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefRandomisationNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefRetriesNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefFlakinessNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefExpectationNode _ sdf -> fmap SubForestNode <$> goForest sdf
  mapM_ outputLine outputTestsHeader
  resultForest <- timeItT $ fromMaybe [] <$> runReaderT (goForest handleForest) 0
  outputLine [chunk " "]
  mapM_ outputLine $ outputFailuresWithHeading settings (timedValue resultForest)
  outputLine [chunk " "]
  mapM_ outputLine $ outputStats (computeTestSuiteStats settings <$> resultForest)
  outputLine [chunk " "]
  pure resultForest

addLevel :: P a -> P a
addLevel = withReaderT succ

type P = ReaderT Int IO

waiter :: MVar () -> HandleForest '[] () -> IO ResultForest
waiter failFastVar handleForest = do
  let goForest :: HandleForest a b -> IO (Maybe ResultForest)
      goForest hts = do
        rts <- catMaybes <$> mapM goTree hts
        pure $ if null rts then Nothing else Just rts

      goTree :: HandleTree a b -> IO (Maybe ResultTree)
      goTree = \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 sf
        DefWrapNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefBeforeAllNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefAroundAllNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefAroundAllWithNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefAfterAllNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefParallelismNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefRandomisationNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefRetriesNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefFlakinessNode _ sdf -> fmap SubForestNode <$> goForest sdf
        DefExpectationNode _ sdf -> fmap SubForestNode <$> goForest sdf
  fromMaybe [] <$> goForest handleForest