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
4 changes: 4 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### 4.10.1

* Tests: added 14 new unit tests covering previously untested functions β€” `AsyncSeq.indexed`, `AsyncSeq.iteriAsync`, `AsyncSeq.tryLast`, `AsyncSeq.replicateUntilNoneAsync`, and `AsyncSeq.reduceAsync` (empty-sequence edge case).

### 4.10.0

* Added `AsyncSeq.withCancellation` β€” returns a new `AsyncSeq` that passes the given `CancellationToken` to `GetAsyncEnumerator`, overriding whatever token would otherwise be supplied. Mirrors `TaskSeq.withCancellation` and is useful when consuming sequences from libraries (e.g. Entity Framework) that accept a cancellation token through `GetAsyncEnumerator`. Part of ongoing design-parity work with FSharp.Control.TaskSeq (see #277).
Expand Down
143 changes: 143 additions & 0 deletions tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2000,7 +2000,7 @@
let actual =
ls
|> AsyncSeq.ofSeq
|> AsyncSeq.groupBy p

Check warning on line 2003 in tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs

View workflow job for this annotation

GitHub Actions / build

The result of groupBy must be consumed with a parallel combinator such as AsyncSeq.mapAsyncParallel. Sequential consumption will deadlock because sub-sequence completion depends on other sub-sequences being consumed concurrently.
|> AsyncSeq.mapAsyncParallel (snd >> AsyncSeq.toListAsync)
Assert.AreEqual(expected, actual)

Expand All @@ -2009,7 +2009,7 @@
let expected = asyncSeq { raise (exn("test")) }
let actual =
asyncSeq { raise (exn("test")) }
|> AsyncSeq.groupBy (fun i -> i % 3)

Check warning on line 2012 in tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs

View workflow job for this annotation

GitHub Actions / build

The result of groupBy must be consumed with a parallel combinator such as AsyncSeq.mapAsyncParallel. Sequential consumption will deadlock because sub-sequence completion depends on other sub-sequences being consumed concurrently.
|> AsyncSeq.mapAsyncParallel (snd >> AsyncSeq.toListAsync)
Assert.AreEqual(expected, actual)

Expand Down Expand Up @@ -3719,3 +3719,146 @@
|> Async.RunSynchronously
|> ignore)
|> ignore

// ===== indexed =====

[<Test>]
let ``AsyncSeq.indexed pairs elements with int64 indices`` () =
let result =
AsyncSeq.ofSeq [ "a"; "b"; "c" ]
|> AsyncSeq.indexed
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| (0L, "a"); (1L, "b"); (2L, "c") |], result)

[<Test>]
let ``AsyncSeq.indexed on empty sequence returns empty`` () =
let result =
AsyncSeq.empty<int>
|> AsyncSeq.indexed
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([||], result)

[<Test>]
let ``AsyncSeq.indexed index starts at zero for singleton`` () =
let result =
AsyncSeq.singleton 42
|> AsyncSeq.indexed
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| (0L, 42) |], result)

[<Test>]
let ``AsyncSeq.indexed produces consecutive int64 indices`` () =
let n = 100
let result =
AsyncSeq.init (int64 n) id
|> AsyncSeq.indexed
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
let indices = result |> Array.map fst
Assert.AreEqual(Array.init n int64, indices)

// ===== iteriAsync =====

[<Test>]
let ``AsyncSeq.iteriAsync calls action with correct indices and values`` () =
let log = ResizeArray<int * int>()
AsyncSeq.ofSeq [ 10; 20; 30 ]
|> AsyncSeq.iteriAsync (fun i v -> async { log.Add(i, v) })
|> Async.RunSynchronously
Assert.AreEqual([ (0, 10); (1, 20); (2, 30) ], log |> Seq.toList)

[<Test>]
let ``AsyncSeq.iteriAsync on empty sequence does not call action`` () =
let mutable callCount = 0
AsyncSeq.empty<int>
|> AsyncSeq.iteriAsync (fun _ _ -> async { callCount <- callCount + 1 })
|> Async.RunSynchronously
Assert.AreEqual(0, callCount)

[<Test>]
let ``AsyncSeq.iteriAsync index is zero-based`` () =
let indices = ResizeArray<int>()
AsyncSeq.ofSeq [ "x"; "y"; "z" ]
|> AsyncSeq.iteriAsync (fun i _ -> async { indices.Add(i) })
|> Async.RunSynchronously
Assert.AreEqual([ 0; 1; 2 ], indices |> Seq.toList)

// ===== tryLast =====

[<Test>]
let ``AsyncSeq.tryLast returns Some last element for non-empty sequence`` () =
let result =
AsyncSeq.ofSeq [ 1; 2; 3 ]
|> AsyncSeq.tryLast
|> Async.RunSynchronously
Assert.AreEqual(Some 3, result)

[<Test>]
let ``AsyncSeq.tryLast returns None for empty sequence`` () =
let result =
AsyncSeq.empty<int>
|> AsyncSeq.tryLast
|> Async.RunSynchronously
Assert.AreEqual(None, result)

[<Test>]
let ``AsyncSeq.tryLast returns Some for singleton sequence`` () =
let result =
AsyncSeq.singleton 99
|> AsyncSeq.tryLast
|> Async.RunSynchronously
Assert.AreEqual(Some 99, result)

// ===== replicateUntilNoneAsync =====

[<Test>]
let ``AsyncSeq.replicateUntilNoneAsync generates elements until None`` () =
let mutable counter = 0
let gen = async {
counter <- counter + 1
if counter <= 3 then return Some counter
else return None
}
let result =
AsyncSeq.replicateUntilNoneAsync gen
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 1; 2; 3 |], result)

[<Test>]
let ``AsyncSeq.replicateUntilNoneAsync returns empty for immediate None`` () =
let result =
AsyncSeq.replicateUntilNoneAsync (async { return None })
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([||], result)

[<Test>]
let ``AsyncSeq.replicateUntilNoneAsync returns single element then stops`` () =
let mutable called = false
let gen = async {
if not called then
called <- true
return Some 42
else
return None
}
let result =
AsyncSeq.replicateUntilNoneAsync gen
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 42 |], result)

// ===== reduceAsync edge case =====

[<Test>]
let ``AsyncSeq.reduceAsync raises InvalidOperationException on empty sequence`` () =
Assert.Throws<System.InvalidOperationException>(fun () ->
AsyncSeq.empty<int>
|> AsyncSeq.reduceAsync (fun a b -> async { return a + b })
|> Async.RunSynchronously
|> ignore)
|> ignore
Loading