packages feed

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

module Unit.Atelier.Effects.YieldSpec (spec_Yield) where

import Effectful (runEff, runPureEff)
import Effectful.Concurrent (runConcurrent)
import Effectful.State.Static.Shared (execState, modify)
import Effectful.Writer.Static.Shared (execWriter, tell)
import Test.Hspec (Spec, describe, it, shouldBe, shouldMatchList)

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_Yield :: Spec
spec_Yield = do
    describe "yieldToList" testYieldToList
    describe "yieldToReverseList" testYieldToReverseList
    describe "forEach" testForEach
    describe "ignoreYield" testIgnoreYield
    describe "inFoldable" testInFoldable
    describe "cycleToYield" testCycleToYield
    describe "withYieldToList" testWithYieldToList
    describe "enumerate" testEnumerate
    describe "enumerateFrom" testEnumerateFrom
    describe "map" testMap
    describe "mapMaybe" testMapMaybe
    describe "catMaybes" testCatMaybes
    describe "filter" testFilter
    describe "changes" testChanges


testYieldToList :: Spec
testYieldToList = do
    it "collects yields in order" do
        let ((), xs) = runPureEff $ Yield.yieldToList @Int do
                Yield.yield 1
                Yield.yield 2
                Yield.yield 3
        xs `shouldMatchList` [1, 2, 3]

    it "returns empty list when nothing is yielded" do
        let (_, xs) = runPureEff $ Yield.yieldToList @Int $ pure ()
        xs `shouldMatchList` []

    it "also returns the result of the computation" do
        let (r :: Text, xs) = runPureEff $ Yield.yieldToList do
                Yield.yield (1 :: Int)
                pure "result"
        r `shouldBe` "result"
        xs `shouldMatchList` [1]


testYieldToReverseList :: Spec
testYieldToReverseList = do
    it "collects yields in reverse order" do
        let ((), xs) = runPureEff $ Yield.yieldToReverseList @Int do
                Yield.yield 1
                Yield.yield 2
                Yield.yield 3
        xs `shouldBe` [3, 2, 1]


testForEach :: Spec
testForEach = do
    it "calls the action for each yielded value" do
        let xs = runPureEff
                $ execState @[Int] mempty
                $ Yield.forEach (\x -> modify (x :)) do
                    Yield.yield 1
                    Yield.yield 2
                    Yield.yield 3
        xs `shouldBe` [3, 2, 1]

    it "can discard values" do
        let xs = runPureEff $ Yield.forEach @Int (const $ pure ()) do
                Yield.yield 1
        xs `shouldBe` ()


testIgnoreYield :: Spec
testIgnoreYield = do
    it "discards all yielded values" do
        let x = runPureEff $ Yield.ignoreYield @Int do
                Yield.yield 1
                Yield.yield 2
        x `shouldBe` ()


testInFoldable :: Spec
testInFoldable = do
    it "yields all elements of a list in order" do
        let ((), xs) = runPureEff $ Yield.yieldToList $ Yield.inFoldable @Int [1, 2, 3]
        xs `shouldBe` [1, 2, 3]

    it "yields nothing for an empty list" do
        let ((), xs) = runPureEff $ Yield.yieldToList $ Yield.inFoldable @Int []
        xs `shouldBe` []


testCycleToYield :: Spec
testCycleToYield = do
    it "yields elements of a list repeatedly in order" do
        xs <-
            runTest
                $ Await.awaitYield
                    (Yield.cycleToYield @Int [1, 2, 3])
                    (replicateM_ 7 (Await.await >>= \x -> tell [x]))
        xs `shouldBe` [1, 2, 3, 1, 2, 3, 1]
  where
    runTest = runEff . runConcurrent . runConc . runChan . execWriter


testWithYieldToList :: Spec
testWithYieldToList = do
    it "passes the collected yields to the returned function" do
        let result = runPureEff $ Yield.withYieldToList @Int do
                Yield.yield 1
                Yield.yield 2
                Yield.yield 3
                pure length
        result `shouldBe` 3

    it "passes yields in order to the function" do
        let result = runPureEff $ Yield.withYieldToList @Int do
                Yield.yield 1
                Yield.yield 2
                Yield.yield 3
                pure id
        result `shouldBe` [1, 2, 3]

    it "passes an empty list when nothing is yielded" do
        let result = runPureEff $ Yield.withYieldToList @Int do
                pure null
        result `shouldBe` True


