packages feed

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