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 `AsyncSeq.zipWithIndexAsync` (obsolete alias for `mapiAsync`) and `AsyncSeq.ofIQueryable`, which previously had no dedicated test coverage. No functional changes.
* Test coverage: Added tests for previously-untested public API functions `AsyncSeq.tryFirst`, `AsyncSeq.firstOrDefault`, `AsyncSeq.zipWithParallel`, `AsyncSeq.combineLatestWithAsync`, and `AsyncSeq.toObservable`. No functional changes.
* Test coverage: Added tests for `AsyncSeq.distinctUntilChanged` (default-equality variant), `AsyncSeq.takeWhile`, and `AsyncSeq.skipWhile` (sync-predicate variants), which previously had no direct tests. No functional changes.
* Test coverage: Added tests for `AsyncSeq.bufferByCount` (obsolete alias of `chunkBySize`) and `Seq.ofAsyncSeq`, which previously had no dedicated test coverage. No functional changes.
Expand Down
64 changes: 64 additions & 0 deletions tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2096,7 +2096,7 @@
let actual =
ls
|> AsyncSeq.ofSeq
|> AsyncSeq.groupBy p

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

Check warning on line 2108 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 @@ -4886,7 +4886,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 4889 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 @@ -4899,7 +4899,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 4902 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 @@ -4908,7 +4908,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 4911 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 @@ -5356,3 +5356,67 @@
AsyncSeq.empty<int>
|> Async.RunSynchronously
Assert.AreEqual(0, result)

// ===== zipWithIndexAsync (obsolete alias for mapiAsync) =====

[<Test>]
let ``AsyncSeq.zipWithIndexAsync maps elements with their int64 index`` () =
let result =
AsyncSeq.ofSeq ["a"; "b"; "c"]
|> AsyncSeq.zipWithIndexAsync (fun i x -> async { return sprintf "%d:%s" i x })
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| "0:a"; "1:b"; "2:c" |], result)

[<Test>]
let ``AsyncSeq.zipWithIndexAsync on empty sequence returns empty`` () =
let result =
AsyncSeq.empty<string>
|> AsyncSeq.zipWithIndexAsync (fun i x -> async { return sprintf "%d:%s" i x })
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([||], result)

[<Test>]
let ``AsyncSeq.zipWithIndexAsync matches mapiAsync behavior`` () =
let xs = AsyncSeq.ofSeq [10; 20; 30]
let f i x = async { return int i * x }
let viaZipWithIndexAsync = xs |> AsyncSeq.zipWithIndexAsync f |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
let viaMapiAsync = xs |> AsyncSeq.mapiAsync f |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
Assert.AreEqual(viaMapiAsync, viaZipWithIndexAsync)

#if (NETSTANDARD2_1 || NETCOREAPP3_0)
// ===== ofIQueryable =====

/// Minimal IQueryable<'T> implementation that also implements IAsyncEnumerable<'T>,
/// as required by AsyncSeq.ofIQueryable (which casts its input to IAsyncEnumerable<'T>).
type private FakeAsyncQueryable<'T>(data: 'T[]) =
let inner = data.AsQueryable()
interface System.Collections.IEnumerable with
member _.GetEnumerator() : System.Collections.IEnumerator = (inner :> System.Collections.IEnumerable).GetEnumerator()
interface Collections.Generic.IEnumerable<'T> with
member _.GetEnumerator() : Collections.Generic.IEnumerator<'T> = (inner :> Collections.Generic.IEnumerable<'T>).GetEnumerator()
interface Linq.IQueryable<'T> with
member _.ElementType = inner.ElementType
member _.Expression = inner.Expression
member _.Provider = inner.Provider
interface Collections.Generic.IAsyncEnumerable<'T> with
member _.GetAsyncEnumerator(_ct: CancellationToken) : Collections.Generic.IAsyncEnumerator<'T> =
let e = (data :> Collections.Generic.IEnumerable<'T>).GetEnumerator()
{ new Collections.Generic.IAsyncEnumerator<'T> with
member _.Current = e.Current
member _.MoveNextAsync() = Threading.Tasks.ValueTask<bool>(e.MoveNext())
member _.DisposeAsync() = e.Dispose(); Threading.Tasks.ValueTask() }

[<Test>]
let ``AsyncSeq.ofIQueryable returns elements in order`` () =
let query = FakeAsyncQueryable<int>([| 1; 2; 3 |]) :> Linq.IQueryable<int>
let result = AsyncSeq.ofIQueryable query |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
Assert.AreEqual([| 1; 2; 3 |], result)

[<Test>]
let ``AsyncSeq.ofIQueryable on empty queryable returns empty`` () =
let query = FakeAsyncQueryable<int>([||]) :> Linq.IQueryable<int>
let result = AsyncSeq.ofIQueryable query |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
Assert.AreEqual([||], result)
#endif
Loading