packages feed

testcontainers-0.4.0.0: src/TestContainers/Tasty.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

module TestContainers.Tasty
  ( -- * Tasty Ingredient
    ingredient,

    -- * Running containers for tests
    withContainers,

    -- * Re-exports for convenience
    module Reexports,
  )
where

import Control.Applicative ((<|>))
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Resource
  ( InternalState,
    getInternalState,
    liftResourceT,
  )
import Control.Monad.Trans.Resource.Internal
  ( stateAlloc,
    stateCleanup,
  )
import Data.Acquire (ReleaseType (ReleaseNormal))
import Data.Data (Proxy (Proxy))
import Test.Tasty
  ( TestTree,
    askOption,
    withResource,
  )
import qualified Test.Tasty as Tasty
import Test.Tasty.Ingredients (Ingredient)
import Test.Tasty.Options
  ( IsOption (..),
    OptionDescription (..),
    mkFlagCLParser,
    safeRead,
  )
import TestContainers as Reexports hiding
  ( Trace,
  )
import TestContainers.Monad (runTestContainer)

newtype DefaultTimeout = DefaultTimeout (Maybe Int)

instance IsOption DefaultTimeout where
  defaultValue =
    DefaultTimeout Nothing

  parseValue =
    fmap (DefaultTimeout . Just) . safeRead

  optionName =
    pure "testcontainers-default-timeout"

  optionHelp =
    pure "The max. number of seconds to wait for a container to become ready"

newtype Trace = Trace Bool

instance IsOption Trace where
  defaultValue =
    Trace False

  parseValue =
    const Nothing

  optionCLParser =
    mkFlagCLParser mempty (Trace True)

  optionName =
    pure "testcontainers-trace"

  optionHelp =
    pure "Turns on tracing of the underlying Docker operations"

-- | Tasty `Ingredient` that adds useful options to control defaults within the
-- TetContainers library.
--
-- @
-- main :: IO ()
-- main = `Tasty.defaultMainWithIngredients` (`ingredient` : `Tasty.defaultIngredients`) tests
-- @
--
-- @since 0.3.0.0
ingredient :: Ingredient
ingredient =
  Tasty.includingOptions
    [ Option (Proxy :: Proxy DefaultTimeout),
      Option (Proxy :: Proxy Trace)
    ]

withContainers ::
  forall a.
  TestContainer a ->
  (IO a -> TestTree) ->
  TestTree
withContainers startContainers tests =
  askOption $ \(DefaultTimeout defaultTimeout) ->
    askOption $ \(Trace enableTrace) ->
      let tracer :: Tracer
          tracer
            | enableTrace = newTracer $ \message ->
                putStrLn (show message)
            | otherwise =
                mempty

          runC action = do
            config <- determineConfig

            let actualConfig :: Config
                actualConfig =
                  config
                    { configDefaultWaitTimeout =
                        defaultTimeout <|> configDefaultWaitTimeout config,
                      configTracer = tracer
                    }

            runTestContainer actualConfig action

          -- Correct resource handling is tricky here:
          -- Tasty offers a bracket alike in IO. We  have
          -- to transfer the ReleaseMap of the ResIO safely
          -- to the release function. Fortunately resourcet
          -- let's us access the internal state..
          acquire :: IO (a, InternalState)
          acquire = runC $ do
            result <- startContainers
            releaseMap <- liftResourceT getInternalState

            -- N.B. runResourceT runs the finalizers on every
            -- resource. We don't want it to! We want to run
            -- finalization in the release function that is
            -- called by Tasty! stateAlloc increments a references
            -- count to accomodate for exactly these kind of
            -- cases.
            liftIO $ stateAlloc releaseMap
            pure (result, releaseMap)

          release :: (a, InternalState) -> IO ()
          release (_, internalState) =
            stateCleanup ReleaseNormal internalState
       in withResource acquire release $ \mk ->
            tests (fmap fst mk)