packages feed

testcontainers 0.2.0.0 → 0.3.0.0

raw patch · 6 files changed

+366/−171 lines, 6 files

Files

+ README.md view
@@ -0,0 +1,76 @@+# About++Testcontainers is a Haskell library that provides a friendly API to run Docker+containers. It is designed to create a runtime environment to use during your+integration tests++# Example++``` haskell+{-# LANGUAGE FlexibleContexts  #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}+module Main where++import qualified Test.Tasty           as Tasty+import qualified Test.Tasty.HUnit     as Tasty+import qualified TestContainers.Tasty as TC+++data Endpoints = Endpoints+  {+    redisHost :: String+  , redisPort :: Int+  }+++-- | Sets up and runs the containers required for this test suite.+setupContainers :: TC.MonadDocker m => m Endpoints+setupContainers = do++  -- Launch the container based on the redis:5.0.0 image.+  redisContainer <- TC.run $ TC.containerRequest (TC.fromTag "redis:5.0.0")+    -- Expose the port 6379 from within the container. The respective port+    -- on the host machine can be looked up using `containerPort` (see below).+    TC.& TC.setExpose [ 6379 ]+    -- Wait until the container is ready to accept requests. `run` blocks until+    -- readiness can be established.+    TC.& TC.setWaitingFor (TC.waitUntilMappedPortReachable 6379)++  pure $ Endpoints+    {+      redisHost = "0.0.0.0"+    , redisPort =+        -- Look up the corresponding port on the host machine for the exposed+        -- port 6379.+        TC.containerPort redisContainer 6379+    }+++main :: IO ()+main =+  Tasty.defaultMain $+  -- Use `withContainers` to make the containers available in the closed over+  -- tests. Due to how Tasty handles resources `withContainers` passes down+  -- an IO action `start` to actually start up the containers. `start` can be+  -- invoked multiple times, Tasty makes sure to only start up the containrs+  -- once.+  --+  -- `withContainers` ensures the started containers are shut down correctly+  -- once execution leaves its scope.+  TC.withContainers setupContainers $ \start ->+    Tasty.testGroup "Some test group"+      [+        Tasty.testCase "Redis test" $ do+          -- Actually start the containers!!+          Endpoints {..} <- start+          -- ... assert some properties+          pure ()++      , Tasty.testCase "Another Redis test" $ do+          -- Invoking `start` twice gives the same Endpoints!+          Endpoints {..} <- start+          -- ... assert some properties+          pure ()+      ]+```
src/TestContainers.hs view
@@ -80,9 +80,14 @@    , Config(..)   , defaultDockerConfig-  , dockerForMacConfig   , determineConfig +    -- *++  , Tracer+  , Trace(..)+  , newTracer+     -- * Exceptions    , M.DockerException(..)@@ -91,8 +96,8 @@      -- * Misc. Docker functions -  , dockerVersion-  , isDockerForDesktop+  , dockerHostOs+  , isDockerOnLinux      -- * Predefined Docker images @@ -108,5 +113,3 @@  import           TestContainers.Docker as M import           TestContainers.Image  as M--import           Data.Function         as M ((&))
src/TestContainers/Docker.hs view
@@ -1,12 +1,17 @@-{-# LANGUAGE BangPatterns          #-}-{-# LANGUAGE ConstraintKinds       #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts      #-}-{-# LANGUAGE NamedFieldPuns        #-}-{-# LANGUAGE OverloadedStrings     #-}-{-# LANGUAGE PatternSynonyms       #-}-{-# LANGUAGE RankNTypes            #-}-{-# LANGUAGE ScopedTypeVariables   #-}+{-# LANGUAGE BangPatterns               #-}+{-# LANGUAGE ConstraintKinds            #-}+{-# LANGUAGE DerivingStrategies         #-}+{-# LANGUAGE DuplicateRecordFields      #-}+{-# LANGUAGE FlexibleContexts           #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE NamedFieldPuns             #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE PatternSynonyms            #-}+{-# LANGUAGE RankNTypes                 #-}+{-# LANGUAGE ScopedTypeVariables        #-}+{-# LANGUAGE StandaloneDeriving         #-}+{-# LANGUAGE UndecidableInstances       #-} module TestContainers.Docker   (     MonadDocker@@ -15,9 +20,15 @@    , Config(..)   , defaultDockerConfig-  , dockerForMacConfig   , determineConfig +  -- * Exeuction tracing++  , Tracer+  , Trace(..)+  , newTracer+  , withTrace+   -- * Docker image    , ImageTag@@ -89,8 +100,8 @@    -- * Misc. Docker functions -  , dockerVersion-  , isDockerForDesktop+  , dockerHostOs+  , isDockerOnLinux    -- * Wait until a socket is reachable   , waitUntilMappedPortReachable@@ -102,11 +113,9 @@   , (&)   ) where +import           Control.Applicative          ((<|>)) import           Control.Concurrent           (threadDelay) import           Control.Exception            (IOException, throw)-import           Optics.Operators             ((^?))-import           Optics.Optic                 ((%))-import           Optics.Fold                  (pre) import           Control.Monad.Catch          (Exception, MonadCatch, MonadMask,                                                MonadThrow, bracket, throwM, try) import           Control.Monad.Fix            (mfix)@@ -119,16 +128,18 @@ import           Data.Aeson                   (Value, decode') import qualified Data.Aeson.Optics            as Optics import qualified Data.ByteString.Lazy.Char8   as LazyByteString+import           Data.Foldable                (traverse_) import           Data.Function                ((&)) import           Data.List                    (find)-import           Data.Text                    (Text, isInfixOf, pack, strip,-                                               unpack)+import           Data.Text                    (Text, pack, strip, unpack) import           Data.Text.Encoding.Error     (lenientDecode) import qualified Data.Text.Lazy               as LazyText import qualified Data.Text.Lazy.Encoding      as LazyText-import           GHC.Stack                    (HasCallStack,-                                               withFrozenCallStack)+import           GHC.Stack                    (withFrozenCallStack) import qualified Network.Socket               as Socket+import           Optics.Fold                  (pre)+import           Optics.Operators             ((^?))+import           Optics.Optic                 ((%)) import           Prelude                      hiding (error, id) import qualified Prelude import           System.Exit                  (ExitCode (..))@@ -138,24 +149,66 @@ import           System.Timeout               (timeout)  +-- | Type representing various events during testcontainer execution.+data Trace+  -- | The low-level invocation of @docker@ command+  --+  -- @+  --   TraceDockerInvocation args stdin exitcode+  -- @+  = TraceDockerInvocation [Text] Text ExitCode -- docker [args] [stdin]+  -- | Line written to STDOUT by a Docker process.+  | TraceDockerStdout Text+  -- | Line written to STDERR by a Docker process.+  | TraceDockerStderr Text+  -- | Waiting for a container to become ready. Attached with the+  -- timeout to wait (in seconds).+  | TraceWaitUntilReady (Maybe Int)+  -- | Opening socket+  | TraceOpenSocket Text Int (Maybe IOException)+++deriving stock instance Eq Trace+deriving stock instance Show Trace+++-- | Traces execution within testcontainers library.+newtype Tracer = Tracer { unTracer :: Trace -> IO () }+++deriving newtype instance Semigroup Tracer+deriving newtype instance Monoid Tracer+++-- | Construct a new `Tracer` from a tracing function.+newTracer+  :: (Trace -> IO ())+  -> Tracer+newTracer action = Tracer+  {+    unTracer = action+  }+++withTrace :: MonadIO m => Tracer -> Trace -> m ()+withTrace tracer trace =+  liftIO $ unTracer tracer trace+{-# INLINE withTrace #-}++ -- | Configuration for defaulting behavior. ----- Note that IP address returned by `containerIp` is not reachable when using--- Docker For Mac (See https://docs.docker.com/docker-for-mac/networking/#per-container-ip-addressing-is-not-possible).------ If you are targeting Docker For Mac you should use `dockerForMacConfig`, instead--- use `defaultDockerConfig`.--- -- @since 0.2.0.0 -- data Config = Config   {-    -- | How to retrieve the IP address of a Docker container.-    -- There some known limitations around Docker for Mac in that-    -- it doesn't support accessing containers by their IP (see `containerIp`).+    -- | The number of seconds to maximally wait for a container to+    -- become ready. Default is `Just 60`.     ---    -- This configuration let's you define how to access the IP.-    configContainerIp :: Container -> Text+    -- @Nothing@ <=> waits indefinitely.+    configDefaultWaitTimeout :: Maybe Int+    -- | Traces execution inside testcontainers library.+  , configTracer             :: Tracer   }  @@ -166,32 +219,18 @@ defaultDockerConfig :: Config defaultDockerConfig = Config   {-    configContainerIp = internalContainerIp-  }----- | A default configuration to use with Docker for Mac installations. It doesn't--- use a Docker container's IP address to access a container, instead it always uses--- the loopback interface @0.0.0.0@.------ @since 0.2.0.0----dockerForMacConfig :: Config-dockerForMacConfig = defaultDockerConfig-  {-    configContainerIp = \_ -> "0.0.0.0"+    configDefaultWaitTimeout = Just 60+  , configTracer = mempty   }   -- | Autoselect the default configuration depending on wether you use Docker For--- Mac/Desktop or not.+-- Mac or not. determineConfig :: IO Config-determineConfig = do-  -- TODO We used to be clever here but it turned out that it wouldn't let-  -- hosts reach containers. Even on linux! Figure out how to do Docker-  -- networking properly.-  pure dockerForMacConfig+determineConfig =+  pure defaultDockerConfig + -- | Failing to interact with Docker results in this exception -- being thrown. --@@ -388,8 +427,10 @@       [ [ tag ] ] ++       [ command | Just command <- [cmd] ] -  stdout <- docker dockerRun+  config@Config { configTracer } <- ask +  stdout <- docker configTracer dockerRun+   let     id :: ContainerId     !id =@@ -398,9 +439,8 @@      -- Careful, this is really meant to be lazy     ~inspectOutput = unsafePerformIO $-      internalInspect id+      internalInspect configTracer id -  config <- ask   container <- liftResourceT $ mfix $ \container -> do       -- Note: We have to tie the knot as the resulting container       -- carries the release key as well.@@ -428,9 +468,9 @@ -- -- @since 0.1.0.0 ---docker :: MonadIO m => [Text] -> m String-docker args =-  dockerWithStdin args ""+docker :: MonadIO m => Tracer -> [Text] -> m String+docker tracer args =+  dockerWithStdin tracer args ""   -- | Internal function that runs Docker. Takes care of throwing an exception@@ -438,11 +478,17 @@ -- -- @since 0.1.0.0 ---dockerWithStdin :: MonadIO m => [Text] -> Text -> m String-dockerWithStdin args stdin = liftIO $ do+dockerWithStdin :: MonadIO m => Tracer -> [Text] -> Text -> m String+dockerWithStdin tracer args stdin = liftIO $ do   (exitCode, stdout, stderr) <- Process.readProcessWithExitCode "docker"     (map unpack args) (unpack stdin) +  withTrace tracer (TraceDockerInvocation args stdin exitCode)++  -- TODO output these concurrently with the process+  traverse_ (withTrace tracer . TraceDockerStdout . pack) (lines stdout)+  traverse_ (withTrace tracer . TraceDockerStderr . pack) (lines stderr)+   case exitCode of     ExitSuccess -> pure stdout     _ -> throwM $ DockerException@@ -458,7 +504,8 @@ -- kill :: MonadDocker m => Container -> m () kill Container { id } = do-  _ <- docker [ "kill", id ]+  tracer <- askTracer+  _ <- docker tracer [ "kill", id ]   return ()  @@ -468,7 +515,8 @@ -- stop :: MonadDocker m => Container -> m () stop Container { id } = do-  _ <- docker [ "stop", id ]+  tracer <- askTracer+  _ <- docker tracer [ "stop", id ]   return ()  @@ -478,7 +526,8 @@ -- rm :: MonadDocker m => Container -> m () rm Container { id } = do-  _ <- docker [ "rm", "-f", "-v", id ]+  tracer <- askTracer+  _ <- docker tracer [ "rm", "-f", "-v", id ]   return ()  @@ -563,7 +612,8 @@ -- fromTag :: ImageTag -> ToImage fromTag tag = defaultToImage $ do-  output <- docker [ "pull", "--quiet", tag ]+  tracer <- askTracer+  output <- docker tracer [ "pull", "--quiet", tag ]   return $ Image     {       tag = strip (pack output)@@ -586,7 +636,8 @@           [ "build", "-f", pack dockerfile, pack path ]       | otherwise =           [ "build", pack path ]-  output <- docker args+  tracer <- askTracer+  output <- docker tracer args   return $ Image     {       tag = strip (pack output)@@ -601,7 +652,8 @@   :: Text   -> ToImage fromDockerfile dockerfile = defaultToImage $ do-  output <- dockerWithStdin [ "build", "--quiet", "-" ] dockerfile+  tracer <- askTracer+  output <- dockerWithStdin tracer [ "build", "--quiet", "-" ] dockerfile   return $ Image     {       tag = strip (pack output)@@ -615,33 +667,6 @@ type ContainerId = Text  --- | Dumb logger abstraction to allow us to trace container execution.------ @since 0.1.0.0----data Logger = Logger-  {-    debug :: forall m . (HasCallStack, MonadIO m) => Text -> m ()-  , info  :: forall m . (HasCallStack, MonadIO m) => Text -> m ()-  , warn  :: forall m . (HasCallStack, MonadIO m) => Text -> m ()-  , error :: forall m . (HasCallStack, MonadIO m) => Text -> m ()-  }----- | Logger that doesn't log anything.------ @since 0.1.0.0----silentLogger :: Logger-silentLogger = Logger-  {-    debug = \_ -> pure ()-  , info  = \_ -> pure ()-  , warn  = \_ -> pure ()-  , error = \_ -> pure ()-  }-- -- | A strategy that describes how to asses readiness of a `Container`. Allows -- Users to plug in their definition of readiness. --@@ -649,7 +674,7 @@ -- newtype WaitUntilReady = WaitUntilReady   {-    checkContainerReady :: Logger -> Config -> Container -> ResIO ()+    checkContainerReady :: Config -> Container -> (Maybe Int, ResIO ())   }  @@ -691,15 +716,15 @@ -- @since 0.1.0.0 -- waitUntilTimeout :: Int -> WaitUntilReady -> WaitUntilReady-waitUntilTimeout seconds wait = WaitUntilReady $ \logger config container@Container{ id } -> do-  withRunInIO $ \runInIO -> do-    result <- timeout (seconds * 1000000) $-      runInIO (checkContainerReady wait logger config container)-    case result of-      Nothing ->-        throwM $ TimeoutException { id }-      Just _ ->-        pure ()+waitUntilTimeout seconds wait = WaitUntilReady $ \config container ->+  case checkContainerReady wait config container of+    (Nothing, check) ->+      (Just seconds, check)+    (Just innerTimeout, check)+      | innerTimeout > seconds ->+          (Just seconds, check)+      | otherwise ->+          (Just innerTimeout, check)   -- | Waits until the port of a container is ready to accept connections.@@ -710,19 +735,23 @@ waitUntilMappedPortReachable   :: Int   -> WaitUntilReady-waitUntilMappedPortReachable port = WaitUntilReady $ \logger _config container ->-  withFrozenCallStack $ do+waitUntilMappedPortReachable port = WaitUntilReady $ \config container ->+  withFrozenCallStack $    let+    Config { configTracer } = config++    -- TODO add a parameterizable function when we will support host+    -- mapping exposure     hostIp :: String-    hostIp = unpack (containerIp container)+    hostIp = "0.0.0.0" -    hostPort :: String-    hostPort = show $ containerPort container port+    hostPort :: Int+    hostPort = containerPort container port      resolve = do       let hints = Socket.defaultHints { Socket.addrSocketType = Socket.Stream }-      head <$> Socket.getAddrInfo (Just hints) (Just hostIp) (Just hostPort)+      head <$> Socket.getAddrInfo (Just hints) (Just hostIp) (Just (show hostPort))      open addr = do       socket <- Socket.socket@@ -735,21 +764,21 @@       pure socket      retry = do-      debug logger $ "Trying to open socket to " <> pack hostIp <> ":" <> pack hostPort       result <- try (resolve >>= open)       case result of         Right socket -> do-          debug logger $ "Successfully opened socket to " <> pack hostIp <> ":" <> pack hostPort+          withTrace configTracer (TraceOpenSocket (pack hostIp) hostPort Nothing)           Socket.close socket           pure ()          Left (exception :: IOException) -> do-          debug logger $ "Failed to open socket to " <> pack hostIp <> ":" <>-            pack hostPort <> " with " <> pack (show exception)+          withTrace configTracer+            (TraceOpenSocket (pack hostIp) hostPort (Just exception))           threadDelay 500000           retry -  liftIO retry+  in+    (Nothing, liftIO retry)   -- | A low-level primitive that allows scanning the logs for specific log lines@@ -761,9 +790,12 @@ -- @since 0.1.0.0 -- waitWithLogs :: (Container -> Handle -> Handle -> IO ()) -> WaitUntilReady-waitWithLogs waiter = WaitUntilReady $ \_logger config container -> do-  flip runReaderT config $ withLogs container $ \stdout stderr ->-    liftIO $ waiter container stdout stderr+waitWithLogs waiter = WaitUntilReady $ \config container ->+  let+    check = flip runReaderT config $ withLogs container $ \stdout stderr ->+      liftIO $ waiter container stdout stderr+  in+    (Nothing, check)   -- | A data type indicating which pipe to scan for a specific log line.@@ -818,11 +850,31 @@ -- @since 0.1.0.0 -- waitUntilReady :: MonadDocker m => Container -> WaitUntilReady -> m ()-waitUntilReady container waiter = do-  config <- ask-  liftResourceT $ checkContainerReady waiter silentLogger config container+waitUntilReady container@Container { id } waiter = do+  config@Config { configDefaultWaitTimeout, configTracer } <- ask+  let+    (timeoutInSeconds, check) =+      checkContainerReady waiter config container +    runAction action timeoutSeconds = do+      withTrace configTracer (TraceWaitUntilReady timeoutSeconds)+      action +    withTimeout action = case timeoutInSeconds <|> configDefaultWaitTimeout of+      Nothing ->+        runAction action Nothing+      Just seconds ->+        withRunInIO $ \runInIO -> do+          result <- timeout (seconds * 1000000) $ runInIO (runAction action (Just seconds))+          case result of+            Nothing ->+              throwM $ TimeoutException { id }+            Just _ ->+              pure ()++  liftResourceT (withTimeout check)++ -- | Handle to a Docker image. -- -- @since 0.1.0.0@@ -902,8 +954,8 @@ -- @since 0.1.0.0 -- containerIp :: Container -> Text-containerIp container@Container { config = Config { configContainerIp } } =-  configContainerIp container+containerIp =+  internalContainerIp   -- | Get the IP address of a running Docker container using @docker inspect@.@@ -966,9 +1018,9 @@ -- -- @since 0.1.0.0 ---internalInspect :: (MonadThrow m, MonadIO m) => ContainerId -> m InspectOutput-internalInspect id = do-  stdout <- docker [ "inspect", id ]+internalInspect :: (MonadThrow m, MonadIO m) => Tracer -> ContainerId -> m InspectOutput+internalInspect tracer id = do+  stdout <- docker tracer [ "inspect", id ]   case decode' (LazyByteString.pack stdout) of     Nothing ->       throwM $ InspectOutputInvalidJSON { id }@@ -980,13 +1032,19 @@       Prelude.error "Internal: Multiple results where I expected single result"  -dockerVersion :: (MonadResource m, MonadMask m, MonadIO m) => m Text-dockerVersion = do-  stdout <- docker [ "version", "--format", "{{.Server.KernelVersion}}" ]-  return (pack stdout)+askTracer :: MonadReader Config m => m Tracer+askTracer = do+  Config { configTracer } <- ask+  pure configTracer+{-# INLINE askTracer #-}  -isDockerForDesktop :: (MonadResource m, MonadMask m, MonadIO m) => m Bool-isDockerForDesktop = do-  version <- dockerVersion-  pure $ "linuxkit" `isInfixOf` version+dockerHostOs :: MonadDocker m => m Text+dockerHostOs = do+  tracer <- askTracer+  strip . pack <$> docker tracer [ "version", "--format", "{{.Server.Os}}" ]+++isDockerOnLinux :: MonadDocker m => m Bool+isDockerOnLinux =+  ("linux" ==) <$> dockerHostOs
src/TestContainers/Tasty.hs view
@@ -3,66 +3,123 @@ {-# LANGUAGE ScopedTypeVariables #-} module TestContainers.Tasty   (+    -- * Tasty Ingredient+    ingredient+     -- * Running containers for tests-    withContainers+  , withContainers      -- * Re-exports for convenience   , module Reexports   ) where +import           Control.Applicative                   ((<|>)) import           Control.Monad.IO.Class                (liftIO) import           Control.Monad.Reader                  (runReaderT) import           Control.Monad.Trans.Resource          (InternalState,-                                                        getInternalState,-                                                        runResourceT)+                                                        getInternalState) import           Control.Monad.Trans.Resource.Internal (stateAlloc,                                                         stateCleanup) import           Data.Acquire                          (ReleaseType (ReleaseNormal))-import           Test.Tasty                            (TestTree, withResource)+import           Data.Data                             (Proxy (Proxy))+import           Test.Tasty                            (TestTree, askOption,+                                                        withResource)+import qualified Test.Tasty                            as Tasty+import           Test.Tasty.Ingredients                (Ingredient)+import           Test.Tasty.Options                    (IsOption (..),+                                                        OptionDescription (..),+                                                        mkFlagCLParser,+                                                        safeRead)+import           TestContainers                        as Reexports hiding+                                                                    (Trace) -import           TestContainers                        (Container,-                                                        ContainerRequest,-                                                        MonadDocker, ToImage,-                                                        stop)-import           TestContainers                        as Reexports +newtype DefaultTimeout = DefaultTimeout (Maybe Int) --- | Allow `Tasty.TestTree` to depend on Docker containers. Tasty takes care of--- initialization and de-initialization of the containers.++instance IsOption DefaultTimeout where++  defaultValue =+    DefaultTimeout Nothing++  parseValue =+    fmap (DefaultTimeout . Just) . safeRead++  optionName =+    pure "testcontainers-default-timeout"++  optionHelp =+    pure "The max. number of seconds to wait for a container to become ready"+++newtype Trace = Trace Bool+++instance IsOption Trace where++  defaultValue =+    Trace False++  parseValue =+    const Nothing++  optionCLParser =+    mkFlagCLParser mempty (Trace True)++  optionName =+    pure "testcontainers-trace"++  optionHelp =+    pure "Turns on tracing of the underlying Docker operations"+++-- | Tasty `Ingredient` that adds useful options to control defaults within the+-- TetContainers library. -- -- @------ containers :: MonadDocker m => m ()--- containers = do---   _redis <- TestContainers.run $ TestContainers.containerRequest TestContainers.redis---   _kafka <- TestContainers.run $ TestContainers.containerRequest TestContainers.kafka---   pure ()------ example :: TestTree--- example =---   withContainers containers $ \\runContainers -> testGroup "Example tests"---     [---       testCase "first test" $ do---         -- Actually runs the container.---         runContainers---       testCase "second test" $ do---         -- Start containers. Tasty makes sure to only initialize once as---         --  `first test` might already have started them.---         runContainers---     ]+-- main :: IO ()+-- main = `Tasty.defaultMainWithIngredients` (`ingredient` : `Tasty.defaultIngredients`) tests -- @ ----- `withContainers` allows you naturally scope the handling of containers for your tests.+-- @since 0.3.0.0+--+ingredient :: Ingredient+ingredient = Tasty.includingOptions+  [+    Option (Proxy :: Proxy DefaultTimeout)+  , Option (Proxy :: Proxy Trace)+  ]++ withContainers   :: forall a   .  (forall m. MonadDocker m => m a)   -> (IO a -> TestTree)   -> TestTree withContainers startContainers tests =+  askOption $ \ (DefaultTimeout defaultTimeout) ->+  askOption $ \ (Trace enableTrace) ->   let+    tracer :: Tracer+    tracer+      | enableTrace = newTracer $ \message ->+          putStrLn (show message)+      | otherwise =+          mempty+     runC action = do       config <- determineConfig-      runReaderT (runResourceT action) config++      let+        actualConfig :: Config+        actualConfig = config+          {+            configDefaultWaitTimeout =+              defaultTimeout <|> configDefaultWaitTimeout config+          , configTracer = tracer+          }++      runReaderT (runResourceT action) actualConfig      -- Correct resource handling is tricky here:     -- Tasty offers a bracket alike in IO. We  have
test/Driver.hs view
@@ -1,1 +1,1 @@-{-# OPTIONS_GHC -F -pgmF tasty-discover -optF --tree-display #-}+{-# OPTIONS_GHC -F -pgmF tasty-discover -optF --tree-display -optF --ingredient -optF TestContainers.Tasty.ingredient #-}
testcontainers.cabal view
@@ -1,6 +1,6 @@ cabal-version:       >=1.10 name:                testcontainers-version:             0.2.0.0+version:             0.3.0.0 synopsis:            Docker containers for your integration tests. description:         testcontainers is a Haskell library that provides a friendly API to                      run Docker containers. It is designed to create a runtime environment@@ -13,7 +13,8 @@ copyright:           2020 Alex Biehl category:            Development build-type:          Simple-extra-source-files:  CHANGELOG.md+extra-source-files:  CHANGELOG.md,+                     README.md  source-repository head   type: git