packages feed

hedgehog-extras-0.10.1.0: src/Hedgehog/Extras/Test/Base.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Hedgehog.Extras.Test.Base
  ( propertyOnce

  , workspace
  , workspaceWithConfig
  , WorkspacePolicy(..)
  , moduleWorkspace

  , note
  , note_
  , noteM
  , noteM_
  , noteIO
  , noteIO_

  , noteShow
  , noteShowPretty
  , noteShowIO
  , noteShowPrettyIO
  , noteShowIO_
  , noteShowPrettyIO_
  , noteShowM
  , noteShowPrettyM
  , noteShowM_
  , noteShowPrettyM_
  , noteShow_
  , noteShowPretty_

  , noteEach
  , noteEachPretty
  , noteEachIO
  , noteEachPrettyIO
  , noteEachIO_
  , noteEachPrettyIO_
  , noteEachM
  , noteEachPrettyM
  , noteEachM_
  , noteEachPrettyM_
  , noteEach_
  , noteEachPretty_

  , noteTempFile

  , headM
  , indexM
  , fromJustM

  , nothingFail
  , nothingFailM
  , leftFail
  , leftFailM

  , onLeft
  , onNothing

  , jsonErrorFail
  , jsonErrorFailM

  , failWithCustom
  , failMessage
  , expectFailure
  , expectFailureWith
  , tryAssertion
  , assertFailure
  , assertFailure_

  , assertByDeadlineM
  , assertByDeadlineIO
  , assertByDeadlineMFinally
  , assertByDeadlineIOFinally
  , assertWith
  , assertWithM
  , assertM
  , assertIO
  , assertWithinTolerance

  , byDeadlineM
  , byDeadlineIO
  , byDurationM
  , byDurationIO

  , onFailure

  , Integration
  , release

  , runFinallies

  , retry
  , retry'

  , MonadBaseControl
  , bracket
  , bracket_
  ) where

import           Control.Applicative (Applicative (..))
import           Control.Exception (IOException)
import           Control.Exception.Lifted (bracket, bracket_, try)
import           Control.Monad.Trans.Control (MonadBaseControl)
import           Control.Monad (Functor (fmap), Monad (return, (>>=)), mapM_, unless, void)
import           Control.Monad.Catch (MonadCatch)
import           Control.Monad.Morph (hoist)
import           Control.Monad.Reader (MonadIO (..), MonadReader (ask))
import           Control.Monad.Trans.Resource (MonadResource, ReleaseKey, runResourceT)
import           Data.Aeson (Result (..))
import           Data.Bool (Bool (..), otherwise, (||))
import           Data.Either (Either (..), either)
import           Data.Eq (Eq ((==)))
import           Data.Foldable (for_)
import           Data.Function (const, ($), (.))
import           Data.Functor ((<$>))
import           Data.Int (Int)
import           Data.Maybe (Maybe (..), listToMaybe, maybe)
import           Data.Semigroup (Semigroup (..))
import           Data.String (String)
import           Data.Time.Clock (NominalDiffTime, UTCTime)
import           Data.Traversable (Traversable)
import           Data.Tuple (snd)
import           GHC.Stack
import           Hedgehog.Extras.Internal.Test.Integration (Integration, IntegrationState (..))
import           Hedgehog.Extras.Stock.CallStack (callerModuleName)
import           Hedgehog.Extras.Stock.Monad (forceM)
import           Hedgehog.Extras.Test.MonadAssertion
import           Hedgehog.Extras.Test.Prim
import           Hedgehog.Internal.Source (getCaller)
import           Prelude (Num (..), Ord (..), floor)
import           System.FilePath ((</>))
import           System.IO (FilePath, IO)
import           Text.Show (Show (show))

