Skip to content

Commit a3fc887

Browse files
test: add SideEffects module to TaskSeq.Unfold.Tests.fs
Adds 6 new tests covering: - Generator side-effects accumulate across re-iterations (both sync and async) - take stops the generator after the requested number of elements - Exceptions thrown inside sync and async generators propagate correctly Before: 14 test runs After: 20 test runs Co-authored-by: Copilot <[email protected]>
1 parent fc687a3 commit a3fc887

2 files changed

Lines changed: 112 additions & 0 deletions

File tree

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Release notes:
33

44
Unreleased
5+
- test: add SideEffects module to TaskSeq.Unfold.Tests.fs, verifying generator call counts, re-iteration behaviour, early-termination via take, and exception propagation
56
- test: add SideEffects module and ImmTaskSeq variant tests to TaskSeq.ChunkBy.Tests.fs, improving coverage for chunkBy and chunkByAsync
67
- 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
78
- refactor: simplify splitAt 'rest' taskSeq to use while!, removing redundant go2 mutable and manual MoveNextAsync pre-advance

src/FSharp.Control.TaskSeq.Test/TaskSeq.Unfold.Tests.fs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,114 @@ module Functionality =
158158
first |> should equal second
159159
first |> should equal [| 0..4 |]
160160
}
161+
162+
module SideEffects =
163+
[<Fact>]
164+
let ``TaskSeq-unfold generator side-effects accumulate across re-iterations`` () = task {
165+
// The generator closes over mutable external state. Each re-iteration starts fresh from
166+
// the initial seed (0), but the external counter keeps climbing — demonstrating that
167+
// the IAsyncEnumerable itself is stateless but the captured state is shared.
168+
let mutable totalCalls = 0
169+
170+
let ts =
171+
TaskSeq.unfold
172+
(fun n ->
173+
totalCalls <- totalCalls + 1
174+
if n < 3 then Some(n, n + 1) else None)
175+
0
176+
177+
let! first = ts |> TaskSeq.toArrayAsync
178+
first |> should equal [| 0; 1; 2 |]
179+
totalCalls |> should equal 4 // 3 Some + 1 None
180+
181+
let! second = ts |> TaskSeq.toArrayAsync
182+
second |> should equal [| 0; 1; 2 |]
183+
totalCalls |> should equal 8 // called 4 more times for the second iteration
184+
}
185+
186+
[<Fact>]
187+
let ``TaskSeq-unfoldAsync generator side-effects accumulate across re-iterations`` () = task {
188+
let mutable totalCalls = 0
189+
190+
let ts =
191+
TaskSeq.unfoldAsync
192+
(fun n -> task {
193+
totalCalls <- totalCalls + 1
194+
return if n < 3 then Some(n, n + 1) else None
195+
})
196+
0
197+
198+
let! first = ts |> TaskSeq.toArrayAsync
199+
first |> should equal [| 0; 1; 2 |]
200+
totalCalls |> should equal 4
201+
202+
let! second = ts |> TaskSeq.toArrayAsync
203+
second |> should equal [| 0; 1; 2 |]
204+
totalCalls |> should equal 8
205+
}
206+
207+
[<Fact>]
208+
let ``TaskSeq-unfold with take stops generator calls at the limit`` () = task {
209+
let mutable callCount = 0
210+
211+
// Infinite generator: always returns Some
212+
let ts =
213+
TaskSeq.unfold
214+
(fun n ->
215+
callCount <- callCount + 1
216+
Some(n, n + 1))
217+
0
218+
219+
let! result = ts |> TaskSeq.take 5 |> TaskSeq.toArrayAsync
220+
result |> should equal [| 0; 1; 2; 3; 4 |]
221+
222+
// take 5 pulls exactly 5 elements; with an always-Some generator no
223+
// extra sentinel call is needed, so callCount should be exactly 5.
224+
callCount |> should equal 5
225+
}
226+
227+
[<Fact>]
228+
let ``TaskSeq-unfoldAsync with take stops generator calls at the limit`` () = task {
229+
let mutable callCount = 0
230+
231+
let ts =
232+
TaskSeq.unfoldAsync
233+
(fun n -> task {
234+
callCount <- callCount + 1
235+
return Some(n, n + 1)
236+
})
237+
0
238+
239+
let! result = ts |> TaskSeq.take 5 |> TaskSeq.toArrayAsync
240+
result |> should equal [| 0; 1; 2; 3; 4 |]
241+
callCount |> should equal 5
242+
}
243+
244+
[<Fact>]
245+
let ``TaskSeq-unfold propagates exception thrown inside the generator`` () =
246+
let ts =
247+
TaskSeq.unfold
248+
(fun n ->
249+
if n = 3 then
250+
failwith "generator-boom"
251+
252+
Some(n, n + 1))
253+
0
254+
255+
fun () -> ts |> consumeTaskSeq
256+
|> should throwAsyncExact typeof<System.Exception>
257+
258+
[<Fact>]
259+
let ``TaskSeq-unfoldAsync propagates exception thrown inside the async generator`` () =
260+
let ts =
261+
TaskSeq.unfoldAsync
262+
(fun n -> task {
263+
if n = 3 then
264+
failwith "async-generator-boom"
265+
266+
return Some(n, n + 1)
267+
})
268+
0
269+
270+
fun () -> ts |> consumeTaskSeq
271+
|> should throwAsyncExact typeof<System.Exception>

0 commit comments

Comments
 (0)