packages feed

extensible-effects-concurrent-0.12.0: test/ProcessBehaviourTestCases.hs

module ProcessBehaviourTestCases where

import           Data.List                      ( sort )
import           Data.Foldable                  ( traverse_ )
import qualified Data.Dynamic                  as Dynamic
import           Data.Typeable
import           Data.Default
import           Control.Exception
import           Control.Concurrent
import           Control.Concurrent.STM
import           Control.Eff.Concurrent.Process
import           Control.Eff.Concurrent.Process.Timer
import           Control.Eff.Concurrent.Api
import           Control.Eff.Concurrent.Api.Client
import           Control.Eff.Concurrent.Api.Server
import qualified Control.Eff.Concurrent.Process.ForkIOScheduler
                                               as ForkIO
import qualified Control.Eff.Concurrent.Process.SingleThreadedScheduler
                                               as SingleThreaded
import           Control.Eff
import           Control.Eff.Extend
import           Control.Eff.Log
import           Control.Eff.Loop
import           Control.Eff.Lift
import           Control.Monad
import           Test.Tasty
import           Test.Tasty.HUnit
import           Common
import           Control.Applicative
import           Data.Void

testInterruptReason :: InterruptReason
testInterruptReason = ProcessError "test interrupt"

test_forkIo :: TestTree
test_forkIo = setTravisTestOptions $ withTestLogC
  (\c lc -> handleLoggingAndIO_ (ForkIO.schedule c) lc)
  (\factory -> testGroup "ForkIOScheduler" [allTests factory])


test_singleThreaded :: TestTree
test_singleThreaded = setTravisTestOptions $ withTestLogC
  (\e logC ->
        -- void (runLift (logToChannel logC (SingleThreaded.schedule (return ()) e)))
    let runEff
          :: Eff '[Logs LogMessage, LogWriterReader LogMessage IO, Lift IO] a
          -> IO a
        runEff = flip handleLoggingAndIO logC
    in  void $ SingleThreaded.scheduleM runEff yield e
  )
  (\factory -> testGroup "SingleThreadedScheduler" [allTests factory])

allTests
  :: forall r
   . (Member (Logs LogMessage) r, SetMember Lift (Lift IO) r)
  => IO (Eff (InterruptableProcess r) () -> IO ())
  -> TestTree
allTests schedulerFactory = localOption
  (timeoutSeconds 300)
  (testGroup
    "Process"
    [ errorTests schedulerFactory
    , sendShutdownTests schedulerFactory
    , concurrencyTests schedulerFactory
    , exitTests schedulerFactory
    , pingPongTests schedulerFactory
    , yieldLoopTests schedulerFactory
    , selectiveReceiveTests schedulerFactory
    , linkingTests schedulerFactory
    , monitoringTests schedulerFactory
    , timerTests schedulerFactory
    ]
  )

data ReturnToSender
  deriving Typeable

