diff --git a/release-notes.txt b/release-notes.txt index ae633ba..72c765f 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -2,6 +2,7 @@ Release notes: Unreleased + - test: add SideEffects module and ImmTaskSeq variant tests to TaskSeq.ChunkBy.Tests.fs, improving coverage for chunkBy and chunkByAsync - fixes: `Async.bind` signature corrected from `(Async<'T> -> Async<'U>)` to `('T -> Async<'U>)` to match standard monadic bind semantics (same as `Task.bind`); the previous signature made the function effectively equivalent to direct application - refactor: simplify splitAt 'rest' taskSeq to use while!, removing redundant go2 mutable and manual MoveNextAsync pre-advance diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.ChunkBy.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.ChunkBy.Tests.fs index 76f684c..a8014ef 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.ChunkBy.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.ChunkBy.Tests.fs @@ -133,3 +133,124 @@ module Functionality = let _, arr = result[0] arr |> should haveLength 10 } + + [)>] + let ``TaskSeq-chunkBy each element its own chunk as variants`` variant = task { + let ts = Gen.getSeqImmutable variant + let! result = TaskSeq.chunkBy id ts |> TaskSeq.toArrayAsync + result |> should haveLength 10 + + result + |> Array.iteri (fun i (k, arr) -> + k |> should equal (i + 1) + arr |> should haveLength 1 + arr[0] |> should equal (i + 1)) + } + + [)>] + let ``TaskSeq-chunkByAsync all elements same key as variants`` variant = task { + let ts = Gen.getSeqImmutable variant + + let! result = + TaskSeq.chunkByAsync (fun _ -> Task.fromResult 0) ts + |> TaskSeq.toArrayAsync + + result |> should haveLength 1 + let _, arr = result[0] + arr |> should haveLength 10 + } + + +module SideEffects = + [)>] + let ``TaskSeq-chunkBy on side-effect seq groups all elements under one key`` variant = task { + let! result = + Gen.getSeqWithSideEffect variant + |> TaskSeq.chunkBy (fun _ -> 0) + |> TaskSeq.toArrayAsync + + result |> should haveLength 1 + let _, arr = result[0] + arr |> should equal [| 1..10 |] + } + + [)>] + let ``TaskSeq-chunkByAsync on side-effect seq groups all elements under one key`` variant = task { + let! result = + Gen.getSeqWithSideEffect variant + |> TaskSeq.chunkByAsync (fun _ -> Task.fromResult 0) + |> TaskSeq.toArrayAsync + + result |> should haveLength 1 + let _, arr = result[0] + arr |> should equal [| 1..10 |] + } + + [)>] + let ``TaskSeq-chunkBy on side-effect seq produces correct singleton chunks`` variant = task { + let! result = + Gen.getSeqWithSideEffect variant + |> TaskSeq.chunkBy id + |> TaskSeq.toArrayAsync + + result |> should haveLength 10 + + result + |> Array.iteri (fun i (k, arr) -> + k |> should equal (i + 1) + arr |> should equal [| i + 1 |]) + } + + [] + let ``TaskSeq-chunkBy projection is called exactly once per element`` () = task { + let mutable callCount = 0 + + let ts = taskSeq { yield! [ 1; 1; 2; 3; 3 ] } + + let! result = + ts + |> TaskSeq.chunkBy (fun x -> + callCount <- callCount + 1 + x % 2) + |> TaskSeq.toArrayAsync + + callCount |> should equal 5 + result |> should haveLength 3 + } + + [] + let ``TaskSeq-chunkByAsync projection is called exactly once per element`` () = task { + let mutable callCount = 0 + + let ts = taskSeq { yield! [ 1; 1; 2; 3; 3 ] } + + let! result = + ts + |> TaskSeq.chunkByAsync (fun x -> task { + callCount <- callCount + 1 + return x % 2 + }) + |> TaskSeq.toArrayAsync + + callCount |> should equal 5 + result |> should haveLength 3 + } + + [] + let ``TaskSeq-chunkBy does not evaluate elements before enumeration`` () = task { + let mutable sourceCount = 0 + + let ts = taskSeq { + for i in 1..5 do + sourceCount <- sourceCount + 1 + yield i + } + + let chunked = TaskSeq.chunkBy (fun x -> x % 2 = 0) ts + // Building the pipeline does not consume the source + sourceCount |> should equal 0 + + let! _ = TaskSeq.toArrayAsync chunked + // Only after consuming does the source get evaluated + sourceCount |> should equal 5 + }