Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
121 changes: 121 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.ChunkBy.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,124 @@ module Functionality =
let _, arr = result[0]
arr |> should haveLength 10
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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))
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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 =
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
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 |]
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
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 |]
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
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 |])
}

[<Fact>]
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
}

[<Fact>]
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
}

[<Fact>]
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
}
Loading