tmp-proc-0.8.0.0: test/Test/System/TmpProc/DockerSpec.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{- |
Module : Test.System.TmpProc.DockerSpec
Copyright : (c) 2026 Tim Emiola
Maintainer : adetokunbo@users.noreply.github.com
SPDX-License-Identifier: BSD3
Tests for exception handling during docker proc startup, rootless docker
detection, and rootless startup behaviour.
-}
module Test.System.TmpProc.DockerSpec (spec) where
import Control.Exception (AsyncException (..), SomeAsyncException, throwIO, try)
import qualified Data.ByteString.Char8 as C8
import Data.Proxy (Proxy (..))
import qualified Data.Text as Text
import qualified Network.HTTP.Client as HC
import System.Process (readProcess)
import System.TmpProc.Docker
( Pinged (..)
, Proc (..)
, ProcHandle (..)
, hHead
, isRootless
, only
, startup
, toPinged
, withTmpProcs
)
import Test.Hspec
spec :: Spec
spec = do
describe "Rootless docker detection" $ do
it "returns a Bool without throwing in any docker mode" $ do
_ <- isRootless
pure ()
describe "Proc typeclass defaults" $ do
it "containerPort defaults to 0" $
containerPort @AlwaysFailPing `shouldBe` 0
it "uriOfRootless defaults to uriOf ignoring the host port" $
uriOfRootless @AlwaysFailPing "127.0.0.1" 5432
`shouldBe` uriOf @AlwaysFailPing "127.0.0.1"
it "uriOfRootlessWith delegates to uriOfRootless using the first pair's host port" $
uriOfRootlessWith @AlwaysFailPing "127.0.0.1" [(6379, 49153)]
`shouldBe` uriOfRootless @AlwaysFailPing "127.0.0.1" 49153
it "uriOfRootlessWith passes 0 to uriOfRootless when the pairs list is empty" $
uriOfRootlessWith @AlwaysFailPing "127.0.0.1" []
`shouldBe` uriOfRootless @AlwaysFailPing "127.0.0.1" 0
describe "Exception handling during startup" $ do
it "re-throws UserInterrupt during ping and cleans up the container" $ do
result <- try @SomeAsyncException (startup InterruptOnPing)
case result of
Left _ -> pure ()
Right _ -> expectationFailure "expected UserInterrupt to be re-thrown"
out <- readProcess "docker" ["ps", "-a", "--filter", "name=interrupt-on-ping", "--format", "{{.Names}}"] ""
out `shouldNotContain` "interrupt-on-ping"
it "cleans up the container when ping always fails" $ do
startup AlwaysFailPing `shouldThrow` anyIOException
out <- readProcess "docker" ["ps", "-a", "--filter", "name=always-fail-ping", "--format", "{{.Names}}"] ""
out `shouldNotContain` "always-fail-ping"
describe "Rootless docker startup" $ do
it "hUri is host-accessible and startup succeeds in both normal and rootless mode" $ do
rootless <- isRootless
withTmpProcs (only RootlessTest) $ \handles -> do
let h = hHead handles
if rootless
then hUri h `shouldSatisfy` ("http://127.0.0.1:" `C8.isPrefixOf`)
else hUri h `shouldSatisfy` ("http://" `C8.isPrefixOf`)
-- | A 'Proc' whose 'ping' always throws 'UserInterrupt'.
data InterruptOnPing = InterruptOnPing
instance Proc InterruptOnPing where
type Image InterruptOnPing = "alpine"
type Name InterruptOnPing = "interrupt-on-ping"
uriOf _ = ""
reset _ = pure ()
ping _ = throwIO UserInterrupt
pingCount = 1
-- | A 'Proc' whose 'ping' always returns 'NotOK'.
data AlwaysFailPing = AlwaysFailPing
instance Proc AlwaysFailPing where
type Image AlwaysFailPing = "alpine"
type Name AlwaysFailPing = "always-fail-ping"
uriOf _ = ""
reset _ = pure ()
ping _ = pure NotOK
pingCount = 1
{- | A 'Proc' for testing rootless docker support.
Its 'ping' uses 'hUri' rather than 'hAddr', and 'uriOfRootlessWith' builds
a @127.0.0.1:<hostPort>@ URI so that startup succeeds in rootless mode.
-}
data RootlessTest = RootlessTest
instance Proc RootlessTest where
type Image RootlessTest = "kennethreitz/httpbin"
type Name RootlessTest = "rootless-test-proc"
uriOf ip = "http://" <> C8.pack (Text.unpack ip) <> "/"
runArgs = []
reset _ = pure ()
ping h = toPinged @HC.HttpException Proxy $ do
manager <- HC.newManager HC.defaultManagerSettings
req <- HC.parseRequest $ C8.unpack (hUri h) <> "status/200"
HC.httpNoBody req manager
uriOfRootlessWith addr pairs =
case lookup 80 pairs of
Just hp -> "http://127.0.0.1:" <> C8.pack (show hp) <> "/"
Nothing -> "http://" <> C8.pack (Text.unpack addr) <> "/"
pingCount = 6
pingGap = 2000000