packages feed

sydtest-0.19.0.0: src/Test/Syd/Runner/Single.hs

{-# LANGUAGE ScopedTypeVariables #-}

module Test.Syd.Runner.Single (runSingleTestWithFlakinessMode) where

import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import System.Timeout (timeout)
import Test.Syd.HList
import Test.Syd.OptParse
import Test.Syd.Run
import Test.Syd.SpecDef

-- | Run a single test.
--
-- Run the test up to 'maxRetries' times.
-- Finish as soon as the test passes once, or when we run out of retries.
runSingleTestWithFlakinessMode ::
  forall externalResources t.
  -- | How to report test progress
  ProgressReporter ->
  -- | External resources
  HList externalResources ->
  -- | Test definition
  TDef
    ( ProgressReporter ->
      ((HList externalResources -> () -> t) -> t) ->
      IO TestRunResult
    ) ->
  -- | Timeout
  Timeout ->
  -- | Max retries
  Word ->
  -- | Flakiness mode
  FlakinessMode ->
  -- | Expectation mode
  ExpectationMode ->
  -- | Test result
  IO TestRunReport
runSingleTestWithFlakinessMode progressReporter l td mTimeout maxRetries fm em = do
  results <- runSingleTestWithRetries progressReporter l td mTimeout maxRetries em
  pure
    TestRunReport
      { testRunReportExpectationMode = em,
        testRunReportRawResults = results,
        testRunReportFlakinessMode = fm
      }

runSingleTestWithRetries ::
  forall externalResources t.
  -- | How to report test progress
  ProgressReporter ->
  -- | External resources
  HList externalResources ->
  -- | Test definition
  TDef
    ( ProgressReporter ->
      ((HList externalResources -> () -> t) -> t) ->
      IO TestRunResult
    ) ->
  -- | Timeout
  Timeout ->
  -- | Max retries
  Word ->
  -- | Expectation mode
  ExpectationMode ->
  -- If the test ever passed, and the last test result
  IO (NonEmpty TestRunResult)
runSingleTestWithRetries progressReporter l td mTimeout maxRetries em = go maxRetries
  where
    go :: Word -> IO (NonEmpty TestRunResult)
    go w
      | w <= 1 = (:| []) . either id id <$> runWithTimeout
      | otherwise = do
          mResult <- runWithTimeout
          case mResult of
            -- Don't retry on timeout
            Left result -> pure (result :| [])
            Right result ->
              if testStatusMatchesExpectationMode (testRunResultStatus result) em
                then pure (result :| [])
                else do
                  rest <- go (pred w)
                  pure (result NE.<| rest)
      where
        runWithTimeout :: IO (Either TestRunResult TestRunResult)
        runWithTimeout = case mTimeout of
          DoNotTimeout -> Right <$> runFunc
          TimeoutAfterMicros micros -> do
            mResult <- timeout micros runFunc
            pure $ case mResult of
              Nothing -> Left timeoutResult
              Just result -> Right result

        runFunc :: IO TestRunResult
        runFunc = testDefVal td progressReporter (\f -> f l ())

        timeoutResult :: TestRunResult
        timeoutResult =
          TestRunResult
            { testRunResultStatus = TestFailed,
              testRunResultException = Nothing,
              testRunResultNumTests = Nothing,
              testRunResultNumShrinks = Nothing,
              testRunResultFailingInputs = [],
              testRunResultLabels = Nothing,
              testRunResultClasses = Nothing,
              testRunResultTables = Nothing,
              testRunResultGoldenCase = Nothing,
              testRunResultExtraInfo = Just "Timeout!"
            }