{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-| This module contains: * Assertion functions to get attributes from a `CapatazEvent` * Helpers to run the test (reduce boilerplate) * Actual tests Tests just exercises the __public API__ and asserts all the events delivered via the @notifyEvent@ callback are what we are expecting. NOTE: This tests may be flaky depending on the load of the application, there is a ticket pending to add dejafu tests to ensure our tests are stable. -} module Control.Concurrent.CapatazTest (tests) where import RIO import Control.Exception (ErrorCall (..)) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase) import Control.Concurrent.Capataz (set) import qualified Control.Concurrent.Capataz as SUT import Test.Util -------------------------------------------------------------------------------- -- Actual Tests tests :: TestTree tests = testGroup "capataz core" [ testGroup "capataz without workerOptionsList" [ testCase "initialize and teardown works as expected" $ testCapatazStream [ andP [ assertEventType SupervisorStatusChanged , assertSupervisorStatusChanged SUT.Initializing SUT.Running ] ] (const $ return ()) [] [ andP [ assertEventType SupervisorStatusChanged , assertSupervisorStatusChanged SUT.Running SUT.Halting ] , andP [ assertEventType SupervisorStatusChanged , assertSupervisorStatusChanged SUT.Halting SUT.Halted ] ] Nothing ] , testGroup "capataz with processSpecList" [ testCase "initialize and teardown of workers works as expected" $ testCapatazStreamWithOptions (set SUT.supervisorProcessSpecListL [ SUT.workerSpec "A" (forever $ threadDelay 10001000) id , SUT.workerSpec "B" (forever $ threadDelay 10001000) id ] ) [ assertWorkerStarted "A" , assertWorkerStarted "B" , andP [ assertEventType SupervisorStatusChanged , assertSupervisorStatusChanged SUT.Initializing SUT.Running ] ] (const $ return ()) [] [ andP [ assertEventType SupervisorStatusChanged , assertSupervisorStatusChanged SUT.Running SUT.Halting ] , assertEventType ProcessTerminationStarted , assertWorkerTerminated "A" , assertWorkerTerminated "B" , assertEventType ProcessTerminationFinished , andP [ assertEventType SupervisorStatusChanged , assertSupervisorStatusChanged SUT.Halting SUT.Halted ] ] Nothing , testCase "reports error when worker retries violate restart intensity" $ do lockVar <- newEmptyMVar let (signalIntensityReached, waitTillIntensityReached) = (putMVar lockVar (), takeMVar lockVar) testCapatazStreamWithOptions (set SUT.supervisorOnIntensityReachedL signalIntensityReached) [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (throwIO RestartingWorkerError) id ) capataz waitTillIntensityReached ) [ assertEventType ProcessFailed , assertEventType ProcessFailed , assertEventType ProcessFailed , assertCapatazFailedWith "SupervisorIntensityReached" ] [] Nothing , testGroup "single supervised worker" [ testGroup "callbacks" [ testGroup "workerOnCompletion" [ testCase "does execute callback when worker sub-routine is completed" $ testCapatazStream [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (return ()) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz return () ) [ andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnCompletion ] , assertEventType ProcessCompleted ] [] Nothing , testCase "does not execute callback when worker sub-routine fails" $ testCapatazStream [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (throwIO RestartingWorkerError) id ) capataz return () ) [ andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnFailure ] , assertEventType ProcessFailed ] [assertEventType CapatazTerminated] (Just $ not . andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnCompletion ] ) , testCase "does not execute callback when worker sub-routine is terminated" $ testCapatazStream [] (\capataz -> do workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (forever $ threadDelay 1000100) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz _workerId <- void $ SUT.terminateProcess "testing onCompletion callback" workerId capataz return () ) [assertEventType ProcessTerminated] [assertEventType CapatazTerminated] (Just $ not . andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnCompletion ] ) , testCase "treats as worker sub-routine failed if callback fails" $ testCapatazStream [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (return ()) ( set SUT.workerRestartStrategyL SUT.Temporary . set SUT.workerOnCompletionL (throwIO TimeoutError) ) ) capataz return () ) [ andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnCompletion , assertErrorType "TimeoutError" ] , andP [ assertEventType ProcessFailed , assertErrorType "ProcessCallbackFailed" ] ] [] Nothing ] , testGroup "workerOnFailure" [ testCase "does execute callback when worker sub-routine fails" $ testCapatazStream [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (throwIO RestartingWorkerError) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz return () ) [ andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnFailure ] , assertEventType ProcessFailed ] [assertEventType CapatazTerminated] Nothing , testCase "does not execute callback when worker sub-routine is completed" $ testCapatazStream [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (return ()) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz return () ) [ andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnCompletion ] , assertEventType ProcessCompleted ] [] ( Just $ not . andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnFailure ] ) , testCase "does not execute callback when worker sub-routine is terminated" $ testCapatazStream [] (\capataz -> do workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (forever $ threadDelay 1000100) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz void $ SUT.terminateProcess "testing onFailure callback" workerId capataz ) [assertEventType ProcessTerminated] [] ( Just $ not . andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnFailure ] ) , testCase "treats as worker sub-routine failed if callback fails" $ testCapatazStream [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (throwIO RestartingWorkerError) ( set SUT.workerRestartStrategyL SUT.Temporary . set SUT.workerOnFailureL (const $ throwIO TimeoutError) ) ) capataz return () ) [ andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnFailure , assertErrorType "TimeoutError" ] , andP [ assertEventType ProcessFailed , assertErrorType "ProcessCallbackFailed" ] ] [] Nothing ] , testGroup "workerOnTermination" [ testCase "gets brutally killed when TimeoutSeconds termination policy is not met" $ testCapatazStream [] (\capataz -> do workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (forever $ threadDelay 10001000) ( set SUT.workerRestartStrategyL SUT.Temporary . set SUT.workerTerminationPolicyL (SUT.TimeoutMillis 1) . set SUT.workerOnTerminationL (forever $ threadDelay 100100) ) ) capataz void $ SUT.terminateProcess "testing workerOnTermination callback" workerId capataz ) [ andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnTermination , assertErrorType "BrutallyTerminateProcessException" ] , andP [ assertEventType ProcessFailed , assertCallbackType SUT.OnTermination , assertErrorType "ProcessCallbackFailed" ] ] [] Nothing , testCase "does execute callback when worker sub-routine is terminated" $ testCapatazStream [] (\capataz -> do workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (forever $ threadDelay 1000100) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz void $ SUT.terminateProcess "testing workerOnTermination callback" workerId capataz ) [ andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnTermination ] , assertEventType ProcessTerminated ] [assertEventType CapatazTerminated] Nothing , testCase "does not execute callback when worker sub-routine is completed" $ testCapatazStream [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (return ()) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz return () ) [assertEventType ProcessCompleted] [] (Just $ not . andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnTermination ] ) , testCase "does not execute callback when worker sub-routine fails" $ testCapatazStream [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (throwIO (ErrorCall "surprise!")) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz return () ) [assertEventType ProcessFailed] [] (Just $ not . andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnTermination ] ) , testCase "treats as worker sub-routine failed if callback fails" $ testCapatazStream [] (\capataz -> do workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (forever $ threadDelay 10001000) ( set SUT.workerRestartStrategyL SUT.Temporary . set SUT.workerOnTerminationL (throwIO TimeoutError) ) ) capataz void $ SUT.terminateProcess "testing workerOnTermination callback" workerId capataz ) [ andP [ assertEventType ProcessCallbackExecuted , assertCallbackType SUT.OnTermination , assertErrorType "TimeoutError" ] , andP [ assertEventType ProcessFailed , assertErrorType "ProcessCallbackFailed" ] ] [] Nothing ] ] , testGroup "with transient strategy" [ testCase "does not restart worker on completion" $ testCapatazStream [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (return ()) (set SUT.workerRestartStrategyL SUT.Transient) ) capataz return () ) [assertEventType ProcessStarted, assertEventType ProcessCompleted] [assertEventType CapatazTerminated] (Just $ not . assertEventType ProcessRestarted) , testCase "does not restart worker on termination" $ testCapatazStream [] (\capataz -> do workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (forever $ threadDelay 1000100) (set SUT.workerRestartStrategyL SUT.Transient) ) capataz void $ SUT.terminateProcess "termination test (1)" workerId capataz ) [assertEventType ProcessTerminated] [assertEventType CapatazTerminated] (Just $ not . assertEventType ProcessRestarted) , testCase "does restart on failure" $ testCapatazStream [] (\capataz -> do subRoutineAction <- mkFailingSubRoutine 1 _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" subRoutineAction (set SUT.workerRestartStrategyL SUT.Transient) ) capataz return () ) [ assertEventType ProcessStarted , assertEventType ProcessFailed , andP [assertEventType ProcessRestarted, assertRestartCount (== 1)] ] [] Nothing , testCase "does increase restart count on multiple worker failures" $ testCapatazStream [] (\capataz -> do subRoutineAction <- mkFailingSubRoutine 2 _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" subRoutineAction (set SUT.workerRestartStrategyL SUT.Transient) ) capataz return () ) [ andP [assertEventType ProcessRestarted, assertRestartCount (== 1)] , andP [assertEventType ProcessRestarted, assertRestartCount (== 2)] ] [] Nothing ] , testGroup "with permanent strategy" [ testCase "does restart worker on completion" $ testCapatazStream [] (\capataz -> do subRoutineAction <- mkCompletingOnceSubRoutine _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" subRoutineAction (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz return () ) [ assertEventType ProcessStarted , assertEventType ProcessCompleted , assertEventType ProcessRestarted ] [assertEventType CapatazTerminated] Nothing , testCase "does not increase restart count on multiple worker completions" $ testCapatazStream [] (\capataz -> do -- Note the number is two (2) given the assertion list has two `ProcessRestarted` assertions let expectedRestartCount = 2 subRoutineAction <- mkCompletingBeforeNRestartsSubRoutine expectedRestartCount _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" subRoutineAction (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz return () ) [ andP [assertEventType ProcessRestarted, assertRestartCount (== 1)] , andP [assertEventType ProcessRestarted, assertRestartCount (== 1)] ] [] Nothing , testCase "does restart on worker termination" $ testCapatazStream [] (\capataz -> do workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (forever $ threadDelay 10001000) (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz void $ SUT.terminateProcess "testing termination (1)" workerId capataz ) [assertEventType ProcessTerminated, assertEventType ProcessRestarted] [] Nothing , testCase "does increase restart count on multiple worker terminations" $ do terminationCountVar <- newTVarIO (0 :: Int) let signalWorkerTermination = atomically (modifyTVar' terminationCountVar (+ 1)) waitWorkerTermination i = atomically $ do n <- readTVar terminationCountVar when (n /= i) retrySTM testCapatazStream [] (\capataz -> do workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (forever $ threadDelay 10001000) ( set SUT.workerRestartStrategyL SUT.Permanent . set SUT.workerOnTerminationL signalWorkerTermination ) ) capataz void $ SUT.terminateProcess "testing termination (1)" workerId capataz waitWorkerTermination 1 void $ SUT.terminateProcess "testing termination (2)" workerId capataz waitWorkerTermination 2 ) [ assertEventType ProcessTerminated , andP [assertEventType ProcessRestarted, assertRestartCount (== 1)] , assertEventType ProcessTerminated , andP [assertEventType ProcessRestarted, assertRestartCount (== 2)] ] [] Nothing , testCase "does restart on worker failure" $ testCapatazStream [] (\capataz -> do subRoutineAction <- mkFailingSubRoutine 1 _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" subRoutineAction (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz return () ) [ assertEventType ProcessStarted , assertEventType ProcessFailed , andP [assertEventType ProcessRestarted, assertRestartCount (== 1)] ] [] Nothing , testCase "does increase restart count on multiple worker failures" $ testCapatazStream [] (\capataz -> do subRoutineAction <- mkFailingSubRoutine 2 _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" subRoutineAction (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz return () ) [ andP [assertEventType ProcessRestarted, assertRestartCount (== 1)] , andP [assertEventType ProcessRestarted, assertRestartCount (== 2)] ] [] Nothing ] , testGroup "with temporary strategy" [ testCase "does not restart on worker completion" $ testCapatazStream [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (return ()) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz return () ) [assertEventType ProcessStarted, assertEventType ProcessCompleted] [assertEventType CapatazTerminated] (Just $ not . assertEventType ProcessRestarted) , testCase "does not restart on worker termination" $ testCapatazStream [] (\capataz -> do workerId <- SUT.forkWorker (SUT.buildWorkerOptions "test-worker" (forever $ threadDelay 1000100) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz void $ SUT.terminateProcess "termination test (1)" workerId capataz threadDelay 100 ) [assertEventType ProcessStarted, assertEventType ProcessTerminated] [assertEventType CapatazTerminated] (Just $ not . assertEventType ProcessRestarted) , testCase "does not restart on worker failure" $ testCapatazStream [] (\capataz -> do _workerId <- SUT.forkWorker (SUT.buildWorkerOptions "failing-worker" (error "worker failed!") (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz threadDelay 100 ) [assertEventType ProcessStarted, assertEventType ProcessFailed] [assertEventType CapatazTerminated] (Just $ not . assertEventType ProcessRestarted) ] ] , testGroup "multiple supervised IO sub-routines" [ testCase "terminates all supervised worker sub-routines on teardown" $ testCapatazStream [] (\capataz -> do _workerA <- SUT.forkWorker (SUT.buildWorkerOptions "A" (forever $ threadDelay 1000100) (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz _workerB <- SUT.forkWorker (SUT.buildWorkerOptions "B" (forever $ threadDelay 1000100) (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz return () ) [ andP [assertEventType ProcessStarted, assertProcessName "A"] , andP [assertEventType ProcessStarted, assertProcessName "B"] ] [ andP [assertEventType ProcessTerminated, assertProcessName "A"] , andP [assertEventType ProcessTerminated, assertProcessName "B"] , assertEventType CapatazTerminated ] Nothing , testGroup "with one for one supervisor restart strategy" [ testCase "restarts failing worker sub-routine only" $ testCapatazStreamWithOptions (set SUT.supervisorRestartStrategyL SUT.OneForOne) [] (\capataz -> do _workerA <- SUT.forkWorker (SUT.buildWorkerOptions "A" (forever $ threadDelay 1000100) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz ioB <- mkFailingSubRoutine 1 _workerB <- SUT.forkWorker (SUT.buildWorkerOptions "B" (forever $ ioB >> threadDelay 1000100) (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz return () ) [andP [assertEventType ProcessRestarted, assertProcessName "B"]] [] (Just $ not . andP [assertEventType ProcessRestarted, assertProcessName "A"] ) ] , testGroup "with all for one supervisor restart strategy with newest first order" [ testCase "does terminate all other workers that did not fail" $ testCapatazStreamWithOptions (\supOptions -> supOptions & set SUT.supervisorRestartStrategyL SUT.AllForOne & set SUT.supervisorProcessTerminationOrderL SUT.OldestFirst ) [] (\capataz -> do -- This lockVar guarantees that workerB executes before workerA lockVar <- newEmptyMVar ioA <- mkFailingSubRoutine 1 _workerA <- SUT.forkWorker (SUT.buildWorkerOptions "A" (forever $ readMVar lockVar >> ioA) (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz _workerB <- SUT.forkWorker (SUT.buildWorkerOptions "B" (putMVar lockVar () >> forever (threadDelay 10)) (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz return () ) [ andP [assertEventType ProcessStarted, assertProcessName "A"] , andP [assertEventType ProcessStarted, assertProcessName "B"] , andP [assertEventType ProcessFailed, assertProcessName "A"] , andP [assertEventType ProcessRestarted, assertProcessName "A"] , andP [assertEventType ProcessTerminated, assertProcessName "B"] , andP [assertEventType ProcessRestarted, assertProcessName "B"] ] [] Nothing , testCase "does not restart workers that are temporary" $ testCapatazStreamWithOptions (\supOptions -> supOptions & set SUT.supervisorRestartStrategyL SUT.AllForOne & set SUT.supervisorProcessTerminationOrderL SUT.OldestFirst ) [] (\capataz -> do lockVar <- newEmptyMVar ioA <- mkFailingSubRoutine 1 _workerA <- SUT.forkWorker (SUT.buildWorkerOptions "A" (forever $ readMVar lockVar >> ioA) (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz _workerB <- SUT.forkWorker (SUT.buildWorkerOptions "B" (putMVar lockVar () >> forever (threadDelay 10)) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz return () ) [ andP [assertEventType ProcessStarted, assertProcessName "A"] , andP [assertEventType ProcessStarted, assertProcessName "B"] , andP [assertEventType ProcessFailed, assertProcessName "A"] , andP [assertEventType ProcessRestarted, assertProcessName "A"] , andP [assertEventType ProcessTerminated, assertProcessName "B"] ] [] (Just $ not . andP [assertEventType ProcessRestarted, assertProcessName "B"] ) , testCase "restarts workers that are not temporary" $ testCapatazStreamWithOptions (\supOptions -> supOptions & set SUT.supervisorRestartStrategyL SUT.AllForOne & set SUT.supervisorProcessTerminationOrderL SUT.NewestFirst ) [] (\capataz -> do ioA <- mkFailingSubRoutine 1 lockVar <- newEmptyMVar _workerA <- SUT.forkWorker (SUT.buildWorkerOptions "A" (forever $ readMVar lockVar >> ioA) (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz _workerB <- SUT.forkWorker (SUT.buildWorkerOptions "B" (putMVar lockVar () >> forever (threadDelay 10)) (set SUT.workerRestartStrategyL SUT.Transient) ) capataz return () ) [ andP [assertEventType ProcessRestarted, assertProcessName "B"] , andP [assertEventType ProcessRestarted, assertProcessName "A"] ] [] Nothing ] , testGroup "with all for one capataz restart strategy with oldest first order" [ testCase "does not restart workers that are temporary" $ testCapatazStreamWithOptions (\supOptions -> supOptions & set SUT.supervisorRestartStrategyL SUT.AllForOne & set SUT.supervisorProcessTerminationOrderL SUT.OldestFirst ) [] (\capataz -> do ioA <- mkFailingSubRoutine 1 -- This lockVar guarantees that workerB executes before workerA lockVar <- newEmptyMVar _workerA <- SUT.forkWorker (SUT.buildWorkerOptions "A" (forever $ readMVar lockVar >> ioA) (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz _workerB <- SUT.forkWorker (SUT.buildWorkerOptions "B" (putMVar lockVar () >> forever (threadDelay 10)) (set SUT.workerRestartStrategyL SUT.Temporary) ) capataz return () ) [andP [assertEventType ProcessRestarted, assertProcessName "A"]] [] (Just $ not . andP [assertEventType ProcessRestarted, assertProcessName "B"] ) , testCase "restarts workers that are not temporary" $ testCapatazStreamWithOptions (\supOptions -> supOptions & set SUT.supervisorRestartStrategyL SUT.AllForOne & set SUT.supervisorProcessTerminationOrderL SUT.OldestFirst ) [] (\capataz -> do ioA <- mkFailingSubRoutine 1 -- This lockVar guarantees that workerB executes before workerA lockVar <- newEmptyMVar _workerA <- SUT.forkWorker (SUT.buildWorkerOptions "A" (forever $ readMVar lockVar >> ioA) (set SUT.workerRestartStrategyL SUT.Permanent) ) capataz _workerB <- SUT.forkWorker (SUT.buildWorkerOptions "B" (putMVar lockVar () >> forever (threadDelay 10)) (set SUT.workerRestartStrategyL SUT.Transient) ) capataz return () ) [ andP [assertEventType ProcessRestarted, assertProcessName "A"] , andP [assertEventType ProcessRestarted, assertProcessName "B"] ] [] Nothing ] ] ] ]