Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,35 @@ jobs:
name: ${{ matrix.os }} / ghc ${{ matrix.ghc }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest] # [macOS-latest, windows-latest] do not come with docker :(
cabal: ["3.12.1.0"]
os:
- ubuntu-latest
- ubuntu-24.04-arm
- macos-26-intel
- windows-latest
cabal:
- "3.14"
ghc:
- 9.2.8
- 9.4.8
- 9.6.4
- 9.8.2
- "9.2"
- "9.12"

steps:
- uses: actions/checkout@v2
if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'

- uses: haskell-actions/setup@v2.6.1
- uses: haskell-actions/setup@v2
id: setup-haskell-cabal
name: Setup Haskell
with:
ghc-version: ${{ matrix.ghc }}
cabal-version: ${{ matrix.cabal }}

- name: Configure
run: cabal configure -O0 --enable-tests --enable-benchmarks --test-show-details=direct

- name: Freeze
run: |
cabal freeze
run: cabal freeze

- uses: actions/cache@v3
name: Cache ~/.cabal/store
Expand All @@ -42,11 +48,17 @@ jobs:
key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}

- name: Build
run: |
cabal configure -O0 --enable-tests --enable-benchmarks --test-show-details=direct
cabal build all
run: cabal build all

- name: Set up Docker
if: runner.os == 'macOS'
uses: docker/setup-docker-action@v5

- name: Test
# GitHub-hosted Windows runners have no Linux Docker daemon, so the
# Docker-dependent test suite cannot run there. The Build step above still
# verifies the library compiles on Windows.
if: runner.os != 'Windows'
run: |
cabal test all

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'

- name: Check code is formatted using Ormolu
run: |
curl -L https://github.com/tweag/ormolu/releases/download/0.7.2.0/ormolu-Linux.zip -o ormolu.zip
curl -L https://github.com/tweag/ormolu/releases/download/0.8.1.0/ormolu-x86_64-linux.zip -o ormolu.zip
unzip ormolu.zip

git ls-files | grep \.hs | xargs ./ormolu --mode=inplace
Expand Down
61 changes: 46 additions & 15 deletions src/TestContainers/Docker.hs
Original file line number Diff line number Diff line change
Expand Up @@ -670,21 +670,39 @@
-- @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)
&
-- 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) =
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1020,7 +1047,7 @@

let resolve endpointHost endpointPort = do
let hints = Socket.defaultHints {Socket.addrSocketType = Socket.Stream}
head <$> Socket.getAddrInfo (Just hints) (Just endpointHost) (Just (show endpointPort))

Check warning on line 1050 in src/TestContainers/Docker.hs

View workflow job for this annotation

GitHub Actions / ubuntu-24.04-arm / ghc 9.12

In the use of ‘head’

Check warning on line 1050 in src/TestContainers/Docker.hs

View workflow job for this annotation

GitHub Actions / windows-latest / ghc 9.12

In the use of ‘head’

Check warning on line 1050 in src/TestContainers/Docker.hs

View workflow job for this annotation

GitHub Actions / macos-26-intel / ghc 9.12

In the use of ‘head’

Check warning on line 1050 in src/TestContainers/Docker.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.12

In the use of ‘head’

open addr = do
socket <-
Expand Down Expand Up @@ -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 =
Expand Down
48 changes: 31 additions & 17 deletions src/TestContainers/Docker/Reaper.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

module TestContainers.Docker.Reaper
( Reaper (..),
Expand All @@ -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)
Expand Down Expand Up @@ -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.
--
Expand All @@ -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
Expand All @@ -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))

Check warning on line 112 in src/TestContainers/Docker/Reaper.hs

View workflow job for this annotation

GitHub Actions / ubuntu-24.04-arm / ghc 9.12

In the use of ‘head’

Check warning on line 112 in src/TestContainers/Docker/Reaper.hs

View workflow job for this annotation

GitHub Actions / windows-latest / ghc 9.12

In the use of ‘head’

Check warning on line 112 in src/TestContainers/Docker/Reaper.hs

View workflow job for this annotation

GitHub Actions / macos-26-intel / ghc 9.12

In the use of ‘head’

Check warning on line 112 in src/TestContainers/Docker/Reaper.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.12

In the use of ‘head’
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))

Check warning on line 118 in src/TestContainers/Docker/Reaper.hs

View workflow job for this annotation

GitHub Actions / ubuntu-24.04-arm / ghc 9.12

In the use of ‘head’

Check warning on line 118 in src/TestContainers/Docker/Reaper.hs

View workflow job for this annotation

GitHub Actions / windows-latest / ghc 9.12

In the use of ‘head’

Check warning on line 118 in src/TestContainers/Docker/Reaper.hs

View workflow job for this annotation

GitHub Actions / macos-26-intel / ghc 9.12

In the use of ‘head’

Check warning on line 118 in src/TestContainers/Docker/Reaper.hs

View workflow job for this annotation

GitHub Actions / ubuntu-latest / ghc 9.12

In the use of ‘head’
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
Expand Down
8 changes: 4 additions & 4 deletions src/TestContainers/Image.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ import TestContainers.Docker as Docker
-- | 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"
14 changes: 6 additions & 8 deletions test/TestContainers/TastySpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,19 +51,17 @@ containers1 = do

_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
Expand All @@ -73,15 +71,15 @@ containers1 = do

_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
(waitForHttp "16686/tcp" "/" [200])

_postgres <-
run $
containerRequest (fromTag "postgres:16-alpine")
containerRequest (fromTag "postgres:17-alpine")
& withCopyFileToContainer "test/data/init-script.sql" "/docker-entrypoint-initdb.d/"

_helloWorld <-
Expand Down
2 changes: 1 addition & 1 deletion testcontainers.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ test-suite tests
, 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
Loading