Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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.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
Expand Down
118 changes: 118 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

[<Fact>]
Expand Down Expand Up @@ -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 =
[<Fact>]
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
}

[<Fact>]
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
}

[<Fact>]
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
}

[<Fact>]
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
}

[<Fact>]
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
}

[<Fact>]
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
}

[<Fact>]
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
}
Loading