data instance Api ReturnToSender r where
  ReturnToSender :: ProcessId -> String -> Api ReturnToSender ('Synchronous Bool)
  StopReturnToSender :: Api ReturnToSender ('Synchronous ())

deriving instance Show (Api ReturnToSender x)

deriving instance Typeable (Api ReturnToSender x)

returnToSender
  :: forall q r
   . (HasCallStack, SetMember Process (Process q) r, Member Interrupts r)
  => SchedulerProxy q
  -> Server ReturnToSender
  -> String
  -> Eff r Bool
returnToSender px toP msg = do
  me      <- self px
  _       <- call px toP (ReturnToSender me msg)
  msgEcho <- receiveMessage @String px
  return (msgEcho == msg)

stopReturnToSender
  :: forall q r
   . (HasCallStack, SetMember Process (Process q) r, Member Interrupts r)
  => SchedulerProxy q
  -> Server ReturnToSender
  -> Eff r ()
stopReturnToSender px toP = call px toP StopReturnToSender

returnToSenderServer
  :: forall q r
   . ( HasCallStack
     , SetMember Process (Process q) r
     , Member (Logs LogMessage) q
     , Member Interrupts r
     )
  => SchedulerProxy q
  -> Eff r (Server ReturnToSender)
returnToSenderServer px = asServer <$> spawn
  (serve px $ def
    { _callCallback = Just
                        (\m k -> case m of
                          StopReturnToSender -> do
                            k ()
                            return (StopApiServer testInterruptReason)
                          ReturnToSender fromP echoMsg -> do
                            sendMessage px fromP echoMsg
                            yieldProcess px
                            k True
                            return HandleNextRequest
                        )
    }
  )

selectiveReceiveTests
  :: forall r
   . (Member (Logs LogMessage) r, SetMember Lift (Lift IO) r)
  => IO (Eff (InterruptableProcess r) () -> IO ())
  -> TestTree
selectiveReceiveTests schedulerFactory = setTravisTestOptions
  (testGroup
    "selective receive tests"
    [ testCase "send 10 messages (from 1..10) and receive messages from 10 to 1"
    $ applySchedulerFactory schedulerFactory
    $ do
        let
          nMax = 10
          receiverLoop donePid = go nMax
           where
            go :: Int -> Eff (InterruptableProcess r) ()
            go 0 = sendMessage SP donePid True
            go n = do
              void $ receiveSelectedMessage SP (filterMessage (== n))
              go (n - 1)

          senderLoop receviverPid =
            traverse_ (sendMessage SP receviverPid) [1 .. nMax]

        me          <- self SP
        receiverPid <- spawn (receiverLoop me)
        spawn_ (senderLoop receiverPid)
        ok <- receiveMessage @Bool SP
        lift (ok @? "selective receive failed")
    , testCase "receive a message while waiting for a call reply"
    $ applySchedulerFactory schedulerFactory
    $ do
        srv <- returnToSenderServer SP
        ok  <- returnToSender SP srv "test"
        ()  <- stopReturnToSender SP srv
        lift (ok @? "selective receive failed")
    , testCase "flush messages"
    $ applySchedulerFactory schedulerFactory
    $ withSchedulerProxy SP
    $ do
        let px = SP
        me <- self px
        spawn_
          $ replicateM_ 10 (sendMessage px me True) >> sendMessage px me ()
        spawn_ $ replicateM_
          10
          (sendMessage px me (123.23411 :: Float)) >> sendMessage px me ()
        spawn_
          $ replicateM_ 10 (sendMessage px me "123") >> sendMessage px me ()
        ()   <- receiveMessage px
        ()   <- receiveMessage px
        ()   <- receiveMessage px
        -- replicateCheapM_ 40 (yieldProcess px)
        msgs <- flushMessages
        lift (length msgs @?= 30)
    ]
  )


yieldLoopTests
  :: forall r
   . (Member (Logs LogMessage) r, SetMember Lift (Lift IO) r)
  => IO (Eff (InterruptableProcess r) () -> IO ())
  -> TestTree
yieldLoopTests schedulerFactory
  = let maxN = 100000
    in
      setTravisTestOptions
        (testGroup
          "yield tests"
          [ testCase
            "yield many times (replicateM_)"
            (applySchedulerFactory schedulerFactory
                                   (replicateM_ maxN (yieldProcess SP))
            )
          , testCase
            "yield many times (forM_)"
            (applySchedulerFactory
              schedulerFactory
              (forM_ [1 :: Int .. maxN] (\_ -> yieldProcess SP))
            )
          , testCase
            "construct an effect with an exit first, followed by many yields"
            (applySchedulerFactory
              schedulerFactory
              (do
                void (exitNormally SP)
                replicateM_ 1000000000000 (yieldProcess SP)
              )
            )
          ]
        )


data Ping = Ping ProcessId
  deriving (Eq, Show)

data Pong = Pong
  deriving (Eq, Show)

pingPongTests
  :: forall r
   . (Member (Logs LogMessage) r, SetMember Lift (Lift IO) r)
  => IO (Eff (InterruptableProcess r) () -> IO ())
  -> TestTree
pingPongTests schedulerFactory = testGroup
  "Yield Tests"
  [ testCase "ping pong a message between two processes, both don't yield"
  $ applySchedulerFactory schedulerFactory
  $ do
      let pongProc = foreverCheap $ do
            Ping pinger <- receiveMessage SP
            sendMessage SP pinger Pong
          pingProc ponger parent = do
            me <- self SP
            sendMessage SP ponger (Ping me)
            Pong <- receiveMessage SP
            sendMessage SP parent True
      pongPid <- spawn pongProc
      me      <- self SP
      spawn_ (pingProc pongPid me)
      ok <- receiveMessage @Bool SP
      lift (ok @? "ping pong failed")
  , testCase "ping pong a message between two processes, with massive yielding"
  $ applySchedulerFactory schedulerFactory
  $ do
      yieldProcess SP
      let pongProc = foreverCheap $ do
            yieldProcess SP
            Ping pinger <- receiveMessage SP
            yieldProcess SP
            sendMessage SP pinger Pong
            yieldProcess SP
          pingProc ponger parent = do
            yieldProcess SP
            me <- self SP
            yieldProcess SP
            sendMessage SP ponger (Ping me)
            yieldProcess SP
            Pong <- receiveMessage SP
            yieldProcess SP
            sendMessage SP parent True
            yieldProcess SP
      yieldProcess SP
      pongPid <- spawn pongProc
      yieldProcess SP
      me <- self SP
      yieldProcess SP
      spawn_ (pingProc pongPid me)
      yieldProcess SP
      ok <- receiveMessage @Bool SP
      yieldProcess SP
      lift (ok @? "ping pong failed")
      yieldProcess SP
  , testCase
    "the first message is not delayed, not even in cooperative scheduling (because of yield)"
  $ applySchedulerFactory schedulerFactory
  $ do
      pongVar <- lift newEmptyMVar
      let pongProc = foreverCheap $ do
            Pong <- receiveMessage SP
            lift (putMVar pongVar Pong)
      ponger <- spawn pongProc
      sendMessage SP ponger Pong
      let waitLoop = do
            p <- lift (tryTakeMVar pongVar)
            case p of
              Nothing -> do
                yieldProcess SP
                waitLoop
              Just r -> return r
      p <- waitLoop
      lift (p == Pong @? "ping pong failed")
  ]

errorTests
  :: forall r
   . (Member (Logs LogMessage) r, SetMember Lift (Lift IO) r)
  => IO (Eff (InterruptableProcess r) () -> IO ())
  -> TestTree
errorTests schedulerFactory
  = let
      px :: SchedulerProxy r
      px = SchedulerProxy
    in
      testGroup
        "causing and handling errors"
        [ testGroup
            "exitWithError"
            [ testCase "unhandled exitWithError"
            $ applySchedulerFactory schedulerFactory
            $ do
                void $ exitWithError px "test error"
                error "This should not happen"
            , testCase "cannot catch exitWithError"
            $ applySchedulerFactory schedulerFactory
            $ do
                void $ exitWithError px "test error 4"
                error "This should not happen"
            , testCase "multi process exitWithError"
            $ scheduleAndAssert schedulerFactory
            $ \assertEff -> do
                me <- self px
                let n = 15
                traverse_
                  (\(i :: Int) -> spawn $ foreverCheap
                    (void
                      (  sendMessage px (888888 + fromIntegral i) "test message"
                      >> yieldProcess px
                      )
                    )
                  )
                  [0 .. n]
                traverse_
                  (\(i :: Int) -> spawn $ do
                    void $ sendMessage px me i
                    void (exitWithError px (show i ++ " died"))
                    error "this should not be reached"
                  )
                  [0 .. n]
                oks <- replicateM (length [0 .. n]) (receiveMessage px)
                assertEff "" (sort oks == [0 .. n])
            ]
        ]

concurrencyTests
  :: forall r
   . (Member (Logs LogMessage) r, SetMember Lift (Lift IO) r)
  => IO (Eff (InterruptableProcess r) () -> IO ())
  -> TestTree
concurrencyTests schedulerFactory
  = let
      px :: SchedulerProxy r
      px = SchedulerProxy
      n  = 100
    in
      testGroup
        "concurrency tests"
        [ testCase
          "when main process exits the scheduler kills/cleans and returns"
        $ applySchedulerFactory schedulerFactory
        $ do
            me <- self px
            traverse_
              (const
                (spawn
                  (do
                    m <- receiveAnyMessage px
                    void (sendMessage px me m)
                  )
                )
              )
              [1 .. n]
            lift (threadDelay 1000)
        , testCase "new processes are executed before the parent process"
        $ scheduleAndAssert schedulerFactory
        $ \assertEff -> do -- start massive amount of children that exit as soon as they are
               -- executed, this will only work smoothly when scheduler schedules
               -- the new child before the parent
            traverse_ (const (spawn (exitNormally px))) [1 .. n]
            assertEff "" True
        , testCase "two concurrent processes"
        $ scheduleAndAssert schedulerFactory
        $ \assertEff -> do
            me     <- self px
            child1 <- spawn
              (do
                m <- receiveAnyMessage px
                void (sendAnyMessage px me m)
              )
            child2 <- spawn (foreverCheap (void (sendMessage px 888888 "")))
            sendMessage px child1 "test"
            i <- receiveMessage px
            sendInterrupt px child2 testInterruptReason
            assertEff "" (i == "test")
        , testCase "most processes send foreverCheap"
        $ scheduleAndAssert schedulerFactory
        $ \assertEff -> do
            me <- self px
            traverse_
              (\(i :: Int) -> spawn $ do
                when (i `rem` 5 == 0) $ void $ sendMessage px me i
                foreverCheap $ void (sendMessage px 888 "test message to 888")
              )
              [0 .. n]
            oks <- replicateM (length [0, 5 .. n]) (receiveMessage px)
            assertEff "" (sort oks == [0, 5 .. n])
        , testCase "most processes self foreverCheap"
        $ scheduleAndAssert schedulerFactory
        $ \assertEff -> do
            me <- self px
            traverse_
              (\(i :: Int) -> spawn $ do
                when (i `rem` 5 == 0) $ void $ sendMessage px me i
                foreverCheap $ void (self px)
              )
              [0 .. n]
            oks <- replicateM (length [0, 5 .. n]) (receiveMessage px)
            assertEff "" (sort oks == [0, 5 .. n])
        , testCase "most processes sendShutdown foreverCheap"
        $ scheduleAndAssert schedulerFactory
        $ \assertEff -> do
            me <- self px
            traverse_
              (\(i :: Int) -> spawn $ do
                when (i `rem` 5 == 0) $ void $ sendMessage px me i
                foreverCheap $ void (sendShutdown px 999 ExitNormally)
              )
              [0 .. n]
            oks <- replicateM (length [0, 5 .. n]) (receiveMessage px)
            assertEff "" (sort oks == [0, 5 .. n])
        , testCase "most processes spawn foreverCheap"
        $ scheduleAndAssert schedulerFactory
        $ \assertEff -> do
            me <- self px
            traverse_
              (\(i :: Int) -> spawn $ do
                when (i `rem` 5 == 0) $ void $ sendMessage px me i
                parent <- self px
                foreverCheap
                  $ void
                      (spawn
                        (void (sendMessage px parent "test msg from child"))
                      )
              )
              [0 .. n]
            oks <- replicateM (length [0, 5 .. n]) (receiveMessage px)
            assertEff "" (sort oks == [0, 5 .. n])
        , testCase "most processes receive foreverCheap"
        $ scheduleAndAssert schedulerFactory
        $ \assertEff -> do
            me <- self px
            traverse_
              (\(i :: Int) -> spawn $ do
                when (i `rem` 5 == 0) $ void $ sendMessage px me i
                foreverCheap $ void (receiveAnyMessage px)
              )
              [0 .. n]
            oks <- replicateM (length [0, 5 .. n]) (receiveMessage px)
            assertEff "" (sort oks == [0, 5 .. n])
        ]

exitTests
  :: forall r
   . (Member (Logs LogMessage) r, SetMember Lift (Lift IO) r)
  => IO (Eff (InterruptableProcess r) () -> IO ())
  -> TestTree
exitTests schedulerFactory =
  let
    px :: SchedulerProxy r
    px = SchedulerProxy
  in
    testGroup "process exit tests"
      $ [ testGroup
          "async exceptions"
          [ testCase
                (  "a process dies immediately if a "
                ++ show e
                ++ " is thrown, while "
                ++ busyWith
                )
              $ do
                  tidVar           <- newEmptyTMVarIO
                  schedulerDoneVar <- newEmptyTMVarIO
                  void $ forkIO $ do
                    void
                      $ try @SomeException
                      $ void
                      $ applySchedulerFactory schedulerFactory
                      $ do
                          tid <- lift $ myThreadId
                          lift $ atomically $ putTMVar tidVar tid
                          foreverCheap busyEffect
                    atomically $ putTMVar schedulerDoneVar ()
                  tid <- atomically $ takeTMVar tidVar
                  threadDelay 1000
                  throwTo tid e
                  void $ atomically $ takeTMVar schedulerDoneVar
          | e <- [ThreadKilled, UserInterrupt, HeapOverflow, StackOverflow]
          , (busyWith, busyEffect) <-
            [ ( "receiving"
              , void
                (send
                  (ReceiveSelectedMessage @r (filterMessage (== "test message"))
                  )
                )
              )
            , ( "sending"
              , void
                (send (SendMessage @r 44444 (Dynamic.toDyn "test message")))
              )
            , ( "sending shutdown"
              , void (send (SendShutdown @r 44444 ExitNormally))
              )
            , ("selfpid-ing", void (send (SelfPid @r)))
            , ( "spawn-ing"
              , void
                (send
                  (Spawn @r
                    (void
                      (send (ReceiveSelectedMessage @r selectAnyMessageLazy))
                    )
                  )
                )
              )
            , ("sleeping", lift (threadDelay 100000))
            ]
          ]
        , testGroup
          "main thread exit not blocked by"
          [ testCase ("a child process, busy with " ++ busyWith)
            $ applySchedulerFactory schedulerFactory
            $ do
                void $ spawn $ foreverCheap busyEffect
                lift (threadDelay 10000)
          | (busyWith, busyEffect) <-
            [ ( "receiving"
              , void
                (send
                  (ReceiveSelectedMessage @r (filterMessage (== "test message"))
                  )
                )
              )
            , ( "sending"
              , void
                (send (SendMessage @r 44444 (Dynamic.toDyn "test message")))
              )
            , ( "sending shutdown"
              , void (send (SendShutdown @r 44444 ExitNormally))
              )
            , ("selfpid-ing", void (send (SelfPid @r)))
            , ( "spawn-ing"
              , void
                (send
                  (Spawn @r
                    (void
                      (send (ReceiveSelectedMessage @r selectAnyMessageLazy))
                    )
                  )
                )
              )
            ]
          ]
        , testGroup
          "one process exits, the other continues unimpaired"
          [ testCase
              (  "process 2 exits with: "
              ++ howToExit
              ++ " - while process 1 is busy with: "
              ++ busyWith
              )
            $ scheduleAndAssert schedulerFactory
            $ \assertEff -> do
                p1 <- spawn $ foreverCheap busyEffect
                lift (threadDelay 1000)
                void $ spawn $ do
                  lift (threadDelay 1000)
                  doExit
                lift (threadDelay 100000)
                wasRunningP1 <- isProcessAlive SP p1
                sendShutdown px p1 ExitNormally
                lift (threadDelay 100000)
                stillRunningP1 <- isProcessAlive SP p1
                assertEff "the other process did not die still running"
                          (not stillRunningP1 && wasRunningP1)
          | (busyWith , busyEffect) <-
            [ ( "receiving"
              , void
                (send
                  (ReceiveSelectedMessage @r (filterMessage (== "test message"))
                  )
                )
              )
            , ( "sending"
              , void
                (send (SendMessage @r 44444 (Dynamic.toDyn "test message")))
              )
            , ( "sending shutdown"
              , void (send (SendShutdown @r 44444 ExitNormally))
              )
            , ("selfpid-ing", void (send (SelfPid @r)))
            , ( "spawn-ing"
              , void
                (send
                  (Spawn @r
                    (void
                      (send (ReceiveSelectedMessage @r selectAnyMessageLazy))
                    )
                  )
                )
              )
            ]
          , (howToExit, doExit    ) <-
            [ ("normally"        , void (exitNormally px))
            , ("simply returning", return ())
            , ( "raiseError"
              , void (interrupt (ProcessError "test error raised"))
              )
            , ("exitWithError", void (exitWithError px "test error exit"))
            , ( "sendShutdown to self"
              , do
                me <- self px
                void (sendShutdown px me ExitNormally)
              )
            ]
          ]
        ]


sendShutdownTests
  :: forall r
   . (Member (Logs LogMessage) r, SetMember Lift (Lift IO) r)
  => IO (Eff (InterruptableProcess r) () -> IO ())
  -> TestTree
sendShutdownTests schedulerFactory =
  let px :: SchedulerProxy r
      px = SchedulerProxy
  in  testGroup
        "sendShutdown"
        [ testCase "... self" $ applySchedulerFactory schedulerFactory $ do
          me <- self px
          void $ send (SendShutdown @r me ExitNormally)
          interrupt (ProcessError "sendShutdown must not return")
        , testCase "sendInterrupt to self"
        $ scheduleAndAssert schedulerFactory
        $ \assertEff -> do
            me <- self px
            r  <- send (SendInterrupt @r me (ProcessError "123"))
            assertEff
              "Interrupted must be returned"
              (case r of
                Interrupted (ProcessError "123") -> True
                _ -> False
              )
        , testGroup
          "... other process"
          [ testCase "while it is sending"
          $ scheduleAndAssert schedulerFactory
          $ \assertEff -> do
              me    <- self px
              other <- spawn
                (do
                  untilInterrupted
                    (SendMessage @r 666666 (Dynamic.toDyn "test"))
                  void (sendMessage px me "OK")
                )
              void (sendInterrupt px other testInterruptReason)
              a <- receiveMessage px
              assertEff "" (a == "OK")
          , testCase "while it is receiving"
          $ scheduleAndAssert schedulerFactory
          $ \assertEff -> do
              me    <- self px
              other <- spawn
                (do
                  untilInterrupted
                    (ReceiveSelectedMessage @r selectAnyMessageLazy)
                  void (sendMessage px me "OK")
                )
              void (sendInterrupt px other testInterruptReason)
              a <- receiveMessage px
              assertEff "" (a == "OK")
          , testCase "while it is self'ing"
          $ scheduleAndAssert schedulerFactory
          $ \assertEff -> do
              me    <- self px
              other <- spawn
                (do
                  untilInterrupted (SelfPid @r)
                  void (sendMessage px me "OK")
                )
              void (sendInterrupt px other (ProcessError "testError"))
              a <- receiveMessage px
              assertEff "" (a == "OK")
          , testCase "while it is spawning"
          $ scheduleAndAssert schedulerFactory
          $ \assertEff -> do
              me    <- self px
              other <- spawn
                (do
                  untilInterrupted (Spawn @r (return ()))
                  void (sendMessage px me "OK")
                )
              void (sendInterrupt px other testInterruptReason)
              a <- receiveMessage px
              assertEff "" (a == "OK")
          , testCase "while it is sending shutdown messages"
          $ scheduleAndAssert schedulerFactory
          $ \assertEff -> do
              me    <- self px
              other <- spawn
                (do
                  untilInterrupted (SendShutdown @r 777 ExitNormally)
                  void (sendMessage px me "OK")
                )
              void (sendInterrupt px other testInterruptReason)
              a <- receiveMessage px
              assertEff "" (a == "OK")
          , testCase "handleInterrupt handles my own interrupts"
          $ scheduleAndAssert schedulerFactory
          $ \assertEff ->
              handleInterrupts
                  (\e -> return (ProcessError "test" == e))
                  (interrupt (ProcessError "test") >> return False)
                >>= assertEff "exception handler not invoked"
          ]
        ]

linkingTests
  :: forall r
   . (Member (Logs LogMessage) r, SetMember Lift (Lift IO) r)
  => IO (Eff (InterruptableProcess r) () -> IO ())
  -> TestTree
linkingTests schedulerFactory = setTravisTestOptions
  (testGroup
    "process linking tests"
    [ testCase "link process with it self"
    $ applySchedulerFactory schedulerFactory
    $ do
        me <- self SP
        handleInterrupts
          (\er -> lift (False @? ("unexpected interrupt: " ++ show er)))
          (do
            linkProcess SP me
            lift (threadDelay 10000)
          )
    , testCase "link with not running process"
    $ applySchedulerFactory schedulerFactory
    $ do
        let testPid = 234234234
        handleInterrupts
          (lift . (@?= LinkedProcessCrashed testPid))
          (do
            linkProcess SP testPid
            void (receiveMessage @Void SP)
          )
    , testCase "linked process exit message is NormalExit"
    $ applySchedulerFactory schedulerFactory
    $ do
        foo <- spawn (void (receiveMessage @Void SP))
        handleInterrupts
          (lift . (\e -> e /= LinkedProcessCrashed foo @? show e))
          (do
            linkProcess SP foo
            sendShutdown SP foo ExitNormally
            lift (threadDelay 1000)
          )
    , testCase "linked process exit message is Crash"
    $ applySchedulerFactory schedulerFactory
    $ do
        foo <- spawn (void (receiveMessage @Void SP))
        handleInterrupts
          (lift . (@?= LinkedProcessCrashed foo))
          (do
            linkProcess SP foo
            sendShutdown SP foo Killed
            void (receiveMessage @Void SP)
          )
    , testCase "link multiple times"
    $ applySchedulerFactory schedulerFactory
    $ do
        foo <- spawn (void (receiveMessage @Void SP))
        handleInterrupts
          (lift . (@?= LinkedProcessCrashed foo))
          (do
            linkProcess SP foo
            linkProcess SP foo
            linkProcess SP foo
            linkProcess SP foo
            linkProcess SP foo
            linkProcess SP foo
            linkProcess SP foo
            sendShutdown SP foo Killed
            void (receiveMessage @Void SP)
          )
    , testCase "unlink multiple times"
    $ applySchedulerFactory schedulerFactory
    $ do
        foo <- spawn (void (receiveMessage @Void SP))
        handleInterrupts
          (lift . (False @?) . show)
          (do
            spawn_ (void (receiveAnyMessage SP))
            linkProcess SP foo
            linkProcess SP foo
            linkProcess SP foo
            unlinkProcess SP foo
            unlinkProcess SP foo
            unlinkProcess SP foo
            unlinkProcess SP foo
            withMonitor SP foo $ \ref -> do
              sendShutdown SP foo Killed
              void (receiveSelectedMessage SP (selectProcessDown ref))
          )
    , testCase "spawnLink" $ applySchedulerFactory schedulerFactory $ do
      let foo = void (receiveMessage @Void SP)
      handleInterrupts (\er -> lift (isBecauseDown Nothing er @? show er)) $ do
        x <- spawnLink foo
        sendShutdown SP x Killed
        void (receiveMessage @Void SP)
    , testCase "ignore normal exit"
    $ applySchedulerFactory schedulerFactory
    $ do
        mainProc <- self SP
        let linkingServer = void $ exitOnInterrupt SP $ do
              logNotice "linker"
              foreverCheap $ do
                x <- receiveMessage SP
                linkProcess SP x
                sendMessage SP mainProc True
        linker <- spawnLink linkingServer
        logNotice "mainProc"
        do
          x <- spawnLink (logNotice "x 1" >> void (receiveMessage @Void SP))
          withMonitor SP x $ \xRef -> do
            sendMessage SP linker x
            void $ receiveSelectedMessage SP (filterMessage id)
            sendShutdown SP x ExitNormally
            void (receiveSelectedMessage SP (selectProcessDown xRef))
        do
          x <- spawnLink (logNotice "x 2" >> void (receiveMessage @Void SP))
          withMonitor SP x $ \xRef -> do
            sendMessage SP linker x
            void $ receiveSelectedMessage SP (filterMessage id)
            sendShutdown SP x ExitNormally
            void (receiveSelectedMessage SP (selectProcessDown xRef))
        handleInterrupts (lift . (LinkedProcessCrashed linker @=?)) $ do
          sendShutdown SP linker Killed
          void (receiveMessage @Void SP)
    , testCase "unlink" $ applySchedulerFactory schedulerFactory $ do
      let
        foo1 = void (receiveAnyMessage SP)
        foo2 foo1Pid = do
          linkProcess SP foo1Pid
          ("unlink foo1", barPid) <- receiveMessage SP
          unlinkProcess SP foo1Pid
          sendMessage SP barPid ("unlinked foo1", foo1Pid)
          "the end" <- receiveMessage SP
          exitWithError SP "foo two"
        bar foo2Pid parentPid = do
          linkProcess SP foo2Pid
          me <- self SP
          sendMessage SP foo2Pid ("unlink foo1", me)
          ("unlinked foo1", foo1Pid) <- receiveMessage SP
          handleInterrupts
            (const (return ()))
            (do
              linkProcess SP foo1Pid
              sendShutdown SP foo1Pid Killed
              void (receiveMessage @Void SP)
            )
          handleInterrupts
            (\er ->
              void
                (sendMessage SP parentPid (LinkedProcessCrashed foo2Pid == er))
            )
            (do
              sendMessage SP foo2Pid "the end"
              void (receiveAnyMessage SP)
            )
      foo1Pid <- spawn foo1
      foo2Pid <- spawn (foo2 foo1Pid)
      me      <- self SP
      barPid  <- spawn (bar foo2Pid me)
      handleInterrupts
        (\er -> lift (LinkedProcessCrashed barPid @?= er))
        (do
          res <- receiveMessage @Bool SP
          lift (threadDelay 100000)
          lift (res @?= True)
        )
    ]
  )

monitoringTests
  :: forall r
   . (Member (Logs LogMessage) r, SetMember Lift (Lift IO) r)
  => IO (Eff (InterruptableProcess r) () -> IO ())
  -> TestTree
monitoringTests schedulerFactory = setTravisTestOptions
  (testGroup
    "process monitoring tests"
    [ testCase "monitored process not running"
    $ applySchedulerFactory schedulerFactory
    $ do
        let badPid = 132123
        ref <- monitor SP badPid
        pd  <- receiveSelectedMessage SP (selectProcessDown ref)
        lift (downReason pd @?= SomeExitReason (ProcessNotRunning badPid))
        lift (threadDelay 10000)
    , testCase "monitored process exit normally"
    $ applySchedulerFactory schedulerFactory
    $ do
        target <- spawn (receiveMessage SP >>= exitBecause SP)
        ref    <- monitor SP target
        sendMessage SP target ExitNormally
        pd <- receiveSelectedMessage SP (selectProcessDown ref)
        lift (downReason pd @?= SomeExitReason ExitNormally)
        lift (threadDelay 10000)
    , testCase "multiple monitors some demonitored"
    $ applySchedulerFactory schedulerFactory
    $ do
        target <- spawn (receiveMessage SP >>= exitBecause SP)
        ref1   <- monitor SP target
        ref2   <- monitor SP target
        ref3   <- monitor SP target
        ref4   <- monitor SP target
        ref5   <- monitor SP target
        demonitor SP ref3
        demonitor SP ref5
        sendMessage SP target ExitNormally
        pd1 <- receiveSelectedMessage SP (selectProcessDown ref1)
        lift (downReason pd1 @?= SomeExitReason ExitNormally)
        pd2 <- receiveSelectedMessage SP (selectProcessDown ref2)
        lift (downReason pd2 @?= SomeExitReason ExitNormally)
        pd4 <- receiveSelectedMessage SP (selectProcessDown ref4)
        lift (downReason pd4 @?= SomeExitReason ExitNormally)
        lift (threadDelay 10000)
    , testCase "monitored process killed"
    $ applySchedulerFactory schedulerFactory
    $ do
        target <- spawn (receiveMessage SP >>= exitBecause SP)
        ref    <- monitor SP target
        sendMessage SP target Killed
        pd <- receiveSelectedMessage SP (selectProcessDown ref)
        lift (downReason pd @?= SomeExitReason Killed)
        lift (threadDelay 10000)
    , testCase "demonitored process killed"
    $ applySchedulerFactory schedulerFactory
    $ do
        target <- spawn (receiveMessage SP >>= exitBecause SP)
        ref    <- monitor SP target
        demonitor SP ref
        sendMessage SP target Killed
        me <- self SP
        spawn_ (lift (threadDelay 10000) >> sendMessage SP me ())
        pd <- receiveSelectedMessage
          SP
          (Right <$> selectProcessDown ref <|> Left <$> selectMessage @())
        lift (pd @?= Left ())
        lift (threadDelay 10000)
    ]
  )

timerTests
  :: forall r
   . (Member (Logs LogMessage) r, SetMember Lift (Lift IO) r)
  => IO (Eff (InterruptableProcess r) () -> IO ())
  -> TestTree
timerTests schedulerFactory = setTravisTestOptions
  (testGroup
    "process timer tests"
    [ testCase "receiveAfter into timeout"
    $ applySchedulerFactory schedulerFactory
    $ do
        pd <- receiveAfter @Void SP 1000
        lift (pd @?= Nothing)
        lift (threadDelay 10000)
    , testCase "receiveAfter no timeout"
    $ applySchedulerFactory schedulerFactory
    $ do
        me    <- self SP
        other <- spawn
          (do
            () <- receiveMessage @() SP
            sendMessage SP me (123 :: Int)
          )
        pd1 <- receiveAfter @() SP 10000
        lift (pd1 @?= Nothing)
        sendMessage SP other ()
        pd2 <- receiveAfter @Int SP 10000
        lift (pd2 @?= Just 123)
        lift (threadDelay 10000)
    , testCase "many receiveAfters"
    $ applySchedulerFactory schedulerFactory
    $ do
        let n = 5
            testMsg :: Float
            testMsg = 123
        me    <- self SP
        other <- spawn
          (do
            replicateM_ n $ sendMessage SP me "bad message"
            () <- receiveMessage @() SP
            replicateM_ n $ sendMessage SP me testMsg
          )
        receiveAfter @Float SP 100 >>= lift . (@?= Nothing)
        sendMessage SP other ()
        replicateM_
          n
          (do
            res <- receiveAfter @Float SP 10000
            lift (res @?= Just testMsg)
          )

        lift (threadDelay 100)
    ]
  )