capataz-0.2.0.0: test/testsuite/Test/Util.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module Test.Util where
import RIO
import qualified RIO.Text as T
import Test.Tasty.HUnit (assertBool, assertFailure)
import Text.Show.Pretty (ppShow)
import qualified Control.Concurrent.Capataz as SUT
import qualified Control.Concurrent.Capataz.Event as SUT
--------------------------------------------------------------------------------
-- Util
-- | Utility function that gets the type name of a Record through it's Show
-- output.
fetchRecordName :: Show a => a -> Text
fetchRecordName = T.takeWhile (/= ' ') . tshow
-- | Composes two predicate functions together with a boolean AND
andP :: [a -> Bool] -> a -> Bool
andP predList a = all ($ a) predList
--------------------------------------------------------------------------------
-- Assertions and Testers
-- | This record duplicate the same event names as the ones found in the
-- "CapatazEvent" type, we use this to avoid using Text comparisons on assertion
-- helper functions. The "CapatazEvent" record is imported qualified, so there
-- is no conflict happening.
data EventType
= InvalidCapatazStatusReached
| SupervisorStatusChanged
| ProcessTerminated
| ProcessStarted
| ProcessRestarted
| ProcessCompleted
| ProcessFailed
| ProcessCallbackExecuted
| ProcessTerminationStarted
| ProcessTerminationFinished
| CapatazFailed
| CapatazTerminated
deriving (Show, Eq)
toEventType :: SUT.CapatazEvent -> EventType
toEventType ev = case ev of
SUT.InvalidSupervisorStatusReached{} -> InvalidCapatazStatusReached
SUT.SupervisorStatusChanged{} -> SupervisorStatusChanged
SUT.ProcessTerminated{} -> ProcessTerminated
SUT.ProcessStarted{} -> ProcessStarted
SUT.ProcessRestarted{} -> ProcessRestarted
SUT.ProcessCompleted{} -> ProcessCompleted
SUT.ProcessFailed{} -> ProcessFailed
SUT.ProcessCallbackExecuted{} -> ProcessCallbackExecuted
SUT.ProcessTerminationStarted{} -> ProcessTerminationStarted
SUT.ProcessTerminationFinished{} -> ProcessTerminationFinished
SUT.CapatazFailed{} -> CapatazFailed
SUT.CapatazTerminated{} -> CapatazTerminated
-- | Predicate function to assert "CapatazEvent" types
assertEventType :: EventType -> SUT.CapatazEvent -> Bool
assertEventType evType ev = toEventType ev == evType
-- | Predicate function to assert "CapatazEvent" process type
assertProcessType :: SUT.ProcessType -> SUT.CapatazEvent -> Bool
assertProcessType processTy ev = case ev of
SUT.ProcessFailed { processType } -> processTy == processType
SUT.ProcessTerminated { processType } -> processTy == processType
SUT.ProcessStarted { processType } -> processTy == processType
SUT.ProcessCallbackExecuted { processType } -> processTy == processType
SUT.ProcessRestarted { processType } -> processTy == processType
_ -> False
-- | Predicate function to assert "CapatazEvent" worker name
assertProcessName :: Text -> SUT.CapatazEvent -> Bool
assertProcessName processName' ev = case ev of
SUT.ProcessRestarted { processName } -> processName' == processName
SUT.ProcessFailed { processName } -> processName' == processName
SUT.ProcessTerminated { processName } -> processName' == processName
SUT.ProcessStarted { processName } -> processName' == processName
_ -> False
-- | Predicate function to assert type of an error inside a "CapatazEvent"
assertErrorType :: Text -> SUT.CapatazEvent -> Bool
assertErrorType errType ev = case ev of
SUT.CapatazFailed { supervisorError } ->
fetchRecordName supervisorError == errType
SUT.ProcessFailed { processError } -> fetchRecordName processError == errType
SUT.ProcessCallbackExecuted { processCallbackError } ->
case processCallbackError of
Nothing -> False
Just originalError -> fetchRecordName originalError == errType
_ -> False
-- | Predicate function to assert type of callback executed inside a
-- "CapatazEvent"
assertCallbackType :: SUT.CallbackType -> SUT.CapatazEvent -> Bool
assertCallbackType cbType ev = case ev of
SUT.ProcessFailed { processError } -> case fromException processError of
Just SUT.ProcessCallbackFailed { processCallbackType } ->
cbType == processCallbackType
_ -> False
SUT.ProcessCallbackExecuted { processCallbackType } ->
cbType == processCallbackType
_ -> False
-- | Predicate function to assert restart count inside a "CapatazEvent"
assertRestartCount :: (Int -> Bool) -> SUT.CapatazEvent -> Bool
assertRestartCount predFn ev = case ev of
SUT.ProcessRestarted { processRestartCount } -> predFn processRestartCount
_ -> False
-- | Predicate function to assert a Capataz status change
assertSupervisorStatusChanged
:: SUT.SupervisorStatus -> SUT.SupervisorStatus -> SUT.CapatazEvent -> Bool
assertSupervisorStatusChanged fromEv toEv ev = case ev of
SUT.SupervisorStatusChanged { prevSupervisorStatus, newSupervisorStatus } ->
fromEv == prevSupervisorStatus && toEv == newSupervisorStatus
_ -> False
-- | Predicate function to assert process was supervised by a a supervisor
-- indicated by name
assertSupervisorName :: Text -> SUT.CapatazEvent -> Bool
assertSupervisorName supervisorName' ev = case ev of
SUT.SupervisorStatusChanged { supervisorName } ->
supervisorName' == supervisorName
SUT.ProcessStarted { supervisorName } -> supervisorName' == supervisorName
SUT.ProcessTerminated { supervisorName } -> supervisorName' == supervisorName
SUT.ProcessRestarted { supervisorName } -> supervisorName' == supervisorName
SUT.ProcessCompleted { supervisorName } -> supervisorName' == supervisorName
SUT.ProcessCallbackExecuted { supervisorName } ->
supervisorName' == supervisorName
SUT.ProcessTerminationStarted { supervisorName } ->
supervisorName' == supervisorName
SUT.ProcessTerminationFinished { supervisorName } ->
supervisorName' == supervisorName
_ -> False
-- | Predicate function to assert a supervisor was started
assertSupervisorStarted :: Text -> SUT.CapatazEvent -> Bool
assertSupervisorStarted supervisorName = andP
[ assertEventType ProcessStarted
, assertProcessType SUT.SupervisorType
, assertProcessName supervisorName
]
-- | Predicate function to assert a supervisor was terminated
assertSupervisorTerminated :: Text -> SUT.CapatazEvent -> Bool
assertSupervisorTerminated supervisorName = andP
[ assertEventType ProcessTerminated
, assertProcessType SUT.SupervisorType
, assertProcessName supervisorName
]
assertSupervisorFailed :: Text -> SUT.CapatazEvent -> Bool
assertSupervisorFailed supervisorName = andP
[ assertEventType ProcessFailed
, assertProcessType SUT.SupervisorType
, assertProcessName supervisorName
]
assertSupervisorRestarted :: Text -> SUT.CapatazEvent -> Bool
assertSupervisorRestarted supervisorName = andP
[ assertEventType ProcessRestarted
, assertProcessType SUT.SupervisorType
, assertProcessName supervisorName
]
-- | Predicate function to assert a worker was started
assertWorkerStarted :: Text -> SUT.CapatazEvent -> Bool
assertWorkerStarted workerName = andP
[ assertEventType ProcessStarted
, assertProcessType SUT.WorkerType
, assertProcessName workerName
]
-- | Predicate function to assert a worker was terminated
assertWorkerTerminated :: Text -> SUT.CapatazEvent -> Bool
assertWorkerTerminated workerName = andP
[ assertEventType ProcessTerminated
, assertProcessType SUT.WorkerType
, assertProcessName workerName
]
assertWorkerFailed :: Text -> SUT.CapatazEvent -> Bool
assertWorkerFailed workerName = andP
[ assertEventType ProcessFailed
, assertProcessType SUT.WorkerType
, assertProcessName workerName
]
assertWorkerRestarted :: Text -> SUT.CapatazEvent -> Bool
assertWorkerRestarted workerName = andP
[ assertEventType ProcessRestarted
, assertProcessType SUT.WorkerType
, assertProcessName workerName
]
-- | Predicate function to assert a capataz thread failed with error type
assertCapatazFailedWith :: Text -> SUT.CapatazEvent -> Bool
assertCapatazFailedWith errorName =
andP [assertEventType CapatazFailed, assertErrorType errorName]
rootSupervisorName :: Text
rootSupervisorName = "capataz-root-supervisor"
assertRootSupervisor :: SUT.CapatazEvent -> Bool
assertRootSupervisor = assertSupervisorName rootSupervisorName
--------------------------------------------------------------------------------
-- | Exception used to test failures inside Worker sub-routines
data RestartingWorkerError
= RestartingWorkerError
deriving (Show)
instance Exception RestartingWorkerError
-- | Exception used to test failures inside Worker callback sub-routines
data TimeoutError
= TimeoutError
deriving (Show)
instance Exception TimeoutError
-- | Utility function to create a Worker sub-routine that fails at least a
-- number of times
mkFailingSubRoutine
:: (MonadIO m, MonadThrow m1, MonadIO m1)
=> Int -- ^ Number of times the Worker sub-routine will fail
-> m (m1 ()) -- ^ Sub-routine used on worker creation
mkFailingSubRoutine failCount = do
countRef <- newIORef failCount
let subRoutine = do
shouldFail <- atomicModifyIORef' countRef
(\count -> (count - 1, count > 0))
when shouldFail (throwM RestartingWorkerError)
return subRoutine
-- | A sub-routine that will complete for `initCount` amount of times. This
-- function works great when testing `Permanent` strategies, as you would like
-- to assert restart events once (if it keeps completing it will fill up the log
-- with restart events)
mkCompletingBeforeNRestartsSubRoutine :: MonadIO m => Int -> m (m ())
mkCompletingBeforeNRestartsSubRoutine initCount = do
countRef <- newIORef initCount
let subRoutine = do
shouldStop <- atomicModifyIORef' countRef
(\count -> (count - 1, count > 0))
if shouldStop then return () else forever $ threadDelay 1000100
return subRoutine
-- | A sub-routine that will complete once. This function works great when
-- testing `Permanent` strategies, as you would like to assert restart events
-- once (if it keeps completing it will fill up the log with restart events)
mkCompletingOnceSubRoutine :: MonadIO m => m (m ())
mkCompletingOnceSubRoutine = mkCompletingBeforeNRestartsSubRoutine 1
-- | Utility function to build a test environment for a Capataz execution.
-- It is composed by:
--
-- * List of assertions that represent events that should be triggered by the
-- capataz instance in order
--
-- * A function to modify the default "CapatazOptions", this utility function injects
-- a special @notifyEvent@ callback to execute given assertions.
testCapatazStreamWithOptions
:: (MonadUnliftIO m)
=> (SUT.CapatazOptions (RIO LogFunc) -> SUT.CapatazOptions (RIO LogFunc)) -- ^ Function to modify default
-- @CapatazOptions@
-> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening before setup function
-- is called
-> (SUT.Capataz (RIO LogFunc) -> RIO LogFunc ()) -- ^ Function used to test public the supervisor
-- public API (a.k.a setup function)
-> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the setup
-- function
-> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the capataz
-- record is terminated
-> Maybe (SUT.CapatazEvent -> Bool) -- ^ An assertion checked across all
-- @CapatazEvents@ that happened in a
-- test, great when testing that an event
-- __did not__ happen
-> m ()
testCapatazStreamWithOptions optionModFn preSetupAssertion setupFn postSetupAssertions postTeardownAssertions mAllEventsAssertion
= do
eventStream <- newTQueueIO
accRef <- newIORef []
pendingCountVar <- newIORef
(sum $ fmap
length
[preSetupAssertion, postSetupAssertions, postTeardownAssertions]
)
logOptions <- logOptionsHandle stdout False
withLogFunc logOptions $ \logFunc -> runRIO logFunc $ do
capataz <- SUT.forkCapataz
rootSupervisorName
( set
SUT.onSystemEventL
(\ev -> logDebug (display ev) >> trackEvent accRef eventStream ev)
. optionModFn
)
-- We check preSetup assertions are met before we execute the setup
-- function. This serves to test initialization of capataz instance
runAssertions "PRE-SETUP"
(eventStream, accRef)
pendingCountVar
preSetupAssertion
capataz
-- We execute the setup sub-routine, which is going to use the Capataz public
-- API to assert events
setupResult <- try (setupFn capataz)
case setupResult of
-- If the sub-routine fails, show exception
Left err -> liftIO $ assertFailure (show (err :: SomeException))
Right _ -> do
-- We now run post-setup assertions
runAssertions "POST-SETUP"
(eventStream, accRef)
pendingCountVar
postSetupAssertions
capataz
-- We now shutdown the capataz instance
SUT.terminateCapataz_ capataz
-- We run assertions for after the capataz has been shut down
runAssertions "POST-TEARDOWN"
(eventStream, accRef)
pendingCountVar
postTeardownAssertions
capataz
-- Lastly, we check if there is a function that we want to execute
-- across all events that happened in the test, this is to assert the
-- absence of an event
case mAllEventsAssertion of
Nothing -> return ()
Just allEventsAssertion -> do
events <- reverse <$> readIORef accRef
liftIO $ assertBool
("On AFTER-TEST, expected all events to match predicate, but didn't ("
<> show (length events)
<> " events tried)\n"
<> ppShow (zip ([0 ..] :: [Int]) events)
)
(all allEventsAssertion events)
where
-- Utility functions that runs the readEventLoop function with a timeout
-- of a second, this way we can guarantee assertions are met without having
-- to add @threadDelays@ to the test execution
runAssertions stageName (eventStream, accRef) pendingCountVar assertions capataz
= do
raceResult <- race
(liftIO $ threadDelay 1000100)
(readEventLoop eventStream pendingCountVar assertions)
case raceResult of
Left _ -> do
events <- reverse <$> readIORef accRef
pendingCount <- readIORef pendingCountVar
SUT.terminateCapataz_ capataz
liftIO $ assertFailure
( "On "
<> stageName
<> " stage, expected all assertions to match, but didn't ("
<> show pendingCount
<> " assertions remaining, "
<> show (length events)
<> " events tried)\n"
<> ppShow (zip ([0 ..] :: [Int]) events)
)
Right _ -> return ()
-- Sub-routine that accumulates all events that have happened in the Capataz
-- instance so far
trackEvent accRef eventStream event = do
atomicModifyIORef' accRef (\old -> (event : old, ()))
atomically $ writeTQueue eventStream event
-- Sub-routine that reads the event stream, and ensures that all assertions
-- are executed, this loop won't stop until all assertions are met
readEventLoop eventStream pendingCount assertions = do
writeIORef pendingCount (length assertions)
case assertions of
[] -> return ()
(assertionFn : assertions1) -> do
event <- atomically $ readTQueue eventStream
if assertionFn event
then readEventLoop eventStream pendingCount assertions1
else readEventLoop eventStream pendingCount assertions
-- | A version of "testCapatazStreamWithOptions" that does not receive the
-- function that modifies a "CapatazOptions" record.
testCapatazStream
:: (MonadUnliftIO m)
=> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening before setup function
-- is called
-> (SUT.Capataz (RIO LogFunc) -> RIO LogFunc ()) -- ^ Function used to test public the supervisor
-- public API (a.k.a setup function)
-> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the setup
-- function
-> [SUT.CapatazEvent -> Bool] -- ^ Assertions happening after the capataz
-- record is terminated
-> Maybe (SUT.CapatazEvent -> Bool) -- ^ An assertion checked across all
-- @CapatazEvents@ that happened in a
-- test, great when testing that an event
-- __did not__ happen
-> m ()
testCapatazStream = testCapatazStreamWithOptions id