Skip to content
Merged
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.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### Unreleased

* Test coverage: Added tests for previously-untested public API functions `AsyncSeq.tryFirst`, `AsyncSeq.firstOrDefault`, `AsyncSeq.zipWithParallel`, `AsyncSeq.combineLatestWithAsync`, and `AsyncSeq.toObservable`. No functional changes.
* Fixed Fable CI build: `Microsoft.Bcl.AsyncInterfaces` was pinned to a specific version (`10.0.7`) that was older than the version resolved transitively via `System.Threading.Channels`, causing a `NU1605` package downgrade error that made Fable's project cracker fail during `dotnet fable`. The reference now uses `Version="*"` (matching `System.Threading.Channels`) so both resolve consistently. (#334)

### 4.17.0
Expand Down
91 changes: 91 additions & 0 deletions tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2094,7 +2094,7 @@
let actual =
ls
|> AsyncSeq.ofSeq
|> AsyncSeq.groupBy p

Check warning on line 2097 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 @@ -2103,7 +2103,7 @@
let expected = asyncSeq { raise (exn("test")) }
let actual =
asyncSeq { raise (exn("test")) }
|> AsyncSeq.groupBy (fun i -> i % 3)

Check warning on line 2106 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 @@ -4851,7 +4851,7 @@
let ``AsyncSeq.groupByAsync groups elements by async projection`` () =
let result =
AsyncSeq.ofSeq [1..6]
|> AsyncSeq.groupByAsync (fun x -> async { return x % 2 })

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

View workflow job for this annotation

GitHub Actions / build

The result of groupByAsync 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 (fun (key, grp) -> async {
let! items = AsyncSeq.toArrayAsync grp
return key, Array.sort items })
Expand All @@ -4864,7 +4864,7 @@
let ``AsyncSeq.groupByAsync on empty sequence returns empty`` () =
let result =
AsyncSeq.empty<int>
|> AsyncSeq.groupByAsync (fun x -> async { return x % 2 })

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

View workflow job for this annotation

GitHub Actions / build

The result of groupByAsync 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.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([||], result)
Expand All @@ -4873,7 +4873,7 @@
let ``AsyncSeq.groupByAsync with all-same key produces single group`` () =
let result =
AsyncSeq.ofSeq [1; 2; 3]
|> AsyncSeq.groupByAsync (fun _ -> async { return "same" })

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

View workflow job for this annotation

GitHub Actions / build

The result of groupByAsync 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 (fun (key, grp) -> async {
let! items = AsyncSeq.toArrayAsync grp
return key, Array.sort items })
Expand Down Expand Up @@ -5087,3 +5087,94 @@
(AsyncSeq.ofSeq [1;2;3])
|> Async.RunSynchronously
Assert.IsTrue(result)

// ===== tryFirst / firstOrDefault =====

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

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

[<Test>]
let ``AsyncSeq.firstOrDefault returns first element when non-empty`` () =
let result = AsyncSeq.firstOrDefault -1 (AsyncSeq.ofSeq [5;6;7]) |> Async.RunSynchronously
Assert.AreEqual(5, result)

[<Test>]
let ``AsyncSeq.firstOrDefault returns default when empty`` () =
let result = AsyncSeq.firstOrDefault -1 (AsyncSeq.empty<int>) |> Async.RunSynchronously
Assert.AreEqual(-1, result)

// ===== zipWithParallel =====

[<Test>]
let ``AsyncSeq.zipWithParallel combines values from both sequences`` () =
let result =
AsyncSeq.zipWithParallel (fun a b -> a + b) (AsyncSeq.ofSeq [1;2;3]) (AsyncSeq.ofSeq [10;20;30])
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 11;22;33 |], result)

[<Test>]
let ``AsyncSeq.zipWithParallel stops at shorter sequence`` () =
let result =
AsyncSeq.zipWithParallel (fun a b -> a, b) (AsyncSeq.ofSeq [1;2]) (AsyncSeq.ofSeq ["a";"b";"c"])
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| (1,"a"); (2,"b") |], result)

// ===== combineLatestWithAsync =====

[<Test>]
let ``AsyncSeq.combineLatestWithAsync combines initial values then emits on each new update`` () =
let result =
AsyncSeq.combineLatestWithAsync
(fun a b -> async { return a + b })
(AsyncSeq.ofSeq [1;2])
(AsyncSeq.ofSeq [10])
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
// First combination of initial pair, then subsequent updates to source1 combined with latest source2 value.
Assert.AreEqual([| 11; 12 |], result)

[<Test>]
let ``AsyncSeq.combineLatestWithAsync produces empty sequence when either source is empty`` () =
let result =
AsyncSeq.combineLatestWithAsync
(fun a b -> async { return a + b })
(AsyncSeq.empty<int>)
(AsyncSeq.ofSeq [1;2;3])
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([||], result)

// ===== toObservable =====

type private TestObserver<'T>(onNext, onCompleted) =
interface IObserver<'T> with
member _.OnNext(v) = onNext v
member _.OnCompleted() = onCompleted ()
member _.OnError(_e) = ()

[<Test>]
let ``AsyncSeq.toObservable emits all values then completes`` () =
let received = ResizeArray<int>()
let completedEvent = new System.Threading.ManualResetEventSlim(false)
let observer = TestObserver<int>(received.Add, completedEvent.Set)
use _sub = (AsyncSeq.toObservable (AsyncSeq.ofSeq [1;2;3])).Subscribe(observer)
Assert.IsTrue(completedEvent.Wait(2000))
Assert.AreEqual([| 1;2;3 |], received.ToArray())

[<Test>]
let ``AsyncSeq.toObservable on empty sequence emits nothing`` () =
let received = ResizeArray<int>()
let completedEvent = new System.Threading.ManualResetEventSlim(false)
let observer = TestObserver<int>(received.Add, completedEvent.Set)
use _sub = (AsyncSeq.toObservable (AsyncSeq.empty<int>)).Subscribe(observer)
Assert.IsTrue(completedEvent.Wait(2000))
Assert.AreEqual([||], received.ToArray())
Loading