From ad28b5d5e17855d893ae5f9a5fda2bb4af5954dd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 01:30:56 +0000 Subject: [PATCH 1/2] test: add SideEffects module to TaskSeq.WithCancellation.Tests.fs Adds three tests that verify TaskSeq.withCancellation preserves the re-iteration semantics of the underlying sequence: - Applied multiple times (Theory over all side-effect variants): each iteration yields the next batch of values from the source, confirming withCancellation is a transparent wrapper that does not cache state. - Applied multiple times with an active CancellationToken: same as above but with a live CancellationToken, verifying the token is threaded through correctly without disrupting re-iteration. - Evaluates each source element exactly once per iteration: mutable counter confirms the side effect runs exactly N times per pass (no duplication or skipping). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- release-notes.txt | 1 + .../TaskSeq.WithCancellation.Tests.fs | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/release-notes.txt b/release-notes.txt index 72c765f9..d2ff4555 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -3,6 +3,7 @@ Release notes: Unreleased - test: add SideEffects module and ImmTaskSeq variant tests to TaskSeq.ChunkBy.Tests.fs, improving coverage for chunkBy and chunkByAsync + - test: add SideEffects module to TaskSeq.WithCancellation.Tests.fs, verifying re-iteration semantics are preserved when wrapping with a CancellationToken - 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.WithCancellation.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.WithCancellation.Tests.fs index 10b31b7e..5c42b4da 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.WithCancellation.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.WithCancellation.Tests.fs @@ -170,3 +170,57 @@ module ``Sequence contents`` = collected |> Seq.toArray |> should equal [| 1..5 |] } + +module SideEffects = + + [)>] + let ``TaskSeq-withCancellation applied multiple times`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let wrapped = TaskSeq.withCancellation CancellationToken.None ts + + let! first = wrapped |> TaskSeq.toArrayAsync + let! second = wrapped |> TaskSeq.toArrayAsync + let! third = wrapped |> TaskSeq.toArrayAsync + + first |> should equal [| 1..10 |] + second |> should equal [| 11..20 |] + third |> should equal [| 21..30 |] + } + + [)>] + let ``TaskSeq-withCancellation with active CancellationToken applied multiple times`` variant = task { + use cts = new CancellationTokenSource() + let ts = Gen.getSeqWithSideEffect variant + let wrapped = TaskSeq.withCancellation cts.Token ts + + let! first = wrapped |> TaskSeq.toArrayAsync + let! second = wrapped |> TaskSeq.toArrayAsync + + first |> should equal [| 1..10 |] + second |> should equal [| 11..20 |] + } + + [] + let ``TaskSeq-withCancellation evaluates each source element exactly once per iteration`` () = task { + let mutable count = 0 + + let ts = taskSeq { + for i in 1..5 do + count <- count + 1 + yield i + } + + let! _ = + ts + |> TaskSeq.withCancellation CancellationToken.None + |> TaskSeq.toArrayAsync + + count |> should equal 5 + + let! _ = + ts + |> TaskSeq.withCancellation CancellationToken.None + |> TaskSeq.toArrayAsync + + count |> should equal 10 + } From e7f04dabde6cc417843f0d2133e45d087e602ad5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 26 Apr 2026 01:30:59 +0000 Subject: [PATCH 2/2] ci: trigger checks