From 8e6219e7c52f11419a52488c94b5b6f6204fa849 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:28:59 +0000 Subject: [PATCH 1/2] test: add SideEffects module to TaskSeq.Using.Tests.fs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 7 new tests in a SideEffects module that verify disposal semantics for the use and use! CE bindings: - Dispose/DisposeAsync called exactly once per full iteration - Dispose/DisposeAsync called on each re-iteration (3×) - Dispose called on early termination via TaskSeq.take - Multiple use bindings each trigger their own Dispose - Each re-iteration creates and disposes a fresh resource Two new helper types (CountingDisposable, CountingAsyncDisposable) make it easy to assert exact call counts without the bool-ref pattern used by the existing tests. All 5258 tests pass (Release, net10.0). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- release-notes.txt | 1 + .../TaskSeq.Using.Tests.fs | 118 ++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/release-notes.txt b/release-notes.txt index 72c765f9..12b1e12e 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -2,6 +2,7 @@ Release notes: Unreleased + - test: add SideEffects module to TaskSeq.Using.Tests.fs; 7 new tests verify Dispose/DisposeAsync call counts, re-iteration semantics, and early-termination disposal for use and use! CE bindings - 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 diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs index 557381e0..87a83ff6 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs @@ -32,6 +32,18 @@ type private MultiDispose(disposed: int ref) = interface IAsyncDisposable with member _.DisposeAsync() = ValueTask(task { do disposed.Value <- -1 }) +/// Tracks how many times Dispose/DisposeAsync has been called. +type private CountingDisposable(disposeCount: int ref) = + interface IDisposable with + member _.Dispose() = disposeCount.Value <- disposeCount.Value + 1 + +/// Tracks how many times DisposeAsync has been called. +type private CountingAsyncDisposable(disposeCount: int ref) = + interface IAsyncDisposable with + member _.DisposeAsync() = + disposeCount.Value <- disposeCount.Value + 1 + ValueTask.CompletedTask + let private check = TaskSeq.length >> Task.map (should equal 1) [] @@ -105,3 +117,109 @@ let ``CE taskSeq: Using! when type implements IDisposable and IAsyncDisposable`` check ts |> Task.map (fun _ -> disposed.Value |> should equal -1) // should prefer IAsyncDisposable, which returns -1 + +module SideEffects = + [] + let ``CE taskSeq: use - Dispose called exactly once per full iteration`` () = task { + let disposeCount = ref 0 + + let ts = taskSeq { + use _ = new CountingDisposable(disposeCount) + yield 1 + } + + do! ts |> TaskSeq.iter ignore + disposeCount.Value |> should equal 1 + } + + [] + let ``CE taskSeq: use - Dispose called on each re-iteration`` () = task { + let disposeCount = ref 0 + + let ts = taskSeq { + use _ = new CountingDisposable(disposeCount) + yield 1 + } + + do! ts |> TaskSeq.iter ignore + do! ts |> TaskSeq.iter ignore + do! ts |> TaskSeq.iter ignore + disposeCount.Value |> should equal 3 + } + + [] + let ``CE taskSeq: use! - DisposeAsync called exactly once per full iteration`` () = task { + let disposeCount = ref 0 + + let ts = taskSeq { + use! _ = task { return new CountingAsyncDisposable(disposeCount) } + yield 1 + } + + do! ts |> TaskSeq.iter ignore + disposeCount.Value |> should equal 1 + } + + [] + let ``CE taskSeq: use! - DisposeAsync called on each re-iteration`` () = task { + let disposeCount = ref 0 + + let ts = taskSeq { + use! _ = task { return new CountingAsyncDisposable(disposeCount) } + yield 1 + } + + do! ts |> TaskSeq.iter ignore + do! ts |> TaskSeq.iter ignore + do! ts |> TaskSeq.iter ignore + disposeCount.Value |> should equal 3 + } + + [] + let ``CE taskSeq: use - Dispose called on early termination via take`` () = task { + let disposeCount = ref 0 + + let ts = taskSeq { + use _ = new CountingDisposable(disposeCount) + yield 1 + yield 2 + yield 3 + } + + // Only take 1 item — enumerator is disposed before the rest of the sequence runs + do! ts |> TaskSeq.take 1 |> TaskSeq.iter ignore + disposeCount.Value |> should equal 1 + } + + [] + let ``CE taskSeq: use - multiple use bindings each get their own Dispose`` () = task { + let disposeCount = ref 0 + + let ts = taskSeq { + use _ = new CountingDisposable(disposeCount) + use _ = new CountingDisposable(disposeCount) + yield 1 + } + + do! ts |> TaskSeq.iter ignore + disposeCount.Value |> should equal 2 + } + + [] + let ``CE taskSeq: use - each re-iteration creates and disposes a fresh resource`` () = task { + let createCount = ref 0 + + let ts = taskSeq { + createCount.Value <- createCount.Value + 1 + use _ = new CountingDisposable(ref 0) // fresh ref each time + yield createCount.Value + } + + let! first = ts |> TaskSeq.toListAsync + let! second = ts |> TaskSeq.toListAsync + + // Each re-iteration re-runs the CE body and creates a new resource + first |> should equal [ 1 ] + second |> should equal [ 2 ] + createCount.Value |> should equal 2 + } From 90463f7dbc526557dd56cf6159d414547d225668 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Apr 2026 01:29:02 +0000 Subject: [PATCH 2/2] ci: trigger checks