Skip to content
Open
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 to TaskSeq.Unfold.Tests.fs, verifying generator call counts, re-iteration behaviour, early-termination via take, and exception propagation
- 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
111 changes: 111 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Unfold.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,114 @@ module Functionality =
first |> should equal second
first |> should equal [| 0..4 |]
}

module SideEffects =
[<Fact>]
let ``TaskSeq-unfold generator side-effects accumulate across re-iterations`` () = task {
// The generator closes over mutable external state. Each re-iteration starts fresh from
// the initial seed (0), but the external counter keeps climbing β€” demonstrating that
// the IAsyncEnumerable itself is stateless but the captured state is shared.
let mutable totalCalls = 0

let ts =
TaskSeq.unfold
(fun n ->
totalCalls <- totalCalls + 1
if n < 3 then Some(n, n + 1) else None)
0

let! first = ts |> TaskSeq.toArrayAsync
first |> should equal [| 0; 1; 2 |]
totalCalls |> should equal 4 // 3 Some + 1 None

let! second = ts |> TaskSeq.toArrayAsync
second |> should equal [| 0; 1; 2 |]
totalCalls |> should equal 8 // called 4 more times for the second iteration
}

[<Fact>]
let ``TaskSeq-unfoldAsync generator side-effects accumulate across re-iterations`` () = task {
let mutable totalCalls = 0

let ts =
TaskSeq.unfoldAsync
(fun n -> task {
totalCalls <- totalCalls + 1
return if n < 3 then Some(n, n + 1) else None
})
0

let! first = ts |> TaskSeq.toArrayAsync
first |> should equal [| 0; 1; 2 |]
totalCalls |> should equal 4

let! second = ts |> TaskSeq.toArrayAsync
second |> should equal [| 0; 1; 2 |]
totalCalls |> should equal 8
}

[<Fact>]
let ``TaskSeq-unfold with take stops generator calls at the limit`` () = task {
let mutable callCount = 0

// Infinite generator: always returns Some
let ts =
TaskSeq.unfold
(fun n ->
callCount <- callCount + 1
Some(n, n + 1))
0

let! result = ts |> TaskSeq.take 5 |> TaskSeq.toArrayAsync
result |> should equal [| 0; 1; 2; 3; 4 |]

// take 5 pulls exactly 5 elements; with an always-Some generator no
// extra sentinel call is needed, so callCount should be exactly 5.
callCount |> should equal 5
}

[<Fact>]
let ``TaskSeq-unfoldAsync with take stops generator calls at the limit`` () = task {
let mutable callCount = 0

let ts =
TaskSeq.unfoldAsync
(fun n -> task {
callCount <- callCount + 1
return Some(n, n + 1)
})
0

let! result = ts |> TaskSeq.take 5 |> TaskSeq.toArrayAsync
result |> should equal [| 0; 1; 2; 3; 4 |]
callCount |> should equal 5
}

[<Fact>]
let ``TaskSeq-unfold propagates exception thrown inside the generator`` () =
let ts =
TaskSeq.unfold
(fun n ->
if n = 3 then
failwith "generator-boom"

Some(n, n + 1))
0

fun () -> ts |> consumeTaskSeq
|> should throwAsyncExact typeof<System.Exception>

[<Fact>]
let ``TaskSeq-unfoldAsync propagates exception thrown inside the async generator`` () =
let ts =
TaskSeq.unfoldAsync
(fun n -> task {
if n = 3 then
failwith "async-generator-boom"

return Some(n, n + 1)
})
0

fun () -> ts |> consumeTaskSeq
|> should throwAsyncExact typeof<System.Exception>
Loading