Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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