packages feed

testcontainers 0.5.1.0 → 0.5.2.0

raw patch · 7 files changed

+170/−51 lines, 7 filesdep ~networknew-uploaderPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: network

API changes (from Hackage documentation)

- TestContainers: [$sel:id:TimeoutException] :: TimeoutException -> ContainerId
- TestContainers: [$sel:id:UnexpectedEndOfPipe] :: UnexpectedEndOfPipe -> ContainerId
- TestContainers: [$sel:port:Port] :: Port -> Int
- TestContainers: [$sel:protocol:Port] :: Port -> Text
- TestContainers: newtype TimeoutException
- TestContainers.Docker: [$sel:id:TimeoutException] :: TimeoutException -> ContainerId
- TestContainers.Docker: [$sel:id:UnexpectedEndOfPipe] :: UnexpectedEndOfPipe -> ContainerId
- TestContainers.Docker: [$sel:port:Port] :: Port -> Int
- TestContainers.Docker: [$sel:protocol:Port] :: Port -> Text
- TestContainers.Docker: newtype TimeoutException
- TestContainers.Tasty: [$sel:id:TimeoutException] :: TimeoutException -> ContainerId
- TestContainers.Tasty: [$sel:id:UnexpectedEndOfPipe] :: UnexpectedEndOfPipe -> ContainerId
- TestContainers.Tasty: [$sel:port:Port] :: Port -> Int
- TestContainers.Tasty: [$sel:protocol:Port] :: Port -> Text
- TestContainers.Tasty: newtype TimeoutException
+ TestContainers: [containerName] :: TimeoutException -> Maybe Text
+ TestContainers: [imageName] :: TimeoutException -> Maybe ImageTag
+ TestContainers: [protocol] :: Port -> Text
+ TestContainers: data TimeoutException
+ TestContainers: prefixedLogConsumer :: Text -> LogConsumer
+ TestContainers: withCopyFileToContainer :: FilePath -> FilePath -> ContainerRequest -> ContainerRequest
+ TestContainers.Docker: [containerName] :: TimeoutException -> Maybe Text
+ TestContainers.Docker: [imageName] :: TimeoutException -> Maybe ImageTag
+ TestContainers.Docker: [protocol] :: Port -> Text
+ TestContainers.Docker: data TimeoutException
+ TestContainers.Docker: prefixedLogConsumer :: Text -> LogConsumer
+ TestContainers.Docker: withCopyFileToContainer :: FilePath -> FilePath -> ContainerRequest -> ContainerRequest
+ TestContainers.Docker.Internal: [containerName] :: DockerException -> Maybe Text
+ TestContainers.Docker.Internal: [imageName] :: DockerException -> Maybe Text
+ TestContainers.Docker.Internal: prefixedLogConsumer :: Text -> LogConsumer
+ TestContainers.Tasty: [containerName] :: TimeoutException -> Maybe Text
+ TestContainers.Tasty: [imageName] :: TimeoutException -> Maybe ImageTag
+ TestContainers.Tasty: [protocol] :: Port -> Text
+ TestContainers.Tasty: data TimeoutException
+ TestContainers.Tasty: prefixedLogConsumer :: Text -> LogConsumer
+ TestContainers.Tasty: withCopyFileToContainer :: FilePath -> FilePath -> ContainerRequest -> ContainerRequest
- TestContainers: InspectOutputMissingNetwork :: ContainerId -> DockerException
+ TestContainers: InspectOutputMissingNetwork :: ContainerId -> Maybe Text -> Maybe Text -> DockerException
- TestContainers: InspectOutputUnexpected :: ContainerId -> DockerException
+ TestContainers: InspectOutputUnexpected :: ContainerId -> Maybe Text -> Maybe Text -> DockerException
- TestContainers: TimeoutException :: ContainerId -> TimeoutException
+ TestContainers: TimeoutException :: ContainerId -> Maybe ImageTag -> Maybe Text -> TimeoutException
- TestContainers: UnknownPortMapping :: ContainerId -> Text -> DockerException
+ TestContainers: UnknownPortMapping :: ContainerId -> Text -> Maybe Text -> Maybe Text -> DockerException
- TestContainers: [id] :: DockerException -> ContainerId
+ TestContainers: [id] :: UnexpectedEndOfPipe -> ContainerId
- TestContainers: type MonadDocker m = (m ~ TestContainer)
+ TestContainers: type MonadDocker (m :: Type -> Type) = m ~ TestContainer
- TestContainers.Docker: InspectOutputMissingNetwork :: ContainerId -> DockerException
+ TestContainers.Docker: InspectOutputMissingNetwork :: ContainerId -> Maybe Text -> Maybe Text -> DockerException
- TestContainers.Docker: InspectOutputUnexpected :: ContainerId -> DockerException
+ TestContainers.Docker: InspectOutputUnexpected :: ContainerId -> Maybe Text -> Maybe Text -> DockerException
- TestContainers.Docker: TimeoutException :: ContainerId -> TimeoutException
+ TestContainers.Docker: TimeoutException :: ContainerId -> Maybe ImageTag -> Maybe Text -> TimeoutException
- TestContainers.Docker: UnknownPortMapping :: ContainerId -> Text -> DockerException
+ TestContainers.Docker: UnknownPortMapping :: ContainerId -> Text -> Maybe Text -> Maybe Text -> DockerException
- TestContainers.Docker: [id] :: DockerException -> ContainerId
+ TestContainers.Docker: [id] :: UnexpectedEndOfPipe -> ContainerId
- TestContainers.Docker: type MonadDocker m = (m ~ TestContainer)
+ TestContainers.Docker: type MonadDocker (m :: Type -> Type) = m ~ TestContainer
- TestContainers.Docker.Internal: InspectOutputMissingNetwork :: ContainerId -> DockerException
+ TestContainers.Docker.Internal: InspectOutputMissingNetwork :: ContainerId -> Maybe Text -> Maybe Text -> DockerException
- TestContainers.Docker.Internal: InspectOutputUnexpected :: ContainerId -> DockerException
+ TestContainers.Docker.Internal: InspectOutputUnexpected :: ContainerId -> Maybe Text -> Maybe Text -> DockerException
- TestContainers.Docker.Internal: UnknownPortMapping :: ContainerId -> Text -> DockerException
+ TestContainers.Docker.Internal: UnknownPortMapping :: ContainerId -> Text -> Maybe Text -> Maybe Text -> DockerException
- TestContainers.Hspec: withContainers :: forall a. TestContainer a -> (a -> IO ()) -> IO ()
+ TestContainers.Hspec: withContainers :: TestContainer a -> (a -> IO ()) -> IO ()
- TestContainers.Monad: type MonadDocker m = (m ~ TestContainer)
+ TestContainers.Monad: type MonadDocker (m :: Type -> Type) = m ~ TestContainer
- TestContainers.Tasty: InspectOutputMissingNetwork :: ContainerId -> DockerException
+ TestContainers.Tasty: InspectOutputMissingNetwork :: ContainerId -> Maybe Text -> Maybe Text -> DockerException
- TestContainers.Tasty: InspectOutputUnexpected :: ContainerId -> DockerException
+ TestContainers.Tasty: InspectOutputUnexpected :: ContainerId -> Maybe Text -> Maybe Text -> DockerException
- TestContainers.Tasty: TimeoutException :: ContainerId -> TimeoutException
+ TestContainers.Tasty: TimeoutException :: ContainerId -> Maybe ImageTag -> Maybe Text -> TimeoutException
- TestContainers.Tasty: UnknownPortMapping :: ContainerId -> Text -> DockerException
+ TestContainers.Tasty: UnknownPortMapping :: ContainerId -> Text -> Maybe Text -> Maybe Text -> DockerException
- TestContainers.Tasty: [id] :: DockerException -> ContainerId
+ TestContainers.Tasty: [id] :: UnexpectedEndOfPipe -> ContainerId
- TestContainers.Tasty: [port] :: DockerException -> Text
+ TestContainers.Tasty: [port] :: Port -> Int
- TestContainers.Tasty: type MonadDocker m = (m ~ TestContainer)
+ TestContainers.Tasty: type MonadDocker (m :: Type -> Type) = m ~ TestContainer
- TestContainers.Tasty: withContainers :: forall a. TestContainer a -> (IO a -> TestTree) -> TestTree
+ TestContainers.Tasty: withContainers :: TestContainer a -> (IO a -> TestTree) -> TestTree