import qualified Control.Concurrent as IO
import qualified Control.Concurrent.STM as STM
import qualified Control.Monad.Trans.Resource as IO
import qualified Data.List as L
import qualified Data.Time.Clock as DTC
import qualified GHC.Stack as GHC
import qualified Hedgehog as H
import qualified Hedgehog.Extras.Internal.Test.Integration as H
import qualified Hedgehog.Extras.Test.MonadAssertion as H
import qualified Hedgehog.Internal.Property as H
import qualified Hedgehog.Internal.Show as H
import qualified System.Directory as IO
import qualified System.Environment as IO
import qualified System.Info as IO
import qualified System.IO as IO
import qualified System.IO.Temp as IO
import Hedgehog.Internal.Property

{- HLINT ignore "Reduce duplication" -}

-- | Policy for workspace directory cleanup behavior
data WorkspacePolicy
  = PreserveWorkspace    -- ^ Always preserve workspace directories, even on success
  | CleanupOnSuccess     -- ^ Clean up workspace directories only when tests succeed
  deriving (Eq, Show)

-- | Run a property with only one test.  This is intended for allowing hedgehog
-- to run unit tests.
propertyOnce :: HasCallStack => Integration () -> H.Property
propertyOnce = H.withTests 1 . H.property . hoist runResourceT . hoist H.runIntegrationReaderT

-- | Invert the behavior of a property: success becomes failure and vice versa.
expectFailure :: (MonadTest m, MonadIO m, HasCallStack) => H.TestT IO a -> m ()
expectFailure = GHC.withFrozenCallStack $ expectFailureWith (pure . const ())

-- | Invert the behavior of a property: success becomes failure and vice versa.
-- This function behaves like 'expectFailure' but it allows to check the failure
-- is as expected. The function takes a 'Failure' and should itself be a test
-- that fails if the failure is not as expected.
expectFailureWith :: (MonadTest m, MonadIO m, HasCallStack) => (H.Failure -> m ()) -> H.TestT IO b -> m ()
expectFailureWith checkFailure prop = GHC.withFrozenCallStack $ do
  (res, _) <- H.evalIO $ H.runTestT prop
  case res of
    Left e -> checkFailure e
    _ -> H.failWith Nothing "Expected the test to fail but it passed" -- Property pased but we expected a failure

-- | Create a workspace directory which will exist for at least the duration of
-- the supplied block.
--
-- The directory will have the supplied prefix but contain a generated random
-- suffix to prevent interference between tests
--
-- The directory will be deleted if the block succeeds, but left behind if
-- the block fails.
workspace
  :: HasCallStack
  => MonadCatch m
  => MonadBaseControl IO m
  => MonadResource m
  => MonadTest m
  => H.MonadAssertion m
  => FilePath
  -> (FilePath -> m ())
  -> m ()
workspace prefixPath f = withFrozenCallStack $ do
  maybeKeepWorkspace <- H.evalIO $ IO.lookupEnv "KEEP_WORKSPACE"
  let policy = if IO.os == "mingw32" || maybeKeepWorkspace == Just "1"
              then PreserveWorkspace
              else CleanupOnSuccess
  workspaceWithConfig policy prefixPath f

