packages feed

atelier-core-0.1.0.0: test/Unit/Atelier/Effects/AwaitSpec.hs

module Unit.Atelier.Effects.AwaitSpec (spec_Await) where

import Effectful (runEff, runPureEff)
import Effectful.Concurrent (runConcurrent)
import Effectful.State.Static.Shared (evalState, state)
import Effectful.Writer.Static.Shared (runWriter, tell)
import Test.Hspec (Spec, describe, it, shouldBe)

import Effectful.Concurrent.STM qualified as STM

import Atelier.Effects.Chan (runChan)
import Atelier.Effects.Conc (runConc)

import Atelier.Effects.Await qualified as Await
import Atelier.Effects.Yield qualified as Yield


spec_Await :: Spec
spec_Await = do
    describe "eachAwait" testEachAwait
    describe "takeAwait" testTakeAwait
    describe "awaitYield" testAwaitYield


testEachAwait :: Spec
testEachAwait = do
    it "answers each await with the given action's result" do
        let xs =
                runPureEff
                    . evalState @Int 0
                    . Await.eachAwait (state \s -> (s, s + 1))
                    $ replicateM 4 Await.await
        xs `shouldBe` [0, 1, 2, 3]


testTakeAwait :: Spec
testTakeAwait = do
    it "yields exactly N values from the await stream" do
        let (_, xs) =
                runPureEff
                    . evalState @Int 0
                    . Yield.yieldToList
                    . Await.eachAwait @Int (state \s -> (s, s + 1))
                    $ Await.takeAwait 3
        xs `shouldBe` [0, 1, 2]

    describe "when N is zero" $ it "yields nothing" do
        let (_, xs) =
                runPureEff
                    . Yield.yieldToList
                    . Await.eachAwait @Int (pure 7)
                    $ Await.takeAwait 0
        xs `shouldBe` []


testAwaitYield :: Spec
testAwaitYield = do
    it "pipes yielded values into the await stream" do
        (_, result) <-
            runTest
                $ Await.awaitYield (Yield.inFoldable @Int [1, 2, 3] >> blockForever)
                $ replicateM 3 Await.await >>= tell

        result `shouldBe` [1, 2, 3]

    it "returns the result of the awaiting computation" do
        -- The yielder blocks after yielding so the awaiter reliably wins the
        -- termination race and its 'tell' is observed; otherwise the yielder can
        -- finish first and cancel the awaiter before it writes (flaky under load).
        (_, result) <-
            runTest
                $ Await.awaitYield @Int (Yield.yield 99 >> blockForever)
                $ fmap (* 2) Await.await >>= tell . one
        result `shouldBe` [198]

    describe "when the awaiter finishes before the yielder"
        $ it "terminates with the awaiter's result" do
            (result, _) <-
                runTest . runConcurrent $ do
                    Await.awaitYield @Int
                        (Yield.inFoldable [1 .. 100] >> blockForever)
                        Await.await
            result `shouldBe` 1

    describe "when the yielder finishes before the awaiter"
        $ it "terminates with the yielder's result" do
            (result :: Int, _) <-
                runTest
                    $ Await.awaitYield @Int (Yield.yield 1 >> pure 2)
                    $ Await.await >> Await.await >> pure 1
            result `shouldBe` 2
  where
    runTest = runEff . runConcurrent . runConc . runChan . runWriter
    -- Block the current (forked) computation indefinitely; used to keep a yielder
    -- alive so the awaiter wins the awaitYield termination race. The explicit
    -- signature keeps it polymorphic across the differing effect stacks below.
    blockForever :: (STM.Concurrent :> es) => Eff es a
    blockForever = STM.atomically STM.retry