polysemy-test-0.11.0.1: lib/Polysemy/Test/Run.hs
{-# options_haddock prune #-}
-- |Test Interpreters, Internal
module Polysemy.Test.Run where
import qualified Control.Exception as Base
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except (ExceptT (..))
import qualified Control.Monad.Trans.Writer.Lazy as MTL
import qualified Data.Text as Text
import GHC.Stack (callStack)
import GHC.Stack.Types (SrcLoc (SrcLoc, srcLocFile), getCallStack, srcLocModule)
import Hedgehog.Internal.Property (Failure, Journal, TestT (..), failWith)
import Path (Abs, Dir, Path, parseAbsDir, parseRelDir, (</>))
import Path.IO (canonicalizePath, createTempDir, getCurrentDir, getTempDir, removeDirRecur)
import System.IO.Error (IOError)
import Polysemy.Test.Data.Hedgehog (Hedgehog, liftH)
import qualified Polysemy.Test.Data.Test as Test
import Polysemy.Test.Data.Test (Test)
import Polysemy.Test.Data.TestError (SkipTestDefaultValue (..), TestError (..))
import qualified Polysemy.Test.Files as Files
import Polysemy.Test.Hedgehog (rewriteHedgehog)
ignoringIOErrors ::
IO () ->
IO ()
ignoringIOErrors ioe =
Base.catch ioe handler
where
handler :: Monad m => IOError -> m ()
handler =
const unit
interpretTestIn' ::
Member (Embed IO) r =>
Path Abs Dir ->
Path Abs Dir ->
InterpreterFor Test r
interpretTestIn' base tempBase =
interpret \case
Test.TestDir ->
pure base
Test.TempDir path ->
Files.tempDir tempBase path
Test.TempFile content path ->
Files.tempFile tempBase content path
Test.TempFileContent path ->
Files.tempFileContent tempBase path
Test.FixturePath path ->
Files.fixturePath base path
Test.Fixture path ->
Files.fixture base path
createTemp ::
Members [Error TestError, Embed IO] r =>
Sem r (Path Abs Dir)
createTemp =
fromEither . first TestError =<< tryAny do
systemTmp <- getTempDir
createTempDir systemTmp "polysemy-test-"
-- |Interpret 'Test' so that fixtures are read from the directory @base@ and temp operations are performed in
-- @/tmp/polysemy-test-XXX@.
--
-- This library uses 'Path' for all file system related tasks, so in order to construct paths manually, you'll have to
-- use the quasiquoters 'Path.absdir' and 'Path.reldir' or the functions 'parseAbsDir' and 'parseRelDir'.
interpretTestKeepTemp ::
Members [Error TestError, Embed IO] r =>
Path Abs Dir ->
InterpreterFor Test r
interpretTestKeepTemp base sem = do
tempBase <- createTemp
interpretTestIn' base tempBase sem
-- |like 'interpretTestKeepTemp', but deletes the temp dir after the test.
interpretTest ::
Members [Error TestError, Resource, Embed IO] r =>
Path Abs Dir ->
InterpreterFor Test r
interpretTest base sem = do
bracket createTemp release use
where
release tempBase =
embed (ignoringIOErrors (removeDirRecur tempBase))
use tempBase =
interpretTestIn' base tempBase sem
-- |Call 'interpretTest' with the subdirectory @prefix@ of the current working directory as the base dir, which is
-- most likely something like @test@.
-- This is not necessarily consistent, it depends on which directory your test runner uses as cwd.
interpretTestInSubdir ::
Members [Error TestError, Resource, Embed IO] r =>
Text ->
InterpreterFor Test r
interpretTestInSubdir prefix sem = do
prefixPath <- embed (parseRelDir @IO (toString prefix))
base <- embed (canonicalizePath @_ @IO prefixPath)
interpretTest base sem
errorToFailure ::
∀ m r a .
Monad m =>
Member (Hedgehog m) r =>
Maybe a ->
Either TestError a ->
Sem r a
errorToFailure fallback = \case
Right a -> pure a
Left (SkipTest msg)
| Just fb <- fallback
-> pure fb
| otherwise
-> liftH @m (failWith Nothing (cannotSkip <> toString msg))
Left (TestError msg) ->
liftH @m (failWith Nothing (toString msg))
where
cannotSkip = "Test requested to be skipped, but no default result was provided. Original message: "
failToFailure ::
Member (Error TestError) r =>
InterpreterFor Fail r
failToFailure =
failToError (TestError . toText)
-- |Like 'unwrapLiftedTestT', but with an explicit fallback value override for 'SkipTest'.
unwrapLiftedTestTSkippable ::
∀ m r a .
Monad m =>
SkipTestDefaultValue a =>
Member (Embed m) r =>
Maybe a ->
Sem (Fail : Error TestError : Hedgehog m : Error Failure : r) a ->
Sem r (Journal, Either Failure a)
unwrapLiftedTestTSkippable override =
runWriter .
runError .
rewriteHedgehog .
raise2Under .
(>>= errorToFailure @m fallback) .
runError .
failToFailure
where
fallback = override <|> skipTestDefaultValue
-- |Run 'Hedgehog' and its dependent effects that correspond to the monad stack of 'TestT', exposing the monadic state.
unwrapLiftedTestT ::
∀ m r a .
Monad m =>
SkipTestDefaultValue a =>
Member (Embed m) r =>
Sem (Fail : Error TestError : Hedgehog m : Error Failure : r) a ->
Sem r (Journal, Either Failure a)
unwrapLiftedTestT =
unwrapLiftedTestTSkippable Nothing
-- |Run 'Hedgehog' with 'unwrapLiftedTestT' and wrap it back into the 'TestT' stack.
semToTestT ::
Monad m =>
SkipTestDefaultValue a =>
Member (Embed m) r =>
(∀ x . Sem r x -> m x) ->
Sem (Fail : Error TestError : Hedgehog m : Error Failure : r) a ->
TestT m a
semToTestT =
semToTestTSkippable Nothing
-- |Like 'semToTestT', but with an explicit fallback value override for 'SkipTest'.
semToTestTSkippable ::
Monad m =>
SkipTestDefaultValue a =>
Member (Embed m) r =>
Maybe a ->
(∀ x . Sem r x -> m x) ->
Sem (Fail : Error TestError : Hedgehog m : Error Failure : r) a ->
TestT m a
semToTestTSkippable override runSem sem = do
(journal, result) <- lift (runSem (unwrapLiftedTestTSkippable override sem))
TestT (ExceptT (result <$ MTL.tell journal))
-- |'Final' version of 'semToTestT'.
semToTestTFinal ::
Monad m =>
SkipTestDefaultValue a =>
Sem [Fail, Error TestError, Hedgehog m, Error Failure, Embed m, Final m] a ->
TestT m a
semToTestTFinal =
semToTestT (runFinal . embedToFinal)
-- |Like 'semToTestTFinal', but with an explicit fallback value override for 'SkipTest'.
semToTestTFinalSkippable ::
Monad m =>
SkipTestDefaultValue a =>
Maybe a ->
Sem [Fail, Error TestError, Hedgehog m, Error Failure, Embed m, Final m] a ->
TestT m a
semToTestTFinalSkippable override =
semToTestTSkippable override (runFinal . embedToFinal)
type TestEffects =
[
Test,
Resource,
Fail,
Error TestError,
Hedgehog IO,
Error Failure,
Embed IO,
Final IO
]
-- |Convenience combinator that runs both 'Hedgehog' and 'Test' and rewraps the result in @'TestT' IO@, ready for
-- execution as a property.
runTest ::
SkipTestDefaultValue a =>
Path Abs Dir ->
Sem TestEffects a ->
TestT IO a
runTest =
runTestSkippable Nothing
-- |Like 'runTest', but with an explicit fallback value override for 'SkipTest'.
runTestSkippable ::
SkipTestDefaultValue a =>
Maybe a ->
Path Abs Dir ->
Sem TestEffects a ->
TestT IO a
runTestSkippable override dir =
semToTestTFinalSkippable override .
resourceToIOFinal .
interpretTest dir
-- |Like 'runTestInSubdir', but with an explicit fallback value override for 'SkipTest'.
runTestInSubdirSkippable ::
SkipTestDefaultValue a =>
Maybe a ->
Text ->
Sem TestEffects a ->
TestT IO a
runTestInSubdirSkippable override prefix =
semToTestTFinalSkippable override .
resourceToIOFinal .
interpretTestInSubdir prefix
-- |Same as 'runTest', but uses 'interpretTestInSubdir'.
runTestInSubdir ::
SkipTestDefaultValue a =>
Text ->
Sem TestEffects a ->
TestT IO a
runTestInSubdir =
runTestInSubdirSkippable Nothing
callingTestDir ::
Members [Error TestError, Embed IO] r =>
HasCallStack =>
Sem r (Path Abs Dir)
callingTestDir = do
SrcLoc { srcLocFile = toText -> file, srcLocModule = toText -> modl } <- note emptyCallStack deepestSrcLoc
dirPrefix <- note badSrcLoc (Text.stripSuffix (Text.replace "." "/" modl <> ".hs") file)
cwd <- embed @IO getCurrentDir
note badSrcLoc (parseDir cwd (toString dirPrefix))
where
emptyCallStack =
TestError "empty call stack"
deepestSrcLoc =
snd <$> listToMaybe (reverse (getCallStack callStack))
badSrcLoc =
TestError "call stack couldn't be processed"
parseDir cwd dirPrefix =
parseAbsDir dirPrefix <|> (cwd </>) <$> parseRelDir dirPrefix
-- |Like 'runTestAutoWith', but with an explicit fallback value override for 'SkipTest'.
runTestAutoWithSkippable ::
HasCallStack =>
SkipTestDefaultValue a =>
Members [Resource, Embed IO] r =>
Maybe a ->
(∀ x . Sem r x -> IO x) ->
Sem (Test : Fail : Error TestError : Hedgehog IO : Error Failure : r) a ->
TestT IO a
runTestAutoWithSkippable override runSem sem =
semToTestTSkippable override runSem do
base <- callingTestDir
interpretTest base sem
-- |Wrapper for 'semToTestT' that uses the call stack to determine the base dir of the test run.
-- Note that if you wrap this function, you'll have to use the 'HasCallStack' constraint to supply the implicit
-- 'GHC.Stack.Types.CallStack'.
runTestAutoWith ::
HasCallStack =>
SkipTestDefaultValue a =>
Members [Resource, Embed IO] r =>
(∀ x . Sem r x -> IO x) ->
Sem (Test : Fail : Error TestError : Hedgehog IO : Error Failure : r) a ->
TestT IO a
runTestAutoWith =
runTestAutoWithSkippable Nothing
-- |Like 'runTestAuto', but with an explicit fallback value override for 'SkipTest'.
runTestAutoSkippable ::
HasCallStack =>
SkipTestDefaultValue a =>
Maybe a ->
Sem [Test, Fail, Error TestError, Hedgehog IO, Error Failure, Embed IO, Resource, Final IO] a ->
TestT IO a
runTestAutoSkippable override =
runTestAutoWithSkippable override (runFinal . resourceToIOFinal . embedToFinal)
-- |Version of 'runTestAutoWith' specialized to @'Final' IO@
runTestAuto ::
HasCallStack =>
SkipTestDefaultValue a =>
Sem [Test, Fail, Error TestError, Hedgehog IO, Error Failure, Embed IO, Resource, Final IO] a ->
TestT IO a
runTestAuto =
runTestAutoSkippable Nothing