effectful-zoo 0.0.2.0 → 0.0.3.0
raw patch · 77 files changed
+3455/−940 lines, 77 filesdep +amazonka-rdsdep +amazonka-secretsmanagerdep +base64-bytestringdep ~hw-preludedep ~tasty
Dependencies added: amazonka-rds, amazonka-secretsmanager, base64-bytestring, binary, directory, exceptions, filepath, http-conduit, lifted-base, process, stm, tasty-discover, temporary, testcontainers, ulid, uuid
Dependency ranges changed: hw-prelude, tasty
Files
- components/amazonka/Effectful/Zoo/Amazonka/Api.hs +2/−0
- components/amazonka/Effectful/Zoo/Amazonka/Api/Run.hs +30/−0
- components/blockfrost/Effectful/Zoo/Blockfrost/Api.hs +1/−10
- components/blockfrost/Effectful/Zoo/Blockfrost/Dynamic.hs +1/−1
- components/core/Effectful/Zoo/Aeson.hs +21/−0
- components/core/Effectful/Zoo/Core.hs +3/−2
- components/core/Effectful/Zoo/Core/Error/Dynamic.hs +0/−100
- components/core/Effectful/Zoo/Core/Error/Static.hs +0/−175
- components/core/Effectful/Zoo/Core/Exception.hs +0/−35
- components/core/Effectful/Zoo/Core/Function.hs +0/−26
- components/core/Effectful/Zoo/Core/Prim.hs +0/−7
- components/core/Effectful/Zoo/DataLog/Api.hs +50/−1
- components/core/Effectful/Zoo/Environment.hs +88/−0
- components/core/Effectful/Zoo/Error/Dynamic.hs +212/−0
- components/core/Effectful/Zoo/Error/Static.hs +212/−0
- components/core/Effectful/Zoo/Errors/EnvironmentVariableInvalid.hs +24/−0
- components/core/Effectful/Zoo/Errors/EnvironmentVariableMissing.hs +20/−0
- components/core/Effectful/Zoo/Exception.hs +35/−0
- components/core/Effectful/Zoo/FileSystem.hs +129/−7
- components/core/Effectful/Zoo/Function.hs +26/−0
- components/core/Effectful/Zoo/Log/Api/Generic.hs +2/−1
- components/core/Effectful/Zoo/Log/Api/LazyText.hs +2/−1
- components/core/Effectful/Zoo/Log/Api/String.hs +2/−1
- components/core/Effectful/Zoo/Log/Api/Text.hs +2/−1
- components/core/Effectful/Zoo/Log/Data/LogMessage.hs +1/−1
- components/core/Effectful/Zoo/Log/Data/Logger.hs +6/−5
- components/core/Effectful/Zoo/Log/Dynamic.hs +5/−6
- components/core/Effectful/Zoo/Log/Static.hs +10/−11
- components/core/Effectful/Zoo/Prim.hs +12/−0
- components/core/Effectful/Zoo/Process.hs +273/−0
- components/core/Effectful/Zoo/Reader/Static.hs +21/−0
- components/core/Effectful/Zoo/Resource.hs +6/−0
- components/hedgehog-test/Effectful/Zoo/Hedgehog/Test/HedgehogTest.hs +0/−22
- components/hedgehog-test/Effectful/Zoo/Hedgehog/Test/PropertySpec.hs +23/−0
- components/hedgehog-test/Main.hs +1/−16
- components/hedgehog/Effectful/Zoo/Hedgehog/Api.hs +9/−7
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Assert.hs +179/−55
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Classify.hs +0/−20
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Eval.hs +0/−64
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Failure.hs +4/−57
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen.hs +7/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen/Time.hs +18/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen/Ulid.hs +32/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Hedgehog.hs +24/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/Cabal.hs +90/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/Cabal/Types.hs +36/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/FilePath.hs +16/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/OS.hs +12/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Journal.hs +291/−141
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/MonadAssertion.hs +47/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Process.hs +313/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Process/Internal.hs +28/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Range.hs +5/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Run.hs +0/−47
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Stack.hs +10/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Tasty.hs +20/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Tasty/Orphans.hs +18/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Api/Workspace.hs +112/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Data.hs +15/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Data/PackagePath.hs +11/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Data/ProjectRoot.hs +11/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Data/PropertyTest.hs +8/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Data/TestResult.hs +11/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Data/UnitTest.hs +8/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Data/Workspace.hs +11/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Dynamic.hs +0/−67
- components/hedgehog/Effectful/Zoo/Hedgehog/Effect/Hedgehog.hs +83/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Effect/HedgehogGen.hs +41/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/Effect/Run.hs +156/−0
- components/hedgehog/Effectful/Zoo/Hedgehog/MonadTestProxy.hs +0/−31
- components/rds-data-test/Effectful/Zoo/RdsData/Test/Cluster.hs +166/−0
- components/rds-data-test/Effectful/Zoo/RdsData/Test/Env.hs +109/−0
- components/rds-data-test/Effectful/Zoo/RdsData/Test/Workspace.hs +46/−0
- components/rds-data/Effectful/Zoo/RdsData/Core.hs +1/−1
- components/testcontainers-localstack/Effectful/Zoo/TestContainers/LocalStack.hs +139/−0
- components/testcontainers-localstack/Effectful/Zoo/TestContainers/LocalStack/Types.hs +10/−0
- effectful-zoo.cabal +138/−21
components/amazonka/Effectful/Zoo/Amazonka/Api.hs view
@@ -3,7 +3,9 @@ module Effectful.Zoo.Amazonka.Api ( module Effectful.Zoo.Amazonka.Api.Discover, module Effectful.Zoo.Amazonka.Api.Log,+ module Effectful.Zoo.Amazonka.Api.Send, ) where import Effectful.Zoo.Amazonka.Api.Discover import Effectful.Zoo.Amazonka.Api.Log+import Effectful.Zoo.Amazonka.Api.Send
components/amazonka/Effectful/Zoo/Amazonka/Api/Run.hs view
@@ -4,14 +4,19 @@ ( runDataLogAwsLogEntryToLog, runDataLogAwsLogEntryToLogWith, runDataLogAwsLogEntryLocalToLogWith,+ runReaderAwsEnvDiscover, ) where +import Amazonka qualified as AWS import Control.Monad.IO.Class import Data.ByteString.Builder qualified as B+import Data.Generics.Product.Any import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Encoding qualified as LT import Data.Time.Clock qualified as IO import Effectful+import Effectful.Environment+import Effectful.Reader.Static import Effectful.Zoo.Amazonka.Api import Effectful.Zoo.Amazonka.Data import Effectful.Zoo.Core@@ -21,6 +26,8 @@ import Effectful.Zoo.Log.Data.LogMessage import Effectful.Zoo.Log.Data.Severity import HaskellWorks.Prelude+import Lens.Micro+import System.IO qualified as IO runDataLogAwsLogEntryToLog :: forall a r. () => r <: DataLog (LogEntry (LogMessage Text))@@ -61,3 +68,26 @@ , time = now , source = entry.callStack }++runReaderAwsEnvDiscover :: forall a r. ()+ => r <: Environment+ => r <: IOE+ => Eff (Reader AwsEnv : r) a+ -> Eff r a+runReaderAwsEnvDiscover f = do+ logger' <- liftIO $ AWS.newLogger AWS.Debug IO.stdout++ mLocalStackHost <- lookupEnv "AWS_LOCALSTACK_HOST"+ mLocalStackPort <- lookupEnv "AWS_LOCALSTACK_PORT"++ mLocalStackEndpoint <- pure $ (,)+ <$> mLocalStackHost+ <*> mLocalStackPort++ discoveredAwsEnv <- liftIO $ AWS.newEnv AWS.discover++ awsEnv <- pure $ discoveredAwsEnv+ & the @"logger" .~ logger'+ & the @"overrides" %~ maybeSetEndpoint mLocalStackEndpoint++ runReader awsEnv f
components/blockfrost/Effectful/Zoo/Blockfrost/Api.hs view
@@ -339,10 +339,10 @@ import Data.Text import Effectful import Effectful.Dispatch.Dynamic-import Effectful.Error.Static import Effectful.Zoo.Blockfrost.Dynamic (Blockfrost, runBlockfrost) import Effectful.Zoo.Blockfrost.Dynamic qualified as BF import Effectful.Zoo.Core+import Effectful.Zoo.Error.Static import HaskellWorks.Prelude -- Client@@ -507,15 +507,6 @@ getTxMetadataByLabelJSON :: r <: Blockfrost => r <: Error BlockfrostError => Text -> Eff r [TxMetaJSON] getTxMetadataByLabelCBOR' :: r <: Blockfrost => r <: Error BlockfrostError => Text -> Paged -> SortOrder -> Eff r [TxMetaCBOR] getTxMetadataByLabelCBOR :: r <: Blockfrost => r <: Error BlockfrostError => Text -> Eff r [TxMetaCBOR]---fromEither :: forall e a r. ()- => Show e- => r <: Error e- => Either e a- -> Eff r a-fromEither =- either throwError pure -- Client getRoot = fromEither =<< send do BF.GetRoot
components/blockfrost/Effectful/Zoo/Blockfrost/Dynamic.hs view
@@ -11,7 +11,7 @@ import Data.Maybe import Effectful import Effectful.Dispatch.Dynamic-import Effectful.Reader.Dynamic+import Effectful.Reader.Static import Effectful.Zoo.Core import HaskellWorks.Prelude
+ components/core/Effectful/Zoo/Aeson.hs view
@@ -0,0 +1,21 @@+module Effectful.Zoo.Aeson+ ( aesonDecode,+ ) where++import Data.Aeson (FromJSON)+import Data.Aeson qualified as Aeson+import Data.ByteString.Lazy qualified as LBS+import Effectful+import Effectful.Zoo.Core+import Effectful.Zoo.Error.Static+import HaskellWorks.Error.Types.JsonDecodeError+import HaskellWorks.Prelude++aesonDecode :: forall a r. ()+ => r <: Error JsonDecodeError+ => FromJSON a+ => LBS.ByteString+ -> Eff r a+aesonDecode bs =+ fromEither (Aeson.eitherDecode bs)+ & mapError newJsonDecodeError
components/core/Effectful/Zoo/Core.hs view
@@ -1,5 +1,6 @@ module Effectful.Zoo.Core- ( type (<:)+ ( type (<:),+ type (<<:), ) where -import Effectful.Zoo.Core.Prim+import Effectful.Zoo.Prim
− components/core/Effectful/Zoo/Core/Error/Dynamic.hs
@@ -1,100 +0,0 @@-module Effectful.Zoo.Core.Error.Dynamic- ( Error,- catch,- catch_,- throw,- trap,- trap_,-- catchWithCallStack,- catchWithCallStack_,- trapWithCallStack,- trapWithCallStack_,- ) where--import Effectful-import Effectful.Error.Dynamic (Error)-import Effectful.Error.Dynamic qualified as E-import Effectful.Zoo.Core-import HaskellWorks.Prelude--catchWithCallStack :: forall e es a. ()- => HasCallStack- => es <: Error e- => Eff es a- -> (CallStack -> e -> Eff es a)- -> Eff es a-catchWithCallStack =- E.catchError--catchWithCallStack_ :: forall e es a. ()- => HasCallStack- => es <: Error e- => Eff es a- -> (CallStack -> Eff es a)- -> Eff es a-catchWithCallStack_ f h =- catchWithCallStack @e f \cs _ -> h cs--trapWithCallStack :: forall e es a. ()- => HasCallStack- => es <: Error e- => (CallStack -> e -> Eff es a)- -> Eff es a- -> Eff es a-trapWithCallStack =- flip catchWithCallStack--trapWithCallStack_ :: forall e es a. ()- => HasCallStack- => es <: Error e- => (CallStack -> Eff es a)- -> Eff es a- -> Eff es a-trapWithCallStack_ h =- trapWithCallStack @e (const . h)--catch :: forall e es a. ()- => HasCallStack- => es <: Error e- => Eff es a- -> (e -> Eff es a)- -> Eff es a-catch action handler =- catchWithCallStack action (const handler)--catch_ :: forall e es a. ()- => HasCallStack- => es <: Error e- => Eff es a- -> Eff es a- -> Eff es a-catch_ action handler =- catch @e action (const handler)--throw :: forall e es a. ()- => HasCallStack- => es <: Error e- => Show e- => e- -> Eff es a-throw =- E.throwError--trap :: forall e es a. ()- => HasCallStack- => es <: Error e- => (e -> Eff es a)- -> Eff es a- -> Eff es a-trap =- flip catch--trap_ :: forall e es a. ()- => HasCallStack- => es <: Error e- => Eff es a- -> Eff es a- -> Eff es a-trap_ handler =- trap @e (const handler)
− components/core/Effectful/Zoo/Core/Error/Static.hs
@@ -1,175 +0,0 @@-module Effectful.Zoo.Core.Error.Static- ( Error,-- throw,-- catch,- catch_,- trap,- trap_,-- catchWithCallStack,- catchWithCallStack_,- trapWithCallStack,- trapWithCallStack_,-- catchIn,- catchIn_,- trapIn,- trapIn_,-- catchWithCallStackIn,- catchWithCallStackIn_,- trapWithCallStackIn,- trapWithCallStackIn_,- ) where--import Effectful-import Effectful.Error.Static (Error)-import Effectful.Error.Static qualified as E-import Effectful.Zoo.Core-import HaskellWorks.Prelude--throw :: forall e r a. ()- => HasCallStack- => r <: Error e- => Show e- => e- -> Eff r a-throw =- E.throwError--catchWithCallStack :: forall e r a. ()- => HasCallStack- => Eff (Error e : r) a- -> (CallStack -> e -> Eff r a)- -> Eff r a-catchWithCallStack action handler =- E.runError action- & onLeftM (uncurry handler)--catchWithCallStack_ :: forall e r a. ()- => HasCallStack- => Eff (Error e : r) a- -> (CallStack -> Eff r a)- -> Eff r a-catchWithCallStack_ f h =- catchWithCallStack @e f \cs _ -> h cs--trapWithCallStack :: forall e r a. ()- => HasCallStack- => (CallStack -> e -> Eff r a)- -> Eff (Error e : r) a- -> Eff r a-trapWithCallStack =- flip catchWithCallStack--trapWithCallStack_ :: forall e r a. ()- => HasCallStack- => (CallStack -> Eff r a)- -> Eff (Error e : r) a- -> Eff r a-trapWithCallStack_ h =- trapWithCallStack @e (const . h)--catch :: forall e r a. ()- => Eff (Error e : r) a- -> (e -> Eff r a)- -> Eff r a-catch action handler =- E.runError action- & onLeftM (handler . snd)--catch_ :: forall e r a. ()- => Eff (Error e : r) a- -> Eff r a- -> Eff r a-catch_ action handler =- catch @e action (const handler)--trap :: forall e r a. ()- => (e -> Eff r a)- -> Eff (Error e : r) a- -> Eff r a-trap =- flip catch--trap_ :: forall e r a. ()- => Eff r a- -> Eff (Error e : r) a- -> Eff r a-trap_ handler =- trap @e (const handler)---catchWithCallStackIn :: forall e es a. ()- => HasCallStack- => es <: Error e- => Eff es a- -> (CallStack -> e -> Eff es a)- -> Eff es a-catchWithCallStackIn =- E.catchError--catchWithCallStackIn_ :: forall e es a. ()- => HasCallStack- => es <: Error e- => Eff es a- -> (CallStack -> Eff es a)- -> Eff es a-catchWithCallStackIn_ f h =- catchWithCallStackIn @e f \cs _ -> h cs--trapWithCallStackIn :: forall e es a. ()- => HasCallStack- => es <: Error e- => (CallStack -> e -> Eff es a)- -> Eff es a- -> Eff es a-trapWithCallStackIn =- flip catchWithCallStackIn--trapWithCallStackIn_ :: forall e es a. ()- => HasCallStack- => es <: Error e- => (CallStack -> Eff es a)- -> Eff es a- -> Eff es a-trapWithCallStackIn_ h =- trapWithCallStackIn @e (const . h)--catchIn :: forall e es a. ()- => HasCallStack- => es <: Error e- => Eff es a- -> (e -> Eff es a)- -> Eff es a-catchIn action handler =- catchWithCallStackIn action (const handler)--catchIn_ :: forall e es a. ()- => HasCallStack- => es <: Error e- => Eff es a- -> Eff es a- -> Eff es a-catchIn_ action handler =- catchIn @e action (const handler)--trapIn :: forall e es a. ()- => HasCallStack- => es <: Error e- => (e -> Eff es a)- -> Eff es a- -> Eff es a-trapIn =- flip catchIn--trapIn_ :: forall e es a. ()- => HasCallStack- => es <: Error e- => Eff es a- -> Eff es a- -> Eff es a-trapIn_ handler =- trapIn @e (const handler)
− components/core/Effectful/Zoo/Core/Exception.hs
@@ -1,35 +0,0 @@-module Effectful.Zoo.Core.Exception- ( Exception,- catchIO,- throwIO,- trapIO,- ) where--import Effectful-import Effectful.Exception (Exception)-import Effectful.Exception qualified as E-import HaskellWorks.Prelude--catchIO :: ()- => E.Exception e- => Eff es a- -> (e -> Eff es a)- -> Eff es a-catchIO =- E.catch--throwIO :: ()- => HasCallStack- => E.Exception e- => e- -> Eff es a-throwIO =- E.throwIO--trapIO- :: E.Exception e- => (e -> Eff es a)- -> Eff es a- -> Eff es a-trapIO =- flip catchIO
− components/core/Effectful/Zoo/Core/Function.hs
@@ -1,26 +0,0 @@-module Effectful.Zoo.Core.Function- ( once,- ) where--import Effectful-import Effectful.Concurrent-import Effectful.Concurrent.MVar-import Effectful.Zoo.Core.Prim-import HaskellWorks.Prelude--once :: ()- => r <: Concurrent- => Eff r a- -> Eff r (Eff r a)-once f = do- cache <- newMVar Nothing- return $ do- resultOrRunning <- takeMVar cache- case resultOrRunning of- Just result -> do- putMVar cache (Just result)- return result- Nothing -> do- result <- f- putMVar cache (Just result)- return result
− components/core/Effectful/Zoo/Core/Prim.hs
@@ -1,7 +0,0 @@-module Effectful.Zoo.Core.Prim- ( type (<:)- ) where--import Effectful ((:>))--type (<:) r e = (:>) e r
components/core/Effectful/Zoo/DataLog/Api.hs view
@@ -1,14 +1,25 @@ module Effectful.Zoo.DataLog.Api ( dataLog,+ logEntryToJson,+ logMessageToJson,+ putJsonStdout, ) where +import Data.Aeson (Value, object, (.=))+import Data.Aeson qualified as J+import Data.ByteString.Lazy qualified as LBS import Effectful import Effectful.Dispatch.Dynamic+import Effectful.Dispatch.Static import Effectful.Zoo.Core+import Effectful.Zoo.DataLog.Data.LogEntry import Effectful.Zoo.DataLog.Dynamic+import Effectful.Zoo.Log.Data.LogMessage+import GHC.Stack qualified as GHC import HaskellWorks.Prelude+import System.IO qualified as IO -dataLog :: ()+dataLog :: forall i r. () => HasCallStack => r <: DataLog i => i@@ -16,3 +27,41 @@ dataLog i = withFrozenCallStack do send $ DataLog i++logEntryToJson :: forall a. ()+ => (a -> Value)+ -> LogEntry a+ -> Value+logEntryToJson aToJson (LogEntry value time callstack) =+ object+ [ "time" .= time+ , "data" .= aToJson value+ , "callstack" .= fmap callsiteToJson (GHC.getCallStack callstack)+ ]+ where+ callsiteToJson :: ([Char], GHC.SrcLoc) -> Value+ callsiteToJson (caller, srcLoc) =+ object+ [ "caller" .= caller+ , "package" .= GHC.srcLocPackage srcLoc+ , "module" .= GHC.srcLocModule srcLoc+ , "file" .= GHC.srcLocFile srcLoc+ , "startLine" .= GHC.srcLocStartLine srcLoc+ , "startCol" .= GHC.srcLocStartCol srcLoc+ , "endLine" .= GHC.srcLocEndLine srcLoc+ , "endCol" .= GHC.srcLocEndCol srcLoc+ ]++logMessageToJson :: LogMessage Text -> Value+logMessageToJson (LogMessage severity message) =+ object+ [ "severity" .= show severity+ , "message" .= message+ ]++putJsonStdout :: ()+ => Value+ -> Eff r ()+putJsonStdout value = do+ unsafeEff_ $ LBS.putStr $ J.encode value <> "\n"+ unsafeEff_ $ IO.hFlush IO.stdout
+ components/core/Effectful/Zoo/Environment.hs view
@@ -0,0 +1,88 @@+module Effectful.Zoo.Environment+ ( -- * Effect+ Environment,++ -- ** Handlers+ E.runEnvironment,++ -- * Querying the environment+ E.getArgs,+ E.getProgName,+ E.getExecutablePath,+ E.getEnv,+ E.getEnvironment,+ lookupEnv,+ lookupEnvMaybe,+ lookupMapEnv,+ lookupParseMaybeEnv,+ lookupParseEitherEnv,++ -- * Modifying the environment+ E.setEnv,+ E.unsetEnv,+ E.withArgs,+ E.withProgName,+ )+ where++import Data.Text qualified as T+import Effectful+import Effectful.Environment (Environment)+import Effectful.Environment qualified as E+import Effectful.Zoo.Core+import Effectful.Zoo.Error.Static+import Effectful.Zoo.Errors.EnvironmentVariableInvalid+import Effectful.Zoo.Errors.EnvironmentVariableMissing+import HaskellWorks.Prelude++lookupEnv :: ()+ => r <: Environment+ => r <: Error EnvironmentVariableMissing+ => Text+ -> Eff r Text+lookupEnv envName =+ lookupEnvMaybe envName+ & onNothingM (throw $ EnvironmentVariableMissing envName)++lookupMapEnv :: ()+ => r <: Environment+ => r <: Error EnvironmentVariableMissing+ => Text+ -> (Text -> a)+ -> Eff r a+lookupMapEnv envName f =+ f <$> lookupEnv envName++lookupParseMaybeEnv :: ()+ => r <: Environment+ => r <: Error EnvironmentVariableInvalid+ => r <: Error EnvironmentVariableMissing+ => Text+ -> (Text -> Maybe a)+ -> Eff r a+lookupParseMaybeEnv envName parse = do+ text <- lookupEnv envName++ parse text+ & onNothing (throw $ EnvironmentVariableInvalid envName text Nothing)++lookupParseEitherEnv :: ()+ => r <: Environment+ => r <: Error EnvironmentVariableInvalid+ => r <: Error EnvironmentVariableMissing+ => Text+ -> (Text -> Either Text a)+ -> Eff r a+lookupParseEitherEnv envName parse = do+ text <- lookupEnv envName++ parse text+ & onLeft (throw . EnvironmentVariableInvalid envName text . Just)++lookupEnvMaybe :: ()+ => r <: Environment+ => Text+ -> Eff r (Maybe Text)+lookupEnvMaybe envName = do+ value <- E.lookupEnv $ T.unpack envName+ pure $ T.pack <$> value
+ components/core/Effectful/Zoo/Error/Dynamic.hs view
@@ -0,0 +1,212 @@+module Effectful.Zoo.Error.Dynamic+ ( Error,++ throw,++ catch,+ catch_,+ trap,+ trap_,++ catchWithCallStack,+ catchWithCallStack_,+ trapWithCallStack,+ trapWithCallStack_,++ catchIn,+ catchIn_,+ trapIn,+ trapIn_,++ catchWithCallStackIn,+ catchWithCallStackIn_,+ trapWithCallStackIn,+ trapWithCallStackIn_,++ fromEither,+ mapError,+ runError,+ runError_,+ runErrorMap_,+ ) where++import Effectful+import Effectful.Error.Dynamic (Error, runError)+import Effectful.Error.Dynamic qualified as E+import Effectful.Zoo.Core+import HaskellWorks.Prelude++throw :: forall e r a. ()+ => HasCallStack+ => r <: Error e+ => Show e+ => e+ -> Eff r a+throw =+ E.throwError++catchWithCallStack :: forall e r a. ()+ => HasCallStack+ => Eff (Error e : r) a+ -> (CallStack -> e -> Eff r a)+ -> Eff r a+catchWithCallStack action handler =+ E.runError action+ & onLeftM (uncurry handler)++catchWithCallStack_ :: forall e r a. ()+ => HasCallStack+ => Eff (Error e : r) a+ -> (CallStack -> Eff r a)+ -> Eff r a+catchWithCallStack_ f h =+ catchWithCallStack @e f \cs _ -> h cs++trapWithCallStack :: forall e r a. ()+ => HasCallStack+ => (CallStack -> e -> Eff r a)+ -> Eff (Error e : r) a+ -> Eff r a+trapWithCallStack =+ flip catchWithCallStack++trapWithCallStack_ :: forall e r a. ()+ => HasCallStack+ => (CallStack -> Eff r a)+ -> Eff (Error e : r) a+ -> Eff r a+trapWithCallStack_ h =+ trapWithCallStack @e (const . h)++catch :: forall e r a. ()+ => Eff (Error e : r) a+ -> (e -> Eff r a)+ -> Eff r a+catch action handler =+ E.runError action+ & onLeftM (handler . snd)++catch_ :: forall e r a. ()+ => Eff (Error e : r) a+ -> Eff r a+ -> Eff r a+catch_ action handler =+ catch @e action (const handler)++trap :: forall e r a. ()+ => (e -> Eff r a)+ -> Eff (Error e : r) a+ -> Eff r a+trap =+ flip catch++trap_ :: forall e r a. ()+ => Eff r a+ -> Eff (Error e : r) a+ -> Eff r a+trap_ handler =+ trap @e (const handler)++catchWithCallStackIn :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => Eff es a+ -> (CallStack -> e -> Eff es a)+ -> Eff es a+catchWithCallStackIn =+ E.catchError++catchWithCallStackIn_ :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => Eff es a+ -> (CallStack -> Eff es a)+ -> Eff es a+catchWithCallStackIn_ f h =+ catchWithCallStackIn @e f \cs _ -> h cs++trapWithCallStackIn :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => (CallStack -> e -> Eff es a)+ -> Eff es a+ -> Eff es a+trapWithCallStackIn =+ flip catchWithCallStackIn++trapWithCallStackIn_ :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => (CallStack -> Eff es a)+ -> Eff es a+ -> Eff es a+trapWithCallStackIn_ h =+ trapWithCallStackIn @e (const . h)++catchIn :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => Eff es a+ -> (e -> Eff es a)+ -> Eff es a+catchIn action handler =+ catchWithCallStackIn action (const handler)++catchIn_ :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => Eff es a+ -> Eff es a+ -> Eff es a+catchIn_ action handler =+ catchIn @e action (const handler)++trapIn :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => (e -> Eff es a)+ -> Eff es a+ -> Eff es a+trapIn =+ flip catchIn++trapIn_ :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => Eff es a+ -> Eff es a+ -> Eff es a+trapIn_ handler =+ trapIn @e (const handler)++fromEither :: forall e a r. ()+ => Show e+ => r <: Error e+ => Either e a+ -> Eff r a+fromEither =+ either throw pure++mapError :: forall d e a r. ()+ => HasCallStack+ => r <: Error e+ => Show e+ => (d -> e)+ -> Eff (Error d : r) a+ -> Eff r a+mapError f g =+ g & trap (throw . f)++runError_ :: ()+ => Eff (Error e : r) a+ -> Eff r (Either e a)+runError_ =+ fmap (first snd) . runError++-- | Run an 'Error' effect and map the error value to a different type.+runErrorMap_ :: ()+ => (e -> d)+ -> Eff (Error e : r) a+ -> Eff r (Either d a)+runErrorMap_ f =+ fmap (first (f . snd)) . runError
+ components/core/Effectful/Zoo/Error/Static.hs view
@@ -0,0 +1,212 @@+module Effectful.Zoo.Error.Static+ ( Error,++ throw,++ catch,+ catch_,+ trap,+ trap_,++ catchWithCallStack,+ catchWithCallStack_,+ trapWithCallStack,+ trapWithCallStack_,++ catchIn,+ catchIn_,+ trapIn,+ trapIn_,++ catchWithCallStackIn,+ catchWithCallStackIn_,+ trapWithCallStackIn,+ trapWithCallStackIn_,++ fromEither,+ mapError,+ runError,+ runError_,+ runErrorMap_,+ ) where++import Effectful+import Effectful.Error.Static (Error, runError)+import Effectful.Error.Static qualified as E+import Effectful.Zoo.Prim+import HaskellWorks.Prelude++throw :: forall e r a. ()+ => HasCallStack+ => r <: Error e+ => Show e+ => e+ -> Eff r a+throw =+ E.throwError++catchWithCallStack :: forall e r a. ()+ => HasCallStack+ => Eff (Error e : r) a+ -> (CallStack -> e -> Eff r a)+ -> Eff r a+catchWithCallStack action handler =+ E.runError action+ & onLeftM (uncurry handler)++catchWithCallStack_ :: forall e r a. ()+ => HasCallStack+ => Eff (Error e : r) a+ -> (CallStack -> Eff r a)+ -> Eff r a+catchWithCallStack_ f h =+ catchWithCallStack @e f \cs _ -> h cs++trapWithCallStack :: forall e r a. ()+ => HasCallStack+ => (CallStack -> e -> Eff r a)+ -> Eff (Error e : r) a+ -> Eff r a+trapWithCallStack =+ flip catchWithCallStack++trapWithCallStack_ :: forall e r a. ()+ => HasCallStack+ => (CallStack -> Eff r a)+ -> Eff (Error e : r) a+ -> Eff r a+trapWithCallStack_ h =+ trapWithCallStack @e (const . h)++catch :: forall e r a. ()+ => Eff (Error e : r) a+ -> (e -> Eff r a)+ -> Eff r a+catch action handler =+ E.runError action+ & onLeftM (handler . snd)++catch_ :: forall e r a. ()+ => Eff (Error e : r) a+ -> Eff r a+ -> Eff r a+catch_ action handler =+ catch @e action (const handler)++trap :: forall e r a. ()+ => (e -> Eff r a)+ -> Eff (Error e : r) a+ -> Eff r a+trap =+ flip catch++trap_ :: forall e r a. ()+ => Eff r a+ -> Eff (Error e : r) a+ -> Eff r a+trap_ handler =+ trap @e (const handler)++catchWithCallStackIn :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => Eff es a+ -> (CallStack -> e -> Eff es a)+ -> Eff es a+catchWithCallStackIn =+ E.catchError++catchWithCallStackIn_ :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => Eff es a+ -> (CallStack -> Eff es a)+ -> Eff es a+catchWithCallStackIn_ f h =+ catchWithCallStackIn @e f \cs _ -> h cs++trapWithCallStackIn :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => (CallStack -> e -> Eff es a)+ -> Eff es a+ -> Eff es a+trapWithCallStackIn =+ flip catchWithCallStackIn++trapWithCallStackIn_ :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => (CallStack -> Eff es a)+ -> Eff es a+ -> Eff es a+trapWithCallStackIn_ h =+ trapWithCallStackIn @e (const . h)++catchIn :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => Eff es a+ -> (e -> Eff es a)+ -> Eff es a+catchIn action handler =+ catchWithCallStackIn action (const handler)++catchIn_ :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => Eff es a+ -> Eff es a+ -> Eff es a+catchIn_ action handler =+ catchIn @e action (const handler)++trapIn :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => (e -> Eff es a)+ -> Eff es a+ -> Eff es a+trapIn =+ flip catchIn++trapIn_ :: forall e es a. ()+ => HasCallStack+ => es <: Error e+ => Eff es a+ -> Eff es a+ -> Eff es a+trapIn_ handler =+ trapIn @e (const handler)++fromEither :: forall e a r. ()+ => Show e+ => r <: Error e+ => Either e a+ -> Eff r a+fromEither =+ either throw pure++mapError :: forall d e a r. ()+ => HasCallStack+ => r <: Error e+ => Show e+ => (d -> e)+ -> Eff (Error d : r) a+ -> Eff r a+mapError f g =+ g & trap (throw . f)++runError_ :: ()+ => Eff (Error e : r) a+ -> Eff r (Either e a)+runError_ =+ fmap (first snd) . runError++-- | Run an 'Error' effect and map the error value to a different type.+runErrorMap_ :: ()+ => (e -> d)+ -> Eff (Error e : r) a+ -> Eff r (Either d a)+runErrorMap_ f =+ fmap (first (f . snd)) . runError
+ components/core/Effectful/Zoo/Errors/EnvironmentVariableInvalid.hs view
@@ -0,0 +1,24 @@+module Effectful.Zoo.Errors.EnvironmentVariableInvalid+ ( EnvironmentVariableInvalid (..),+ ) where++import Data.Aeson (ToJSON (..), (.=))+import Data.Aeson qualified as J+import GHC.Generics+import HaskellWorks.Prelude++data EnvironmentVariableInvalid = EnvironmentVariableInvalid+ { variable :: Text+ , text :: Text+ , reason :: Maybe Text+ }+ deriving stock (Eq, Show, Generic)++instance ToJSON EnvironmentVariableInvalid where+ toJSON e =+ J.object+ [ "error" .= id @Text "EnvironmentVariableInvalid"+ , "variable" .= e.variable+ , "text" .= e.text+ , "reason" .= e.reason+ ]
+ components/core/Effectful/Zoo/Errors/EnvironmentVariableMissing.hs view
@@ -0,0 +1,20 @@+module Effectful.Zoo.Errors.EnvironmentVariableMissing+ ( EnvironmentVariableMissing (..),+ ) where++import Data.Aeson (ToJSON (..), (.=))+import Data.Aeson qualified as J+import GHC.Generics+import HaskellWorks.Prelude++newtype EnvironmentVariableMissing = EnvironmentVariableMissing+ { variable :: Text+ }+ deriving stock (Eq, Show, Generic)++instance ToJSON EnvironmentVariableMissing where+ toJSON e =+ J.object+ [ "error" .= id @Text "EnvironmentVariableMissing"+ , "variable" .= e.variable+ ]
+ components/core/Effectful/Zoo/Exception.hs view
@@ -0,0 +1,35 @@+module Effectful.Zoo.Exception+ ( Exception,+ catchIO,+ throwIO,+ trapIO,+ ) where++import Effectful+import Effectful.Exception (Exception)+import Effectful.Exception qualified as E+import HaskellWorks.Prelude++catchIO :: ()+ => E.Exception e+ => Eff es a+ -> (e -> Eff es a)+ -> Eff es a+catchIO =+ E.catch++throwIO :: ()+ => HasCallStack+ => E.Exception e+ => e+ -> Eff es a+throwIO =+ E.throwIO++trapIO+ :: E.Exception e+ => (e -> Eff es a)+ -> Eff es a+ -> Eff es a+trapIO =+ flip catchIO
components/core/Effectful/Zoo/FileSystem.hs view
@@ -4,8 +4,20 @@ YamlDecodeError(..), readByteStringFile, readLazyByteStringFile,+ readStringFile, readJsonFile, readYamlFile,+ writeStringFile,+ getCanonicalTemporaryDirectory,+ createTempDirectory,+ removePathForcibly,+ doesFileExist,+ doesDirectoryExist,++ runFileSystem,++ getCurrentDirectory,+ canonicalizePath, ) where import Data.Aeson (FromJSON)@@ -16,17 +28,32 @@ import Data.Yaml qualified as Y import Effectful import Effectful.Dispatch.Static-import Effectful.FileSystem+import Effectful.FileSystem (FileSystem, runFileSystem) import Effectful.Zoo.Core-import Effectful.Zoo.Core.Error.Static-import Effectful.Zoo.Core.Exception+import Effectful.Zoo.Error.Static+import Effectful.Zoo.Exception import Effectful.Zoo.Log.Api.Text import Effectful.Zoo.Log.Dynamic import Effectful.Zoo.Unsafe import HaskellWorks.Error.Types.JsonDecodeError import HaskellWorks.Error.Types.YamlDecodeError import HaskellWorks.Prelude+import System.Directory qualified as D+import System.IO qualified as IO+import System.IO.Temp qualified as IO +readStringFile :: ()+ => HasCallStack+ => r <: Error IOException+ => r <: FileSystem+ => r <: Log Text+ => FilePath+ -> Eff r String+readStringFile filePath = withFrozenCallStack do+ info $ "Reading bytestring from file: " <> T.pack filePath+ unsafeFileSystemEff_ (IO.readFile filePath)+ & trapIO @IOException throw+ readLazyByteStringFile :: () => HasCallStack => r <: Error IOException@@ -34,7 +61,7 @@ => r <: Log Text => FilePath -> Eff r LBS.ByteString-readLazyByteStringFile filePath = withFrozenCallStack $ do+readLazyByteStringFile filePath = withFrozenCallStack do info $ "Reading bytestring from file: " <> T.pack filePath unsafeFileSystemEff_ (LBS.readFile filePath) & trapIO @IOException throw@@ -46,7 +73,7 @@ => r <: Log Text => FilePath -> Eff r ByteString-readByteStringFile filePath = withFrozenCallStack $ do+readByteStringFile filePath = withFrozenCallStack do info $ "Reading bytestring from file: " <> T.pack filePath unsafeFileSystemEff_ (BS.readFile filePath) & trapIO @IOException throw@@ -61,7 +88,7 @@ => r <: Log Text => FilePath -> Eff r a-readJsonFile filePath = withFrozenCallStack $ do+readJsonFile filePath = withFrozenCallStack do info $ "Reading JSON file: " <> T.pack filePath contents <- readLazyByteStringFile filePath J.eitherDecode contents@@ -77,8 +104,103 @@ => r <: Log Text => FilePath -> Eff r a-readYamlFile filePath = withFrozenCallStack $ do+readYamlFile filePath = withFrozenCallStack do info $ "Reading YAML file: " <> T.pack filePath contents <- LBS.toStrict <$> readLazyByteStringFile filePath Y.decodeEither' contents & onLeft (throw . YamlDecodeError . T.pack . Y.prettyPrintParseException)++writeStringFile :: ()+ => HasCallStack+ => r <: Error IOException+ => r <: FileSystem+ => r <: Log Text+ => FilePath+ -> String+ -> Eff r ()+writeStringFile filePath contents = withFrozenCallStack do+ info $ "Writing string to file: " <> T.pack filePath+ unsafeFileSystemEff_ (IO.writeFile filePath contents)+ & trapIO @IOException throw++getCanonicalTemporaryDirectory :: ()+ => HasCallStack+ => r <: Error IOException+ => r <: FileSystem+ => Eff r FilePath+getCanonicalTemporaryDirectory = withFrozenCallStack do+ unsafeFileSystemEff_ IO.getCanonicalTemporaryDirectory+ & trapIO @IOException throw+{-# INLINE getCanonicalTemporaryDirectory #-}++createTempDirectory :: ()+ => HasCallStack+ => r <: Error IOException+ => r <: FileSystem+ => FilePath+ -> String+ -> Eff r FilePath+createTempDirectory fp template = withFrozenCallStack do+ unsafeFileSystemEff_ (IO.createTempDirectory fp template)+ & trapIO @IOException throw+{-# INLINE createTempDirectory #-}++removePathForcibly :: ()+ => HasCallStack+ => r <: Error IOException+ => r <: FileSystem+ => r <: Log Text+ => FilePath+ -> Eff r ()+removePathForcibly fp = withFrozenCallStack $ do+ info $ "Calling: removePathForcibly " <> tshow fp++ unsafeFileSystemEff_ (D.removePathForcibly fp)+ & trapIO @IOException throw++doesFileExist :: ()+ => HasCallStack+ => r <: Error IOException+ => r <: FileSystem+ => r <: Log Text+ => FilePath+ -> Eff r Bool+doesFileExist fp = withFrozenCallStack $ do+ info "Calling: doesFileExist"++ unsafeFileSystemEff_ (D.doesFileExist fp)+ & trapIO @IOException throw++doesDirectoryExist :: ()+ => HasCallStack+ => r <: Error IOException+ => r <: FileSystem+ => r <: Log Text+ => FilePath+ -> Eff r Bool+doesDirectoryExist fp = withFrozenCallStack $ do+ info "Calling: doesDirectoryExist"++ unsafeFileSystemEff_ (D.doesDirectoryExist fp)+ & trapIO @IOException throw++getCurrentDirectory :: ()+ => HasCallStack+ => r <: Error IOException+ => r <: FileSystem+ => Eff r FilePath+getCurrentDirectory = withFrozenCallStack do+ unsafeFileSystemEff_ D.getCurrentDirectory+ & trapIO @IOException throw+{-# INLINE getCurrentDirectory #-}++canonicalizePath :: ()+ => HasCallStack+ => r <: Error IOException+ => r <: FileSystem+ => FilePath+ -> Eff r FilePath+canonicalizePath fp = withFrozenCallStack do+ unsafeFileSystemEff_ (D.canonicalizePath fp)+ & trapIO @IOException throw+{-# INLINE canonicalizePath #-}
+ components/core/Effectful/Zoo/Function.hs view
@@ -0,0 +1,26 @@+module Effectful.Zoo.Function+ ( once,+ ) where++import Effectful+import Effectful.Concurrent+import Effectful.Concurrent.MVar+import Effectful.Zoo.Prim+import HaskellWorks.Prelude++once :: ()+ => r <: Concurrent+ => Eff r a+ -> Eff r (Eff r a)+once f = do+ cache <- newMVar Nothing+ return $ do+ resultOrRunning <- takeMVar cache+ case resultOrRunning of+ Just result -> do+ putMVar cache (Just result)+ return result+ Nothing -> do+ result <- f+ putMVar cache (Just result)+ return result
components/core/Effectful/Zoo/Log/Api/Generic.hs view
@@ -11,6 +11,7 @@ import Effectful import Effectful.Dispatch.Dynamic import Effectful.Zoo.Core+import Effectful.Zoo.Log.Data.LogMessage import Effectful.Zoo.Log.Data.Severity import Effectful.Zoo.Log.Dynamic import HaskellWorks.Prelude@@ -23,7 +24,7 @@ -> Eff r () log severity message = withFrozenCallStack $- send (Log severity message)+ send (Log (LogMessage severity message)) trace :: () => HasCallStack
components/core/Effectful/Zoo/Log/Api/LazyText.hs view
@@ -11,6 +11,7 @@ import Effectful import Effectful.Dispatch.Dynamic import Effectful.Zoo.Core+import Effectful.Zoo.Log.Data.LogMessage import Effectful.Zoo.Log.Data.Severity import Effectful.Zoo.Log.Dynamic import HaskellWorks.Prelude@@ -23,7 +24,7 @@ -> Eff r () log severity message = withFrozenCallStack $- send (Log severity message)+ send (Log (LogMessage severity message)) trace :: () => HasCallStack
components/core/Effectful/Zoo/Log/Api/String.hs view
@@ -11,6 +11,7 @@ import Effectful import Effectful.Dispatch.Dynamic import Effectful.Zoo.Core+import Effectful.Zoo.Log.Data.LogMessage import Effectful.Zoo.Log.Data.Severity import Effectful.Zoo.Log.Dynamic import HaskellWorks.Prelude@@ -23,7 +24,7 @@ -> Eff r () log severity message = withFrozenCallStack $- send (Log severity message)+ send (Log (LogMessage severity message)) trace :: () => HasCallStack
components/core/Effectful/Zoo/Log/Api/Text.hs view
@@ -12,6 +12,7 @@ import Effectful.Dispatch.Dynamic import Effectful.Zoo.Core import Effectful.Zoo.Log.Data.Severity+import Effectful.Zoo.Log.Data.LogMessage import Effectful.Zoo.Log.Dynamic import HaskellWorks.Prelude @@ -23,7 +24,7 @@ -> Eff r () log severity message = withFrozenCallStack $- send (Log severity message)+ send (Log (LogMessage severity message)) trace :: () => HasCallStack
components/core/Effectful/Zoo/Log/Data/LogMessage.hs view
@@ -8,4 +8,4 @@ { severity :: !Severity , message :: i }- deriving stock (Eq, Generic, Show)+ deriving stock (Eq, Functor, Generic, Show)
components/core/Effectful/Zoo/Log/Data/Logger.hs view
@@ -5,21 +5,22 @@ import Effectful import Effectful.Zoo.Core-import Effectful.Zoo.Log.Data.Severity+import Effectful.Zoo.Log.Data.LogMessage import HaskellWorks.Prelude newtype Logger i = Logger- { run :: CallStack -> Severity -> i -> IO ()+ { run :: CallStack -> LogMessage i -> IO () } deriving stock Generic instance Contravariant Logger where- contramap f (Logger g) = Logger \cs severity -> g cs severity . f+ contramap f (Logger g) =+ Logger \cs m -> g cs (fmap f m) mkLogger :: () => r <: IOE => UnliftStrategy- -> (CallStack -> Severity -> i -> Eff r ())+ -> (CallStack -> LogMessage i -> Eff r ()) -> Eff r (Logger i) mkLogger strategy run = withEffToIO strategy $ \effToIO ->- pure $ Logger $ \cs severity i -> effToIO $ run cs severity i+ pure $ Logger $ \cs m -> effToIO $ run cs m
components/core/Effectful/Zoo/Log/Dynamic.hs view
@@ -6,15 +6,14 @@ import Effectful import Effectful.Dispatch.Dynamic import Effectful.Zoo.Core-import Effectful.Zoo.Log.Data.Severity+import Effectful.Zoo.Log.Data.LogMessage import Effectful.Zoo.Log.Static qualified as S import HaskellWorks.Prelude data Log i :: Effect where Log :: HasCallStack- => Severity- -> i+ => LogMessage i -> Log i m () Local@@ -24,13 +23,13 @@ type instance DispatchOf (Log a) = Dynamic -runLog :: ()+runLog :: forall i a r. () => r <: IOE => UnliftStrategy- -> (CallStack -> Severity -> i -> Eff r ())+ -> (CallStack -> LogMessage i -> Eff r ()) -> Eff (Log i : r) a -> Eff r a runLog s run = reinterpret (S.runLog s run) $ \env -> \case- Log severity i -> S.log severity i+ Log m -> S.log m Local f m -> localSeqUnlift env $ \unlift -> S.local f (unlift m)
components/core/Effectful/Zoo/Log/Static.hs view
@@ -15,7 +15,7 @@ import Effectful.Dispatch.Static import Effectful.Zoo.Core import Effectful.Zoo.Log.Data.Logger-import Effectful.Zoo.Log.Data.Severity+import Effectful.Zoo.Log.Data.LogMessage import GHC.Stack qualified as GHC import HaskellWorks.Prelude import System.IO qualified as IO@@ -30,7 +30,7 @@ => r <: IOE => HasCallStack => UnliftStrategy- -> (CallStack -> Severity -> i -> Eff r ())+ -> (CallStack -> LogMessage i -> Eff r ()) -> Eff (Log i : r) a -> Eff r a runLog strategy run f = do@@ -40,16 +40,16 @@ runLogToHandle :: () => HasCallStack => Handle- -> (Severity -> a -> Text)+ -> (LogMessage a -> Text) -> Eff (Log a : r) a -> Eff r a runLogToHandle h f =- evalStaticRep $ Log $ Logger $ \_ severity i ->- T.hPutStrLn h $ f severity i+ evalStaticRep $ Log $ Logger $ \_ m ->+ T.hPutStrLn h $ f m runLogToStdout :: () => HasCallStack- => (Severity -> a -> Text)+ => (LogMessage a -> Text) -> Eff (Log a : r) a -> Eff r a runLogToStdout =@@ -57,7 +57,7 @@ runLogToStderr :: () => HasCallStack- => (Severity -> a -> Text)+ => (LogMessage a -> Text) -> Eff (Log a : r) a -> Eff r a runLogToStderr =@@ -93,13 +93,12 @@ => HasCallStack => r <: Log i => r <: IOE- => Severity- -> i+ => LogMessage i -> Eff r ()-log severity i =+log m = withFrozenCallStack do dataLogger <- getDataLogger- liftIO $ dataLogger.run GHC.callStack severity i+ liftIO $ dataLogger.run GHC.callStack m local :: () => HasCallStack
+ components/core/Effectful/Zoo/Prim.hs view
@@ -0,0 +1,12 @@+{-# OPTIONS_GHC -fno-warn-deprecations #-}++module Effectful.Zoo.Prim+ ( type (<:),+ type (<<:)+ ) where++import Effectful ((:>), (:>>))++type (<:) r e = (:>) e r++type (<<:) r e = (:>>) e r
+ components/core/Effectful/Zoo/Process.hs view
@@ -0,0 +1,273 @@+module Effectful.Zoo.Process+ ( IO.CreateProcess(..),+ IO.CmdSpec(..),+ IO.StdStream(..),+ Handle,+ ProcessHandle,+ ExitCode(..),+ FD,+ Pid,+ createProcess,+ createProcess_,+ IO.shell,+ IO.proc,+ callProcess,+ callCommand,+ spawnProcess,+ spawnCommand,+ readCreateProcess,+ readProcess,+ readCreateProcessWithExitCode,+ readProcessWithExitCode,+ cleanupProcess,+ getPid,+ getCurrentPid,+ interruptProcessGroupOf,+ createPipe,+ createPipeFd,+ runProcess,+ runCommand,+ runInteractiveProcess,+ runInteractiveCommand,+ system,+ rawSystem,++ waitSecondsForProcess,+ ) where++import Control.Exception qualified as CE+import HaskellWorks.Error+import HaskellWorks.Error.Types+import HaskellWorks.IO.Process qualified as IO+import HaskellWorks.Prelude+import Effectful+import Effectful.Zoo.Core+import Effectful.Zoo.Error.Static+import System.Exit (ExitCode (..))+import System.Posix.Internals (FD)+import System.Process (Pid, ProcessHandle)+import System.Process qualified as IO++createProcess :: ()+ => r <: Error IOException+ => r <: IOE+ => IO.CreateProcess+ -> Eff r (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)+createProcess cp = do+ r <- liftIO $ CE.try @IOException $ IO.createProcess cp+ fromEither r++createProcess_ :: ()+ => r <: Error IOException+ => r <: IOE+ => String+ -> IO.CreateProcess+ -> Eff r (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)+createProcess_ cmd cp = do+ r <- liftIO $ CE.try @IOException $ IO.createProcess_ cmd cp+ fromEither r++callProcess :: ()+ => r <: Error IOException+ => r <: IOE+ => String+ -> [String]+ -> Eff r ()+callProcess cmd args = do+ r <- liftIO $ CE.try @IOException $ IO.callProcess cmd args+ fromEither r++callCommand :: ()+ => r <: Error IOException+ => r <: IOE+ => String+ -> Eff r ()+callCommand cmd = do+ r <- liftIO $ CE.try @IOException $ IO.callCommand cmd+ fromEither r++spawnProcess :: ()+ => r <: Error IOException+ => r <: IOE+ => String+ -> [String]+ -> Eff r ProcessHandle+spawnProcess cmd args = do+ r <- liftIO $ CE.try @IOException $ IO.spawnProcess cmd args+ fromEither r++spawnCommand :: ()+ => r <: Error IOException+ => r <: IOE+ => String+ -> Eff r ProcessHandle+spawnCommand cmd = do+ r <- liftIO $ CE.try @IOException $ IO.spawnCommand cmd+ fromEither r++readCreateProcess :: ()+ => r <: Error IOException+ => r <: IOE+ => IO.CreateProcess+ -> String+ -> Eff r String+readCreateProcess cp input = do+ r <- liftIO $ CE.try @IOException $ IO.readCreateProcess cp input+ fromEither r++readProcess :: ()+ => r <: Error IOException+ => r <: IOE+ => String+ -> [String]+ -> String+ -> Eff r String+readProcess cmd args input = do+ r <- liftIO $ CE.try @IOException $ IO.readProcess cmd args input+ fromEither r++readCreateProcessWithExitCode :: ()+ => r <: Error IOException+ => r <: IOE+ => IO.CreateProcess+ -> String+ -> Eff r (ExitCode, String, String)+readCreateProcessWithExitCode cp input = do+ r <- liftIO $ CE.try @IOException $ IO.readCreateProcessWithExitCode cp input+ fromEither r++readProcessWithExitCode :: ()+ => r <: Error IOException+ => r <: IOE+ => String+ -> [String]+ -> String+ -> Eff r (ExitCode, String, String)+readProcessWithExitCode cmd args input = do+ r <- liftIO $ CE.try @IOException $ IO.readProcessWithExitCode cmd args input+ fromEither r++cleanupProcess :: ()+ => r <: Error IOException+ => r <: IOE+ => (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)+ -> Eff r ()+cleanupProcess (mIn, mOut, mErr, ph) = do+ r <- liftIO $ CE.try @IOException $ IO.cleanupProcess (mIn, mOut, mErr, ph)+ fromEither r++getPid :: ()+ => r <: Error IOException+ => r <: IOE+ => ProcessHandle+ -> Eff r (Maybe Pid)+getPid ph = do+ r <- liftIO $ CE.try @IOException $ IO.getPid ph+ fromEither r++getCurrentPid :: ()+ => r <: Error IOException+ => r <: IOE+ => Eff r Pid+getCurrentPid = do+ r <- liftIO $ CE.try @IOException $ IO.getCurrentPid+ fromEither r++interruptProcessGroupOf :: ()+ => r <: Error IOException+ => r <: IOE+ => ProcessHandle+ -> Eff r ()+interruptProcessGroupOf ph = do+ r <- liftIO $ CE.try @IOException $ IO.interruptProcessGroupOf ph+ fromEither r++createPipe :: ()+ => r <: Error IOException+ => r <: IOE+ => Eff r (Handle, Handle)+createPipe = do+ r <- liftIO $ CE.try @IOException $ IO.createPipe+ fromEither r++createPipeFd :: ()+ => r <: Error IOException+ => r <: IOE+ => Eff r (FD, FD)+createPipeFd = do+ r <- liftIO $ CE.try @IOException $ IO.createPipeFd+ fromEither r++runProcess :: ()+ => r <: Error IOException+ => r <: IOE+ => FilePath+ -> [String]+ -> Maybe FilePath+ -> Maybe [(String, String)]+ -> Maybe Handle+ -> Maybe Handle+ -> Maybe Handle+ -> Eff r ProcessHandle+runProcess cmd args mbStdIn mbEnv mbCwd mbStdOut mbStdErr = do+ r <- liftIO $ CE.try @IOException $ IO.runProcess cmd args mbStdIn mbEnv mbCwd mbStdOut mbStdErr+ fromEither r++runCommand :: ()+ => r <: Error IOException+ => r <: IOE+ => String+ -> Eff r ProcessHandle+runCommand cmd = do+ r <- liftIO $ CE.try @IOException $ IO.runCommand cmd+ fromEither r++runInteractiveProcess :: ()+ => r <: Error IOException+ => r <: IOE+ => FilePath+ -> [String]+ -> Maybe FilePath+ -> Maybe [(String, String)]+ -> Eff r (Handle, Handle, Handle, ProcessHandle)+runInteractiveProcess cmd args mbCwd mbEnv = do+ r <- liftIO $ CE.try @IOException $ IO.runInteractiveProcess cmd args mbCwd mbEnv+ fromEither r++runInteractiveCommand :: ()+ => r <: Error IOException+ => r <: IOE+ => String+ -> Eff r (Handle, Handle, Handle, ProcessHandle)+runInteractiveCommand cmd = do+ r <- liftIO $ CE.try @IOException $ IO.runInteractiveCommand cmd+ fromEither r++system :: ()+ => r <: Error IOException+ => r <: IOE+ => String+ -> Eff r ExitCode+system cmd = do+ r <- liftIO $ CE.try @IOException $ IO.system cmd+ fromEither r++rawSystem :: ()+ => r <: Error IOException+ => r <: IOE+ => String+ -> [String]+ -> Eff r ExitCode+rawSystem cmd args = do+ r <- liftIO $ CE.try @IOException $ IO.rawSystem cmd args+ fromEither r++waitSecondsForProcess :: ()+ => r <: Error TimedOut+ => r <: IOE+ => Int+ -> ProcessHandle+ -> Eff r (Maybe ExitCode)+waitSecondsForProcess seconds hProcess =+ liftIO (IO.waitSecondsForProcess seconds hProcess)+ & onLeftM @TimedOut throw
+ components/core/Effectful/Zoo/Reader/Static.hs view
@@ -0,0 +1,21 @@+module Effectful.Zoo.Reader.Static+ ( Reader,+ ask,+ asks,+ local,+ runReader,++ runReaderM,+ ) where++import Effectful+import Effectful.Reader.Static+import HaskellWorks.Prelude++runReaderM ::+ forall i r a.+ Eff r i ->+ Eff (Reader i ': r) a ->+ Eff r a+runReaderM f action = do+ f >>= flip runReader action
+ components/core/Effectful/Zoo/Resource.hs view
@@ -0,0 +1,6 @@+module Effectful.Zoo.Resource+ ( Resource,+ runResource,+ ) where++import Effectful.Resource
− components/hedgehog-test/Effectful/Zoo/Hedgehog/Test/HedgehogTest.hs
@@ -1,22 +0,0 @@-module Effectful.Zoo.Hedgehog.Test.HedgehogTest where--import Effectful-import Effectful.Zoo.Core-import Effectful.Zoo.Log.Dynamic-import Effectful.Zoo.Log.Api.Text-import Effectful.Zoo.Hedgehog-import HaskellWorks.Prelude--foo :: ()- => HasCallStack- => r <: Log Text- => Eff r ()-foo =- info "This is a log"--test_simple :: UnitTest-test_simple =- hedgehog do- jot_ "This is a jot"-- foo
+ components/hedgehog-test/Effectful/Zoo/Hedgehog/Test/PropertySpec.hs view
@@ -0,0 +1,23 @@+{- HLINT ignore "Use camelCase" -}++module Effectful.Zoo.Hedgehog.Test.PropertySpec where++import Effectful.Zoo.Hedgehog+import Effectful.Zoo.Hedgehog.Effect.Run+import HaskellWorks.Prelude+import Hedgehog qualified as H+import Hedgehog.Gen qualified as G+import Hedgehog.Range qualified as R++tasty_property_spec :: PropertyT IO ()+tasty_property_spec = property do+ a <- forAll $ G.int (R.linear 0 100)+ True === True+ a === a+ H.success++tasty_unit_spec :: TestT IO ()+tasty_unit_spec = unit do+ True === True+ True === True+ H.success
components/hedgehog-test/Main.hs view
@@ -1,16 +1,1 @@-module Main where--import Effectful.Zoo.Hedgehog (unitTest)-import Effectful.Zoo.Hedgehog.Test.HedgehogTest (test_simple)-import Test.Tasty (TestTree, defaultMain, testGroup)-import HaskellWorks.Prelude--tests :: TestTree-tests =- testGroup "all" [- unitTest "Simple test" test_simple- ]--main :: IO ()-main =- defaultMain tests+{-# OPTIONS_GHC -F -pgmF tasty-discover #-}
components/hedgehog/Effectful/Zoo/Hedgehog/Api.hs view
@@ -1,15 +1,17 @@ module Effectful.Zoo.Hedgehog.Api- ( module Effectful.Zoo.Hedgehog.Api.Assert,- module Effectful.Zoo.Hedgehog.Api.Classify,- module Effectful.Zoo.Hedgehog.Api.Eval,+ ( Property,+ PropertyT,+ Test,+ TestT,+ module Effectful.Zoo.Hedgehog.Api.Assert,+ module Effectful.Zoo.Hedgehog.Api.Hedgehog, module Effectful.Zoo.Hedgehog.Api.Failure, module Effectful.Zoo.Hedgehog.Api.Journal,- module Effectful.Zoo.Hedgehog.Api.Run, ) where import Effectful.Zoo.Hedgehog.Api.Assert-import Effectful.Zoo.Hedgehog.Api.Classify-import Effectful.Zoo.Hedgehog.Api.Eval import Effectful.Zoo.Hedgehog.Api.Failure+import Effectful.Zoo.Hedgehog.Api.Hedgehog import Effectful.Zoo.Hedgehog.Api.Journal-import Effectful.Zoo.Hedgehog.Api.Run+import Effectful.Zoo.Hedgehog.Api.Tasty.Orphans ()+import Hedgehog
components/hedgehog/Effectful/Zoo/Hedgehog/Api/Assert.hs view
@@ -1,94 +1,218 @@ module Effectful.Zoo.Hedgehog.Api.Assert- ( assert,- (===),- (/==),- onNothingFail,+ ( onNothingFail, onNothingFailM, onLeftFail, onLeftFailM,++ trapFail,+ trapFailJson,+ trapFailJsonPretty,+ trapFailYaml,++ failMessage,+ failWithCustom,++ byDurationM,+ byDeadlineM, ) where +import Data.Aeson+import Data.Aeson qualified as J+import Data.Aeson.Encode.Pretty qualified as J+import Data.Text qualified as T+import Data.Text.Encoding qualified as T+import Data.Text.Lazy qualified as LT+import Data.Text.Lazy.Encoding qualified as LT+import Data.Time.Clock (NominalDiffTime, UTCTime)+import Data.Time.Clock qualified as DTC+import Data.Yaml qualified as Y import Effectful+import Effectful.Concurrent import Effectful.Dispatch.Dynamic import Effectful.Zoo.Core-import Effectful.Zoo.Hedgehog.Api.Failure-import Effectful.Zoo.Hedgehog.Dynamic+import Effectful.Zoo.Error.Static+import Effectful.Zoo.Hedgehog.Api.Hedgehog+import Effectful.Zoo.Hedgehog.Api.Journal+import Effectful.Zoo.Hedgehog.Api.MonadAssertion+import Effectful.Zoo.Hedgehog.Effect.Hedgehog+import GHC.Stack qualified as GHC import HaskellWorks.Prelude+import Hedgehog (MonadTest(..)) import Hedgehog qualified as H--infix 4 ===, /==--assert :: forall r. ()- => HasCallStack- => r <: Hedgehog- => Bool- -> Eff r ()-assert condition =- withFrozenCallStack $- H.assert condition--(===) :: forall a r. ()- => HasCallStack- => Eq a- => Show a- => r <: Hedgehog- => a- -> a- -> Eff r ()-(===) a b =- withFrozenCallStack $- a H.=== b--(/==) :: forall a r. ()- => HasCallStack- => Eq a- => Show a- => r <: Hedgehog- => a- -> a- -> Eff r ()-(/==) a b =- withFrozenCallStack $- a H./== b+import Hedgehog.Internal.Property qualified as H+import Hedgehog.Internal.Source qualified as H -onNothingFail :: forall a r. ()+onNothingFail :: forall a m. () => HasCallStack- => r <: Hedgehog+ => MonadTest m => Maybe a- -> Eff r a+ -> m a onNothingFail mv = withFrozenCallStack $ case mv of Just a -> pure a Nothing -> failWith Nothing "Expected Just, but got Nothing" -onNothingFailM :: forall a r. ()+onNothingFailM :: forall a m. () => HasCallStack- => r <: Hedgehog- => Eff r (Maybe a)- -> Eff r a+ => MonadTest m+ => m (Maybe a)+ -> m a onNothingFailM f = withFrozenCallStack $ f >>= onNothingFail -onLeftFail :: forall e a r. ()+onLeftFail :: forall e a m. () => HasCallStack+ => MonadTest m => Show e- => r <: Hedgehog => Either e a- -> Eff r a+ -> m a onLeftFail ea = withFrozenCallStack $ case ea of Right a -> pure a Left e -> failWith Nothing $ "Expected Just, but got Left: " <> show e -onLeftFailM :: forall e a r. ()+onLeftFailM :: forall e a m. () => HasCallStack+ => MonadTest m => Show e- => r <: Hedgehog- => Eff r (Either e a)- -> Eff r a+ => m (Either e a)+ -> m a onLeftFailM f = withFrozenCallStack $ f >>= onLeftFail++failMessage :: ()+ => H.MonadTest m+ => CallStack+ -> String+ -> m a+failMessage cs =+ withFrozenCallStack $+ failWithCustom cs Nothing++failWithCustom :: ()+ => H.MonadTest m+ => CallStack+ -> Maybe H.Diff+ -> String+ -> m a+failWithCustom cs mdiff msg =+ H.liftTest $ H.mkTest (Left $ H.Failure (H.getCaller cs) msg mdiff, mempty)++trapFail :: forall e a r. ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error H.Failure+ => r <: Hedgehog+ => Show e+ => Eff (Error e ': r) a+ -> Eff r a+trapFail f =+ withFrozenCallStack do+ r <- f & runError_+ case r of+ Right a -> pure a+ Left e -> failWith Nothing $ show e++trapFailJson :: forall e a r. ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error H.Failure+ => r <: Hedgehog+ => ToJSON e+ => Eff (Error e ': r) a+ -> Eff r a+trapFailJson f =+ withFrozenCallStack do+ r <- withFrozenCallStack $ f & runError_+ case r of+ Right a -> pure a+ Left e -> do+ let msg = LT.unpack $ LT.decodeUtf8 $ J.encode e+ failWith Nothing msg++trapFailJsonPretty :: forall e a r. ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error H.Failure+ => r <: Hedgehog+ => ToJSON e+ => Eff (Error e ': r) a+ -> Eff r a+trapFailJsonPretty f =+ withFrozenCallStack do+ r <- withFrozenCallStack $ f & runError_+ case r of+ Right a -> pure a+ Left e -> do+ let msg = LT.unpack $ LT.decodeUtf8 $ J.encodePretty e+ failWith Nothing msg++trapFailYaml :: forall e a r. ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error H.Failure+ => r <: Hedgehog+ => ToJSON e+ => Eff (Error e ': r) a+ -> Eff r a+trapFailYaml f =+ withFrozenCallStack do+ r <- withFrozenCallStack $ f & runError_+ case r of+ Right a -> pure a+ Left e -> do+ let msg = T.unpack $ T.decodeUtf8 $ Y.encode e+ failWith Nothing msg++-- | Run the operation 'f' once a second until it returns 'True' or the deadline expires.+--+-- Expiration of the deadline results in an assertion failure+byDeadlineM :: forall a r. ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error H.Failure+ => r <: Hedgehog+ => r <: IOE+ => NominalDiffTime+ -> UTCTime+ -> String+ -> Eff r a+ -> Eff r a+byDeadlineM period deadline errorMessage f = GHC.withFrozenCallStack $ do+ start <- liftIO DTC.getCurrentTime+ a <- goM+ end <- liftIO DTC.getCurrentTime+ jot_ $ "Operation completed in " <> tshow (DTC.diffUTCTime end start)+ return a+ where goM = catchAssertion f $ \e -> do+ currentTime <- liftIO DTC.getCurrentTime+ if currentTime < deadline+ then do+ threadDelay (floor (DTC.nominalDiffTimeToSeconds period * 1000000))+ goM+ else do+ jotShow_ currentTime+ void $ failMessage GHC.callStack $ "Condition not met by deadline: " <> errorMessage+ throwAssertion e++-- | Run the operation 'f' once a second until it returns 'True' or the duration expires.+--+-- Expiration of the duration results in an assertion failure+byDurationM :: forall b r. ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error H.Failure+ => r <: IOE+ => r <: Hedgehog+ => NominalDiffTime+ -> NominalDiffTime+ -> String+ -> Eff r b+ -> Eff r b+byDurationM period duration errorMessage f = GHC.withFrozenCallStack $ do+ deadline <- DTC.addUTCTime duration <$> liftIO DTC.getCurrentTime+ byDeadlineM period deadline errorMessage f
− components/hedgehog/Effectful/Zoo/Hedgehog/Api/Classify.hs
@@ -1,20 +0,0 @@-module Effectful.Zoo.Hedgehog.Api.Classify- ( classify,- ) where--import Effectful-import Effectful.Dispatch.Dynamic-import Effectful.Zoo.Core-import Effectful.Zoo.Hedgehog.Dynamic-import HaskellWorks.Prelude-import Hedgehog qualified as H--classify :: forall r. ()- => HasCallStack- => r <: Hedgehog- => H.LabelName- -> Bool- -> Eff r ()-classify name b =- withFrozenCallStack $- H.classify name b
− components/hedgehog/Effectful/Zoo/Hedgehog/Api/Eval.hs
@@ -1,64 +0,0 @@-module Effectful.Zoo.Hedgehog.Api.Eval- ( eval,- evalIO,- evalM,-- evalEither,- evalMaybe,- ) where--import Effectful-import Effectful.Dispatch.Dynamic-import Effectful.Zoo.Core-import Effectful.Zoo.Hedgehog.Dynamic-import HaskellWorks.Prelude-import Hedgehog qualified as H--eval :: forall a r. ()- => HasCallStack- => r <: Hedgehog- => a- -> Eff r a-eval a =- withFrozenCallStack do- H.eval a---- |Embeds 'Hedgehog.evalEither'.-evalEither :: forall a e r. ()- => HasCallStack- => r <: Hedgehog- => Show e- => Either e a- -> Eff r a-evalEither e =- withFrozenCallStack do- H.evalEither e---- |Embeds 'Hedgehog.evalMaybe'.-evalMaybe :: forall a r. ()- => HasCallStack- => r <: Hedgehog- => Maybe a- -> Eff r a-evalMaybe e =- withFrozenCallStack do- H.evalMaybe e--evalIO :: forall a r. ()- => HasCallStack- => r <: IOE- => r <: Hedgehog- => IO a- -> Eff r a-evalIO f =- withFrozenCallStack do- H.evalIO f--evalM :: forall a r. ()- => HasCallStack- => r <: Hedgehog- => Eff r a- -> Eff r a-evalM f =- withFrozenCallStack do- H.evalM f
components/hedgehog/Effectful/Zoo/Hedgehog/Api/Failure.hs view
@@ -1,74 +1,21 @@ module Effectful.Zoo.Hedgehog.Api.Failure- ( failure,- failException,- failWith,+ ( H.Failure, failWithCallStack,-- catchAssertion,- throwAssertion, ) where import Effectful-import Effectful.Dispatch.Dynamic-import Effectful.Error.Static import Effectful.Zoo.Core-import Effectful.Zoo.Hedgehog.Dynamic+import Effectful.Zoo.Error.Static import HaskellWorks.Prelude import Hedgehog.Internal.Property qualified as H import Hedgehog.Internal.Source qualified as H -failure :: forall a r. ()- => HasCallStack- => r <: Hedgehog- => Eff r a-failure =- withFrozenCallStack- H.failure--failException :: forall a r. ()- => HasCallStack- => r <: Hedgehog- => SomeException- -> Eff r a-failException e =- withFrozenCallStack $- H.failException e--failWith :: forall a r. ()- => HasCallStack- => r <: Hedgehog- => Maybe H.Diff- -> String- -> Eff r a-failWith diff msg =- withFrozenCallStack $- H.failWith diff msg- failWithCallStack :: forall a r. ()- => r <: Hedgehog+ => r <: Error H.Failure => CallStack -> Maybe H.Diff -> String -> Eff r a failWithCallStack cs diff msg = withFrozenCallStack $- throwAssertion (H.Failure (H.getCaller cs) msg diff)--catchAssertion :: forall a r. ()- => HasCallStack- => r <: Hedgehog- => Eff r a - -> (H.Failure -> Eff r a) - -> Eff r a-catchAssertion m h =- withFrozenCallStack $- send $ CatchAssertion m h--throwAssertion :: forall a r. ()- => HasCallStack- => r <: Hedgehog- => H.Failure- -> Eff r a-throwAssertion e =- withFrozenCallStack $- send $ ThrowAssertion e+ throw (H.Failure (H.getCaller cs) msg diff)
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen.hs view
@@ -0,0 +1,7 @@+module Effectful.Zoo.Hedgehog.Api.Gen+ ( module Hedgehog.Gen,+ module Effectful.Zoo.Hedgehog.Api.Gen.Ulid,+ ) where++import Effectful.Zoo.Hedgehog.Api.Gen.Ulid+import Hedgehog.Gen
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen/Time.hs view
@@ -0,0 +1,18 @@+module Effectful.Zoo.Hedgehog.Api.Gen.Time+ ( genPosixTime,+ ) where++import Data.Time.Clock.POSIX (POSIXTime)+import HaskellWorks.Prelude+import Hedgehog+import Hedgehog.Gen qualified as Gen+import Hedgehog.Range qualified as Range++genPosixTime :: Gen POSIXTime+genPosixTime = do+ -- Generate a random integer within a reasonable range for POSIX time+ -- POSIXTime is a type synonym for NominalDiffTime, which is in seconds+ -- We'll use a range from 0 to a large number of seconds to cover a wide time span+ seconds <- Gen.integral (Range.linear 0 4_102_444_800) -- Up to year 2100++ pure $ fromIntegral @Word64 seconds
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Gen/Ulid.hs view
@@ -0,0 +1,32 @@+module Effectful.Zoo.Hedgehog.Api.Gen.Ulid (+ genUlid,+ genUlidRandom,+ genUlidTimeStamp,+) where++import Data.Binary (decodeOrFail)+import Data.ByteString.Lazy qualified as LBS+import Data.ULID (ULID (..))+import Data.ULID.Random (ULIDRandom)+import Data.ULID.TimeStamp (ULIDTimeStamp, mkULIDTimeStamp)+import Effectful.Zoo.Hedgehog.Api.Gen.Time+import HaskellWorks.Prelude+import Hedgehog+import Hedgehog.Gen qualified as Gen+import Hedgehog.Range qualified as Range++genUlidRandom :: Gen ULIDRandom+genUlidRandom = do+ bytes <- Gen.bytes (Range.singleton 10) -- 80 bits+ let lazyBytes = LBS.fromStrict bytes+ case decodeOrFail lazyBytes of+ Left (_, _, err) -> fail $ "Failed to decode ULIDRandom: " <> err -- This shouldn't happen.+ Right (_, _, ulid) -> pure ulid++genUlidTimeStamp :: Gen ULIDTimeStamp+genUlidTimeStamp =+ mkULIDTimeStamp <$> genPosixTime++genUlid :: Gen ULID+genUlid =+ ULID <$> genUlidTimeStamp <*> genUlidRandom
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Hedgehog.hs view
@@ -0,0 +1,24 @@+module Effectful.Zoo.Hedgehog.Api.Hedgehog+ ( H.classify,++ H.eval,+ H.evalIO,+ H.evalM,++ H.evalEither,+ H.evalMaybe,++ H.assert,+ (H.===),+ (H./==),++ H.failure,+ H.failException,+ H.failWith,++ H.success,++ ) where++import Hedgehog qualified as H+import Hedgehog.Internal.Property qualified as H
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/Cabal.hs view
@@ -0,0 +1,90 @@+module Effectful.Zoo.Hedgehog.Api.Internal.Cabal+ ( findDefaultPlanJsonFile,+ getPlanJsonFile,+ binDist,+ ) where++import Data.Aeson+import Data.List qualified as L+import Data.Text qualified as T+import Effectful+import Effectful.Zoo.Core+import Effectful.Zoo.Environment+import Effectful.Zoo.Error.Static+import Effectful.Zoo.FileSystem+import Effectful.Zoo.Hedgehog.Api.Internal.Cabal.Types+import Effectful.Zoo.Hedgehog.Api.Internal.FilePath+import Effectful.Zoo.Log.Dynamic+import HaskellWorks.Error.Types+import HaskellWorks.Prelude+import System.FilePath (takeDirectory, (</>))++-- | Find the nearest plan.json going upwards from the current directory.+findDefaultPlanJsonFile :: ()+ => r <: Error IOException+ => r <: FileSystem+ => r <: IOE+ => r <: Log Text+ => Eff r FilePath+findDefaultPlanJsonFile = getCurrentDirectory >>= go+ where go :: ()+ => r <: Error IOException+ => r <: FileSystem+ => r <: IOE+ => r <: Log Text+ => FilePath+ -> Eff r FilePath+ go d = do+ let file = d </> "dist-newstyle/cache/plan.json"+ exists <- doesFileExist file+ if exists+ then return file+ else do+ let parent = takeDirectory d+ if parent == d+ then return "dist-newstyle/cache/plan.json"+ else go parent+++getPlanJsonFile :: ()+ => r <: Environment+ => r <: Error IOException+ => r <: FileSystem+ => r <: IOE+ => r <: Log Text+ => Eff r FilePath+getPlanJsonFile = do+ maybeBuildDir <- lookupEnvMaybe "CABAL_BUILDDIR"+ case maybeBuildDir of+ Just buildDir -> pure $ ".." </> T.unpack buildDir </> "cache/plan.json"+ Nothing -> findDefaultPlanJsonFile++-- | Consult the "plan.json" generated by cabal to get the path to the executable corresponding.+-- to a haskell package. It is assumed that the project has already been configured and the+-- executable has been built.+binDist:: ()+ => r <: Environment+ => r <: Error GenericError+ => r <: Error IOException+ => r <: FileSystem+ => r <: IOE+ => r <: Log Text+ => String+ -- ^ Package name+ -> Eff r FilePath+ -- ^ Path to executable+binDist pkg = do+ planJsonFile <- getPlanJsonFile+ contents <- readLazyByteStringFile planJsonFile++ case eitherDecode @Plan contents of+ Right plan -> case L.filter matching plan.installPlan of+ (component:_) -> case component.binFile of+ Just bin -> return $ addExeSuffix (T.unpack bin)+ Nothing -> throw $ GenericError $ "Missing bin-file in " <> tshow component+ [] -> throw $ GenericError $ "Cannot find exe " <> tshow pkg <> " in plan"+ Left msg -> throw $ GenericError $ "Cannot decode plan: " <> T.pack msg+ where matching :: Component -> Bool+ matching component = case component.componentName of+ Just name -> name == "exe:" <> T.pack pkg+ Nothing -> False
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/Cabal/Types.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++module Effectful.Zoo.Hedgehog.Api.Internal.Cabal.Types+ ( Plan(..),+ Component(..),+ ) where++import Control.Applicative+import Data.Aeson+import Data.Eq+import Data.Function+import Data.Maybe+import Data.Text (Text)+import GHC.Generics+import Text.Show++data Component = Component+ { componentName :: Maybe Text+ , binFile :: Maybe Text+ }+ deriving stock (Generic, Eq, Show)++newtype Plan = Plan+ { installPlan :: [Component]+ }+ deriving stock (Generic, Eq, Show)++instance FromJSON Plan where+ parseJSON = withObject "Plan" $ \v -> Plan+ <$> v .: "install-plan"++instance FromJSON Component where+ parseJSON = withObject "Plan" $ \v -> Component+ <$> v .:? "component-name"+ <*> v .:? "bin-file"
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/FilePath.hs view
@@ -0,0 +1,16 @@+module Effectful.Zoo.Hedgehog.Api.Internal.FilePath+ ( exeSuffix,+ addExeSuffix,+ ) where++import Data.List qualified as L+import Effectful.Zoo.Hedgehog.Api.Internal.OS qualified as OS+import HaskellWorks.Prelude++exeSuffix :: String+exeSuffix = if OS.isWin32 then ".exe" else ""++addExeSuffix :: String -> String+addExeSuffix s = if ".exe" `L.isSuffixOf` s+ then s+ else s <> exeSuffix
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Internal/OS.hs view
@@ -0,0 +1,12 @@+module Effectful.Zoo.Hedgehog.Api.Internal.OS+ ( isWin32,+ ) where++import Data.Bool+import Data.Eq+import System.Info++-- | Determine if the operating system is Windows.+isWin32 :: Bool+isWin32 = os == "mingw32"+{-# INLINE isWin32 #-}
components/hedgehog/Effectful/Zoo/Hedgehog/Api/Journal.hs view
@@ -1,9 +1,15 @@ module Effectful.Zoo.Hedgehog.Api.Journal- ( jot,+ ( Failure,++ jot, jot_, jotWithCallStack,+ jotTextWithCallStack, + jotString,+ jotString_,+ jotText, jotText_, jotM, jotBsUtf8M,@@ -37,6 +43,11 @@ jotEachIO, jotEachIO_, + jotPkgInputFile,+ jotPkgGoldenFile,+ jotRootInputFile,+ jotTempFile,+ jotLogTextWithCallStack, jotShowDataLog,@@ -44,6 +55,7 @@ writeLog, ) where +import Control.Monad.Catch (MonadCatch) import Data.Aeson (ToJSON(..)) import Data.Aeson qualified as J import Data.Aeson.Encode.Pretty qualified as J@@ -55,91 +67,144 @@ import Data.Traversable import Data.Yaml qualified as Y import Effectful+import Effectful.Concurrent import Effectful.Dispatch.Dynamic import Effectful.Zoo.Core import Effectful.Zoo.DataLog.Dynamic import Effectful.Zoo.DataLog.Dynamic qualified as DataLog-import Effectful.Zoo.Hedgehog.Api.Eval-import Effectful.Zoo.Hedgehog.Api.Failure-import Effectful.Zoo.Hedgehog.Dynamic+import Effectful.Zoo.Error.Static+import Effectful.Zoo.Hedgehog.Api.Hedgehog+import Effectful.Zoo.Hedgehog.Data.PackagePath+import Effectful.Zoo.Hedgehog.Data.ProjectRoot+import Effectful.Zoo.Hedgehog.Data.Workspace+import Effectful.Zoo.Hedgehog.Effect.Hedgehog import Effectful.Zoo.Log.Data.Severity+import Effectful.Zoo.Reader.Static import GHC.Stack qualified as GHC import HaskellWorks.Prelude import HaskellWorks.String import HaskellWorks.ToText+import Hedgehog (MonadTest(..))+import Hedgehog.Internal.Property (Failure) import Hedgehog.Internal.Property qualified as H import Hedgehog.Internal.Source qualified as H -- | Annotate the given string at the context supplied by the callstack.-jotWithCallStack :: forall r. ()- => r <: Hedgehog+jotWithCallStack :: forall m. ()+ => MonadTest m => CallStack -> String- -> Eff r ()+ -> m () jotWithCallStack cs a = writeLog $ H.Annotation (H.getCaller cs) a +-- | Annotate the given string at the context supplied by the callstack.+jotTextWithCallStack :: forall m. ()+ => MonadTest m+ => CallStack+ -> Text+ -> m ()+jotTextWithCallStack cs a =+ writeLog $ H.Annotation (H.getCaller cs) $ T.unpack a+ -- | Annotate with the given string.-jot :: forall r. ()- => r <: Hedgehog+jot :: forall m. () => HasCallStack- => String- -> Eff r String+ => MonadTest m+ => Text+ -> m Text jot a = withFrozenCallStack do+ jotText a++-- | Annotate the given string returning unit.+jot_ :: forall m. ()+ => HasCallStack+ => MonadTest m+ => Text+ -> m ()+jot_ a =+ withFrozenCallStack do+ jotText_ a++-- | Annotate with the given string.+jotString :: forall m. ()+ => HasCallStack+ => MonadTest m+ => String+ -> m String+jotString a =+ withFrozenCallStack do !b <- eval a jotWithCallStack GHC.callStack b- return b+ pure b --- | Annotate the given string returning unit.-jot_ :: forall r. ()- => r <: Hedgehog+-- | Annotate with the given string.+jotString_ :: forall m. () => HasCallStack+ => MonadTest m+ => String+ -> m ()+jotString_ a =+ withFrozenCallStack do+ !b <- eval a+ jotWithCallStack GHC.callStack b++-- | Annotate the given text returning unit.+jotText :: forall m. ()+ => HasCallStack+ => MonadTest m => Text- -> Eff r ()-jot_ =+ -> m Text+jotText a = withFrozenCallStack do- jotText_+ !b <- eval a+ jotWithCallStack GHC.callStack $ T.unpack a+ pure b -- | Annotate the given text returning unit.-jotText_ :: forall r. ()- => r <: Hedgehog+jotText_ :: forall m. () => HasCallStack+ => MonadTest m => Text- -> Eff r ()+ -> m () jotText_ a = withFrozenCallStack do- jotWithCallStack GHC.callStack $ T.unpack a+ !b <- eval a+ jotWithCallStack GHC.callStack $ T.unpack b -- | Annotate the given string in a monadic context.-jotM :: forall a r. ()- => ToString a- => r <: Hedgehog+jotM :: forall a m. () => HasCallStack- => Eff r a- -> Eff r a+ => MonadCatch m+ => MonadTest m+ => ToString a+ => m a+ -> m a jotM a = withFrozenCallStack do !b <- evalM a jotWithCallStack GHC.callStack $ toString b return b -jotBsUtf8M :: forall r. ()- => r <: Hedgehog+jotBsUtf8M :: forall m. () => HasCallStack- => Eff r ByteString- -> Eff r ByteString+ => MonadCatch m+ => MonadTest m+ => m ByteString+ -> m ByteString jotBsUtf8M a = withFrozenCallStack do !b <- evalM a jotWithCallStack GHC.callStack $ T.unpack $ T.decodeUtf8 b return b -jotLbsUtf8M :: forall r. ()- => r <: Hedgehog+jotLbsUtf8M :: forall m. () => HasCallStack- => Eff r LBS.ByteString- -> Eff r LBS.ByteString+ => MonadCatch m+ => MonadTest m+ => m LBS.ByteString+ -> m LBS.ByteString jotLbsUtf8M a = withFrozenCallStack do !b <- evalM a@@ -147,11 +212,12 @@ return b -- | Annotate the given string in a monadic context returning unit.-jotM_ :: forall r. ()- => r <: Hedgehog+jotM_ :: forall m. () => HasCallStack- => Eff r String- -> Eff r ()+ => MonadCatch m+ => MonadTest m+ => m String+ -> m () jotM_ a = withFrozenCallStack do !b <- evalM a@@ -159,12 +225,12 @@ return () -- | Annotate the given string in IO.-jotIO :: forall r. ()- => r <: Hedgehog- => r <: IOE+jotIO :: forall m. () => HasCallStack+ => MonadIO m+ => MonadTest m => IO String- -> Eff r String+ -> m String jotIO f = withFrozenCallStack do !a <- evalIO f@@ -172,12 +238,12 @@ return a -- | Annotate the given string in IO returning unit.-jotIO_ :: forall r. ()- => r <: Hedgehog- => r <: IOE+jotIO_ :: forall m. () => HasCallStack+ => MonadIO m+ => MonadTest m => IO String- -> Eff r ()+ -> m () jotIO_ f = withFrozenCallStack do !a <- evalIO f@@ -185,12 +251,13 @@ return () -- | Annotate the given value.-jotShow :: forall a r. ()- => r <: Hedgehog+jotShow :: forall a m. () => HasCallStack+ => MonadIO m+ => MonadTest m => Show a => a- -> Eff r a+ -> m a jotShow a = withFrozenCallStack do !b <- eval a@@ -198,23 +265,24 @@ return b -- | Annotate the given value returning unit.-jotShow_ :: forall a r. ()- => r <: Hedgehog+jotShow_ :: forall a m. () => HasCallStack+ => MonadTest m => Show a => a- -> Eff r ()+ -> m () jotShow_ a = withFrozenCallStack do jotWithCallStack GHC.callStack (show a) -- | Annotate the given value in a monadic context.-jotShowM :: forall a r. ()- => r <: Hedgehog+jotShowM :: forall a m. () => HasCallStack+ => MonadCatch m+ => MonadTest m => Show a- => Eff r a- -> Eff r a+ => m a+ -> m a jotShowM a = withFrozenCallStack do !b <- evalM a@@ -222,12 +290,13 @@ return b -- | Annotate the given value in a monadic context returning unit.-jotShowM_ :: forall a r. ()- => r <: Hedgehog+jotShowM_ :: forall a m. () => HasCallStack+ => MonadCatch m+ => MonadTest m => Show a- => Eff r a- -> Eff r ()+ => m a+ -> m () jotShowM_ a = withFrozenCallStack do !b <- evalM a@@ -235,13 +304,13 @@ return () -- | Annotate the given value in IO.-jotShowIO :: forall a r. ()- => r <: Hedgehog- => r <: IOE+jotShowIO :: forall a m. () => HasCallStack+ => MonadIO m+ => MonadTest m => Show a => IO a- -> Eff r a+ -> m a jotShowIO f = withFrozenCallStack do !a <- evalIO f@@ -249,13 +318,13 @@ return a -- | Annotate the given value in IO returning unit.-jotShowIO_ :: forall a r. ()- => r <: Hedgehog- => r <: IOE+jotShowIO_ :: forall a m. () => HasCallStack+ => MonadIO m+ => MonadTest m => Show a => IO a- -> Eff r ()+ -> m () jotShowIO_ f = withFrozenCallStack do !a <- evalIO f@@ -263,13 +332,14 @@ return () -- | Annotate the given value.-jotShowRead :: forall a r. ()+jotShowRead :: forall a m. () => HasCallStack- => r <: Hedgehog+ => MonadIO m+ => MonadTest m => Read a => Show a => String- -> Eff r a+ -> m a jotShowRead s = withFrozenCallStack do !result <- eval (readEither @a s)@@ -280,12 +350,13 @@ return a -- | Annotate the given value as JSON.-jotJson :: forall a r. ()- => r <: Hedgehog+jotJson :: forall a m. () => HasCallStack+ => MonadIO m+ => MonadTest m => ToJSON a => a- -> Eff r a+ -> m a jotJson a = withFrozenCallStack do !b <- eval a@@ -293,12 +364,13 @@ return b -- | Annotate the given value as JSON.-jotJson_ :: forall a r. ()- => r <: Hedgehog+jotJson_ :: forall a m. () => HasCallStack+ => MonadIO m+ => MonadTest m => ToJSON a => a- -> Eff r ()+ -> m () jotJson_ a = withFrozenCallStack do !b <- eval a@@ -306,12 +378,14 @@ return () -- | Annotate the given value as JSON in a monadic context.-jotJsonM :: forall a r. ()- => r <: Hedgehog+jotJsonM :: forall a m. () => HasCallStack+ => MonadCatch m+ => MonadIO m+ => MonadTest m => ToJSON a- => Eff r a- -> Eff r a+ => m a+ -> m a jotJsonM a = withFrozenCallStack do !b <- evalM a@@ -319,12 +393,14 @@ return b -- | Annotate the given value as JSON in a monadic context.-jotJsonM_ :: forall a r. ()- => r <: Hedgehog+jotJsonM_ :: forall a m. () => HasCallStack+ => MonadCatch m+ => MonadIO m+ => MonadTest m => ToJSON a- => Eff r a- -> Eff r ()+ => m a+ -> m () jotJsonM_ a = withFrozenCallStack do !b <- evalM a@@ -332,12 +408,13 @@ return () -- | Annotate the given value as JSON.-jotJsonPretty :: forall a r. ()- => r <: Hedgehog+jotJsonPretty :: forall a m. () => HasCallStack+ => MonadIO m+ => MonadTest m => ToJSON a => a- -> Eff r a+ -> m a jotJsonPretty a = withFrozenCallStack do !b <- eval a@@ -345,12 +422,13 @@ return b -- | Annotate the given value as JSON.-jotJsonPretty_ :: forall a r. ()- => r <: Hedgehog+jotJsonPretty_ :: forall a m. () => HasCallStack+ => MonadIO m+ => MonadTest m => ToJSON a => a- -> Eff r ()+ -> m () jotJsonPretty_ a = withFrozenCallStack do !b <- eval a@@ -358,12 +436,14 @@ return () -- | Annotate the given value as JSON in a monadic context.-jotJsonPrettyM :: forall a r. ()- => r <: Hedgehog+jotJsonPrettyM :: forall a m. () => HasCallStack+ => MonadCatch m+ => MonadIO m+ => MonadTest m => ToJSON a- => Eff r a- -> Eff r a+ => m a+ -> m a jotJsonPrettyM a = withFrozenCallStack do !b <- evalM a@@ -371,12 +451,14 @@ return b -- | Annotate the given value as JSON in a monadic context.-jotJsonPrettyM_ :: forall a r. ()- => r <: Hedgehog+jotJsonPrettyM_ :: forall a m. () => HasCallStack+ => MonadCatch m+ => MonadIO m+ => MonadTest m => ToJSON a- => Eff r a- -> Eff r ()+ => m a+ -> m () jotJsonPrettyM_ a = withFrozenCallStack do !b <- evalM a@@ -384,12 +466,13 @@ return () -- | Annotate the given value as JSON.-jotYaml :: forall a r. ()- => r <: Hedgehog+jotYaml :: forall a m. () => HasCallStack+ => MonadIO m+ => MonadTest m => ToJSON a => a- -> Eff r a+ -> m a jotYaml a = withFrozenCallStack do !b <- eval a@@ -397,12 +480,13 @@ return b -- | Annotate the given value as JSON.-jotYaml_ :: forall a r. ()- => r <: Hedgehog+jotYaml_ :: forall a m. () => HasCallStack+ => MonadIO m+ => MonadTest m => ToJSON a => a- -> Eff r ()+ -> m () jotYaml_ a = withFrozenCallStack do !b <- eval a@@ -410,12 +494,14 @@ return () -- | Annotate the given value as JSON in a monadic context.-jotYamlM :: forall a r. ()- => r <: Hedgehog+jotYamlM :: forall a m. () => HasCallStack+ => MonadCatch m+ => MonadIO m+ => MonadTest m => ToJSON a- => Eff r a- -> Eff r a+ => m a+ -> m a jotYamlM a = withFrozenCallStack do !b <- evalM a@@ -423,12 +509,14 @@ return b -- | Annotate the given value as JSON in a monadic context.-jotYamlM_ :: forall a r. ()- => r <: Hedgehog+jotYamlM_ :: forall a m. () => HasCallStack+ => MonadCatch m+ => MonadIO m+ => MonadTest m => ToJSON a- => Eff r a- -> Eff r ()+ => m a+ -> m () jotYamlM_ a = withFrozenCallStack do !b <- evalM a@@ -436,37 +524,40 @@ return () -- | Annotate the each value in the given traversable.-jotEach :: forall a f r. ()- => r <: Hedgehog+jotEach :: forall a m f. () => HasCallStack+ => MonadIO m+ => MonadTest m => Show a => Traversable f => f a- -> Eff r (f a)+ -> m (f a) jotEach as = withFrozenCallStack do for_ as $ jotWithCallStack GHC.callStack . show return as -- | Annotate the each value in the given traversable returning unit.-jotEach_ :: forall a f r. ()- => r <: Hedgehog+jotEach_ :: forall a m f. () => HasCallStack+ => MonadIO m+ => MonadTest m => Show a => Traversable f => f a- -> Eff r ()+ -> m () jotEach_ as = withFrozenCallStack $ for_ as $ jotWithCallStack GHC.callStack . show -- | Annotate the each value in the given traversable in a monadic context.-jotEachM :: forall a f r. ()- => r <: Hedgehog+jotEachM :: forall a m f. () => HasCallStack+ => MonadIO m+ => MonadTest m => Show a => Traversable f- => Eff r (f a)- -> Eff r (f a)+ => m (f a)+ -> m (f a) jotEachM f = withFrozenCallStack do !as <- f@@ -474,27 +565,28 @@ return as -- | Annotate the each value in the given traversable in a monadic context returning unit.-jotEachM_ :: forall a f r. ()- => r <: Hedgehog+jotEachM_ :: forall a m f. () => HasCallStack+ => MonadIO m+ => MonadTest m => Show a => Traversable f- => Eff r (f a)- -> Eff r ()+ => m (f a)+ -> m () jotEachM_ f = withFrozenCallStack do !as <- f for_ as $ jotWithCallStack GHC.callStack . show -- | Annotate the each value in the given traversable in IO.-jotEachIO :: forall a f r. ()- => r <: Hedgehog- => r <: IOE+jotEachIO :: forall a m f. () => HasCallStack+ => MonadIO m+ => MonadTest m => Show a => Traversable f => IO (f a)- -> Eff r (f a)+ -> m (f a) jotEachIO f = withFrozenCallStack do !as <- evalIO f@@ -502,25 +594,81 @@ return as -- | Annotate the each value in the given traversable in IO returning unit.-jotEachIO_ :: forall a f r. ()- => r <: Hedgehog- => r <: IOE+jotEachIO_ :: forall a m f. () => HasCallStack+ => MonadIO m+ => MonadTest m => Show a => Traversable f => IO (f a)- -> Eff r ()+ -> m () jotEachIO_ f = withFrozenCallStack do !as <- evalIO f for_ as $ jotWithCallStack GHC.callStack . show -jotLogTextWithCallStack :: forall r. ()+-- | Return the input file path after annotating it relative to the package directory+jotPkgInputFile :: forall r. ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error Failure => r <: Hedgehog+ => r <: Reader PackagePath+ => FilePath+ -> Eff r FilePath+jotPkgInputFile fp = withFrozenCallStack $ do+ PackagePath { filePath = pkgPath } <- ask+ jotString_ $ pkgPath <> "/" <> fp+ return fp++-- | Return the golden file path after annotating it relative to the package directory+jotPkgGoldenFile :: forall r. ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error Failure+ => r <: Hedgehog+ => r <: Reader PackagePath+ => FilePath+ -> Eff r FilePath+jotPkgGoldenFile fp = withFrozenCallStack $ do+ PackagePath { filePath = pkgPath } <- ask+ jotString_ $ pkgPath <> "/" <> fp+ return fp++jotRootInputFile :: forall r. ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error Failure+ => r <: Hedgehog+ => r <: Reader ProjectRoot+ => FilePath+ -> Eff r FilePath+jotRootInputFile fp = withFrozenCallStack $ do+ ProjectRoot { filePath = pkgPath } <- ask+ jotString $ pkgPath <> "/" <> fp++-- | Return the test file path after annotating it relative to the project root directory+jotTempFile :: forall r. ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error Failure+ => r <: Hedgehog+ => r <: Reader Workspace+ => FilePath+ -> Eff r FilePath+jotTempFile fp = withFrozenCallStack $ do+ Workspace { filePath = workspace } <- ask+ let relPath = workspace <> "/" <> fp+ jotString_ $ workspace <> "/" <> relPath+ return relPath++jotLogTextWithCallStack :: forall m. ()+ => HasCallStack+ => MonadTest m => CallStack -> Severity -> Text- -> Eff r ()+ -> m () jotLogTextWithCallStack cs severity a = withFrozenCallStack do jotWithCallStack cs $ T.unpack $ "[" <> toText severity <> "] " <> a@@ -528,6 +676,8 @@ jotShowDataLog :: forall i a r. () => HasCallStack => Show i+ => r <: Concurrent+ => r <: Error Failure => r <: Hedgehog => Eff (DataLog i : r) a -> Eff r a@@ -536,11 +686,11 @@ DataLog.runDataLog jotShow_ {-# inline jotShowDataLog #-} -writeLog :: forall r. ()+writeLog :: forall m. () => HasCallStack- => r <: Hedgehog+ => MonadTest m => H.Log- -> Eff r ()+ -> m () writeLog message = withFrozenCallStack $ H.writeLog message
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/MonadAssertion.hs view
@@ -0,0 +1,47 @@+module Effectful.Zoo.Hedgehog.Api.MonadAssertion+ ( MonadAssertion(..),+ tryAssertion,+ tryExceptAssertion,+ ) where++import Control.Monad.Catch (MonadCatch(..))+import Control.Monad.Catch qualified as C+import Control.Monad.Trans.Class+import Control.Monad.Trans.Except qualified as E+import Control.Monad.Trans.Resource qualified as IO+import Control.Monad.Trans.Resource.Internal qualified as IO+import Effectful.Zoo.Hedgehog.Data.TestResult+import HaskellWorks.Prelude+import Hedgehog.Internal.Property qualified as H++class Monad m => MonadAssertion m where+ throwAssertion :: H.Failure -> m a+ catchAssertion :: m a -> (H.Failure -> m a) -> m a++instance Monad m => MonadAssertion (H.TestT m) where+ throwAssertion f = H.liftTest $ H.mkTest (Left f, mempty)+ catchAssertion g h = H.TestT $ E.catchE (H.unTest g) (H.unTest . h)++instance MonadAssertion m => MonadAssertion (IO.ResourceT m) where+ throwAssertion = lift . throwAssertion+ catchAssertion r h = IO.ResourceT $ \i -> IO.unResourceT r i `catchAssertion` \e -> IO.unResourceT (h e) i++deriving newtype instance Monad m => MonadAssertion (H.PropertyT m)++tryAssertion :: ()+ => MonadAssertion m+ => m a+ -> m (Either H.Failure a)+tryAssertion m =+ catchAssertion (Right <$> m) (pure . Left)++tryExceptAssertion :: ()+ => MonadAssertion m+ => MonadCatch m+ => m a+ -> m (TestResult a)+tryExceptAssertion m =+ tryAssertion (C.try m) >>= \case+ Right (Right a) -> pure $ TestResult a+ Right (Left e) -> pure $ TestError e+ Left f -> pure $ TestFailure f
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Process.hs view
@@ -0,0 +1,313 @@+module Effectful.Zoo.Hedgehog.Api.Process+ ( ExecConfig(..),+ defaultExecConfig,+ execDetailFlex,+ execFlex,+ execFlexOk,+ execFlexOk',+ execOk,+ execOk_,+ exec,+ procFlex,+ procFlex',+ binFlex,++ waitSecondsForProcess,+ waitSecondsForProcessOk,++ ) where++import Data.List qualified as L+import Data.Monoid (Last (..))+import Data.Text qualified as T+import Effectful+import Effectful.Concurrent+import Effectful.Zoo.Core+import Effectful.Zoo.Environment+import Effectful.Zoo.Error.Static+import Effectful.Zoo.Hedgehog.Api.Assert+import Effectful.Zoo.Hedgehog.Api.Internal.Cabal+import Effectful.Zoo.Hedgehog.Api.Journal+import Effectful.Zoo.Hedgehog.Api.Process.Internal+import Effectful.Zoo.Hedgehog.Effect.Hedgehog+import Effectful.Zoo.Log.Dynamic+import Effectful.Zoo.Process+import GHC.Stack (callStack)+import HaskellWorks.Error.Types+import HaskellWorks.Prelude+import Effectful.FileSystem (FileSystem)++-- | Configuration for starting a new process. This is a subset of 'IO.CreateProcess'.+data ExecConfig = ExecConfig+ { execConfigEnv :: Last [(String, String)]+ , execConfigCwd :: Last FilePath+ } deriving stock (Eq, Generic, Show)++defaultExecConfig :: ExecConfig+defaultExecConfig = ExecConfig+ { execConfigEnv = mempty+ , execConfigCwd = mempty+ }++-- | Create a process returning its stdout.+--+-- Being a 'flex' function means that the environment determines how the process is launched.+--+-- When running in a nix environment, the 'envBin' argument describes the environment variable+-- that defines the binary to use to launch the process.+--+-- When running outside a nix environment, the `pkgBin` describes the name of the binary+-- to launch via cabal exec.+execFlexOk :: ()+ => HasCallStack+ => r <: Concurrent+ => r <: Environment+ => r <: Error Failure+ => r <: Error GenericError+ => r <: Error IOException+ => r <: FileSystem+ => r <: Hedgehog+ => r <: IOE+ => r <: Log Text+ => String+ -> String+ -> [String]+ -> Eff r String+execFlexOk = execFlexOk' defaultExecConfig++execFlexOk' :: ()+ => HasCallStack+ => r <: Concurrent+ => r <: Environment+ => r <: Error Failure+ => r <: Error GenericError+ => r <: Error IOException+ => r <: FileSystem+ => r <: Hedgehog+ => r <: IOE+ => r <: Log Text+ => ExecConfig+ -> String+ -> String+ -> [String]+ -> Eff r String+execFlexOk' execConfig pkgBin envBin arguments =+ withFrozenCallStack do+ (exitResult, stdout, stderr) <- execFlex execConfig pkgBin envBin arguments+ case exitResult of+ ExitFailure exitCode -> do+ jotString_ $ L.unlines $+ [ "Process exited with non-zero exit-code: " <> show @Int exitCode ]+ <> (if L.null stdout then [] else ["━━━━ stdout ━━━━" , stdout])+ <> (if L.null stderr then [] else ["━━━━ stderr ━━━━" , stderr])+ failMessage callStack "Execute process failed"+ ExitSuccess -> return stdout++-- | Run a process, returning its exit code, its stdout, and its stderr.+-- Contrary to @execFlexOk'@, this function doesn't fail if the call fails.+-- So, if you want to test something negative, this is the function to use.+execFlex :: ()+ => HasCallStack+ => r <: Concurrent+ => r <: Environment+ => r <: Error Failure+ => r <: Error GenericError+ => r <: Error IOException+ => r <: FileSystem+ => r <: Hedgehog+ => r <: IOE+ => r <: Log Text+ => ExecConfig+ -> String -- ^ @pkgBin@: name of the binary to launch via 'cabal exec'+ -> String -- ^ @envBin@: environment variable defining the binary to launch the process, when in Nix+ -> [String]+ -> Eff r (ExitCode, String, String) -- ^ exit code, stdout, stderr+execFlex execConfig pkgBin envBin arguments =+ withFrozenCallStack do+ cp <- procFlex' execConfig pkgBin envBin arguments+ jotString_ . ("━━━━ command ━━━━\n" <>) $ case cmdspec cp of+ ShellCommand cmd -> cmd+ RawCommand cmd args -> cmd <> " " <> L.unwords (argQuote <$> args)++ readCreateProcessWithExitCode cp ""++execDetailFlex :: ()+ => HasCallStack+ => r <: Concurrent+ => r <: Environment+ => r <: Error Failure+ => r <: Error GenericError+ => r <: Error IOException+ => r <: FileSystem+ => r <: Hedgehog+ => r <: IOE+ => r <: Log Text+ => ExecConfig+ -> String+ -> String+ -> [String]+ -> Eff r (ExitCode, String, String)+execDetailFlex execConfig pkgBin envBin arguments =+ withFrozenCallStack do+ cp <- procFlex' execConfig pkgBin envBin arguments+ jotString_ . ("Command: " <>) $ case cmdspec cp of+ ShellCommand cmd -> cmd+ RawCommand cmd args -> cmd <> " " <> L.unwords args+ readCreateProcessWithExitCode cp ""++-- | Execute a process, returning '()'.+execOk_ :: ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error Failure+ => r <: Error IOException+ => r <: Hedgehog+ => r <: IOE+ => ExecConfig+ -> String+ -> [String]+ -> Eff r ()+execOk_ execConfig bin arguments =+ void $ execOk execConfig bin arguments++-- | Execute a process, returning the stdout. Fail if the call returns+-- with a non-zero exit code. For a version that doesn't fail upon receiving+-- a non-zero exit code, see 'execAny'.+execOk :: ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error Failure+ => r <: Error IOException+ => r <: Hedgehog+ => r <: IOE+ => ExecConfig+ -> String+ -> [String]+ -> Eff r String+execOk execConfig bin arguments =+ withFrozenCallStack do+ (exitResult, stdout, stderr) <- exec execConfig bin arguments+ case exitResult of+ ExitFailure exitCode ->failMessage callStack . L.unlines $+ [ "Process exited with non-zero exit-code: " <> show @Int exitCode ]+ <> (if L.null stdout then [] else ["━━━━ stdout ━━━━" , stdout])+ <> (if L.null stderr then [] else ["━━━━ stderr ━━━━" , stderr])+ ExitSuccess -> return stdout++-- | Execute a process, returning the error code, the stdout, and the stderr.+exec :: ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error Failure+ => r <: Error IOException+ => r <: Hedgehog+ => r <: IOE+ => ExecConfig+ -> String -- ^ The binary to launch+ -> [String] -- ^ The binary's arguments+ -> Eff r (ExitCode, String, String) -- ^ exit code, stdout, stderr+exec execConfig bin arguments =+ withFrozenCallStack do+ let cp = (proc bin arguments)+ { env = getLast execConfig.execConfigEnv+ , cwd = getLast execConfig.execConfigCwd+ }+ jotString_ . ( "━━━━ command ━━━━\n" <>) $ bin <> " " <> L.unwords (argQuote <$> arguments)+ readCreateProcessWithExitCode cp ""++-- | Wait a maximum of 'seconds' secons for process to exit.+waitSecondsForProcessOk :: ()+ => HasCallStack+ => r <: Concurrent+ => r <: Error Failure+ => r <: Hedgehog+ => r <: IOE+ => Int+ -> ProcessHandle+ -> Eff r ExitCode+waitSecondsForProcessOk seconds hProcess =+ withFrozenCallStack do+ maybeExitCode <- waitSecondsForProcess seconds hProcess+ & trapFail @TimedOut++ case maybeExitCode of+ Nothing -> failMessage callStack "No exit code for process"+ Just exitCode -> do+ jotString_ $ "Process exited " <> show exitCode+ return exitCode++-- | Compute the path to the binary given a package name or an environment variable override.+binFlex :: ()+ => HasCallStack+ => r <: Environment+ => r <: Error GenericError+ => r <: Error IOException+ => r <: FileSystem+ => r <: IOE+ => r <: Log Text+ => String+ -- ^ Package name+ -> String+ -- ^ Environment variable pointing to the binary to run+ -> Eff r FilePath+ -- ^ Path to executable+binFlex pkg binaryEnv =+ withFrozenCallStack do+ maybeEnvBin <- lookupEnvMaybe $ T.pack binaryEnv+ case maybeEnvBin of+ Just envBin -> return $ T.unpack envBin+ Nothing -> binDist pkg++-- | Create a 'CreateProcess' describing how to start a process given the Cabal package name+-- corresponding to the executable, an environment variable pointing to the executable,+-- and an argument list.+--+-- The actual executable used will the one specified by the environment variable, but if+-- the environment variable is not defined, it will be found instead by consulting the+-- "plan.json" generated by cabal. It is assumed that the project has already been+-- configured and the executable has been built.+procFlex :: ()+ => HasCallStack+ => r <: Environment+ => r <: Error GenericError+ => r <: Error IOException+ => r <: FileSystem+ => r <: IOE+ => r <: Log Text+ => String+ -- ^ Cabal package name corresponding to the executable+ -> String+ -- ^ Environment variable pointing to the binary to run+ -> [String]+ -- ^ Arguments to the CLI command+ -> Eff r CreateProcess+ -- ^ Captured stdout+procFlex =+ procFlex' defaultExecConfig++procFlex' :: ()+ => HasCallStack+ => r <: Environment+ => r <: Error GenericError+ => r <: Error IOException+ => r <: FileSystem+ => r <: IOE+ => r <: Log Text+ => ExecConfig+ -> String+ -- ^ Cabal package name corresponding to the executable+ -> String+ -- ^ Environment variable pointing to the binary to run+ -> [String]+ -- ^ Arguments to the CLI command+ -> Eff r CreateProcess+ -- ^ Captured stdout+procFlex' execConfig pkg binaryEnv arguments =+ withFrozenCallStack do+ bin <- binFlex pkg binaryEnv+ return (proc bin arguments)+ { env = getLast execConfig.execConfigEnv+ , cwd = getLast execConfig.execConfigCwd+ -- this allows sending signals to the created processes, without killing the test-suite process+ , create_group = True+ }
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Process/Internal.hs view
@@ -0,0 +1,28 @@+module Effectful.Zoo.Hedgehog.Api.Process.Internal+ ( argQuote+ ) where++import Data.Bool+import Data.Semigroup+import Data.String++import qualified Data.List as L++-- | Format argument for a shell CLI command.+--+-- This includes automatically embedding string in double quotes if necessary, including any necessary escaping.+--+-- Note, this function does not cover all the edge cases for shell processing, so avoid use in production code.+argQuote :: String -> String+argQuote arg = if ' ' `L.elem` arg || '"' `L.elem` arg || '$' `L.elem` arg+ then "\"" <> escape arg <> "\""+ else arg+ where escape :: String -> String+ escape ('"':xs) = '\\':'"':escape xs+ escape ('\\':xs) = '\\':'\\':escape xs+ escape ('\n':xs) = '\\':'n':escape xs+ escape ('\r':xs) = '\\':'r':escape xs+ escape ('\t':xs) = '\\':'t':escape xs+ escape ('$':xs) = '\\':'$':escape xs+ escape (x:xs) = x:escape xs+ escape "" = ""
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Range.hs view
@@ -0,0 +1,5 @@+module Effectful.Zoo.Hedgehog.Api.Range+ ( module Hedgehog.Range,+ ) where++import Hedgehog.Range
− components/hedgehog/Effectful/Zoo/Hedgehog/Api/Run.hs
@@ -1,47 +0,0 @@-module Effectful.Zoo.Hedgehog.Api.Run- ( UnitTest,-- hedgehog,- unitTest,- ) where--import Control.Monad.Trans.Writer.Lazy qualified as MTL-import Effectful-import Effectful.Error.Static-import Effectful.Writer.Static.Local-import Effectful.Zoo.Hedgehog.Api.Journal-import Effectful.Zoo.Hedgehog.Dynamic-import Effectful.Zoo.Log.Dynamic-import HaskellWorks.Prelude-import Hedgehog (TestT)-import Hedgehog qualified as H-import Hedgehog.Internal.Property (Failure, Journal)-import Hedgehog.Internal.Property qualified as H-import Test.Tasty (TestName, TestTree)-import Test.Tasty.Hedgehog (testProperty)--type UnitTest = TestT IO ()--hedgehog :: forall a. ()- => Eff- [ Log Text- , Hedgehog- , Error Failure- , Writer Journal- , IOE- ] a- -> H.TestT IO a-hedgehog f =- f- & runLog (ConcUnlift Persistent Unlimited) jotLogTextWithCallStack- & runHedgehogIO- & MTL.WriterT- & ExceptT- & H.TestT--unitTest :: ()- => TestName - -> UnitTest - -> TestTree-unitTest desc =- testProperty desc . H.withTests 1 . H.property . H.test
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Stack.hs view
@@ -0,0 +1,10 @@+module Effectful.Zoo.Hedgehog.Api.Stack+ ( callerModuleName,+ ) where++import GHC.Stack (callStack, getCallStack, srcLocModule)+import HaskellWorks.Prelude++-- | Get the module name of the caller.+callerModuleName :: HasCallStack => String+callerModuleName = maybe "<no-module>" (srcLocModule . snd) (listToMaybe (getCallStack callStack))
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Tasty.hs view
@@ -0,0 +1,20 @@+module Effectful.Zoo.Hedgehog.Api.Tasty+ ( ToTestTree(..),+ ) where++import HaskellWorks.Prelude+import Hedgehog (PropertyT, TestT)+import Hedgehog qualified as H+import Test.Tasty (TestName, TestTree)+import Test.Tasty.Hedgehog (testProperty)++class ToTestTree a where+ toTestTree :: TestName -> a -> TestTree++instance ToTestTree (PropertyT IO ()) where+ toTestTree desc =+ testProperty desc . H.property++instance ToTestTree (TestT IO ()) where+ toTestTree desc =+ testProperty desc . H.withTests 1 . H.property . H.test
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Tasty/Orphans.hs view
@@ -0,0 +1,18 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Effectful.Zoo.Hedgehog.Api.Tasty.Orphans where++import Data.Monoid+import HaskellWorks.Prelude+import Hedgehog (PropertyT, TestT)+import Hedgehog qualified as H+import Test.Tasty.Discover+import Test.Tasty.Discover.TastyInfo+import Test.Tasty.Hedgehog++instance Tasty (PropertyT IO ()) where+ tasty info = pure . testProperty testName . H.property+ where testName = fromMaybe "" $ getLast info.name++instance Tasty (TestT IO ()) where+ tasty info = tasty info . H.test
+ components/hedgehog/Effectful/Zoo/Hedgehog/Api/Workspace.hs view
@@ -0,0 +1,112 @@+module Effectful.Zoo.Hedgehog.Api.Workspace+ ( PackagePath(..),+ ProjectRoot(..),+ Workspace(..),+ workspace,+ moduleWorkspace,+ findCabalProjectDir,+ ) where++import Data.Text qualified as T+import Effectful+import Effectful.Concurrent+import Effectful.Environment+import Effectful.Reader.Static+import Effectful.Zoo.Core+import Effectful.Zoo.Error.Static+import Effectful.Zoo.FileSystem+import Effectful.Zoo.Hedgehog.Api.Assert+import Effectful.Zoo.Hedgehog.Api.Hedgehog+import Effectful.Zoo.Hedgehog.Api.Journal+import Effectful.Zoo.Hedgehog.Api.Stack+import Effectful.Zoo.Hedgehog.Data.PackagePath+import Effectful.Zoo.Hedgehog.Data.ProjectRoot+import Effectful.Zoo.Hedgehog.Data.Workspace+import Effectful.Zoo.Hedgehog.Effect.Hedgehog+import Effectful.Zoo.Log.Dynamic+import HaskellWorks.Prelude+import Hedgehog.Internal.Property qualified as H+import System.FilePath ((</>))+import System.Info++-- | 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+ => r <: Concurrent+ => r <: Environment+ => r <: Error H.Failure+ => r <: FileSystem+ => r <: Hedgehog+ => r <: Log Text+ => FilePath+ -> Eff (Reader Workspace : r) ()+ -> Eff r ()+workspace prefixPath f = withFrozenCallStack $ do+ systemTemp <- getCanonicalTemporaryDirectory+ & trapFail @IOException+ maybeKeepWorkspace <- lookupEnv "KEEP_WORKSPACE"+ ws <- createTempDirectory systemTemp (prefixPath <> "-test")+ & trapFail @IOException+ jot_ $ "Workspace: " <> T.pack ws+ writeStringFile (ws </> "module") callerModuleName+ & trapFail @IOException+ runReader (Workspace ws) f+ when (os /= "mingw32" && maybeKeepWorkspace /= Just "1") $ do+ removePathForcibly ws+ & trapFail @IOException++-- | 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+ => r <: Concurrent+ => r <: Environment+ => r <: Error H.Failure+ => r <: FileSystem+ => r <: Hedgehog+ => r <: Log Text+ => String+ -> Eff (Reader Workspace : r) ()+ -> Eff r ()+moduleWorkspace prefix f = withFrozenCallStack $+ workspace (prefix <> "-" <> callerModuleName) f++-- | Compute the project base. This will be the first parent directory that contains+-- the `cabal.project` file.+-- This should should point to the root directory of the Github project checkout.+findCabalProjectDir :: ()+ => r <: Concurrent+ => r <: Environment+ => r <: Error H.Failure+ => r <: FileSystem+ => r <: Hedgehog+ => r <: Log Text+ => FilePath+ -> Eff r FilePath+findCabalProjectDir dir = do+ atBase <- doesFileExist (dir </> "cabal.project")+ & trap_ @IOException (pure False)+ if atBase+ then return dir+ else do+ let up = dir </> ".."+ upExist <- doesDirectoryExist up+ & trap_ @IOException (pure False)+ if upExist+ then findCabalProjectDir up+ else failWith Nothing "Could not detect project base directory (containing cabal.project)"
+ components/hedgehog/Effectful/Zoo/Hedgehog/Data.hs view
@@ -0,0 +1,15 @@+module Effectful.Zoo.Hedgehog.Data+ ( PackagePath(..),+ ProjectRoot(..),+ PropertyTest,+ TestResult(..),+ UnitTest,+ Workspace(..),+ ) where++import Effectful.Zoo.Hedgehog.Data.PackagePath+import Effectful.Zoo.Hedgehog.Data.ProjectRoot+import Effectful.Zoo.Hedgehog.Data.PropertyTest+import Effectful.Zoo.Hedgehog.Data.TestResult+import Effectful.Zoo.Hedgehog.Data.UnitTest+import Effectful.Zoo.Hedgehog.Data.Workspace
+ components/hedgehog/Effectful/Zoo/Hedgehog/Data/PackagePath.hs view
@@ -0,0 +1,11 @@+module Effectful.Zoo.Hedgehog.Data.PackagePath+ ( PackagePath(..),+ ) where++import GHC.Generics+import HaskellWorks.Prelude++newtype PackagePath = PackagePath+ { filePath :: FilePath+ }+ deriving stock (Eq, Generic, Show)
+ components/hedgehog/Effectful/Zoo/Hedgehog/Data/ProjectRoot.hs view
@@ -0,0 +1,11 @@+module Effectful.Zoo.Hedgehog.Data.ProjectRoot+ ( ProjectRoot(..),+ ) where++import GHC.Generics+import HaskellWorks.Prelude++newtype ProjectRoot = ProjectRoot+ { filePath :: FilePath+ }+ deriving stock (Eq, Generic, Show)
+ components/hedgehog/Effectful/Zoo/Hedgehog/Data/PropertyTest.hs view
@@ -0,0 +1,8 @@+module Effectful.Zoo.Hedgehog.Data.PropertyTest+ ( PropertyTest,+ ) where++import Hedgehog+import HaskellWorks.Prelude++type PropertyTest = PropertyT IO ()
+ components/hedgehog/Effectful/Zoo/Hedgehog/Data/TestResult.hs view
@@ -0,0 +1,11 @@+module Effectful.Zoo.Hedgehog.Data.TestResult+ ( TestResult(..),+ ) where++import HaskellWorks.Prelude+import Hedgehog.Internal.Property qualified as H++data TestResult a+ = TestResult a+ | TestFailure H.Failure+ | TestError SomeException
+ components/hedgehog/Effectful/Zoo/Hedgehog/Data/UnitTest.hs view
@@ -0,0 +1,8 @@+module Effectful.Zoo.Hedgehog.Data.UnitTest+ ( UnitTest,+ ) where++import Hedgehog+import HaskellWorks.Prelude++type UnitTest = TestT IO ()
+ components/hedgehog/Effectful/Zoo/Hedgehog/Data/Workspace.hs view
@@ -0,0 +1,11 @@+module Effectful.Zoo.Hedgehog.Data.Workspace+ ( Workspace(..),+ ) where++import GHC.Generics+import HaskellWorks.Prelude++newtype Workspace = Workspace+ { filePath :: FilePath+ }+ deriving stock (Eq, Generic, Show)
− components/hedgehog/Effectful/Zoo/Hedgehog/Dynamic.hs
@@ -1,67 +0,0 @@-{-# OPTIONS_GHC -fno-warn-orphans #-}--module Effectful.Zoo.Hedgehog.Dynamic- ( Hedgehog(..),- MonadTest(..),-- runHedgehogIO,- runHedgehog,- ) where--import Effectful-import Effectful.Dispatch.Dynamic-import Effectful.Error.Static-import Effectful.Writer.Static.Local-import Effectful.Zoo.Core-import Effectful.Zoo.Hedgehog.MonadTestProxy-import HaskellWorks.Prelude-import Hedgehog (MonadTest(..))-import Hedgehog qualified as H-import Hedgehog.Internal.Property (Failure, Journal)-import Hedgehog.Internal.Property qualified as H--data Hedgehog :: Effect where- CatchAssertion :: HasCallStack- => m a- -> (H.Failure -> m a)- -> Hedgehog m a-- LiftTest :: HasCallStack- => H.Test a- -> Hedgehog m a-- ThrowAssertion :: HasCallStack- => H.Failure- -> Hedgehog m a--type instance DispatchOf Hedgehog = Dynamic--instance (r <: Hedgehog) => MonadTest (Eff r) where- liftTest t = send $ LiftTest t--runHedgehogIO :: forall a. ()- => Eff- [ Hedgehog- , Error Failure- , Writer Journal- , IOE- ] a- -> IO (Either Failure a, Journal)-runHedgehogIO f =- f- & runHedgehog- & runError @H.Failure- & runWriter @H.Journal- & fmap (first (first snd))- & runEff--runHedgehog :: forall a r. ()- => r <: Error Failure- => r <: Writer Journal- => Eff (Hedgehog : r) a- -> Eff r a-runHedgehog =- interpret $ \env -> \case- CatchAssertion f h -> localUnlift env SeqUnlift $ \unlift -> catchError (unlift f) (const (unlift . h))- LiftTest f -> liftTestProxy f- ThrowAssertion failure -> throwError failure
+ components/hedgehog/Effectful/Zoo/Hedgehog/Effect/Hedgehog.hs view
@@ -0,0 +1,83 @@+{- HLINT ignore "Eta reduce" -}++{-# OPTIONS_GHC -Wno-orphans #-}++module Effectful.Zoo.Hedgehog.Effect.Hedgehog+ ( Hedgehog,+ HedgehogEnv(..),+ runHedgehogProperty,+ runHedgehogUnit,+ ) where++import Control.Concurrent.STM qualified as IO+import Control.Monad+import Control.Monad.Catch (MonadThrow(..))+import Effectful+import Effectful.Concurrent+import Effectful.Concurrent.STM+import Effectful.Dispatch.Static+import Effectful.Zoo.Core+import Effectful.Zoo.Error.Static+import Effectful.Zoo.Hedgehog.Api.MonadAssertion+import Effectful.Zoo.Hedgehog.Data.TestResult+import HaskellWorks.Prelude+import Hedgehog (MonadTest(..))+import Hedgehog qualified as H+import Hedgehog.Internal.Property qualified as H++-- | An effect for interacting with the filesystem.+data Hedgehog :: Effect++type instance DispatchOf Hedgehog = Static WithSideEffects+newtype instance StaticRep Hedgehog = Hedgehog HedgehogEnv++data HedgehogEnv+ = PropertyEnv (TMVar (H.PropertyT IO ()))+ | UnitTestEnv (TMVar (H.TestT IO ()))++instance {-# OVERLAPS #-}+ ( r <: Concurrent+ , r <: Error H.Failure+ , r <: Hedgehog+ ) => MonadTest (Eff r) where+ liftTest f = do+ Hedgehog env <- getStaticRep+ mvA <- newEmptyTMVarIO+ case env of+ PropertyEnv mvAction ->+ atomically $ putTMVar mvAction (tryExceptAssertion (liftTest f) >>= liftIO . IO.atomically . IO.putTMVar mvA)+ UnitTestEnv mvAction ->+ atomically $ putTMVar mvAction (tryExceptAssertion (liftTest f) >>= liftIO . IO.atomically . IO.putTMVar mvA)+ testResult <- atomically $ takeTMVar mvA+ getTestResult testResult++instance+ ( r <: Error H.Failure+ ) => MonadAssertion (Eff r) where+ throwAssertion f = throw f+ catchAssertion g h = g & trapIn h++getTestResult :: ()+ => r <: Error H.Failure+ => TestResult a+ -> Eff r a+getTestResult = \case+ TestResult a -> pure a+ TestFailure f -> throw f+ TestError e -> throwM e++runHedgehogProperty :: ()+ => r <: IOE+ => TMVar (H.PropertyT IO ())+ -> Eff (Hedgehog : r) a+ -> Eff r a+runHedgehogProperty tvAction =+ evalStaticRep (Hedgehog (PropertyEnv tvAction))++runHedgehogUnit :: ()+ => r <: IOE+ => TMVar (H.TestT IO ())+ -> Eff (Hedgehog : r) a+ -> Eff r a+runHedgehogUnit tvAction =+ evalStaticRep (Hedgehog (UnitTestEnv tvAction))
+ components/hedgehog/Effectful/Zoo/Hedgehog/Effect/HedgehogGen.hs view
@@ -0,0 +1,41 @@+{- HLINT ignore "Eta reduce" -}++{-# OPTIONS_GHC -Wno-orphans #-}++module Effectful.Zoo.Hedgehog.Effect.HedgehogGen+ ( HedgehogGen,+ HedgehogGenEnv(..),+ askHedgehogGenEnv,+ runHedgehogGenProperty,+ ) where++import Effectful+import Effectful.Concurrent.STM+import Effectful.Dispatch.Static+import Effectful.Zoo.Core+import HaskellWorks.Prelude+import Hedgehog qualified as H++-- | An effect for interacting with the filesystem.+data HedgehogGen :: Effect++type instance DispatchOf HedgehogGen = Static WithSideEffects+newtype instance StaticRep HedgehogGen = HedgehogGen HedgehogGenEnv++newtype HedgehogGenEnv+ = HedgehogGenEnv (TMVar (H.PropertyT IO ()))++askHedgehogGenEnv :: ()+ => r <: HedgehogGen+ => Eff r HedgehogGenEnv+askHedgehogGenEnv = do+ HedgehogGen env <- getStaticRep+ pure env++runHedgehogGenProperty :: ()+ => r <: IOE+ => TMVar (H.PropertyT IO ())+ -> Eff (HedgehogGen : r) a+ -> Eff r a+runHedgehogGenProperty tvAction =+ evalStaticRep (HedgehogGen (HedgehogGenEnv tvAction))
+ components/hedgehog/Effectful/Zoo/Hedgehog/Effect/Run.hs view
@@ -0,0 +1,156 @@+{- HLINT ignore "Eta reduce" -}++{-# OPTIONS_GHC -Wno-orphans #-}++module Effectful.Zoo.Hedgehog.Effect.Run+ ( Hedgehog,+ property,+ unit,+ forAll,+ ) where++import Control.Concurrent qualified as IO+import Control.Concurrent.STM qualified as IO+import Control.Exception qualified as IO+import Control.Exception.Lifted qualified as CEL+import Control.Monad+import Effectful+import Effectful.Concurrent+import Effectful.Concurrent.STM+import Effectful.Concurrent.STM qualified as CC+import Effectful.Environment+import Effectful.FileSystem+import Effectful.Zoo.Core+import Effectful.Zoo.Error.Static+import Effectful.Zoo.Hedgehog.Api.Failure+import Effectful.Zoo.Hedgehog.Api.Journal+import Effectful.Zoo.Hedgehog.Api.MonadAssertion+import Effectful.Zoo.Hedgehog.Effect.Hedgehog+import Effectful.Zoo.Hedgehog.Effect.HedgehogGen+import Effectful.Zoo.Log.Data.LogMessage+import Effectful.Zoo.Log.Dynamic+import Effectful.Zoo.Resource+import HaskellWorks.Control.Monad+import HaskellWorks.Prelude+import HaskellWorks.ToText+import Hedgehog (Gen)+import Hedgehog qualified as H+import Hedgehog.Internal.Property qualified as H++property :: ()+ => Eff+ [ Log Text+ , HedgehogGen+ , Hedgehog+ , Error H.Failure+ , Resource+ , FileSystem+ , Environment+ , Concurrent+ , IOE+ ] ()+ -> H.PropertyT IO ()+property f = do+ tvResult <- liftIO IO.newEmptyTMVarIO+ tvAction <- liftIO IO.newEmptyTMVarIO+ CEL.bracket+ do liftIO $ IO.forkFinally+ do f+ & runLog @Text (ConcUnlift Persistent Unlimited) logTextToHedgehog+ & runHedgehogGenProperty tvAction+ & runHedgehogProperty tvAction+ & runError @H.Failure+ & runResource+ & runFileSystem+ & runEnvironment+ & runConcurrent+ & runEff+ (liftIO . IO.atomically . IO.putTMVar tvResult)+ do liftIO . IO.killThread+ do \_ -> do+ whileNothingM do+ mActionOrResult <- liftIO $ IO.atomically do+ mAction <- CC.tryTakeTMVar tvAction+ case mAction of+ Nothing -> do+ mResult <- CC.tryTakeTMVar tvResult+ case mResult of+ Nothing -> pure Nothing+ Just a -> pure $ Just $ Right a+ Just action -> pure $ Just $ Left action++ case mActionOrResult of+ Nothing -> pure Nothing+ Just (Left action) -> action >> pure Nothing+ Just (Right (Left e)) -> liftIO $ IO.throwIO e+ Just (Right (Right (Left (_, e)))) -> throwAssertion e+ Just (Right (Right (Right a))) -> pure $ Just a++logTextToHedgehog :: ()+ => r <: Concurrent+ => r <: Hedgehog+ => r <: Error Failure+ => CallStack+ -> LogMessage Text+ -> Eff r ()+logTextToHedgehog callStack msg = do+ let LogMessage severity text = msg+ jotTextWithCallStack callStack $ toText severity <> " " <> text++unit :: ()+ => Eff+ [ Log Text+ , Hedgehog+ , Error H.Failure+ , Resource+ , FileSystem+ , Environment+ , Concurrent+ , IOE+ ] ()+ -> H.TestT IO ()+unit f = do+ tvResult <- liftIO IO.newEmptyTMVarIO+ tvAction <- liftIO IO.newEmptyTMVarIO+ CEL.bracket+ do liftIO $ IO.forkFinally+ do f & runLog @Text (ConcUnlift Persistent Unlimited) logTextToHedgehog+ & runHedgehogUnit tvAction+ & runError @H.Failure+ & runResource+ & runFileSystem+ & runEnvironment+ & runConcurrent+ & runEff+ (liftIO . IO.atomically . IO.putTMVar tvResult)+ do liftIO . IO.killThread+ do \_ -> do+ whileNothingM do+ mActionOrResult <- liftIO $ IO.atomically do+ mAction <- CC.tryTakeTMVar tvAction+ case mAction of+ Nothing -> do+ mResult <- CC.tryTakeTMVar tvResult+ case mResult of+ Nothing -> pure Nothing+ Just a -> pure $ Just $ Right a+ Just action -> pure $ Just $ Left action++ case mActionOrResult of+ Nothing -> pure Nothing+ Just (Left action) -> action >> pure Nothing+ Just (Right (Left e)) -> liftIO $ IO.throwIO e+ Just (Right (Right (Left (_, e)))) -> throwAssertion e+ Just (Right (Right (Right a))) -> pure $ Just a++forAll :: ()+ => r <: Concurrent+ => r <: HedgehogGen+ => Show a+ => Gen a+ -> Eff r a+forAll gen = do+ HedgehogGenEnv mvAction <- askHedgehogGenEnv+ mvA <- newEmptyTMVarIO+ atomically $ putTMVar mvAction (H.forAll gen >>= liftIO . IO.atomically . IO.putTMVar mvA)+ atomically $ takeTMVar mvA
− components/hedgehog/Effectful/Zoo/Hedgehog/MonadTestProxy.hs
@@ -1,31 +0,0 @@-module Effectful.Zoo.Hedgehog.MonadTestProxy- ( MonadTestProxy(..),- ) where--import Control.Monad.Trans.Writer.Lazy qualified as MTL-import Data.Functor.Identity-import Effectful-import Effectful.Error.Static-import Effectful.Writer.Static.Local-import Effectful.Zoo.Core-import HaskellWorks.Prelude-import Hedgehog (MonadTest(..))-import Hedgehog.Internal.Property (Failure, Journal)-import Hedgehog.Internal.Property qualified as H--class Monad m => MonadTestProxy m where- liftTestProxy :: H.Test a -> m a--instance MonadTestProxy (H.TestT IO) where- liftTestProxy = liftTest--instance (r <: Error Failure, r <: Writer Journal) => MonadTestProxy (Eff r) where- liftTestProxy = \case- H.TestT m -> do- let (result, journal) = runIdentity $ MTL.runWriterT $ runExceptT m- tell journal- case result of- Left (H.Failure loc err diff) ->- throwError (H.Failure loc err diff)- Right a ->- pure a
+ components/rds-data-test/Effectful/Zoo/RdsData/Test/Cluster.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- HLINT ignore "Redundant id" -}+{- HLINT ignore "Redundant pure" -}+{- HLINT ignore "Use let" -}++module Effectful.Zoo.RdsData.Test.Cluster+ ( RdsClusterDetails(..),+ createRdsDbCluster,+ waitUntilRdsDbClusterAvailable,+ ) where++import Amazonka qualified as AWS+import Amazonka.RDS qualified as AWS+import Amazonka.SecretsManager qualified as AWS+import Data.Aeson ((.=))+import Data.Aeson qualified as J+import Data.ByteString.Base64 qualified as B64+import Data.ByteString.Lazy qualified as LBS+import Data.Function+import Data.Generics.Product.Any+import Data.RdsData.Migration.Types (RdsClusterDetails (RdsClusterDetails))+import Data.Text.Encoding qualified as T+import Data.UUID qualified as UUID+import Data.UUID.V4 qualified as UUID+import Effectful+import Effectful.Concurrent+import Effectful.Zoo.Amazonka.Api.Send+import Effectful.Zoo.Amazonka.Data.AwsError+import Effectful.Zoo.Amazonka.Data.AwsLogEntry+import Effectful.Zoo.Amazonka.Dynamic+import Effectful.Zoo.Core+import Effectful.Zoo.DataLog.Dynamic+import Effectful.Zoo.Error.Static+import Effectful.Zoo.Hedgehog.Api.Assert+import Effectful.Zoo.Hedgehog.Api.Failure+import Effectful.Zoo.Hedgehog.Api.Journal+import Effectful.Zoo.Hedgehog.Effect.Hedgehog+import Effectful.Zoo.TestContainers.LocalStack+import HaskellWorks.Control.Monad+import HaskellWorks.Prelude+import Lens.Micro++createRdsDbCluster :: ()+ => HasCallStack+ => r <: Amazonka+ => r <: Concurrent+ => r <: Error Failure+ => r <: Hedgehog+ => r <: IOE+ => Text+ -> IO Container+ -> Eff r RdsClusterDetails+createRdsDbCluster databaseName getContainer = withFrozenCallStack do+ container <- liftIO getContainer+ jotShowM_ $ getLocalStackEndpoint container+ jotYamlM_ $ inspectContainer container+ masterUsername <- pure "masterUsername"+ masterPassword <- pure "masterPassword"++ let dbClusterId = "my-cluster"++ createDbClusterRequest <-+ pure $+ AWS.newCreateDBCluster dbClusterId "aurora-postgresql"+ & the @"masterUsername" .~ Just masterUsername+ & the @"masterUserPassword" .~ Just masterPassword+ & the @"enableHttpEndpoint" .~ Just True+ & the @"databaseName" .~ Just databaseName++ createDbClusterResponse <-+ sendAws createDbClusterRequest+ & trapFail @AwsError+ & jotShowDataLog @AwsLogEntry+++ -- sendAws :: forall a r. ()+ -- => HasCallStack+ -- => AwsRequest a+ -- => r <: Amazonka+ -- => r <: DataLog AwsLogEntry+ -- => r <: Error AwsError+ -- => r <: IOE+ -- => Typeable (AwsResponse a)+ -- => Typeable a+ -- => a+ -- -> Eff r (AwsResponse a)++ -- jotShowDataLog :: forall i a r. ()+ -- => HasCallStack+ -- => Show i+ -- => r <: Concurrent+ -- => r <: Error Failure+ -- => r <: Hedgehog+ -- => Eff (DataLog i : r) a+ -- -> Eff r a++ let secretName = "my-aurora-cluster"++ secretString <-+ jotYaml $+ J.object+ [ "engine" .= id @Text "aurora-postgresql"+ , "username" .= id @Text masterUsername+ , "password" .= id @Text masterPassword+ , "host" .= id @Text "localhost"+ , "dbname" .= id @Text databaseName+ , "port" .= id @Text "4510"+ ]++ uuid <- liftIO UUID.nextRandom++ let clientRequestToken = T.encodeUtf8 $ UUID.toText uuid++ let secretStringText = T.decodeUtf8 $ LBS.toStrict $ J.encode secretString++ createSecretReq <-+ pure $+ AWS.newCreateSecret secretName+ & the @"secretString" ?~ AWS.Sensitive secretStringText+ & the @"clientRequestToken" ?~ T.decodeUtf8 (B64.encode clientRequestToken)++ createSecetResp <-+ sendAws createSecretReq+ & trapFail+ & jotShowDataLog @AwsLogEntry++ createDbInstanceReq <-+ pure $+ AWS.newCreateDBInstance dbClusterId "my-db-instance" "db.t3.medium"+ & the @"engine" .~ "aurora-postgresql"++ _dbInstanceIdResp <-+ jotShowM $+ sendAws createDbInstanceReq+ & trapFail+ & jotShowDataLog @AwsLogEntry++ pure (RdsClusterDetails createDbClusterResponse createSecetResp)++waitUntilRdsDbClusterAvailable :: ()+ => HasCallStack+ => r <: Amazonka+ => r <: Concurrent+ => r <: DataLog AwsLogEntry+ => r <: Error AWS.Error+ => r <: IOE+ => Text+ -> Eff r ()+waitUntilRdsDbClusterAvailable dbClusterArn =+ withFrozenCallStack do+ repeatNWhileM_ 120 $ \_ -> do+ result <- sendAws $+ AWS.newDescribeDBClusters+ & the @"dbClusterIdentifier" .~ Just dbClusterArn++ let mStatus = result ^? the @"dbClusters" . _Just . each . the @"status" . _Just++ if mStatus == Just "available"+ then threadDelay 1_000_000 >> pure False+ else threadDelay 1_000_000 >> pure True
+ components/rds-data-test/Effectful/Zoo/RdsData/Test/Env.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}++{- HLINT ignore "Redundant pure" -}+{- HLINT ignore "Use let" -}++module Effectful.Zoo.RdsData.Test.Env+ ( AwsResourceArn(..),+ AwsSecretArn(..),+ runLocalTestEnv,+ runTestEnv,+ runReaderFromEnvOrFail,+ runReaderStatementContextFromClusterDetails,+ ) where++import Amazonka qualified as AWS+import Data.Generics.Product.Any+import Data.RdsData.Aws+import Data.RdsData.Migration.Types+import Effectful+import Effectful.Concurrent+import Effectful.Zoo.Amazonka.Api.Run+import Effectful.Zoo.Core+import Effectful.Zoo.Environment+import Effectful.Zoo.Error.Static+import Effectful.Zoo.Hedgehog.Api+import Effectful.Zoo.Hedgehog.Effect.Hedgehog+import Effectful.Zoo.Reader.Static+import Effectful.Zoo.TestContainers.LocalStack+import HaskellWorks.Prelude+import Lens.Micro++runTestEnv :: ()+ => HasCallStack+ => r <: Concurrent+ => r <: Environment+ => r <: Error Failure+ => r <: Hedgehog+ => r <: IOE+ => Eff+ ( Reader AWS.Env+ : Reader AwsResourceArn+ : Reader AwsSecretArn+ : r)+ a+ -> Eff r a+runTestEnv f =+ withFrozenCallStack $ f+ & runReaderAwsEnvDiscover+ & runReaderFromEnvOrFail AwsResourceArn "AURORA_RESOURCE_ARN"+ & runReaderFromEnvOrFail AwsSecretArn "AURORA_SECRET_ARN"++runLocalTestEnv :: ()+ => HasCallStack+ => r <: IOE+ => IO Container+ -> Eff+ ( Reader AWS.Env+ : r)+ a+ -> Eff r a+runLocalTestEnv getContainer f =+ withFrozenCallStack $ f+ & runReaderLocalAwsEnvDiscover getContainer++runReaderFromEnvOrFail :: forall i r a. ()+ => r <: Concurrent+ => r <: Environment+ => r <: Error Failure+ => r <: Hedgehog+ => (Text -> i)+ -> Text+ -> Eff (Reader i ': r) a+ -> Eff r a+runReaderFromEnvOrFail f envVar action = do+ env <- lookupEnv envVar+ & trapFail++ runReader (f env) action++runReaderStatementContextFromClusterDetails :: ()+ => r <: Concurrent+ => r <: Error Failure+ => r <: Hedgehog+ => RdsClusterDetails+ -> Eff (Reader StatementContext : r) a+ -> Eff r a+runReaderStatementContextFromClusterDetails details f = do+ resourceArn <- (details ^. the @"createDbClusterResponse" . the @"dbCluster" . _Just . the @"dbClusterArn")+ & onNothingFail++ secretArn <- (details ^. the @"createSecretResponse" ^. the @"arn")+ & onNothingFail++ mDatabase <- pure $ (details ^? the @"createDbClusterResponse" . the @"dbCluster" . _Just . the @"databaseName" . _Just)+ <&> Database++ statementContext <- pure $ newStatementContext (AwsResourceArn resourceArn) (AwsSecretArn secretArn)+ & the @"database" .~ mDatabase+++ f & runReader statementContext
+ components/rds-data-test/Effectful/Zoo/RdsData/Test/Workspace.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeOperators #-}++module Effectful.Zoo.RdsData.Test.Workspace+ ( localWorkspace,+ ) where++import Data.Text qualified as T+import Effectful+import Effectful.Concurrent+import Effectful.Zoo.Core+import Effectful.Zoo.Environment+import Effectful.Zoo.Error.Static+import Effectful.Zoo.FileSystem+import Effectful.Zoo.Hedgehog.Api+import Effectful.Zoo.Hedgehog.Api.Workspace+import Effectful.Zoo.Hedgehog.Effect.Hedgehog+import Effectful.Zoo.Log.Dynamic+import Effectful.Zoo.Reader.Static+import HaskellWorks.Prelude++localWorkspace :: ()+ => HasCallStack+ => r <: Concurrent+ => r <: Environment+ => r <: Error Failure+ => r <: FileSystem+ => r <: Hedgehog+ => r <: Log Text+ => Text+ -> Eff+ ( Reader Workspace+ : Reader ProjectRoot+ : Reader PackagePath+ : r)+ ()+ -> Eff r ()+localWorkspace prefix f =+ withFrozenCallStack $ do+ cabalProjectDir <- findCabalProjectDir "."++ f & moduleWorkspace (T.unpack prefix)+ & runReader (ProjectRoot cabalProjectDir)+ & runReader (PackagePath "rds-data")
components/rds-data/Effectful/Zoo/RdsData/Core.hs view
@@ -24,8 +24,8 @@ import Effectful.Zoo.Amazonka.Data import Effectful.Zoo.Amazonka.Dynamic import Effectful.Zoo.Core-import Effectful.Zoo.Core.Error.Static import Effectful.Zoo.DataLog.Dynamic+import Effectful.Zoo.Error.Static import Effectful.Zoo.Log.Api import Effectful.Zoo.Log.Dynamic import Effectful.Zoo.RdsData.Errors
+ components/testcontainers-localstack/Effectful/Zoo/TestContainers/LocalStack.hs view
@@ -0,0 +1,139 @@+{- HLINT ignore "Use camelCase" -}++module Effectful.Zoo.TestContainers.LocalStack (+ LocalStackEndpoint (..),+ TC.Container,+ setupContainers,+ setupContainers',+ waitForLocalStack,++ runReaderLocalAwsEnvDiscover,+ getLocalStackEndpoint,+ inspectContainer,+) where++import Amazonka qualified as AWS+import Amazonka.Auth qualified as AWS+import Control.Concurrent (threadDelay)+import Control.Concurrent qualified as IO+import Control.Exception (try)+import Control.Exception qualified as E+import Control.Monad.IO.Class+import Data.Aeson qualified as J+import Data.ByteString.Lazy qualified as LBS+import Data.Function+import Data.Generics.Product.Any+import Data.Text qualified as T+import Data.Time.Clock.POSIX (getPOSIXTime)+import Effectful+import Effectful.Zoo.Core+import Effectful.Zoo.Reader.Static+import Effectful.Zoo.TestContainers.LocalStack.Types (LocalStackEndpoint (LocalStackEndpoint))+import Effectful.Zoo.TestContainers.LocalStack.Types qualified as Z+import HaskellWorks.Prelude+import Lens.Micro+import Network.HTTP.Conduit (HttpException, simpleHttp)+import System.Environment qualified as IO+import System.IO qualified as IO+import TestContainers.Monad qualified as TC+import TestContainers.Tasty qualified as TC++-- | Sets up and runs the containers required for this test suite.+setupContainers ::+ () =>+ (TC.MonadDocker m) =>+ m TC.Container+setupContainers = setupContainers' "localstack/localstack-pro:latest"++-- | Sets up and runs the containers required for this test suite.+setupContainers' ::+ () =>+ (TC.MonadDocker m) =>+ Text ->+ m TC.Container+setupContainers' dockerTag = do+ authToken <- liftIO $ IO.lookupEnv "LOCALSTACK_AUTH_TOKEN"+ -- Launch the container based on the postgres image.+ localstackContainer <-+ TC.run $+ TC.containerRequest (TC.fromTag dockerTag)+ & TC.setEnv [("LOCALSTACK_AUTH_TOKEN", maybe "" T.pack authToken)]+ -- Expose the port 4566 from within the container. The respective port+ -- on the host machine can be looked up using `containerPort` (see below).+ & TC.setExpose+ ( mconcat+ [ [4566]+ ]+ )+ -- Wait until the container is ready to accept requests. `run` blocks until+ -- readiness can be established.+ & TC.setWaitingFor (TC.waitUntilMappedPortReachable 4566)++ -- Look up the corresponding port on the host machine for the exposed port 4566.+ let localStackPort = TC.containerPort localstackContainer 4566++ liftIO $ waitForLocalStack "localhost" localStackPort 100++ pure localstackContainer++waitForLocalStack :: String -> Int -> Int -> IO ()+waitForLocalStack host port timeout = do+ startTime <- getPOSIXTime+ let url = "http://" <> host <> ":" <> show port+ checkLoop startTime url+ where+ checkLoop startTime url = do+ result <- try $ simpleHttp url :: IO (Either HttpException LBS.ByteString)+ case result of+ Right _ -> do+ IO.threadDelay 1_000_000+ IO.putStrLn ""+ Left e -> do+ currentTime <- getPOSIXTime+ let elapsedTime = currentTime - startTime+ when (elapsedTime < fromIntegral timeout) $ do+ threadDelay 500_000+ checkLoop startTime url+ when (elapsedTime >= fromIntegral timeout) $ do+ IO.putStrLn "Timeout reached. LocalStack is not ready."+ E.throw e++runReaderLocalAwsEnvDiscover :: forall a r. ()+ => r <: IOE+ => IO TC.Container+ -> Eff (Reader AWS.Env : r) a+ -> Eff r a+runReaderLocalAwsEnvDiscover mk f = do+ container <- liftIO mk+ ep <- getLocalStackEndpoint container++ logger' <- liftIO $ AWS.newLogger AWS.Debug IO.stdout++ let creds = AWS.fromKeys (AWS.AccessKey "test") (AWS.SecretKey "test")++ credEnv <- liftIO $ AWS.newEnv (AWS.runCredentialChain [pure . creds])++ awsEnv <- pure $+ credEnv+ & the @"logger" .~ logger'+ & the @"overrides" %~ (. AWS.setEndpoint False "localhost" ep.port)++ runReader awsEnv f++getLocalStackEndpoint :: ()+ => TC.Container+ -> Eff r LocalStackEndpoint+getLocalStackEndpoint container = do+ let localStackPort = TC.containerPort container 4566++ pure Z.LocalStackEndpoint+ { Z.host = "0.0.0.0"+ , Z.port = localStackPort+ }++inspectContainer :: ()+ => r <: IOE+ => TC.Container+ -> Eff r J.Value+inspectContainer container =+ liftIO $ TC.runTestContainer TC.defaultDockerConfig $ TC.inspect container
+ components/testcontainers-localstack/Effectful/Zoo/TestContainers/LocalStack/Types.hs view
@@ -0,0 +1,10 @@+module Effectful.Zoo.TestContainers.LocalStack.Types+ ( LocalStackEndpoint(..)+ ) where++import HaskellWorks.Prelude++data LocalStackEndpoint = LocalStackEndpoint+ { host :: String+ , port :: Int+ } deriving stock (Eq, Show, Generic)
effectful-zoo.cabal view
@@ -1,7 +1,7 @@ cabal-version: 3.4 name: effectful-zoo-version: 0.0.2.0+version: 0.0.3.0 synopsis: Effectful effects for testing description: See https://hackage.haskell.org/package/effectful-zoo/docs/effectful-zoo.html category: Development@@ -16,6 +16,11 @@ extra-doc-files: README.md CHANGELOG.md +flag dev+ description: Enable development mode+ manual: True+ default: False+ source-repository head type: git location: https://github.com/haskell-works/effectful-zoo@@ -26,31 +31,49 @@ common aeson-pretty { build-depends: aeson-pretty >= 0.8.10 && < 1 } common amazonka { build-depends: amazonka >= 2 && < 3 } common amazonka-core { build-depends: amazonka-core >= 2 && < 3 }+common amazonka-rds { build-depends: amazonka-rds >= 2 && < 3 } common amazonka-rds-data { build-depends: amazonka-rds-data >= 2 && < 3 }+common amazonka-secretsmanager { build-depends: amazonka-secretsmanager >= 2 && < 3 }+common base64-bytestring { build-depends: base64-bytestring >= 1.2.1 && < 2 }+common binary { build-depends: binary >= 0.8.9 && < 0.9 } common blockfrost-api { build-depends: blockfrost-api >= 0.11 && < 0.12 } common blockfrost-client { build-depends: blockfrost-client >= 0.8 && < 0.9 } common bytestring { build-depends: bytestring >= 0.11 && < 1 }+common directory { build-depends: directory >= 1.2 && < 2 } common effectful { build-depends: effectful >= 2.5 && < 3 } common effectful-core { build-depends: effectful-core >= 2.5 && < 3 } common effectful-plugin { build-depends: effectful-plugin >= 1.1.0.4 && < 2 }+common exceptions { build-depends: exceptions >= 0.10 && < 0.11 }+common filepath { build-depends: filepath >= 1.4 && < 2 } common generic-lens { build-depends: generic-lens >= 2.2.2 && < 3 } common hedgehog { build-depends: hedgehog >= 1.5 && < 2 }+common http-conduit { build-depends: http-conduit >= 2.3.9.1 && < 3 } common HUnit { build-depends: HUnit >= 1.6.2 && < 2 }-common hw-prelude { build-depends: hw-prelude >= 0.0.4.2 && < 0.1 }+common hw-prelude { build-depends: hw-prelude >= 0.0.4.4 && < 0.1 }+common lifted-base { build-depends: lifted-base >= 0.2.3.12 && < 0.3 } common microlens { build-depends: microlens >= 0.4.13.1 && < 0.5 }+common process { build-depends: process >= 1.6.19 && < 2 } common rds-data-codecs { build-depends: rds-data:codecs >= 0.1.1.1 && < 0.2 } common resourcet { build-depends: resourcet >= 1.3 && < 2 } common resourcet-effectful { build-depends: resourcet-effectful >= 1.0.1 && < 2 }-common tasty { build-depends: tasty >= 1.4.3 && < 1.5 }+common stm { build-depends: stm >= 2.5.1 && < 3 }+common tasty { build-depends: tasty >= 1.5 && < 2 }+common tasty-discover { build-depends: tasty-discover >= 5 && < 6 } common tasty-hedgehog { build-depends: tasty-hedgehog >= 1.1.0.0 && < 1.5 }+common temporary { build-depends: temporary >= 1.3 && < 2 }+common testcontainers { build-depends: testcontainers >= 0.5 && < 0.6 } common text { build-depends: text >= 2 && < 3 } common time { build-depends: time >= 1.12 && < 2 } common transformers { build-depends: transformers >= 0.5.6.2 && < 0.7 }+common ulid { build-depends: ulid >= 0.3.2 && < 0.4 }+common uuid { build-depends: uuid >= 1.3.16 && < 2 } common yaml { build-depends: yaml >= 0.11.11.2 && < 1 } -common effectful-zoo-amazonka { build-depends: effectful-zoo:amazonka }-common effectful-zoo-core { build-depends: effectful-zoo:core }-common effectful-zoo-hedgehog { build-depends: effectful-zoo:hedgehog }+common effectful-zoo-amazonka { build-depends: effectful-zoo:amazonka }+common effectful-zoo-core { build-depends: effectful-zoo:core }+common effectful-zoo-hedgehog { build-depends: effectful-zoo:hedgehog }+common effectful-zoo-rds-data { build-depends: effectful-zoo:rds-data }+common effectful-zoo-testcontainers-localstack { build-depends: effectful-zoo:testcontainers-localstack } common project-config default-extensions: AllowAmbiguousTypes@@ -97,21 +120,28 @@ -Wredundant-constraints -Wunused-packages default-language: GHC2021+ if (flag(dev))+ ghc-options: -Werror library core import: base, project-config, aeson, bytestring,+ directory, effectful-core, effectful-core, effectful-plugin, effectful, hw-prelude,+ process,+ resourcet-effectful, text,+ temporary, time, yaml, visibility: public- exposed-modules: Effectful.Zoo.Console.Data.Writer+ exposed-modules: Effectful.Zoo.Aeson+ Effectful.Zoo.Console.Data.Writer Effectful.Zoo.Console.Dynamic Effectful.Zoo.Console.Dynamic.Api Effectful.Zoo.Console.Dynamic.Effect@@ -119,17 +149,19 @@ Effectful.Zoo.Console.Effect Effectful.Zoo.Console.Static Effectful.Zoo.Core- Effectful.Zoo.Core.Error.Dynamic- Effectful.Zoo.Core.Error.Static- Effectful.Zoo.Core.Exception- Effectful.Zoo.Core.Function- Effectful.Zoo.Core.Prim Effectful.Zoo.DataLog.Api Effectful.Zoo.DataLog.Data.DataLogger Effectful.Zoo.DataLog.Data.LogEntry Effectful.Zoo.DataLog.Dynamic Effectful.Zoo.DataLog.Static+ Effectful.Zoo.Environment+ Effectful.Zoo.Error.Dynamic+ Effectful.Zoo.Error.Static+ Effectful.Zoo.Errors.EnvironmentVariableInvalid+ Effectful.Zoo.Errors.EnvironmentVariableMissing+ Effectful.Zoo.Exception Effectful.Zoo.FileSystem+ Effectful.Zoo.Function Effectful.Zoo.Lazy.Dynamic Effectful.Zoo.Log.Api Effectful.Zoo.Log.Api.Generic@@ -142,6 +174,10 @@ Effectful.Zoo.Log.Data.Severity Effectful.Zoo.Log.Dynamic Effectful.Zoo.Log.Static+ Effectful.Zoo.Prim+ Effectful.Zoo.Process+ Effectful.Zoo.Reader.Static+ Effectful.Zoo.Resource Effectful.Zoo.Unsafe ghc-options: -fplugin=Effectful.Plugin hs-source-dirs: components/core@@ -220,32 +256,91 @@ ghc-options: -fplugin=Effectful.Plugin hs-source-dirs: components/rds-data -library hedgehog+library rds-data-test import: base, project-config, aeson,+ amazonka,+ amazonka-rds,+ amazonka-secretsmanager,+ base64-bytestring,+ bytestring,+ effectful,+ effectful-core,+ effectful-plugin,+ effectful-zoo-amazonka,+ effectful-zoo-core,+ effectful-zoo-hedgehog,+ effectful-zoo-testcontainers-localstack,+ generic-lens,+ hw-prelude,+ microlens,+ rds-data-codecs,+ text,+ uuid,+ visibility: public+ exposed-modules: Effectful.Zoo.RdsData.Test.Cluster+ Effectful.Zoo.RdsData.Test.Env+ Effectful.Zoo.RdsData.Test.Workspace+ ghc-options: -fplugin=Effectful.Plugin+ hs-source-dirs: components/rds-data-test++library hedgehog+ import: base, project-config, aeson-pretty,+ aeson,+ binary, bytestring, effectful-core, effectful-plugin, effectful-zoo-core,+ effectful,+ exceptions,+ filepath, hedgehog, hw-prelude,+ lifted-base,+ resourcet,+ stm,+ tasty-discover, tasty-hedgehog, tasty, text,+ time, transformers,+ ulid, yaml, visibility: public exposed-modules: Effectful.Zoo.Hedgehog Effectful.Zoo.Hedgehog.Api Effectful.Zoo.Hedgehog.Api.Assert- Effectful.Zoo.Hedgehog.Api.Classify- Effectful.Zoo.Hedgehog.Api.Eval Effectful.Zoo.Hedgehog.Api.Failure+ Effectful.Zoo.Hedgehog.Api.Gen+ Effectful.Zoo.Hedgehog.Api.Gen.Time+ Effectful.Zoo.Hedgehog.Api.Gen.Ulid+ Effectful.Zoo.Hedgehog.Api.Hedgehog+ Effectful.Zoo.Hedgehog.Api.Internal.Cabal+ Effectful.Zoo.Hedgehog.Api.Internal.Cabal.Types+ Effectful.Zoo.Hedgehog.Api.Internal.FilePath+ Effectful.Zoo.Hedgehog.Api.Internal.OS Effectful.Zoo.Hedgehog.Api.Journal- Effectful.Zoo.Hedgehog.Api.Run- Effectful.Zoo.Hedgehog.Dynamic- Effectful.Zoo.Hedgehog.MonadTestProxy+ Effectful.Zoo.Hedgehog.Api.MonadAssertion+ Effectful.Zoo.Hedgehog.Api.Process+ Effectful.Zoo.Hedgehog.Api.Process.Internal+ Effectful.Zoo.Hedgehog.Api.Range+ Effectful.Zoo.Hedgehog.Api.Stack+ Effectful.Zoo.Hedgehog.Api.Tasty+ Effectful.Zoo.Hedgehog.Api.Tasty.Orphans+ Effectful.Zoo.Hedgehog.Api.Workspace+ Effectful.Zoo.Hedgehog.Data+ Effectful.Zoo.Hedgehog.Data.PackagePath+ Effectful.Zoo.Hedgehog.Data.ProjectRoot+ Effectful.Zoo.Hedgehog.Data.PropertyTest+ Effectful.Zoo.Hedgehog.Data.TestResult+ Effectful.Zoo.Hedgehog.Data.UnitTest+ Effectful.Zoo.Hedgehog.Data.Workspace+ Effectful.Zoo.Hedgehog.Effect.Hedgehog+ Effectful.Zoo.Hedgehog.Effect.HedgehogGen+ Effectful.Zoo.Hedgehog.Effect.Run ghc-options: -fplugin=Effectful.Plugin hs-source-dirs: components/hedgehog @@ -258,16 +353,38 @@ exposed-modules: Effectful.Zoo.HUnit hs-source-dirs: components/hunit -test-suite effectful-zoo-test- import: project-config,+library testcontainers-localstack+ import: base, project-config,+ aeson,+ amazonka,+ bytestring, effectful-core,+ effectful-plugin, effectful-zoo-core,+ generic-lens,+ http-conduit,+ hw-prelude,+ microlens,+ testcontainers,+ text,+ time,+ visibility: public+ exposed-modules: Effectful.Zoo.TestContainers.LocalStack+ Effectful.Zoo.TestContainers.LocalStack.Types+ ghc-options: -fplugin=Effectful.Plugin+ hs-source-dirs: components/testcontainers-localstack++test-suite effectful-zoo-test+ import: base, project-config, effectful-zoo-hedgehog,+ hedgehog, hw-prelude, tasty,+ tasty-discover, type: exitcode-stdio-1.0 main-is: Main.hs- other-modules: Effectful.Zoo.Hedgehog.Test.HedgehogTest+ other-modules: Effectful.Zoo.Hedgehog.Test.PropertySpec+ build-tool-depends: tasty-discover:tasty-discover hs-source-dirs: components/hedgehog-test ghc-options: -threaded -rtsopts