Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
- adds TaskSeq.foldWhile and TaskSeq.foldWhileAsync: fold with early termination via a (state, element) -> bool predicate (takeWhile-style, exclusive). When the predicate returns false, iteration halts without folding that element; no further elements are enumerated.
- feat: add `TaskSeq.toChannelAsync` and `TaskSeq.ofChannel` for bidirectional `System.Threading.Channels` integration, closing #415
- eng: update PackageValidationBaselineVersion from 0.4.0 to 1.1.1 to enforce binary compatibility checks against the current stable release
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