packages feed

ychr-0.1.0.0: test/YCHR/Runtime/ReactivationTest.hs

module YCHR.Runtime.ReactivationTest (tests) where

import Control.Monad.IO.Class (liftIO)
import Data.IORef
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))
import YCHR.Internal.Runtime.Monad (Chr, initSessionEnv, runChr)
import YCHR.Internal.Runtime.Reactivation
import YCHR.Internal.Runtime.Types (SuspensionId (..))

tests :: TestTree
tests =
  testGroup
    "YCHR.Internal.Runtime.Reactivation"
    [ emptyTests,
      orderTests,
      reentrancyTests,
      miscTests
    ]

runReactEnv :: Chr a -> IO a
runReactEnv action = do
  env <- initSessionEnv [] [] Map.empty Map.empty Map.empty Map.empty Set.empty
  runChr action env

-- | Drain the queue, collecting all IDs in order.
drainCollect :: Chr [SuspensionId]
drainCollect = do
  ref <- liftIO $ newIORef []
  drainQueue $ \sid -> liftIO $ modifyIORef' ref (sid :)
  liftIO $ reverse <$> readIORef ref

emptyTests :: TestTree
emptyTests =
  testGroup
    "empty queue"
    [ testCase "drain on empty does nothing" $ do
        ids <- runReactEnv drainCollect
        ids @?= []
    ]

orderTests :: TestTree
orderTests =
  testGroup
    "FIFO order"
    [ testCase "single enqueue preserves order" $ do
        ids <- runReactEnv $ do
          enqueue [SuspensionId 0, SuspensionId 1]
          drainCollect
        ids @?= [SuspensionId 0, SuspensionId 1],
      testCase "multiple enqueues preserve combined order" $ do
        ids <- runReactEnv $ do
          enqueue [SuspensionId 0, SuspensionId 1]
          enqueue [SuspensionId 2, SuspensionId 3]
          drainCollect
        ids @?= [SuspensionId 0, SuspensionId 1, SuspensionId 2, SuspensionId 3],
      testCase "empty list enqueue is a no-op" $ do
        ids <- runReactEnv $ do
          enqueue []
          drainCollect
        ids @?= []
    ]

reentrancyTests :: TestTree
reentrancyTests =
  testGroup
    "reentrancy"
    [ testCase "callback enqueues more IDs" $ do
        ids <- runReactEnv $ do
          ref <- liftIO $ newIORef []
          enqueue [SuspensionId 0]
          drainQueue $ \sid -> do
            liftIO $ modifyIORef' ref (sid :)
            case sid of
              SuspensionId 0 -> enqueue [SuspensionId 10, SuspensionId 11]
              _ -> pure ()
          liftIO $ reverse <$> readIORef ref
        ids @?= [SuspensionId 0, SuspensionId 10, SuspensionId 11],
      testCase "deep reentrancy (N < 3 -> enqueue N+1)" $ do
        ids <- runReactEnv $ do
          ref <- liftIO $ newIORef []
          enqueue [SuspensionId 0]
          drainQueue $ \sid@(SuspensionId n) -> do
            liftIO $ modifyIORef' ref (sid :)
            if n < 3
              then enqueue [SuspensionId (n + 1)]
              else pure ()
          liftIO $ reverse <$> readIORef ref
        ids @?= [SuspensionId 0, SuspensionId 1, SuspensionId 2, SuspensionId 3]
    ]

miscTests :: TestTree
miscTests =
  testGroup
    "misc"
    [ testCase "queue empty after drain" $ do
        ids <- runReactEnv $ do
          enqueue [SuspensionId 0, SuspensionId 1]
          _ <- drainCollect
          drainCollect
        ids @?= [],
      testCase "duplicates preserved" $ do
        ids <- runReactEnv $ do
          enqueue [SuspensionId 5, SuspensionId 5]
          drainCollect
        ids @?= [SuspensionId 5, SuspensionId 5]
    ]