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 direct unit test coverage for the `ValueTask` helper module (`fromResult`, `ofSource`, `ofTask`, `ignore`, `True`/`False`, plus their deprecated aliases), closing a gap where these functions had no dedicated tests despite the `Task`/`Async` equivalents being covered
- tests: add coverage for side-effect re-execution semantics when re-enumerating a `taskSeq` with independent `CancellationToken`s
- test: add TaskSeq.Issue452.Tests.fs, regression tests wrapping an externally-produced IAsyncEnumerable<'T> (TaskSeq.map and `taskSeq { for .. in .. do yield .. }`) while running on a custom, single-threaded TaskScheduler; investigates #452's reported duplicated-final-item bug, which could not be reproduced outside of Orleans, see #452
- adds TaskSeq.tryMax and TaskSeq.tryMin: safe variants of TaskSeq.max and TaskSeq.min that return None instead of raising ArgumentException when the input sequence is empty
Expand Down
143 changes: 143 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs
Original file line number Diff line number Diff line change
@@ -1,13 +1,156 @@
module TaskSeq.Tests.Utils

#nowarn "44" // deprecated aliases (ValueTask.FromResult / ofIValueTaskSource) intentionally under test

open System
open System.Threading.Tasks
open System.Threading.Tasks.Sources
open Xunit
open FsUnit.Xunit

open FSharp.Control


module ValueTaskExtensions =
[<Fact>]
let ``ValueTask.CompletedTask is already completed successfully`` () =
let vt = ValueTask.CompletedTask
vt.IsCompletedSuccessfully |> should equal true


module ValueTaskConstants =
[<Fact>]
let ``ValueTask.True is a completed ValueTask with value true`` () = task {
ValueTask.True.IsCompletedSuccessfully |> should equal true
let! result = ValueTask.True
result |> should equal true
}

[<Fact>]
let ``ValueTask.False is a completed ValueTask with value false`` () = task {
ValueTask.False.IsCompletedSuccessfully |> should equal true
let! result = ValueTask.False
result |> should equal false
}


module ValueTaskFromResult =
[<Fact>]
let ``ValueTask.fromResult creates an already-completed ValueTask with the given value`` () = task {
let vt = ValueTask.fromResult 42
vt.IsCompletedSuccessfully |> should equal true
let! result = vt
result |> should equal 42
}

[<Fact>]
let ``ValueTask.FromResult (deprecated alias) behaves the same as fromResult`` () = task {
let vt = ValueTask.FromResult "hello"
let! result = vt
result |> should equal "hello"
}


module ValueTaskOfTask =
[<Fact>]
let ``ValueTask.ofTask wraps an already-completed Task<'T>`` () = task {
let source = Task.FromResult 7
let vt = ValueTask.ofTask source
let! result = vt
result |> should equal 7
}

[<Fact>]
let ``ValueTask.ofTask wraps a not-yet-completed Task<'T>`` () = task {
let source = task {
do! Task.Delay 1
return 99
}

let vt = ValueTask.ofTask source
let! result = vt
result |> should equal 99
}


module ValueTaskIgnore =
[<Fact>]
let ``ValueTask.ignore on an already-completed ValueTask discards the result`` () =
let vt = ValueTask.fromResult 123
let ignored: ValueTask = ValueTask.ignore vt
ignored.IsCompletedSuccessfully |> should equal true

[<Fact>]
let ``ValueTask.ignore still awaits and surfaces exceptions from a non-completed ValueTask`` () = task {
let source =
ValueTask<int>(
task {
do! Task.Delay 1
return raise (InvalidOperationException "boom")
}
)

let ignored: ValueTask = ValueTask.ignore source

let run () = task { do! ignored }

let! ex = Assert.ThrowsAsync<InvalidOperationException>(fun () -> run () :> Task)
ex.Message |> should equal "boom"
}

[<Fact>]
let ``ValueTask.ignore on a not-yet-completed ValueTask still awaits to completion`` () = task {
let mutable sideEffect = 0

let source =
ValueTask<int>(
task {
do! Task.Delay 1
sideEffect <- 1
return 5
}
)

let ignored: ValueTask = ValueTask.ignore source
do! ignored
sideEffect |> should equal 1
}


/// Minimal IValueTaskSource<bool> used to exercise ValueTask.ofSource / ofIValueTaskSource.
type private ManualBoolSource() =
let mutable core = ManualResetValueTaskSourceCore<bool>()

member _.Version = core.Version
member _.SetResult value = core.SetResult value

interface IValueTaskSource<bool> with
member _.GetResult version = core.GetResult version
member _.GetStatus version = core.GetStatus version

member _.OnCompleted(continuation, state, version, flags) = core.OnCompleted(continuation, state, version, flags)


module ValueTaskOfSource =
[<Fact>]
let ``ValueTask.ofSource creates a ValueTask backed by an IValueTaskSource<bool>`` () = task {
let source = ManualBoolSource()
source.SetResult true
let vt = ValueTask.ofSource source source.Version
let! result = vt
result |> should equal true
}

[<Fact>]
let ``ValueTask.ofIValueTaskSource (deprecated alias) behaves the same as ofSource`` () = task {
let source = ManualBoolSource()
source.SetResult false
let vt = ValueTask.ofIValueTaskSource source source.Version
let! result = vt
result |> should equal false
}


module AsyncBind =
[<Fact>]
let ``Async.bind awaits the async and passes the value to the binder`` () =
Expand Down