packages feed

theatre-dev-0.2: hspec/TheatreDevSpec.hs

{-# OPTIONS_GHC -Wno-unused-local-binds -Wno-unused-binds #-}

module TheatreDevSpec (spec) where

import Control.Concurrent.Async
import Data.IntMap.Strict qualified as IntMap
import Data.IntSet qualified as IntSet
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck
import TheatreDev qualified as Actor
import TheatreDevSpec.IO qualified as IO
import TheatreDevSpec.Preferences qualified as Preferences
import Prelude

spec :: Spec
spec =
  do
    describe "kill" do
      describe "When full" do
        it "Does not block" pending
        it "Lets all messages scheduled before be processed" pending
        it "Makes all messages scheduled after be ignored" pending

    describe "wait" do
      describe "When full" do
        it "Blocks until all messages are processed" pending

    describe "spawnStatefulBatched" do
      let spawnIntUpdater step = Actor.spawnStatefulBatched @Int 0 (const (return ())) step
      let spawnUnit step = Actor.spawnStatefulBatched () (const (return ())) step

      it "Works in batches" do
        acc <- newIORef []
        actorLock <- newEmptyMVar
        emitterLock <- newEmptyMVar
        actor <- spawnUnit $ \_ messages ->
          do
            modifyIORef' acc (messages :)
            putMVar emitterLock ()
            takeMVar actorLock

        Actor.tell actor 1

        takeMVar emitterLock
        Actor.tell actor 2
        Actor.tell actor 3
        putMVar actorLock ()

        takeMVar emitterLock
        Actor.tell actor 4
        putMVar actorLock ()

        takeMVar emitterLock
        Actor.kill actor

        collectedBatches <- reverse . fmap toList <$> readIORef acc
        shouldBe collectedBatches [[1], [2, 3], [4]]

      it "Threads the state" do
        let input = [0 .. 9]
            inputLength = length input

        resultsVar <- newTVarIO []
        actor <- Actor.spawnStatefulBatched [] (const (return ())) $ \state msgs -> do
          let !newState = foldl' (flip (:)) state msgs
          atomically $ writeTVar resultsVar newState
          return newState
        traverse_ (Actor.tell actor) input

        results <- atomically $ do
          results <- readTVar resultsVar
          if length results < inputLength
            then retry
            else return results

        shouldBe (reverse results) input

      it "Kill and wait" do
        let input = [0 .. 9]
            inputLength = length input

        resultVar <- newEmptyMVar
        actor <-
          Actor.spawnStatefulBatched
            []
            ( \state -> do
                threadDelay 1000
                putMVar resultVar state
            )
            ( \state msgs -> return $ foldl' (flip (:)) state msgs
            )
        traverse_ (Actor.tell actor) input

        Actor.kill actor
        Actor.wait actor

        result <- takeMVar resultVar
        shouldBe result $ reverse input

    describe "allOf" . modifyMaxSuccess (max 1000) $ do
      it "Passes 1" do
        let emittersNum = 2
            messagesNum = 10
            actorsNum = 3
            messages = [0 .. messagesNum - 1]
        results <- fmap (fmap sort) (IO.simulateReduction actorsNum emittersNum Actor.allOf messages)
        shouldBe results (replicate actorsNum (sort (concat (replicate emittersNum messages))))
        shouldBe (getSum (foldMap (Sum . length) results)) (actorsNum * emittersNum * messagesNum)
      prop "" $ forAll (chooseInt (0, 99)) $ \size -> forAll arbitrary $ \(messages :: [Int]) ->
        idempotentIOProperty do
          results <- sort . concat <$> IO.simulateReduction Preferences.concurrency size Actor.allOf messages
          return
            $ conjoin
              [ length results === length messages * size * Preferences.concurrency,
                results === sort (concat (replicate (size * Preferences.concurrency) messages))
              ]

    describe "oneOf" . modifyMaxSuccess (max 1000) $ do
      prop "Dispatches correctly" $ forAll (chooseInt (0, 99)) $ \size -> forAll arbitrary $ \(messages :: [Int]) ->
        idempotentIOProperty do
          results <- sort . concat <$> IO.simulateReduction Preferences.concurrency size Actor.oneOf messages
          return
            $ conjoin
              [ length results === length messages * size,
                results
                  === sort (concat (replicate (size) messages))
              ]

    describe "byKeyHash" . modifyMaxSuccess (max Preferences.largePropertyMaxSuccess) $ do
      prop "Dispatches individually" $ forAll (chooseInt (0, 99)) $ \size -> forAll arbitrary $ \(messages :: [Int]) -> idempotentIOProperty $ do
        resultsVar <- newTVarIO []
        actor <-
          fmap (Actor.byKeyHash id)
            $ replicateM size
            $ Actor.spawnStatefulIndividual
              IntMap.empty
              ( \state ->
                  atomically
                    $ modifyTVar' resultsVar
                    $ (:) state
              )
              ( \state msg ->
                  return $ IntMap.alter (Just . maybe 1 succ) msg state
              )

        mapConcurrently id
          $ replicate Preferences.concurrency
          $ for_ messages
          $ Actor.tell actor

        Actor.kill actor
        Actor.wait actor

        results <- readTVarIO resultsVar

        let allKeys = results >>= IntMap.keys
            nubbedKeys = nub allKeys

        return
          $ conjoin
            [ allKeys === nubbedKeys,
              if size == 0
                then nubbedKeys === []
                else IntSet.fromList nubbedKeys === IntSet.fromList messages
            ]