Skip to content

Commit 6f76fb1

Browse files
authored
Merge branch 'main' into repo-assist/improve-sideeffects-naming-20260425-edfad27e8fbe8b06
2 parents d5afed5 + 15e84df commit 6f76fb1

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

release-notes.txt

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

44
Unreleased
55
- test: rename `SideEffect` module to `SideEffects` in TaskSeq.Concat.Tests.fs, TaskSeq.Delay.Tests.fs, and TaskSeq.Item.Tests.fs for consistency with the rest of the test suite (50+ files already use the plural form)
6+
- 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
67
- perf: pairwise, distinctUntilChanged, distinctUntilChangedWith, distinctUntilChangedWithAsync now use explicit enumerator + while! instead of ValueOption tracking + for-in loop, eliminating per-element struct match overhead
78
- test: add SideEffects module to TaskSeq.Unfold.Tests.fs, verifying generator call counts, re-iteration behaviour, early-termination via take, and exception propagation
89
- test: add SideEffects module to TaskSeq.OfXXX.Tests.fs documenting re-iteration semantics (ofSeq re-evaluates source, ofTaskArray re-awaits cached tasks)

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

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ type private MultiDispose(disposed: int ref) =
3232
interface IAsyncDisposable with
3333
member _.DisposeAsync() = ValueTask(task { do disposed.Value <- -1 })
3434

35+
/// Tracks how many times Dispose/DisposeAsync has been called.
36+
type private CountingDisposable(disposeCount: int ref) =
37+
interface IDisposable with
38+
member _.Dispose() = disposeCount.Value <- disposeCount.Value + 1
39+
40+
/// Tracks how many times DisposeAsync has been called.
41+
type private CountingAsyncDisposable(disposeCount: int ref) =
42+
interface IAsyncDisposable with
43+
member _.DisposeAsync() =
44+
disposeCount.Value <- disposeCount.Value + 1
45+
ValueTask.CompletedTask
46+
3547
let private check = TaskSeq.length >> Task.map (should equal 1)
3648

3749
[<Fact>]
@@ -105,3 +117,109 @@ let ``CE taskSeq: Using! when type implements IDisposable and IAsyncDisposable``
105117

106118
check ts
107119
|> Task.map (fun _ -> disposed.Value |> should equal -1) // should prefer IAsyncDisposable, which returns -1
120+
121+
module SideEffects =
122+
[<Fact>]
123+
let ``CE taskSeq: use - Dispose called exactly once per full iteration`` () = task {
124+
let disposeCount = ref 0
125+
126+
let ts = taskSeq {
127+
use _ = new CountingDisposable(disposeCount)
128+
yield 1
129+
}
130+
131+
do! ts |> TaskSeq.iter ignore
132+
disposeCount.Value |> should equal 1
133+
}
134+
135+
[<Fact>]
136+
let ``CE taskSeq: use - Dispose called on each re-iteration`` () = task {
137+
let disposeCount = ref 0
138+
139+
let ts = taskSeq {
140+
use _ = new CountingDisposable(disposeCount)
141+
yield 1
142+
}
143+
144+
do! ts |> TaskSeq.iter ignore
145+
do! ts |> TaskSeq.iter ignore
146+
do! ts |> TaskSeq.iter ignore
147+
disposeCount.Value |> should equal 3
148+
}
149+
150+
[<Fact>]
151+
let ``CE taskSeq: use! - DisposeAsync called exactly once per full iteration`` () = task {
152+
let disposeCount = ref 0
153+
154+
let ts = taskSeq {
155+
use! _ = task { return new CountingAsyncDisposable(disposeCount) }
156+
yield 1
157+
}
158+
159+
do! ts |> TaskSeq.iter ignore
160+
disposeCount.Value |> should equal 1
161+
}
162+
163+
[<Fact>]
164+
let ``CE taskSeq: use! - DisposeAsync called on each re-iteration`` () = task {
165+
let disposeCount = ref 0
166+
167+
let ts = taskSeq {
168+
use! _ = task { return new CountingAsyncDisposable(disposeCount) }
169+
yield 1
170+
}
171+
172+
do! ts |> TaskSeq.iter ignore
173+
do! ts |> TaskSeq.iter ignore
174+
do! ts |> TaskSeq.iter ignore
175+
disposeCount.Value |> should equal 3
176+
}
177+
178+
[<Fact>]
179+
let ``CE taskSeq: use - Dispose called on early termination via take`` () = task {
180+
let disposeCount = ref 0
181+
182+
let ts = taskSeq {
183+
use _ = new CountingDisposable(disposeCount)
184+
yield 1
185+
yield 2
186+
yield 3
187+
}
188+
189+
// Only take 1 item — enumerator is disposed before the rest of the sequence runs
190+
do! ts |> TaskSeq.take 1 |> TaskSeq.iter ignore
191+
disposeCount.Value |> should equal 1
192+
}
193+
194+
[<Fact>]
195+
let ``CE taskSeq: use - multiple use bindings each get their own Dispose`` () = task {
196+
let disposeCount = ref 0
197+
198+
let ts = taskSeq {
199+
use _ = new CountingDisposable(disposeCount)
200+
use _ = new CountingDisposable(disposeCount)
201+
yield 1
202+
}
203+
204+
do! ts |> TaskSeq.iter ignore
205+
disposeCount.Value |> should equal 2
206+
}
207+
208+
[<Fact>]
209+
let ``CE taskSeq: use - each re-iteration creates and disposes a fresh resource`` () = task {
210+
let createCount = ref 0
211+
212+
let ts = taskSeq {
213+
createCount.Value <- createCount.Value + 1
214+
use _ = new CountingDisposable(ref 0) // fresh ref each time
215+
yield createCount.Value
216+
}
217+
218+
let! first = ts |> TaskSeq.toListAsync
219+
let! second = ts |> TaskSeq.toListAsync
220+
221+
// Each re-iteration re-runs the CE body and creates a new resource
222+
first |> should equal [ 1 ]
223+
second |> should equal [ 2 ]
224+
createCount.Value |> should equal 2
225+
}

0 commit comments

Comments
 (0)