testcontainers 0.5.3.0 → 0.5.4.0
raw patch · 6 files changed
+97/−48 lines, 6 filesdep ~tasty-hspecPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: tasty-hspec
API changes (from Hackage documentation)
Files
- CHANGELOG.md +6/−0
- src/TestContainers/Docker.hs +46/−15
- src/TestContainers/Docker/Reaper.hs +31/−17
- src/TestContainers/Image.hs +4/−4
- test/TestContainers/TastySpec.hs +6/−8
- testcontainers.cabal +4/−4
CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for testcontainer-hs +## 0.5.4.0 -- 2026-07-06++* Added support for Windows containers (@nikita-volkov, https://github.com/testcontainers/testcontainers-hs/pull/72)++* Default image versions updated for Redis, Mongo, RabbitMQ, Nginx, Jaeger and Postgres (@nikita-volkov, https://github.com/testcontainers/testcontainers-hs/pull/72)+ ## 0.5.3.0 -- 2026-03-31 * Removed dependency on `optics-core` and `aeson-optics` (@alexbiehl, https://github.com/testcontainers/testcontainers-hs/pull/70)
src/TestContainers/Docker.hs view
@@ -670,11 +670,24 @@ -- @since 0.5.0.0 createRyukReaper :: TestContainer Reaper createRyukReaper = do- dockerSocketLocation <-- liftIO $- lookupEnv "DOCKER_HOST"- <&> (>>= stripPrefix "unix://")- <&> fromMaybe "/var/run/docker.sock"+ -- Ryuk needs access to the Docker daemon socket to reap resources. The way+ -- that socket is exposed depends on whether the daemon runs Linux or Windows+ -- containers. Docker automatically selects the matching ryuk image variant+ -- from the multi-arch manifest based on the daemon's OS.+ dockerSocketMount <- do+ onLinux <- isDockerOnLinux+ if onLinux+ then do+ dockerSocketLocation <-+ liftIO $+ lookupEnv "DOCKER_HOST"+ <&> (>>= stripPrefix "unix://")+ <&> fromMaybe "/var/run/docker.sock"+ pure (pack dockerSocketLocation, "/var/run/docker.sock")+ else+ -- Windows containers talk to the daemon through the named pipe rather+ -- than a unix socket. This is what the windows/amd64 ryuk build expects.+ pure ("\\\\.\\pipe\\docker_engine", "\\\\.\\pipe\\docker_engine") ryukContainer <- run $ containerRequest (fromTag ryukImageTag)@@ -682,9 +695,14 @@ -- Ryuk destroys itself once it reaped the resources, -- no need to register itself with itself. withoutReaper- & setVolumeMounts [(pack dockerSocketLocation, "/var/run/docker.sock")]+ & setVolumeMounts [dockerSocketMount] & setExpose [ryukPort]- & setWaitingFor (waitUntilMappedPortReachable ryukPort)+ -- Wait for ryuk's own startup log rather than probing its port.+ -- A TCP probe (waitUntilMappedPortReachable) registers as a client+ -- that immediately disconnects without sending any labels; ryuk 0.14.0+ -- treats this as a completed session and exits with its 1 ns+ -- reconnection timeout, so the subsequent real connection gets RST.+ & setWaitingFor (waitForLogLine Stdout ("Started" `LazyText.isInfixOf`)) & setRm True let (ryukContainerAddress, ryukContainerPort) =@@ -786,11 +804,20 @@ fromTag :: ImageTag -> ToImage fromTag tag = defaultToImage $ do tracer <- askTracer- output <- docker tracer ["pull", "--quiet", tag]- return $- Image- { tag = strip (pack output)- }+ pullWithRetry tracer 3+ where+ pull tracer = do+ output <- docker tracer ["pull", "--quiet", tag]+ pure $ Image {tag = strip (pack output)}+ pullWithRetry :: Tracer -> Int -> TestContainer Image+ pullWithRetry tracer 0 = pull tracer+ pullWithRetry tracer n = do+ result <- try (pull tracer)+ case result of+ Right image -> pure image+ Left (_ :: DockerException) -> do+ liftIO (threadDelay 2000000)+ pullWithRetry tracer (n - 1) -- | Get an `Image` from an image id. This doesn't run @docker pull@ or any other Docker command -- on construction.@@ -1104,9 +1131,13 @@ waitUntilReady :: Container -> WaitUntilReady -> TestContainer () waitUntilReady container@Container {id} input = do Config {configDefaultWaitTimeout} <- ask- interpreter $ case configDefaultWaitTimeout of- Just seconds -> waitUntilTimeout seconds input- Nothing -> input+ interpreter $ case input of+ WaitUntilTimeout {} ->+ input+ _ ->+ case configDefaultWaitTimeout of+ Just seconds -> waitUntilTimeout seconds input+ Nothing -> input where interpreter :: WaitUntilReady -> TestContainer () interpreter wait =
src/TestContainers/Docker/Reaper.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} module TestContainers.Docker.Reaper ( Reaper (..),@@ -14,6 +15,8 @@ ) where +import Control.Concurrent (threadDelay)+import Control.Exception (IOException, try) import Control.Monad (replicateM) import Control.Monad.Trans.Resource (MonadResource, allocate) import Data.Text (Text, pack, unpack)@@ -58,7 +61,7 @@ -- @since 0.5.0.0 ryukImageTag :: Text ryukImageTag =- "docker.io/testcontainers/ryuk:0.3.4"+ "docker.io/testcontainers/ryuk:0.14.0" -- | Exposed port for the ryuk reaper. --@@ -84,24 +87,12 @@ (_releaseKey, (_socket, ryuk)) <- allocate ( do- let hints =- Socket.defaultHints {Socket.addrSocketType = Socket.Stream}- address <-- head <$> Socket.getAddrInfo (Just hints) (Just (unpack host)) (Just (show port))- socket <-- Socket.socket- (Socket.addrFamily address)- (Socket.addrSocketType address)- (Socket.addrProtocol address)- Socket.connect socket (Socket.addrAddress address)-- -- Construct the reaper and regiter the session with it.- -- Doing it here intead of in the teardown (like we did before)+ socket <- connectWithRetry 60+ -- Construct the reaper and register the session with it.+ -- Doing it here instead of in the teardown (like we did before) -- guarantees the Reaper knows about our session.- let reaper =- newReaper sessionId (Ryuk socket)+ let reaper = newReaper sessionId (Ryuk socket) register reaper sessionIdLabel sessionId- pure (socket, reaper) ) ( \(socket, _ryuk) -> do@@ -111,6 +102,29 @@ ) pure ryuk+ where+ -- On macOS (Docker Desktop / Colima), port-forwarding from the VM to the+ -- host can lag a moment behind the container's own "Started" log line.+ -- Retry the connect rather than failing immediately.+ connectWithRetry :: Int -> IO Socket.Socket+ connectWithRetry 0 = do+ let hints = Socket.defaultHints {Socket.addrSocketType = Socket.Stream}+ address <- head <$> Socket.getAddrInfo (Just hints) (Just (unpack host)) (Just (show port))+ socket <- Socket.socket (Socket.addrFamily address) (Socket.addrSocketType address) (Socket.addrProtocol address)+ Socket.connect socket (Socket.addrAddress address)+ pure socket+ connectWithRetry attemptsLeft = do+ let hints = Socket.defaultHints {Socket.addrSocketType = Socket.Stream}+ address <- head <$> Socket.getAddrInfo (Just hints) (Just (unpack host)) (Just (show port))+ socket <- Socket.socket (Socket.addrFamily address) (Socket.addrSocketType address) (Socket.addrProtocol address)+ result <- try (Socket.connect socket (Socket.addrAddress address))+ case result of+ Right () ->+ pure socket+ Left (_ :: IOException) -> do+ Socket.close socket+ threadDelay 500000+ connectWithRetry (attemptsLeft - 1) newReaper :: -- | Session id
src/TestContainers/Image.hs view
@@ -23,21 +23,21 @@ -- | Image for Redis database. -- -- @--- redis = fromTag "redis:5.0"+-- redis = fromTag "redis:7.4" -- @ -- -- @since 0.1.0.0 redis :: ToImage redis =- fromTag "redis:5.0"+ fromTag "redis:7.4" -- | Image for Mongo database. -- -- @--- mongo = Tag "mongo:4.0.17"+-- mongo = Tag "mongo:7.0" -- @ -- -- @since 0.1.0.0 mongo :: ToImage mongo =- fromTag "mongo:4.0.17"+ fromTag "mongo:7.0"
test/TestContainers/TastySpec.hs view
@@ -3,7 +3,7 @@ module TestContainers.TastySpec (main, test_all) where -import Data.Text.Lazy (isInfixOf)+import qualified Data.Text.Lazy as LazyText import Test.Tasty (TestTree, defaultMain, testGroup) import Test.Tasty.HUnit import TestContainers.Tasty@@ -51,19 +51,17 @@ _rabbitmq <- run $- containerRequest (fromTag "rabbitmq:3.8.4")+ containerRequest (fromTag "rabbitmq:3.13") & setRm False & setExpose [5672] & withNetwork net & withFollowLogs consoleLogConsumer & setWaitingFor- ( waitForLogLine Stdout (("completed with" `isInfixOf`))- <> waitUntilMappedPortReachable 5672- )+ (waitUntilTimeout 300 (waitForLogLine Stdout ("started TCP listener" `LazyText.isInfixOf`))) _nginx <- run $- containerRequest (fromTag "nginx:1.23.1-alpine")+ containerRequest (fromTag "nginx:1.27-alpine") & setExpose [80] & withNetwork net & setWaitingFor@@ -73,7 +71,7 @@ _jaeger <- run $- containerRequest (fromTag "jaegertracing/all-in-one:1.6")+ containerRequest (fromTag "jaegertracing/all-in-one:1.62.0") & setExpose ["5775/udp", "6831/udp", "6832/udp", "5778", "16686/tcp"] & withNetwork net & setWaitingFor@@ -81,7 +79,7 @@ _postgres <- run $- containerRequest (fromTag "postgres:16-alpine")+ containerRequest (fromTag "postgres:17-alpine") & withCopyFileToContainer "test/data/init-script.sql" "/docker-entrypoint-initdb.d/" _helloWorld <-
testcontainers.cabal view
@@ -1,6 +1,6 @@ cabal-version: >=1.10 name: testcontainers-version: 0.5.3.0+version: 0.5.4.0 synopsis: Docker containers for your integration tests. description: testcontainers is a Haskell library that provides a friendly API to@@ -12,7 +12,7 @@ license-file: LICENSE author: Alex Biehl maintainer: alex.biehl@gmail.com-copyright: 2023 Alex Biehl+copyright: 2026 Alex Biehl category: Development build-type: Simple extra-source-files:@@ -21,7 +21,7 @@ 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+ GHC ==9.2 || ==9.12 source-repository head type: git@@ -86,7 +86,7 @@ , hspec >=2.0 && <3.0 , tasty , tasty-discover >=4.2.1 && <6- , tasty-hspec+ , tasty-hspec >=1.1.3 && <1.3 , tasty-hunit , testcontainers , text