-- | Create a workspace directory with explicit configuration for cleanup behavior.
--
-- The directory will have the supplied prefix but contain a generated random
-- suffix to prevent interference between tests
--
-- The directory will be deleted if the block succeeds, but left behind if
-- the block fails (unless policy is PreserveWorkspace, in which case it's always preserved).
workspaceWithConfig
  :: HasCallStack
  => MonadCatch m
  => MonadBaseControl IO m
  => MonadResource m
  => MonadTest m
  => H.MonadAssertion m
  => WorkspacePolicy  -- ^ Cleanup policy: PreserveWorkspace always preserves, CleanupOnSuccess deletes on success only
  -> FilePath
  -> (FilePath -> m ())
  -> m ()
workspaceWithConfig policy prefixPath f = withFrozenCallStack $ evalM $ do
  ws <- do
    systemTemp <- H.evalIO IO.getCanonicalTemporaryDirectory
    H.evalIO $ IO.createTempDirectory systemTemp $ prefixPath <> "-test"
  
  H.annotate $ "Workspace: " <> ws
  H.evalIO $ IO.writeFile (ws </> "module") callerModuleName
  
  result <- H.catchAssertion (fmap Right (f ws)) (return . Left)
  
  case result of
    Right _ -> do
      -- Test succeeded, clean up if policy allows
      case policy of
        CleanupOnSuccess -> removeWorkspaceRetries ws 20
        PreserveWorkspace -> pure ()
    Left assertion -> do
      -- Test failed, preserve workspace for debugging (unless policy forces cleanup)
      case policy of
        CleanupOnSuccess -> do
          H.annotate $ "Test failed, preserving workspace for debugging: " <> ws
        PreserveWorkspace -> pure ()
      -- Re-throw the original assertion failure
      H.throwAssertion assertion
  where
    removeWorkspaceRetries
      :: MonadBaseControl IO m
      => MonadResource m
      => MonadTest m
      => FilePath
      -> Int
      -> m ()
    removeWorkspaceRetries ws retries = GHC.withFrozenCallStack $ do
      result <- try (liftIO (IO.removePathForcibly ws))
      case result of
        Right () -> return ()
        Left (_ :: IOException) -> do
          if retries > 0
            then do
              liftIO (IO.threadDelay 100000) -- wait 100ms before retrying
              removeWorkspaceRetries ws (retries - 1)
            else do
              failMessage GHC.callStack "Failed to remove workspace directory after multiple attempts"


-- | Create a workspace directory which will exist for at least the duration of
-- the supplied block.
--
-- The directory will have the prefix as "$prefixPath/$moduleName" but contain a generated random
-- suffix to prevent interference between tests
--
-- The directory will be deleted if the block succeeds, but left behind if
-- the block fails.
--
-- The 'prefix' argument should not contain directory delimeters.
moduleWorkspace
  :: HasCallStack
  => MonadCatch m
  => MonadBaseControl IO m
  => MonadResource m
  => MonadTest m
  => H.MonadAssertion m
  => String
  -> (FilePath -> m ())
  -> m ()
moduleWorkspace prefix f = GHC.withFrozenCallStack $ do
  let srcModule = maybe "UnknownModule" (GHC.srcLocModule . snd) (listToMaybe (GHC.getCallStack GHC.callStack))
  workspace (prefix <> "-" <> srcModule) f

-- | Annotate the given string at the context supplied by the callstack.
noteWithCallstack :: MonadTest m => CallStack -> String -> m ()
noteWithCallstack cs a = H.writeLog $ H.Annotation (getCaller cs) a

-- | Annotate with the given string.
note :: (MonadTest m, HasCallStack) => String -> m String
note a = GHC.withFrozenCallStack $ do
  !b <- H.eval a
  noteWithCallstack GHC.callStack b
  return b

-- | Annotate the given string returning unit.
note_ :: (MonadTest m, HasCallStack) => String -> m ()
note_ a = GHC.withFrozenCallStack $ noteWithCallstack GHC.callStack a

-- | Annotate the given string in a monadic context.
noteM :: (MonadTest m, MonadCatch m, HasCallStack) => m String -> m String
noteM a = GHC.withFrozenCallStack $ do
  !b <- H.evalM a
  noteWithCallstack GHC.callStack b
  return b

-- | Annotate the given string in a monadic context returning unit.
noteM_ :: (MonadTest m, MonadCatch m, HasCallStack) => m String -> m ()
noteM_ a = GHC.withFrozenCallStack $ do
  !b <- H.evalM a
  noteWithCallstack GHC.callStack b
  return ()

-- | Annotate the given string in IO.
noteIO :: (MonadTest m, MonadIO m, HasCallStack) => IO String -> m String
noteIO f = GHC.withFrozenCallStack $ do
  !a <- H.evalIO f
  noteWithCallstack GHC.callStack a
  return a

-- | Annotate the given string in IO returning unit.
noteIO_ :: (MonadTest m, MonadIO m, HasCallStack) => IO String -> m ()
noteIO_ f = GHC.withFrozenCallStack $ do
  !a <- H.evalIO f
  noteWithCallstack GHC.callStack a
  return ()

-- | Annotate the given value.
noteShow :: (MonadTest m, HasCallStack, Show a) => a -> m a
noteShow a = GHC.withFrozenCallStack $ do
  !b <- H.eval a
  noteWithCallstack GHC.callStack (show b)
  return b

-- | Annotate the given value, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteShowPretty :: (MonadTest m, HasCallStack, Show a) => a -> m a
noteShowPretty a = GHC.withFrozenCallStack $ do
  !b <- H.eval a
  noteWithCallstack GHC.callStack (H.showPretty b)
  return b

-- | Annotate the given value returning unit.
noteShow_ :: (MonadTest m, HasCallStack, Show a) => a -> m ()
noteShow_ a = GHC.withFrozenCallStack $ noteWithCallstack GHC.callStack (show a)

-- | Annotate the given value returning unit, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteShowPretty_ :: (MonadTest m, HasCallStack, Show a) => a -> m ()
noteShowPretty_ a = GHC.withFrozenCallStack $ noteWithCallstack GHC.callStack (H.showPretty a)

-- | Annotate the given value in a monadic context.
noteShowM :: (MonadTest m, MonadCatch m, HasCallStack, Show a) => m a -> m a
noteShowM a = GHC.withFrozenCallStack $ do
  !b <- H.evalM a
  noteWithCallstack GHC.callStack (show b)
  return b

-- | Annotate the given value in a monadic context, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteShowPrettyM :: (MonadTest m, MonadCatch m, HasCallStack, Show a) => m a -> m a
noteShowPrettyM a = GHC.withFrozenCallStack $ do
  !b <- H.evalM a
  noteWithCallstack GHC.callStack (H.showPretty b)
  return b

-- | Annotate the given value in a monadic context returning unit.
noteShowM_ :: (MonadTest m, MonadCatch m, HasCallStack, Show a) => m a -> m ()
noteShowM_ a = GHC.withFrozenCallStack $ do
  !b <- H.evalM a
  noteWithCallstack GHC.callStack (show b)
  return ()

-- | Annotate the given value in a monadic context returning unit, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteShowPrettyM_ :: (MonadTest m, MonadCatch m, HasCallStack, Show a) => m a -> m ()
noteShowPrettyM_ a = GHC.withFrozenCallStack $ do
  !b <- H.evalM a
  noteWithCallstack GHC.callStack (H.showPretty b)
  return ()

-- | Annotate the given value in IO.
noteShowIO :: (MonadTest m, MonadIO m, HasCallStack, Show a) => IO a -> m a
noteShowIO f = GHC.withFrozenCallStack $ do
  !a <- H.evalIO f
  noteWithCallstack GHC.callStack (show a)
  return a

-- | Annotate the given value in IO, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteShowPrettyIO :: (MonadTest m, MonadIO m, HasCallStack, Show a) => IO a -> m a
noteShowPrettyIO f = GHC.withFrozenCallStack $ do
  !a <- H.evalIO f
  noteWithCallstack GHC.callStack (H.showPretty a)
  return a

-- | Annotate the given value in IO returning unit.
noteShowIO_ :: (MonadTest m, MonadIO m, HasCallStack, Show a) => IO a -> m ()
noteShowIO_ f = GHC.withFrozenCallStack $ do
  !a <- H.evalIO f
  noteWithCallstack GHC.callStack (show a)
  return ()

-- | Annotate the given value in IO returning unit, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteShowPrettyIO_ :: (MonadTest m, MonadIO m, HasCallStack, Show a) => IO a -> m ()
noteShowPrettyIO_ f = GHC.withFrozenCallStack $ do
  !a <- H.evalIO f
  noteWithCallstack GHC.callStack (H.showPretty a)
  return ()

-- | Annotate the each value in the given traversable.
noteEach :: (MonadTest m, HasCallStack, Show a, Traversable f) => f a -> m (f a)
noteEach as = GHC.withFrozenCallStack $ do
  for_ as $ noteWithCallstack GHC.callStack . show
  return as

-- | Annotate the each value in the given traversable, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteEachPretty :: (MonadTest m, HasCallStack, Show a, Traversable f) => f a -> m (f a)
noteEachPretty as = GHC.withFrozenCallStack $ do
  for_ as $ noteWithCallstack GHC.callStack . H.showPretty
  return as

-- | Annotate the each value in the given traversable returning unit.
noteEach_ :: (MonadTest m, HasCallStack, Show a, Traversable f) => f a -> m ()
noteEach_ as = GHC.withFrozenCallStack $ for_ as $ noteWithCallstack GHC.callStack . show

-- | Annotate the each value in the given traversable returning unit, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteEachPretty_ :: (MonadTest m, HasCallStack, Show a, Traversable f) => f a -> m ()
noteEachPretty_ as = GHC.withFrozenCallStack $ for_ as $ noteWithCallstack GHC.callStack . H.showPretty

-- | Annotate the each value in the given traversable in a monadic context.
noteEachM :: (MonadTest m, HasCallStack, Show a, Traversable f) => m (f a) -> m (f a)
noteEachM f = GHC.withFrozenCallStack $ do
  !as <- f
  for_ as $ noteWithCallstack GHC.callStack . show
  return as

-- | Annotate the each value in the given traversable in a monadic context, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteEachPrettyM :: (MonadTest m, HasCallStack, Show a, Traversable f) => m (f a) -> m (f a)
noteEachPrettyM f = GHC.withFrozenCallStack $ do
  !as <- f
  for_ as $ noteWithCallstack GHC.callStack . H.showPretty
  return as

-- | Annotate the each value in the given traversable in a monadic context returning unit.
noteEachM_ :: (MonadTest m, HasCallStack, Show a, Traversable f) => m (f a) -> m ()
noteEachM_ f = GHC.withFrozenCallStack $ do
  !as <- f
  for_ as $ noteWithCallstack GHC.callStack . show

-- | Annotate the each value in the given traversable in a monadic context returning unit, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteEachPrettyM_ :: (MonadTest m, HasCallStack, Show a, Traversable f) => m (f a) -> m ()
noteEachPrettyM_ f = GHC.withFrozenCallStack $ do
  !as <- f
  for_ as $ noteWithCallstack GHC.callStack . H.showPretty

-- | Annotate the each value in the given traversable in IO.
noteEachIO :: (MonadTest m, MonadIO m, HasCallStack, Show a, Traversable f) => IO (f a) -> m (f a)
noteEachIO f = GHC.withFrozenCallStack $ do
  !as <- H.evalIO f
  for_ as $ noteWithCallstack GHC.callStack . show
  return as

-- | Annotate the each value in the given traversable in IO, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteEachPrettyIO :: (MonadTest m, MonadIO m, HasCallStack, Show a, Traversable f) => IO (f a) -> m (f a)
noteEachPrettyIO f = GHC.withFrozenCallStack $ do
  !as <- H.evalIO f
  for_ as $ noteWithCallstack GHC.callStack . H.showPretty
  return as

-- | Annotate the each value in the given traversable in IO returning unit.
noteEachIO_ :: (MonadTest m, MonadIO m, HasCallStack, Show a, Traversable f) => IO (f a) -> m ()
noteEachIO_ f = GHC.withFrozenCallStack $ do
  !as <- H.evalIO f
  for_ as $ noteWithCallstack GHC.callStack . show

-- | Annotate the each value in the given traversable in IO returning unit, pretty printing it with indentation. Note that large data structures will take
-- a significant amount of vertical screen space.
noteEachPrettyIO_ :: (MonadTest m, MonadIO m, HasCallStack, Show a, Traversable f) => IO (f a) -> m ()
noteEachPrettyIO_ f = GHC.withFrozenCallStack $ do
  !as <- H.evalIO f
  for_ as $ noteWithCallstack GHC.callStack . H.showPretty

-- | Return the test file path after annotating it relative to the project root directory
noteTempFile :: (MonadTest m, HasCallStack) => FilePath -> FilePath -> m FilePath
noteTempFile tempDir filePath = GHC.withFrozenCallStack $ do
  let relPath = tempDir </> filePath
  H.annotate relPath
  return relPath

-- | Fail when the result is Nothing.
nothingFail :: (MonadTest m, HasCallStack) => Maybe a -> m a
nothingFail r = GHC.withFrozenCallStack $ case r of
  Just a -> return a
  Nothing -> failMessage GHC.callStack "Expected Just"

-- | Fail when the computed result is Nothing.
nothingFailM :: (MonadTest m, HasCallStack) => m (Maybe a) -> m a
nothingFailM f = GHC.withFrozenCallStack $ f >>= nothingFail

-- | Fail when the result is Left.
leftFail :: (MonadTest m, Show e, HasCallStack) => Either e a -> m a
leftFail r = GHC.withFrozenCallStack $ case r of
  Right a -> return a
  Left e -> failMessage GHC.callStack ("Expected Right: " <> show e)

-- | Fail when the computed result is Left.
leftFailM :: (MonadTest m, Show e, HasCallStack) => m (Either e a) -> m a
leftFailM f = GHC.withFrozenCallStack $ f >>= leftFail

maybeAt :: Int -> [a] -> Maybe a
maybeAt n xs
  | n < 0 = Nothing
  | otherwise = L.foldr go (const Nothing) xs n
      where
        go :: a -> (Int -> Maybe a) -> Int -> Maybe a
        go x r k =
          case k of
            0 -> Just x
            _ -> r (k - 1)

headM :: (MonadTest m, HasCallStack) => [a] -> m a
headM (a:_) = return a
headM [] = GHC.withFrozenCallStack $ failMessage GHC.callStack "Cannot take head of empty list"

indexM :: (MonadTest m, HasCallStack) => Int -> [a] -> m a
indexM n xs =
  case maybeAt n xs of
    Just x -> pure x
    Nothing ->
      GHC.withFrozenCallStack $
        failMessage GHC.callStack $ "Cannot get index " <> show n <> " of list of length " <> show (L.length xs)

onLeft :: Monad m => (e -> m a) -> m (Either e a) -> m a
onLeft h f = f >>= either h pure

onNothing :: Monad m => m a -> m (Maybe a) -> m a
onNothing h f = f >>= maybe h pure

-- | Index into a list.  On failure, a friendly message is included in the test report.
fromJustM :: (MonadTest m, HasCallStack) => Maybe a -> m a
fromJustM (Just a) = return a
fromJustM Nothing = GHC.withFrozenCallStack $ failMessage GHC.callStack "Cannot take head of empty list"

-- | Fail when the result is Error.
jsonErrorFail :: (MonadTest m, HasCallStack) => Result a -> m a
jsonErrorFail r = GHC.withFrozenCallStack $ case r of
  Success a -> return a
  Error msg -> failMessage GHC.callStack ("Expected Right: " <> msg)

-- | Fail when the computed result is Error.
jsonErrorFailM :: (MonadTest m, HasCallStack) => m (Result a) -> m a
jsonErrorFailM f = GHC.withFrozenCallStack $ f >>= jsonErrorFail

-- | Run the operation 'f' once per 'period' until it returns 'True' or the deadline expires.
--
-- Expiration of the deadline results in an assertion failure
byDeadlineIO :: (MonadAssertion m, MonadTest m, MonadIO m, HasCallStack) => NominalDiffTime -> UTCTime -> String -> IO a -> m a
byDeadlineIO period deadline errorMessage f = GHC.withFrozenCallStack $ byDeadlineM period deadline errorMessage $ liftIO f

-- | Run the operation 'f' once per 'period' until it returns 'True' or the deadline expires.
--
-- Expiration of the deadline results in an assertion failure
byDeadlineM :: forall m a. (MonadAssertion m, MonadTest m, MonadIO m, HasCallStack) => NominalDiffTime -> UTCTime -> String -> m a -> m a
byDeadlineM period deadline errorMessage f = GHC.withFrozenCallStack $ do
  start <- liftIO DTC.getCurrentTime
  a <- goM
  end <- liftIO DTC.getCurrentTime
  note_ $ "Operation completed in " <> show (DTC.diffUTCTime end start)
  return a
  where goM :: m a
        goM = H.catchAssertion f $ \e -> do
          currentTime <- liftIO DTC.getCurrentTime
          if currentTime < deadline
            then do
              liftIO $ IO.threadDelay (floor (DTC.nominalDiffTimeToSeconds period * 1000000))
              goM
            else do
              H.annotateShow currentTime
              void $ failMessage GHC.callStack $ "Condition not met by deadline: " <> errorMessage
              H.throwAssertion e

-- | Run the operation 'f' once per 'period' until it returns 'True' or the duration expires.
--
-- Expiration of the duration results in an assertion failure
byDurationIO :: (MonadAssertion m, MonadTest m, MonadIO m, HasCallStack) => NominalDiffTime -> NominalDiffTime -> String -> IO a -> m a
byDurationIO period duration errorMessage f = GHC.withFrozenCallStack $ byDurationM period duration errorMessage $ liftIO f

-- | Run the operation 'f' once per 'period' until it returns 'True' or the duration expires.
--
-- Expiration of the duration results in an assertion failure
byDurationM :: (MonadAssertion m, MonadTest m, MonadIO m, HasCallStack) => NominalDiffTime -> NominalDiffTime -> String -> m a -> m a
byDurationM period duration errorMessage f = GHC.withFrozenCallStack $ do
  deadline <- DTC.addUTCTime duration <$> liftIO DTC.getCurrentTime
  byDeadlineM period deadline errorMessage f

-- | Run the operation 'f' once a second until it returns 'True' or the deadline expires.
--
-- Expiration of the deadline results in an assertion failure
assertByDeadlineIO :: (MonadTest m, MonadIO m, HasCallStack) => UTCTime -> IO Bool -> m ()
assertByDeadlineIO deadline f = GHC.withFrozenCallStack $ do
  succeeded <- liftIO f
  unless succeeded $ do
    currentTime <- liftIO DTC.getCurrentTime
    if currentTime < deadline
      then do
        liftIO $ IO.threadDelay 1000000
        assertByDeadlineIO deadline f
      else do
        H.annotateShow currentTime
        failMessage GHC.callStack "Condition not met by deadline"

-- | Run the operation 'f' once a second until it returns 'True' or the deadline expires.
--
-- Expiration of the deadline results in an assertion failure
assertByDeadlineM :: (MonadTest m, MonadIO m, HasCallStack) => UTCTime -> m Bool -> m ()
assertByDeadlineM deadline f = GHC.withFrozenCallStack $ do
  succeeded <- f
  unless succeeded $ do
    currentTime <- liftIO DTC.getCurrentTime
    if currentTime < deadline
      then do
        liftIO $ IO.threadDelay 1000000
        assertByDeadlineM deadline f
      else do
        H.annotateShow currentTime
        failMessage GHC.callStack "Condition not met by deadline"

-- | Run the operation 'f' once a second until it returns 'True' or the deadline expires.
--
-- The action 'g' is run after expiration of the deadline, but before failure allowing for
-- additional annotations to be presented.
--
-- Expiration of the deadline results in an assertion failure
assertByDeadlineIOFinally :: (MonadTest m, MonadIO m, HasCallStack) => UTCTime -> IO Bool -> m () -> m ()
assertByDeadlineIOFinally deadline f g = GHC.withFrozenCallStack $ do
  succeeded <- liftIO f
  unless succeeded $ do
    currentTime <- liftIO DTC.getCurrentTime
    if currentTime < deadline
      then do
        liftIO $ IO.threadDelay 1000000
        assertByDeadlineIOFinally deadline f g
      else do
        H.annotateShow currentTime
        g
        failMessage GHC.callStack "Condition not met by deadline"

-- | Run the operation 'f' once a second until it returns 'True' or the deadline expires.
--
-- The action 'g' is run after expiration of the deadline, but before failure allowing for
-- additional annotations to be presented.
--
-- Expiration of the deadline results in an assertion failure
assertByDeadlineMFinally :: (MonadTest m, MonadIO m, HasCallStack) => UTCTime -> m Bool -> m () -> m ()
assertByDeadlineMFinally deadline f g = GHC.withFrozenCallStack $ do
  succeeded <- f
  unless succeeded $ do
    currentTime <- liftIO DTC.getCurrentTime
    if currentTime < deadline
      then do
        liftIO $ IO.threadDelay 1000000
        assertByDeadlineMFinally deadline f g
      else do
        H.annotateShow currentTime
        g
        failMessage GHC.callStack "Condition not met by deadline"

-- | Run the test function against the value. Report the value on the failure.
assertWith :: (H.MonadTest m, Show p, HasCallStack) => p -> (p -> Bool) -> m ()
assertWith v f = GHC.withFrozenCallStack $ assertWithM v (pure . f)

-- | Run the test function against the value. Report the value on the failure.
assertWithM :: (H.MonadTest m, Show p, HasCallStack) => p -> (p -> m Bool) -> m ()
assertWithM v f = GHC.withFrozenCallStack $ do
  result <- f v
  if result
     then H.success
     else do
       noteShow_ v
       H.assert result

-- | Run the monadic action 'f' and assert the return value is 'True'.
assertM :: (MonadTest m, HasCallStack) => m Bool -> m ()
assertM f = GHC.withFrozenCallStack $ f >>= H.assert

-- | Run the IO action 'f' and assert the return value is 'True'.
assertIO :: (MonadTest m, MonadIO m, HasCallStack) => IO Bool -> m ()
assertIO f = GHC.withFrozenCallStack $ H.evalIO (forceM f) >>= H.assert

-- | Tests if @|c - v| <= r@
assertWithinTolerance :: (Show a, Ord a, Num a, HasCallStack, H.MonadTest m)
                  => a -- ^ tested value @v@
                  -> a -- ^ expected value @c@
                  -> a -- ^ tolerance range @r@
                  -> m ()
assertWithinTolerance v c r = GHC.withFrozenCallStack $ do
  H.diff v (>=) (c - r)
  H.diff v (<=) (c + r)

-- | Release the given release key.
release :: (MonadTest m, MonadIO m) => ReleaseKey -> m ()
release k = GHC.withFrozenCallStack . H.evalIO $ IO.release k

onFailure :: Integration () -> Integration ()
onFailure f = do
  s <- ask
  liftIO . STM.atomically $ STM.modifyTVar (integrationStateFinals s) (f:)

reportFinally :: Integration () -> Integration ()
reportFinally f = do
  result <- H.catchAssertion (fmap Right f) (return . Left)

  case result of
    Right () -> return ()
    Left a -> note_ $ "Unable to run finally: " <> show a

runFinallies :: Integration a -> Integration a
runFinallies f = do
  result <- H.catchAssertion (fmap Right f) (return . Left)

  case result of
    Right a -> return a
    Left assertion -> do
      s <- ask
      finals <- liftIO . STM.atomically $ STM.swapTVar (integrationStateFinals s) []
      mapM_ reportFinally finals
      H.throwAssertion assertion

retry :: forall a. Int -> (Int -> Integration a) -> Integration a
retry n f = go 0
  where go :: Int -> Integration a
        go i = do
          note_ $ "Retry attempt " <> show i <> " of " <> show n
          result <- H.catchAssertion (fmap Right (f i)) (return . Left)

          case result of
            Right a -> return a
            Left assertion -> do
              if i < n
                then go (i + 1)
                else do
                  note_ $ "All " <> show n <> " attempts failed"
                  H.throwAssertion assertion

retry' :: forall a. Int -> Integration a -> Integration a
retry' n f = retry n (const f)