packages feed

tmp-proc 0.7.2.7 → 0.8.0.0

raw patch · 11 files changed

+389/−193 lines, 11 filesdep −doctestdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies removed: doctest

Dependency ranges changed: base

API changes (from Hackage documentation)

+ System.TmpProc.Docker: addrHost :: HostIpAddress -> SvcURI
+ System.TmpProc.Docker: addrPort :: HostIpAddress -> Word16 -> SvcURI
+ System.TmpProc.Docker: containerPort :: Proc a => Word16
+ System.TmpProc.Docker: isRootless :: IO Bool
+ System.TmpProc.Docker: uriOfRootless :: Proc a => HostIpAddress -> Word16 -> SvcURI
+ System.TmpProc.Docker: uriOfRootless' :: Proc a => a -> HostIpAddress -> Word16 -> SvcURI
+ System.TmpProc.Docker: uriOfRootlessWith :: Proc a => HostIpAddress -> [(Word16, Word16)] -> SvcURI
+ System.TmpProc.Docker: uriOfRootlessWith' :: Proc a => a -> HostIpAddress -> [(Word16, Word16)] -> SvcURI

Files

ChangeLog.md view
@@ -2,6 +2,23 @@  `tmp-proc` uses [PVP Versioning][1]. +## 0.8.0.0 -- 2026-10-18++* Add rootless docker support: `Proc` gains `containerPort`, `uriOfRootless`,+  and `uriOfRootlessWith` with defaults; `startupAll` detects rootless mode and+  publishes ports via `-p`/`-P`; `hUri` returns the correct `127.0.0.1:<port>`+  URI in rootless mode without any change to `MkProcHandle` or `hAddr`++* Add `isRootless :: IO Bool` to detect rootless docker mode++* Fix exception handling during startup: re-throw async exceptions (e.g. Ctrl+C)+  from `ping`, protect the container-start window in `startup'`, and clean up the+  docker network on early failure in `startupAll`++* Add `addrHost` and `addrPort` helpers for constructing service URIs++* Remove doctests+ ## 0.7.2.7 -- 2026-04-20  * Relax the upper version bounds of tls
README.lhs view
@@ -2,8 +2,8 @@  [![GitHub CI](https://github.com/adetokunbo/tmp-proc/actions/workflows/cabal.yml/badge.svg)](https://github.com/adetokunbo/tmp-proc/actions) [![Stackage Nightly][stackage-nightly-badge]][stackage-nightly]+[![Stackage LTS][stackage-lts-badge]][stackage-lts] [![Hackage][hackage-badge]][hackage]-[![Hackage Dependencies][hackage-deps-badge]][hackage-deps] [![BSD-3.0 license](https://img.shields.io/badge/license-BSD--3.0-blue.svg)](https://github.com/adetokunbo/tmp-proc/blob/master/tmp-proc/LICENSE)  `tmp-proc` is a small library designed to simplify integration tests that use@@ -222,8 +222,10 @@ [18]: https://hackage.haskell.org/package/tmp-proc-0.5.0.1/docs/System-TmpProc-Docker.html#v:ping  [hackage-deps-badge]:     <https://img.shields.io/hackage-deps/v/tmp-proc.svg>-[hackage-deps]:           <http://packdeps.haskellers.com/feed?needle=tmp-proc> [hackage-badge]:          <https://img.shields.io/hackage/v/tmp-proc.svg> [hackage]:                <https://hackage.haskell.org/package/tmp-proc>+[stackage-lts]:           <http://stackage.org/lts/package/tmp-proc>+[stackage-lts-badge]:     <http://stackage.org/package/tmp-proc/badge/lts> [stackage-nightly-badge]: <http://stackage.org/package/tmp-proc/badge/nightly> [stackage-nightly]:       <http://stackage.org/nightly/package/tmp-proc>+
− doctest/Main.hs
@@ -1,7 +0,0 @@--- file doctests.hs-import Test.DocTest--main :: IO ()-main = doctest ["-isrc", "src/System/TmpProc/TypeLevel/Sort.hs",-                "-isrc", "src/System/TmpProc/TypeLevel.hs"-               ]
src/System/TmpProc/Docker.hs view
@@ -81,6 +81,8 @@   , startup   , nameOf   , uriOf'+  , uriOfRootless'+  , uriOfRootlessWith'   , runArgs'   , Pinged (..)   , toPinged@@ -131,11 +133,14 @@      -- * Docker status   , hasDocker+  , isRootless   , genNetworkName      -- * Aliases   , HostIpAddress   , SvcURI+  , addrHost+  , addrPort      -- * Re-exports   , module System.TmpProc.TypeLevel@@ -145,16 +150,21 @@ import Control.Concurrent (threadDelay) import Control.Exception   ( Exception+  , Handler (..)+  , IOException+  , SomeAsyncException   , SomeException   , bracket   , catch+  , catches   , onException+  , throwIO   ) import Control.Monad (void, when) import qualified Data.ByteString.Char8 as C8 import Data.Kind (Type)-import Data.List (dropWhileEnd)-import Data.Maybe (fromMaybe, isJust)+import Data.List (dropWhileEnd, isInfixOf)+import Data.Maybe (fromMaybe, isJust, mapMaybe) import Data.Proxy (Proxy (..)) import Data.Text (Text) import qualified Data.Text as Text@@ -206,8 +216,21 @@   , (&:)   , (&:&)   )+import Text.Read (readMaybe)  +-- | Determine if docker is running in rootless mode.+isRootless :: IO Bool+isRootless =+  ( ("name=rootless" `isInfixOf`)+      <$> readProcess+        "docker"+        ["info", "--format", "{{range .SecurityOptions}}{{println .}}{{end}}"]+        ""+  )+    `catch` (\(_ :: IOException) -> pure False)++ -- | Determine if the docker daemon is accessible. hasDocker :: IO Bool hasDocker = do@@ -299,17 +322,20 @@ -} startupAll :: (AreProcs procs) => HList procs -> IO (HandlesOf procs) startupAll ps = do+  rootless <- isRootless+  when rootless $ printDebug "tmp.proc: rootless docker detected; using host port mappings"   name <- genNetworkName   let     name' = Text.unpack name+    removeNetwork = runDockerProcQuietly (removeNetworkArgs name')     go :: SomeProcs as -> HList as -> IO (HandlesOf as)     go SomeProcsNil HNil = pure HNil     go (SomeProcsCons cons) (x `HCons` y) = do       others <- go cons y-      h <- startup' (Just name) (mapSlim others) x `onException` terminateAll others+      h <- startup' rootless (Just name) (mapSlim others) x `onException` terminateAll others       pure $ h `HCons` others   void $ readProcess "docker" (createNetworkArgs name') ""-  go procProof ps+  go procProof ps `onException` removeNetwork   {-# DEPRECATED netwStartupAll "since v0.7 this is no longer needed and will be removed, use startupAll instead" #-}@@ -343,23 +369,21 @@  -- | Start up processes for each 'Proc' type. startupAll' :: (AreProcs procs) => Maybe Text -> HList procs -> IO (NetworkHandlesOf procs)-startupAll' ntwkMb ps =+startupAll' ntwkMb ps = do+  rootless <- isRootless   let     mayCreateNetwork = case ntwkMb of       Nothing -> pure ()       Just name -> void $ readProcess "docker" (createNetworkArgs $ Text.unpack name) ""     wrap x = (fromMaybe "" ntwkMb, x)-     go :: SomeProcs as -> HList as -> IO (HandlesOf as)     go SomeProcsNil HNil = pure HNil     go (SomeProcsCons cons) (x `HCons` y) = do       others <- go cons y-      h <- startup' ntwkMb (mapSlim others) x `onException` terminateAll others+      h <- startup' rootless ntwkMb (mapSlim others) x `onException` terminateAll others       pure $ h `HCons` others-   in-    do-      mayCreateNetwork-      wrap <$> go procProof ps+  mayCreateNetwork+  wrap <$> go procProof ps   -- | Terminate all processes owned by some @'ProcHandle's@.@@ -404,14 +428,14 @@ terminate :: ProcHandle p -> IO () terminate handle = do   let pid = hPid handle-  void $ readProcess "docker" ["stop", pid] ""-  void $ readProcess "docker" ["rm", pid] ""+  runDockerProcQuietly ["stop", pid]+  runDockerProcQuietly ["rm", pid]   mphTidy handle   {- | Prepare resources for use by a  @'Proc'@ - Preparation occurs before the docker container is a launched; once the+ Preparation occurs before the docker container is launched; once the  resources are set up, they are located using the @prepared@ datatype.   Usually, this means it will be used by 'toRunCmd' to provide additional@@ -431,6 +455,9 @@     -- | Clean up any resources associated with @prepared@+  --+  --   'tidy' is called exactly once per 'prepare' invocation — implementations+  --   do not need to be idempotent.   tidy :: a -> prepared -> IO ()  @@ -497,6 +524,30 @@   uriOf :: HostIpAddress -> SvcURI  +  -- | The port the service listens on inside the container.+  --   When @0@ (default), rootless mode publishes all @EXPOSE@d ports via @-P@.+  --   Set to a specific port to use @-p@ instead, for images with many @EXPOSE@s.+  containerPort :: Word16+  containerPort = 0+++  -- | The URI to use when accessing the service from the host in rootless mode.+  --   Receives the host port for 'containerPort' (or @0@ if no mapping was found).+  --   Defaults to 'uriOf', ignoring the host port; override to incorporate it.+  uriOfRootless :: HostIpAddress -> Word16 -> SvcURI+  uriOfRootless addr _ = uriOf @a addr+++  -- | Like 'uriOfRootless' but receives all published port mappings as+  --   @(containerPort, hostPort)@ pairs. Defaults to delegating to 'uriOfRootless'+  --   using the host port of the first pair. Override for multi-port images.+  uriOfRootlessWith :: HostIpAddress -> [(Word16, Word16)] -> SvcURI+  uriOfRootlessWith addr pairs =+    uriOfRootless @a addr $ case pairs of+      (_, hp) : _ -> hp+      [] -> 0++   -- | Resets some state in a tmp proc service.   reset :: ProcHandle a -> IO () @@ -560,6 +611,26 @@ uriOf' _ = uriOf @a  +-- | Simplifies use of 'uriOfRootless'.+uriOfRootless' :: forall a. (Proc a) => a -> HostIpAddress -> Word16 -> SvcURI+uriOfRootless' _ = uriOfRootless @a+++-- | Simplifies use of 'uriOfRootlessWith'.+uriOfRootlessWith' :: forall a. (Proc a) => a -> HostIpAddress -> [(Word16, Word16)] -> SvcURI+uriOfRootlessWith' _ = uriOfRootlessWith @a+++-- | Encode a 'HostIpAddress' as a 'SvcURI' fragment for embedding in URIs.+addrHost :: HostIpAddress -> SvcURI+addrHost = C8.pack . Text.unpack+++-- | Format an address and port as @host:port@ for embedding in a 'SvcURI'.+addrPort :: HostIpAddress -> Word16 -> SvcURI+addrPort addr port = addrHost addr <> ":" <> C8.pack (show port)++ -- | The full args of a @docker run@ command for starting up a 'Proc'. dockerCmdArgs ::   forall a prepared.@@ -567,13 +638,31 @@   a ->   prepared ->   Maybe Text ->+  Bool ->   [Text]-dockerCmdArgs x prep ntwkMb =+dockerCmdArgs x prep ntwkMb rootless =   let toNetworkArgs n = ["--network", n]       networkArg = maybe mempty toNetworkArgs ntwkMb-   in ["run", "-d"] <> nameArg @a <> networkArg <> toRunCmd x prep <> [imageText' @a]+      portArg+        | not rootless = []+        | containerPort @a /= 0 = ["-p", Text.pack (show (containerPort @a))]+        | otherwise = ["-P"]+   in ["run", "-d"] <> nameArg @a <> networkArg <> portArg <> toRunCmd x prep <> [imageText' @a]  +inspectPortPairs :: String -> IO [(Word16, Word16)]+inspectPortPairs pid = do+  output <- readProcess "docker" ["port", pid] ""+  pure $ mapMaybe parseLine (lines output)+  where+    parseLine line = case words line of+      [portProto, "->", endpoint] ->+        let cPortStr = takeWhile (/= '/') portProto+            hPortStr = reverse $ takeWhile (/= ':') (reverse endpoint)+         in (,) <$> readMaybe cPortStr <*> readMaybe hPortStr+      _ -> Nothing++ imageText' :: forall a. (Proc a) => Text imageText' = Text.pack $ symbolVal $ Proxy @(Image a) @@ -610,42 +699,51 @@  startup' ::   (ProcPlus a prepared) =>+  Bool ->   Maybe Text ->   [SlimHandle] ->   a ->   IO (ProcHandle a)-startup' mphNetwork addrs x = do+startup' rootless mphNetwork addrs x = do   x' <- prepare addrs x-  let fullArgs = map Text.unpack $ dockerCmdArgs x x' mphNetwork+  let fullArgs = map Text.unpack $ dockerCmdArgs x x' mphNetwork rootless       isGarbage = flip elem ['\'', '\n']       trim = dropWhileEnd isGarbage . dropWhile isGarbage   printDebug $ Text.pack $ show fullArgs   runCmd <- createDockerCmdProcess fullArgs   mphPid <- trim <$> readCreateProcess runCmd ""-  mphAddr <--    Text.pack . trim-      <$> readProcess-        "docker"-        [ "inspect"-        , mphPid-        , "--format"-        , "'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'"-        ]-        ""-  let h =-        MkProcHandle-          { mphProc = x+  let stopByPid = do+        runDockerProcQuietly ["stop", mphPid]+        runDockerProcQuietly ["rm", mphPid]+        tidy x x'+  flip onException stopByPid $ do+    mphAddr <-+      Text.pack . trim+        <$> readProcess+          "docker"+          [ "inspect"           , mphPid-          , mphUri = uriOf' x mphAddr-          , mphAddr-          , mphNetwork-          , mphTidy = tidy x x'-          }-  (nPings h `onException` terminate h) >>= \case-    OK -> pure h-    pinged -> do-      terminate h-      fail $ pingedMsg x pinged+          , "--format"+          , "'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'"+          ]+          ""+    portPairs <- if rootless then inspectPortPairs mphPid else pure []+    let mphUri =+          if rootless+            then uriOfRootlessWith' x "127.0.0.1" portPairs+            else uriOf' x mphAddr+    let h =+          MkProcHandle+            { mphProc = x+            , mphPid+            , mphUri+            , mphAddr+            , mphNetwork+            , mphTidy = tidy x x'+            }+    nPings h >>= \case+      OK -> pure h+      pinged -> fail $ pingedMsg x pinged   pingedMsg :: (Proc a) => a -> Pinged -> String@@ -686,11 +784,12 @@      ping' x =       ping x-        `catch` ( \(e :: SomeException) -> do-                    let errMsg = Text.pack $ show e-                    badErr errMsg-                    pure $ PingFailed errMsg-                )+        `catches` [ Handler $ \(e :: SomeAsyncException) -> throwIO e+                  , Handler $ \(e :: SomeException) -> do+                      let errMsg = Text.pack $ show e+                      badErr errMsg+                      pure $ PingFailed errMsg+                  ]      go n =       ping' h >>= \case@@ -1075,6 +1174,10 @@ printDebug t = do   canPrint <- showDebug   when canPrint $ Text.hPutStrLn stderr t+++runDockerProcQuietly :: [String] -> IO ()+runDockerProcQuietly args = (`catch` (\(_ :: IOException) -> pure ())) $ void $ readProcess "docker" args ""   {-# DEPRECATED genNetworkName "since v0.7 this is no longer needs to be exported and will be hidden in later releases" #-}
src/System/TmpProc/TypeLevel.hs view
@@ -68,20 +68,13 @@ import System.TmpProc.TypeLevel.Sort  -{- $setup->>> import Data.Proxy->>> :set -XDataKinds->>> :set -XTypeApplications--}-- -- | Obtain the first element of a 'HList'. hHead :: HList (a ': as) -> a hHead (x `HCons` _) = x   -- | Get an item in an 'HList' given its type.-hOf :: forall y xs. IsInProof y xs => Proxy y -> HList xs -> y+hOf :: forall y xs. (IsInProof y xs) => Proxy y -> HList xs -> y hOf proxy = go proxy provedIsIn   where     go :: Proxy x -> IsIn x ys -> HList ys -> x@@ -173,26 +166,21 @@   lookupProof = AtHead @k @t @kvs  -instance MemberKV k t kvs => MemberKV k t (KV ok ot ': kvs) where+instance (MemberKV k t kvs) => MemberKV k t (KV ok ot ': kvs) where   lookupProof = OtherKeys lookupProof   {- | Select an item from an 'HList' of @'KV's@ by /key/. - /N.B/ Returns the first item. It assumes the keys in the KV HList are unique. /TODO:/ enforce this rule using a constraint. --==== __Examples__--->>> select @"d" @Double  @'[KV "b" Bool, KV "d" Double] (V True &:  V (3.1 :: Double) &: HNil)-3.1+For example, @select \@\"d\" \@Double@ on @(V True &: V (3.1 :: Double) &: HNil)@+typed as @HList \'[KV \"b\" Bool, KV \"d\" Double]@ returns @3.1@. -} select ::   forall k t xs.-  MemberKV k t xs =>+  (MemberKV k t xs) =>   HList xs ->   t select = go $ lookupProof @k @t @xs@@ -226,11 +214,11 @@   manyProof = FirstOfMany @k @t @ks  -instance {-# OVERLAPPING #-} ManyMemberKV ks ts kvs => ManyMemberKV (k ': ks) (t ': ts) (KV k t ': kvs) where+instance {-# OVERLAPPING #-} (ManyMemberKV ks ts kvs) => ManyMemberKV (k ': ks) (t ': ts) (KV k t ': kvs) where   manyProof = NextOfMany manyProof  -instance ManyMemberKV ks ts kvs => ManyMemberKV ks ts (KV ok ot ': kvs) where+instance (ManyMemberKV ks ts kvs) => ManyMemberKV ks ts (KV ok ot ': kvs) where   manyProof = ManyOthers manyProof  @@ -241,15 +229,12 @@ The keys must be provided in the same order as they occur in the HList, any other order will likely result in an compiler error. -==== __Examples__--->>> selectMany @'["b"] @'[Bool] @'[KV "b" Bool, KV "d" Double] (V True &:  V (3.1 :: Double) &: HNil)-True &: HNil+For example, @selectMany \@\'[\"b\"] \@\'[Bool]@ on @(V True &: V (3.1 :: Double) &: HNil)@+typed as @HList \'[KV \"b\" Bool, KV \"d\" Double]@ returns @(True &: HNil) :: HList \'[Bool]@. -} selectMany ::   forall ks ts xs.-  ManyMemberKV ks ts xs =>+  (ManyMemberKV ks ts xs) =>   HList xs ->   HList ts selectMany = go $ manyProof @ks @ts @xs@@ -262,14 +247,8 @@  {- | Allows reordering of similar @'HList's@. -==== __Examples__--->>> hReorder @_ @'[Bool, Int] ('c' &: (3 :: Int) &: True &: (3.1 :: Double) &: HNil)-True &: 3 &: HNil-->>> hReorder @_ @'[Double, Bool, Int] ('c' &: (3 :: Int) &: True &: (3.1 :: Double) &: HNil)-3.1 &: True &: 3 &: HNil+For example, @hReorder \@_ \@\'[Bool, Int]@ on @(\'c\' &: (3 :: Int) &: True &: (3.1 :: Double) &: HNil)@+returns @True &: 3 &: HNil@. -} class ReorderH xs ys where   hReorder :: HList xs -> HList ys@@ -298,5 +277,5 @@   provedIsIn = IsHead @t @tys  -instance IsInProof t tys => IsInProof t (a : tys) where+instance (IsInProof t tys) => IsInProof t (a : tys) where   provedIsIn = IsInTail provedIsIn
src/System/TmpProc/TypeLevel/Sort.hs view
@@ -27,42 +27,32 @@ This is an internal module that provides type-level functions used in various constraints in "System.TmpProc.Docker". -}-module System.TmpProc.TypeLevel.Sort (-  -- * Merge sort for @Symbols@.-  SortSymbols,--  -- * Sort combinators-  Take,-  Drop,-  LengthOf,-  HalfOf,-) where--import GHC.TypeLits (-  CmpNat,-  CmpSymbol,-  Nat,-  Symbol,-  type (*),-  type (+),-  type (-),- )+module System.TmpProc.TypeLevel.Sort+  ( -- * Merge sort for @Symbols@.+    SortSymbols +    -- * Sort combinators+  , Take+  , Drop+  , LengthOf+  , HalfOf+  )+where -{- $setup->>> import Data.Proxy->>> :set -XDataKinds->>> :set -XTypeFamilies--}+import GHC.TypeLits+  ( CmpNat+  , CmpSymbol+  , Nat+  , Symbol+  , type (*)+  , type (+)+  , type (-)+  )   {- | Takes 1 element at a time from a list until the desired length is reached. -==== __Examples__-->>> :kind! Take '["a", "b", "c", "d"] 2-Take '["a", "b", "c", "d"] 2 :: [Symbol]-= '["a", "b"]+For example, @Take \'[\"a\", \"b\", \"c\", \"d\"] 2@ reduces to @\'[\"a\", \"b\"]@. -} type family Take (xs :: [k]) (n :: Nat) :: [k] where   Take '[] n = '[]@@ -72,16 +62,8 @@  {- | Drops 1 element at a time until the the dropped target is reached. -==== __Examples__-->>> :kind! Drop '["a", "b", "c", "d"] 2-Drop '["a", "b", "c", "d"] 2 :: [Symbol]-= '["c", "d"]--->>> :kind! Drop '["a"] 2-Drop '["a"] 2 :: [Symbol]-= '[]+For example, @Drop \'[\"a\", \"b\", \"c\", \"d\"] 2@ reduces to @\'[\"c\", \"d\"]@,+and @Drop \'[\"a\"] 2@ reduces to @\'[]@. -} type family Drop (xs :: [k]) (n :: Nat) :: [k] where   Drop '[] n = '[]@@ -91,11 +73,7 @@  {- | Counts a list, 1 element at a time. -==== __Examples__-->>> :kind! CmpNat 4 (LengthOf '[1, 2, 3, 4])-CmpNat 4 (LengthOf '[1, 2, 3, 4]) :: Ordering-= 'EQ+For example, @LengthOf \'[1, 2, 3, 4]@ reduces to @4@. -} type family LengthOf (xs :: [k]) :: Nat where   LengthOf '[] = 0@@ -107,15 +85,7 @@ N.B: maximum value that this works for depends on the reduction limit of the type-checker. -==== __Examples__-->>> :kind! CmpNat 49 (HalfOf 99)-CmpNat 49 (HalfOf 99) :: Ordering-= 'EQ-->>> :kind! CmpNat 50 (HalfOf 100)-CmpNat 50 (HalfOf 100) :: Ordering-= 'EQ+For example, @HalfOf 99@ reduces to @49@ and @HalfOf 100@ reduces to @50@. -} type family HalfOf (n :: Nat) :: Nat where   -- optimizations for faster compilation@@ -141,11 +111,7 @@  {- | Sort a list of type-level @symbols@ using merge sort. -==== __Examples__-->>> :kind! SortSymbols '["xyz", "def", "abc"]-SortSymbols '["xyz", "def", "abc"] :: [Symbol]-= '["abc", "def", "xyz"]+For example, @SortSymbols \'[\"xyz\", \"def\", \"abc\"]@ reduces to @\'[\"abc\", \"def\", \"xyz\"]@. -} type family SortSymbols (xs :: [Symbol]) :: [Symbol] where   SortSymbols '[] = '[]
test/Spec.hs view
@@ -5,13 +5,15 @@ import           Test.Hspec  import           System.IO-import qualified Test.System.TmpProc.HttpBinSpec as HB-import qualified Test.System.TmpProc.WarpSpec    as W3+import qualified Test.System.TmpProc.DockerSpec   as D+import qualified Test.System.TmpProc.HttpBinSpec  as HB+import qualified Test.System.TmpProc.WarpSpec     as W3  main :: IO () main = do   hSetBuffering stdin NoBuffering   hSetBuffering stdout NoBuffering   hspec $ do+    D.spec     HB.spec     W3.spec
test/Test/HttpBin.hs view
@@ -6,12 +6,21 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} +{- |+Module      : Test.HttpBin+Copyright   : (c) 2026 Tim Emiola+Maintainer  : adetokunbo@users.noreply.github.com+SPDX-License-Identifier: BSD3++Test helper types and functions for testing with an httpbin container.+-} module Test.HttpBin where  import qualified Data.ByteString.Char8 as C8 import Data.Proxy (Proxy (..)) import Data.Text (Text) import qualified Data.Text as Text+import Data.Word (Word16) import qualified Network.HTTP.Client as HC import Network.HTTP.Types.Status (statusCode) import System.TmpProc@@ -21,6 +30,8 @@   , Proc (..)   , ProcHandle (..)   , SvcURI+  , addrHost+  , addrPort   , manyNamed   , startupAll   , toPinged@@ -55,6 +66,7 @@   runArgs = []   reset _ = pure ()   ping = pingHttp+  uriOfRootlessWith = httpUriRootless   {- | Another data type representing a connection to a HttpBin server.@@ -73,6 +85,7 @@   runArgs = []   reset _ = pure ()   ping = pingHttp+  uriOfRootlessWith = httpUriRootless   {- | Yet another data type representing a connection to a HttpBin server.@@ -91,25 +104,34 @@   runArgs = []   reset _ = pure ()   ping = pingHttp+  uriOfRootlessWith = httpUriRootless  --- | Make a uri access the http-bin server.+-- | Make a uri to access the http-bin server. httpUri :: HostIpAddress -> SvcURI-httpUri ip = "http://" <> C8.pack (Text.unpack ip) <> "/"+httpUri ip = "http://" <> addrHost ip <> "/"  +-- | Make a rootless-aware uri for the http-bin server using the mapped host port for port 80.+httpUriRootless :: HostIpAddress -> [(Word16, Word16)] -> SvcURI+httpUriRootless addr pairs =+  case lookup 80 pairs of+    Just hp -> "http://" <> addrPort addr hp <> "/"+    Nothing -> httpUri addr++ pingHttp :: ProcHandle a -> IO Pinged pingHttp handle = toPinged @HC.HttpException Proxy $ do   gotStatus <- httpGet handle "/status/200"   if gotStatus == 200 then pure OK else pure NotOK  --- | Determine the status from a Get.+-- | Determine the status from a GET request, using the handle's URI as the base. httpGet :: ProcHandle a -> Text -> IO Int httpGet handle urlPath = do-  let theUri = "http://" <> hAddr handle <> "/" <> Text.dropWhile (== '/') urlPath+  let theUri = C8.unpack (hUri handle) <> Text.unpack (Text.dropWhile (== '/') urlPath)   manager <- HC.newManager HC.defaultManagerSettings-  getReq <- HC.parseRequest $ Text.unpack theUri+  getReq <- HC.parseRequest theUri   statusCode . HC.responseStatus <$> HC.httpLbs getReq manager  
test/Test/NginxGateway.hs view
@@ -10,6 +10,8 @@ Copyright   : (c) 2022 Tim Emiola Maintainer  : Tim Emiola <adetokunbo@emio.la> SPDX-License-Identifier: BSD3++Test helper types and functions for testing with an nginx reverse-proxy container. -} module Test.NginxGateway   ( -- * data types@@ -32,8 +34,6 @@ import qualified Network.HTTP.Client.TLS as HC import Network.HTTP.Types.Header (hHost) import Network.HTTP.Types.Status (statusCode)-import Network.TLS (ClientParams (..), HostName, Shared (..), Supported (..), defaultParamsClient)-import Network.TLS.Extra (ciphersuite_default) import Paths_tmp_proc (getDataDir) import System.Directory (createDirectory, removeDirectoryRecursive) import System.FilePath ((</>))@@ -49,6 +49,8 @@   , SlimHandle (..)   , SvcURI   , ToRunCmd (..)+  , addrHost+  , addrPort   , toPinged   ) import Test.Certs.Temp (CertPaths (..), defaultConfig, generateAndStore)@@ -67,10 +69,14 @@   -- config   type Image NginxGateway = "lscr.io/linuxserver/nginx"   type Name NginxGateway = "nginx-test"-  uriOf = httpUri+  uriOf = httpsUri   runArgs = []   reset _ = pure ()   ping = pingHttps+  uriOfRootlessWith addr pairs =+    case lookup 443 pairs of+      Just hp -> "https://" <> addrPort addr hp <> "/"+      Nothing -> httpsUri addr   instance ToRunCmd NginxGateway NginxPrep where@@ -200,9 +206,9 @@           pure np  --- | Make a uri access the http-bin server.-httpUri :: HostIpAddress -> SvcURI-httpUri ip = "http://" <> C8.pack (Text.unpack ip) <> "/"+-- | Make a uri to access the nginx gateway server.+httpsUri :: HostIpAddress -> SvcURI+httpsUri ip = "https://" <> addrHost ip <> "/"   pingHttps :: ProcHandle a -> IO Pinged@@ -211,20 +217,16 @@   if gotStatus == 200 then pure OK else pure NotOK  --- | Determine the status from a secure Get to host localhost.+-- | Determine the status from a secure GET, using the handle's URI as the base. httpsGet :: ProcHandle a -> Text -> IO Int httpsGet handle urlPath = do-  -- _tlsSettings <- TLSSettings <$> _mkClientParams "localHost"-  let theUri = "https://" <> hAddr handle <> "/" <> Text.dropWhile (== '/') urlPath-  -- use TLS settings that disable hostname verification. What's not-  -- currently possible is to actually specify the hostname to use for SNI-  -- that differs from the connection IP address, that's not supported by-  -- http-client-tls+  let theUri = C8.unpack (hUri handle) <> Text.unpack (Text.dropWhile (== '/') urlPath)+  -- TLS settings disable hostname verification: the cert is for "localhost"+  -- but the connection address may be a container IP or 127.0.0.1:<port>.   manager <- HC.newTlsManagerWith $ HC.mkManagerSettings noCheckSettings Nothing-  getReq <- HC.parseRequest $ Text.unpack theUri+  getReq <- HC.parseRequest theUri   let withHost = getReq {HC.requestHeaders = [(hHost, "localhost")]}   statusCode . HC.responseStatus <$> HC.httpLbs withHost manager-  -- currently unused, since the server specified in ClientParams for SNI is -- overridden by Connection, which resets it to the connection hostname
+ test/Test/System/TmpProc/DockerSpec.hs view
@@ -0,0 +1,130 @@+{-# 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
tmp-proc.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               tmp-proc-version:            0.7.2.7+version:            0.8.0.0 synopsis:           Run 'tmp' processes in integration tests description:   @tmp-proc@ runs services in docker containers for use in integration tests.@@ -76,6 +76,7 @@     Test.NginxGateway     Test.SimpleServer     Test.System.TmpProc.Hspec+    Test.System.TmpProc.DockerSpec     Test.System.TmpProc.HttpBinSpec     Test.System.TmpProc.WarpSpec     Network.Connection.CPP@@ -97,6 +98,7 @@     , test-certs            >=0.1 && <0.2     , text     , tls+    , process     ^>=1.6.3.0     , tmp-proc     , wai     , warp@@ -106,28 +108,6 @@   default-language: Haskell2010   ghc-options:     -threaded -rtsopts -with-rtsopts=-N -fno-ignore-asserts -Wall--flag use-doc-tests-  description: Include the doctests in the package tests-  default:     False--test-suite doctests-  if flag(use-doc-tests)-    buildable: True--  else-    buildable: False--  type:             exitcode-stdio-1.0-  main-is:          Main.hs-  build-depends:-    , base-    , doctest   >=0.8-    , tmp-proc--  hs-source-dirs:   doctest-  default-language: Haskell2010-  ghc-options:      -threaded  flag build-the-readme   description: Allow the readme to build