Files

CHANGELOG.md view
@@ -1,5 +1,9 @@ # Revision history for testcontainer-hs +## 0.5.2.0 -- 2025-08-28++* Introduce `withCopyFileToContainer` to copy local files to the container (@LaurentRDC, https://github.com/testcontainers/testcontainers-hs/pull/62)+ ## 0.5.1.0 -- 2025-01-14  * Introduce `withWorkingDirectory` to set the working directory inside a container (@alexbiehl, https://github.com/testcontainers/testcontainers-hs/pull/37)
src/TestContainers.hs view
@@ -31,6 +31,7 @@     M.setMemory,     M.setCpus,     M.withWorkingDirectory,+    M.withCopyFileToContainer,     M.withNetwork,     M.withNetworkAlias,     M.setLink,@@ -41,6 +42,7 @@     -- * Logs     M.LogConsumer,     M.consoleLogConsumer,+    M.prefixedLogConsumer,      -- * @docker network@ parameters     M.NetworkRequest,
src/TestContainers/Docker.hs view
@@ -89,6 +89,7 @@     setRm,     setEnv,     withWorkingDirectory,+    withCopyFileToContainer,     withNetwork,     withNetworkAlias,     setLink,@@ -99,6 +100,7 @@     -- * Following logs     LogConsumer,     consoleLogConsumer,+    prefixedLogConsumer,     withFollowLogs,      -- * Network related functionality@@ -159,7 +161,7 @@  import Control.Concurrent (threadDelay) import Control.Exception (IOException, throw)-import Control.Monad (forM_, replicateM, unless)+import Control.Monad (forM_, replicateM, unless, void) import Control.Monad.Catch   ( Exception,     MonadCatch,@@ -229,6 +231,7 @@     docker,     dockerFollowLogs,     dockerWithStdin,+    prefixedLogConsumer,   ) import TestContainers.Docker.Network   ( Network,@@ -288,7 +291,8 @@     labels :: [(Text, Text)],     noReaper :: Bool,     followLogs :: Maybe LogConsumer,-    workDirectory :: Maybe Text+    workDirectory :: Maybe Text,+    copyFilesToContainer :: [(FilePath, FilePath)]   }  instance WithoutReaper ContainerRequest where@@ -324,7 +328,8 @@       labels = mempty,       noReaper = False,       followLogs = Nothing,-      workDirectory = Nothing+      workDirectory = Nothing,+      copyFilesToContainer = mempty     }  -- | Set the name of a Docker container. This is equivalent to invoking @docker run@@@ -415,6 +420,27 @@ withWorkingDirectory workdir request =   request {workDirectory = Just workdir} +-- | Copies a file from the host to the container. Call this function+-- multiple times to copy multiple files to the container.+--+-- This can be used, for example, to initialize a database:+--+-- >>> :{+--     containerRequest (fromTag "postgres:16-alpine")+--         & withCopyFileToContainer "my-init-script.sql" "/docker-entrypoint-initdb.d/"+-- :}+--+-- @since 0.5.2.0+withCopyFileToContainer ::+  -- | File on the host+  FilePath ->+  -- | Directory in the container+  FilePath ->+  ContainerRequest ->+  ContainerRequest+withCopyFileToContainer fileFromHost containerDirectory request =+  request {copyFilesToContainer = copyFilesToContainer request <> [(fileFromHost, containerDirectory)]}+ -- | Set the network the container will connect to. This is equivalent to passing -- @--network network_name@ to @docker run@. --@@ -556,7 +582,8 @@           labels,           noReaper,           followLogs,-          workDirectory+          workDirectory,+          copyFilesToContainer         } = request    config@Config {configTracer, configCreateReaper} <-@@ -578,35 +605,43 @@         Just . (prefix <>) . ("-" <>) . pack           <$> replicateM 6 (Random.randomRIO ('a', 'z')) -  let dockerRun :: [Text]-      dockerRun =+  -- Instead of using `docker run`, we use the more manual `docker create` + `docker start`.+  -- This allows to get the container ID early from `docker create`, and thus+  -- optionally copy files using `docker cp`.+  let dockerCreate :: [Text]+      dockerCreate =         concat $-          [["run"]]-            ++ [["--detach"]]-            ++ [["--name", containerName] | Just containerName <- [name]]-            ++ [["--label", label <> "=" <> value] | (label, value) <- additionalLabels ++ labels]+          [["create"]]+            ++ [["--cpus", value] | Just value <- [cpus]]             ++ [["--env", variable <> "=" <> value] | (variable, value) <- env]-            ++ [["--publish", pack (show port) <> "/" <> protocol] | Port {port, protocol} <- exposedPorts]+            ++ [["--label", label <> "=" <> value] | (label, value) <- additionalLabels ++ labels]+            ++ [["--link", container] | container <- links]+            ++ [["--memory", value] | Just value <- [memory]]+            ++ [["--name", containerName] | Just containerName <- [name]]             ++ [["--network", networkName] | Just (Right networkName) <- [network]]             ++ [["--network", networkId dockerNetwork] | Just (Left dockerNetwork) <- [network]]             ++ [["--network-alias", alias] | Just alias <- [networkAlias]]-            ++ [["--link", container] | container <- links]-            ++ [["--volume", src <> ":" <> dest] | (src, dest) <- volumeMounts]+            ++ [["--publish", pack (show port) <> "/" <> protocol] | Port {port, protocol} <- exposedPorts]             ++ [["--rm"] | rmOnExit]+            ++ [["--volume", src <> ":" <> dest] | (src, dest) <- volumeMounts]             ++ [["--workdir", workdir] | Just workdir <- [workDirectory]]-            ++ [["--memory", value] | Just value <- [memory]]-            ++ [["--cpus", value] | Just value <- [cpus]]             ++ [[tag]]-            ++ [command | Just command <- [cmd]] -  stdout <- docker configTracer dockerRun+  (id :: ContainerId) <- strip . pack <$> docker configTracer dockerCreate -  let id :: ContainerId-      !id =-        -- N.B. Force to not leak STDOUT String-        strip (pack stdout)+  forM_ copyFilesToContainer $ \(hostFile, containerFile) ->+    docker configTracer ["cp", pack hostFile, id <> ":" <> pack containerFile] -      -- Careful, this is really meant to be lazy+  let dockerStart :: [Text]+      dockerStart =+        concat $+          [["start"]]+            ++ [[id]]+            ++ [command | Just command <- [cmd]]++  void $ docker configTracer dockerStart++  let -- Careful, this is really meant to be lazy       ~inspectOutput =         unsafePerformIO $           internalInspect configTracer id@@ -848,9 +883,13 @@ -- | The exception thrown by `waitUntilTimeout`. -- -- @since 0.1.0.0-newtype TimeoutException = TimeoutException+data TimeoutException = TimeoutException   { -- | The id of the underlying container that was not ready in time.-    id :: ContainerId+    id :: ContainerId,+    -- | The image tag of the container+    imageName :: Maybe ImageTag,+    -- | The container name+    containerName :: Maybe Text   }   deriving (Eq, Show) @@ -859,9 +898,13 @@ -- | The exception thrown by `waitForState`. -- -- @since 0.1.0.0-newtype InvalidStateException = InvalidStateException+data InvalidStateException = InvalidStateException   { -- | The id of the underlying container that was not ready in time.-    id :: ContainerId+    id :: ContainerId,+    -- | The image tag of the container+    imageName :: Maybe ImageTag,+    -- | The container name+    containerName :: Maybe Text   }   deriving stock (Eq, Show) @@ -872,7 +915,7 @@ -- -- @since 0.5.0.0 waitForState :: (State -> Bool) -> WaitUntilReady-waitForState isReady = WaitReady $ \Container {id} -> do+waitForState isReady = WaitReady $ \Container {id, image} -> do   let wait = do         Config {configTracer} <-           ask@@ -880,6 +923,13 @@           internalInspect configTracer id          let state = containerState inspectOutput+            containerName = inspectOutput ^? Optics.key "Name" % Optics._String+            exception =+              InvalidStateException+                { id = id,+                  imageName = Just (imageTag image),+                  containerName = containerName+                }          if isReady state           then pure ()@@ -887,10 +937,10 @@             case stateStatus state of               Exited ->                 -- Once exited, state won't change!-                throwM InvalidStateException {id}+                throwM exception               Dead ->                 -- Once dead, state won't change!-                throwM InvalidStateException {id}+                throwM exception               _ -> do                 liftIO (threadDelay 500000)                 wait@@ -1071,8 +1121,15 @@               timeout (seconds * 1000000) $                 runInIO (interpreter rest)             case result of-              Nothing ->-                throwM $ TimeoutException {id}+              Nothing -> do+                let Container {image, inspectOutput} = container+                    containerName = inspectOutput ^? Optics.key "Name" % Optics._String+                throwM $+                  TimeoutException+                    { id,+                      imageName = Just (imageTag image),+                      containerName = containerName+                    }               Just {} ->                 pure ()         WaitMany first second -> do@@ -1142,13 +1199,19 @@  -- | Get the IP address of a running Docker container using @docker inspect@. internalContainerIp :: Container -> Text-internalContainerIp Container {id, inspectOutput} =+internalContainerIp Container {id, inspectOutput, image} =   case inspectOutput     ^? Optics.key "NetworkSettings"       % Optics.key "IPAddress"       % Optics._String of-    Nothing ->-      throw $ InspectOutputUnexpected {id}+    Nothing -> do+      let containerName = inspectOutput ^? Optics.key "Name" % Optics._String+      throw $+        InspectOutputUnexpected+          { id,+            imageName = Just (imageTag image),+            containerName = containerName+          }     Just address ->       address @@ -1157,7 +1220,7 @@ -- -- @since 0.5.0.0 containerAlias :: Container -> Text-containerAlias Container {id, inspectOutput} =+containerAlias Container {id, inspectOutput, image} =   case inspectOutput     ^? pre       ( Optics.key "NetworkSettings"@@ -1167,10 +1230,13 @@           % Optics.values           % Optics._String       ) of-    Nothing ->+    Nothing -> do+      let containerName = inspectOutput ^? Optics.key "Name" % Optics._String       throw $         InspectOutputMissingNetwork-          { id+          { id,+            imageName = Just (imageTag image),+            containerName = containerName           }     Just alias ->       alias@@ -1180,7 +1246,7 @@ -- -- @since 0.5.0.0 containerGateway :: Container -> Text-containerGateway Container {id, inspectOutput} =+containerGateway Container {id, inspectOutput, image} =   case inspectOutput     ^? pre       ( Optics.key "NetworkSettings"@@ -1189,10 +1255,13 @@           % Optics.key "Gateway"           % Optics._String       ) of-    Nothing ->+    Nothing -> do+      let containerName = inspectOutput ^? Optics.key "Name" % Optics._String       throw $         InspectOutputMissingNetwork-          { id+          { id,+            imageName = Just (imageTag image),+            containerName = containerName           }     Just gatewayIp ->       gatewayIp@@ -1201,7 +1270,7 @@ -- -- @since 0.1.0.0 containerPort :: Container -> Port -> Int-containerPort Container {id, inspectOutput} Port {port, protocol} =+containerPort Container {id, inspectOutput, image} Port {port, protocol} =   let -- TODO also support UDP ports       -- Using IsString so it works both with Text (aeson<2) and Aeson.Key (aeson>=2)       textPort :: (IsString s) => s@@ -1219,11 +1288,14 @@               % Optics._String           ) of         Nothing ->-          throw $-            UnknownPortMapping-              { id,-                port = textPort-              }+          let containerName = inspectOutput ^? Optics.key "Name" % Optics._String+           in throw $+                UnknownPortMapping+                  { id,+                    port = textPort,+                    imageName = Just (imageTag image),+                    containerName = containerName+                  }         Just hostPort ->           read (unpack hostPort) 
src/TestContainers/Docker/Internal.hs view
@@ -20,6 +20,7 @@     Pipe (..),     LogConsumer,     consoleLogConsumer,+    prefixedLogConsumer,     dockerFollowLogs,      -- * Common abstractions for Docker resources@@ -38,6 +39,7 @@ import qualified Data.ByteString as ByteString import Data.Foldable (traverse_) import Data.Text (Text, pack, unpack)+import Data.Text.Encoding (encodeUtf8) import System.Exit (ExitCode (..)) import qualified System.IO import qualified System.Process as Process@@ -81,15 +83,27 @@       }   | InspectUnknownContainerId {id :: ContainerId}   | InspectOutputInvalidJSON {id :: ContainerId}-  | InspectOutputMissingNetwork {id :: ContainerId}-  | InspectOutputUnexpected {id :: ContainerId}+  | InspectOutputMissingNetwork+      { id :: ContainerId,+        imageName :: Maybe Text,+        containerName :: Maybe Text+      }+  | InspectOutputUnexpected+      { id :: ContainerId,+        imageName :: Maybe Text,+        containerName :: Maybe Text+      }   | UnknownPortMapping       { -- | Id of the `Container` that we tried to lookup the         -- port mapping.         id :: ContainerId,         -- | Textual representation of port mapping we were         -- trying to look up.-        port :: Text+        port :: Text,+        -- | The image tag of the container+        imageName :: Maybe Text,+        -- | The container name+        containerName :: Maybe Text       }   deriving (Eq, Show) @@ -157,6 +171,20 @@       ByteString.hPut System.IO.stdout (ByteString.singleton 0x0a)     Stderr -> do       ByteString.hPutStr System.IO.stderr line+      ByteString.hPut System.IO.stderr (ByteString.singleton 0x0a)++-- | A 'LogConsumer' that prefixes with some prefix identifier.+-- This makes it easier to identify which containers/callsites produced which logs.+prefixedLogConsumer :: Text -> LogConsumer+prefixedLogConsumer prefix pipe line = do+  let prefixText = "[" <> prefix <> "] "+      prefixedLine = encodeUtf8 prefixText <> line+  case pipe of+    Stdout -> do+      ByteString.hPutStr System.IO.stdout prefixedLine+      ByteString.hPut System.IO.stdout (ByteString.singleton 0x0a)+    Stderr -> do+      ByteString.hPutStr System.IO.stderr prefixedLine       ByteString.hPut System.IO.stderr (ByteString.singleton 0x0a)  -- | Forwards container logs to a 'LogConsumer'. This is equivalent of calling @docker logs containerId --follow@
test/TestContainers/TastySpec.hs view
@@ -27,6 +27,7 @@     waitUntilMappedPortReachable,     waitUntilTimeout,     withContainers,+    withCopyFileToContainer,     withFollowLogs,     withNetwork,     (&),@@ -77,6 +78,11 @@         & withNetwork net         & setWaitingFor           (waitForHttp "16686/tcp" "/" [200])++  _postgres <-+    run $+      containerRequest (fromTag "postgres:16-alpine")+        & withCopyFileToContainer "test/data/init-script.sql" "/docker-entrypoint-initdb.d/"    _helloWorld <-     run $
+ test/data/init-script.sql view
@@ -0,0 +1,6 @@++create table customers (+     id bigint not null,+     name varchar not null,+     primary key (id)+);
testcontainers.cabal view
@@ -1,6 +1,6 @@ cabal-version:      >=1.10 name:               testcontainers-version:            0.5.1.0+version:            0.5.2.0 synopsis:           Docker containers for your integration tests. description:   testcontainers is a Haskell library that provides a friendly API to@@ -18,6 +18,7 @@ extra-source-files:   CHANGELOG.md   README.md+  test/data/init-script.sql  tested-with:   GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.4 || ==9.4.2 || ==9.8.2@@ -54,7 +55,7 @@     , http-client    >=0.5.14  && <1     , http-types     >=0.12.3  && <1     , mtl            >=2.2.2   && <3-    , network        >=2.8.0   && <3.2+    , network        >=2.8.0   && <3.3     , optics-core    >=0.1     && <0.5     , process        >=1.6.5   && <1.7     , random         >=1.2     && <2