From 17d32d536f979fd88cd5a5ad87138f549f0e231e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:15:15 +0000 Subject: [PATCH] tests: add coverage for ValueTask helper module Adds direct unit tests for FSharp.Control.ValueTask functions (fromResult, ofSource, ofTask, ignore, True/False constants) and their deprecated aliases (FromResult, ofIValueTaskSource). These functions previously had no dedicated test coverage, unlike their Task/Async counterparts already exercised in Utils.Tests.fs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- release-notes.txt | 1 + .../Utils.Tests.fs | 143 ++++++++++++++++++ 2 files changed, 144 insertions(+) diff --git a/release-notes.txt b/release-notes.txt index f0f7386b..f93638a3 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -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 diff --git a/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs b/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs index c89765c5..3909864f 100644 --- a/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs @@ -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 = + [] + let ``ValueTask.CompletedTask is already completed successfully`` () = + let vt = ValueTask.CompletedTask + vt.IsCompletedSuccessfully |> should equal true + + +module ValueTaskConstants = + [] + 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 + } + + [] + 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 = + [] + 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 + } + + [] + 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 = + [] + 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 + } + + [] + 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 = + [] + 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 + + [] + let ``ValueTask.ignore still awaits and surfaces exceptions from a non-completed ValueTask`` () = task { + let source = + ValueTask( + task { + do! Task.Delay 1 + return raise (InvalidOperationException "boom") + } + ) + + let ignored: ValueTask = ValueTask.ignore source + + let run () = task { do! ignored } + + let! ex = Assert.ThrowsAsync(fun () -> run () :> Task) + ex.Message |> should equal "boom" + } + + [] + let ``ValueTask.ignore on a not-yet-completed ValueTask still awaits to completion`` () = task { + let mutable sideEffect = 0 + + let source = + ValueTask( + task { + do! Task.Delay 1 + sideEffect <- 1 + return 5 + } + ) + + let ignored: ValueTask = ValueTask.ignore source + do! ignored + sideEffect |> should equal 1 + } + + +/// Minimal IValueTaskSource used to exercise ValueTask.ofSource / ofIValueTaskSource. +type private ManualBoolSource() = + let mutable core = ManualResetValueTaskSourceCore() + + member _.Version = core.Version + member _.SetResult value = core.SetResult value + + interface IValueTaskSource 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 = + [] + let ``ValueTask.ofSource creates a ValueTask backed by an IValueTaskSource`` () = task { + let source = ManualBoolSource() + source.SetResult true + let vt = ValueTask.ofSource source source.Version + let! result = vt + result |> should equal true + } + + [] + 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 = [] let ``Async.bind awaits the async and passes the value to the binder`` () =