testEnumerate :: Spec
testEnumerate = do
    it "pairs each value with its zero-based index" do
        let ((), xs) = runPureEff $ Yield.yieldToList $ Yield.enumerate do
                Yield.yield 'a'
                Yield.yield 'b'
                Yield.yield 'c'
        xs `shouldBe` [(0, 'a'), (1, 'b'), (2, 'c')]


testEnumerateFrom :: Spec
testEnumerateFrom = do
    it "pairs each value with its index starting from the given value" do
        let ((), xs) = runPureEff $ Yield.yieldToList $ Yield.enumerateFrom 5 do
                Yield.yield 'x'
                Yield.yield 'y'
        xs `shouldBe` [(5, 'x'), (6, 'y')]


testMap :: Spec
testMap = do
    it "transforms each yielded value" do
        let (_, xs) = runPureEff $ Yield.yieldToList $ Yield.map @Int (* 2) do
                Yield.yield 1
                Yield.yield 2
                Yield.yield 3
        xs `shouldBe` [2, 4, 6]

    it "preserves order" do
        let (_, xs :: [Text]) = runPureEff $ Yield.yieldToList $ Yield.map show do
                Yield.yield @Int 1
                Yield.yield 2
        xs `shouldBe` ["1", "2"]


testMapMaybe :: Spec
testMapMaybe = do
    describe "when the function returns Just" $ it "yields transformed values" do
        let ((), xs) = runPureEff $ Yield.yieldToList $ Yield.mapMaybe (\x -> if even x then Just (x * 10) else Nothing) do
                Yield.yield @Int 1
                Yield.yield 2
                Yield.yield 3
                Yield.yield 4
        xs `shouldMatchList` [20, 40]

    describe "when the function always returns Nothing" $ it "yields nothing" do
        let (_, xs) = runPureEff $ Yield.yieldToList $ Yield.mapMaybe @Int @Int (const Nothing) do
                Yield.yield 1
        xs `shouldMatchList` []


testCatMaybes :: Spec
testCatMaybes = do
    it "unwraps Just values and drops Nothings" do
        let (_, xs) = runPureEff $ Yield.yieldToList $ Yield.catMaybes do
                Yield.yield (Just 1)
                Yield.yield Nothing
                Yield.yield (Just 2)
        xs `shouldBe` [1 :: Int, 2]

    it "yields nothing when all values are Nothing" do
        let (_, xs) = runPureEff $ Yield.yieldToList $ Yield.catMaybes do
                Yield.yield (Nothing :: Maybe Int)
                Yield.yield Nothing
        xs `shouldBe` []


testFilter :: Spec
testFilter = do
    it "passes values satisfying the predicate" do
        let (_, xs) = runPureEff $ Yield.yieldToList $ Yield.filter even do
                Yield.yield 1
                Yield.yield 2
                Yield.yield 3
                Yield.yield 4
        xs `shouldBe` [2 :: Int, 4]

    it "drops all values when predicate is always false" do
        let (_, xs) = runPureEff $ Yield.yieldToList $ Yield.filter (const False) do
                Yield.yield (1 :: Int)
        xs `shouldBe` []

    it "passes all values when predicate is always true" do
        let (_, xs) = runPureEff $ Yield.yieldToList $ Yield.filter (const True) do
                Yield.yield 1
                Yield.yield 2
        xs `shouldBe` [1 :: Int, 2]


testChanges :: Spec
testChanges = do
    it "suppresses yields equal to the initial value" do
        let (_, xs) = runPureEff $ Yield.yieldToList $ Yield.changes 0 do
                Yield.yield 0
                Yield.yield 1
        xs `shouldBe` [1 :: Int]

    it "passes values that differ from the initial value" do
        let (_, xs) = runPureEff $ Yield.yieldToList $ Yield.changes 0 do
                Yield.yield 1
                Yield.yield 2
        xs `shouldBe` [1 :: Int, 2]

    it "suppresses initial value interspersed with other values" do
        let (_, xs) = runPureEff $ Yield.yieldToList $ Yield.changes 0 do
                Yield.yield 0
                Yield.yield 1
                Yield.yield 0
                Yield.yield 2
                Yield.yield 0
                Yield.yield 3
        xs `shouldBe` [1 :: Int, 2, 3]

    it "does not suppress non-initial values even if they repeat" do
        let (_, xs) = runPureEff $ Yield.yieldToList $ Yield.changes 0 do
                Yield.yield 1
                Yield.yield 1
                Yield.yield 2
        xs `shouldBe` [1 :: Int, 1, 2]