diff --git a/src/IcedTasks/AsyncEx.fs b/src/IcedTasks/AsyncEx.fs index 7a5fd2a..7d9cfb2 100644 --- a/src/IcedTasks/AsyncEx.fs +++ b/src/IcedTasks/AsyncEx.fs @@ -10,43 +10,84 @@ type private Async = async.Bind(x, (fun v -> async.Return(f v))) type AsyncEx = + + /// + /// Return an asynchronous computation that will wait for the given Awaiter to complete and return + /// its result. + /// + /// The Awaiter to await + /// + /// + /// This is based on How to use awaitable inside async? and Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// static member inline AwaitAwaiter(awaiter: 'Awaiter) = - Async.FromContinuations(fun (cont, econt, ccont) -> - Awaiter.OnCompleted( - awaiter, - (fun () -> - try - cont (Awaiter.GetResult awaiter) - with - | :? TaskCanceledException as ce -> ccont ce - | :? OperationCanceledException as ce -> ccont ce - | :? AggregateException as ae -> - if ae.InnerExceptions.Count = 1 then - econt ae.InnerExceptions.[0] - else - econt ae - | e -> econt e + Async.FromContinuations(fun (onNext, onError, onCancel) -> + if Awaiter.IsCompleted awaiter then + try + onNext (Awaiter.GetResult awaiter) + with + | :? TaskCanceledException as ce -> onCancel ce + | :? OperationCanceledException as ce -> onCancel ce + | :? AggregateException as ae -> + if ae.InnerExceptions.Count = 1 then + onError ae.InnerExceptions.[0] + else + onError ae + | e -> onError e + else + Awaiter.OnCompleted( + awaiter, + (fun () -> + try + onNext (Awaiter.GetResult awaiter) + with + | :? TaskCanceledException as ce -> onCancel ce + | :? OperationCanceledException as ce -> onCancel ce + | :? AggregateException as ae -> + if ae.InnerExceptions.Count = 1 then + onError ae.InnerExceptions.[0] + else + onError ae + | e -> onError e + ) ) - ) ) + /// + /// Return an asynchronous computation that will wait for the given Awaitable to complete and return + /// its result. + /// + /// The Awaitable to await + /// + /// + /// This is based on How to use awaitable inside async? and Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// static member inline AwaitAwaitable(awaitable: 'Awaitable) = AsyncEx.AwaitAwaiter(Awaitable.GetAwaiter awaitable) + /// + /// Return an asynchronous computation that will wait for the given Task to complete and return + /// its result. + /// + /// The Awaitable to await + /// + /// + /// This is based on Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// static member AwaitTask(task: Task) : Async = - Async.FromContinuations(fun (cont, econt, ccont) -> + Async.FromContinuations(fun (onNext, onError, onCancel) -> if task.IsCompleted then if task.IsFaulted then let e = task.Exception if e.InnerExceptions.Count = 1 then - econt e.InnerExceptions.[0] + onError e.InnerExceptions.[0] else - econt e + onError e elif task.IsCanceled then - ccont (TaskCanceledException(task)) + onCancel (TaskCanceledException(task)) else - cont () + onNext () else task.ContinueWith( (fun (task: Task) -> @@ -54,34 +95,43 @@ type AsyncEx = let e = task.Exception if e.InnerExceptions.Count = 1 then - econt e.InnerExceptions.[0] + onError e.InnerExceptions.[0] else - econt e + onError e elif task.IsCanceled then - ccont (TaskCanceledException(task)) + onCancel (TaskCanceledException(task)) else - cont () + onNext () ), TaskContinuationOptions.ExecuteSynchronously ) |> ignore ) + /// + /// Return an asynchronous computation that will wait for the given Task to complete and return + /// its result. + /// + /// The Awaitable to await + /// + /// + /// This is based on Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// static member AwaitTask(task: Task<'T>) : Async<'T> = - Async.FromContinuations(fun (cont, econt, ccont) -> + Async.FromContinuations(fun (onNext, onError, onCancel) -> if task.IsCompleted then if task.IsFaulted then let e = task.Exception if e.InnerExceptions.Count = 1 then - econt e.InnerExceptions.[0] + onError e.InnerExceptions.[0] else - econt e + onError e elif task.IsCanceled then - ccont (TaskCanceledException(task)) + onCancel (TaskCanceledException(task)) else - cont task.Result + onNext task.Result else task.ContinueWith( (fun (task: Task<'T>) -> @@ -89,13 +139,13 @@ type AsyncEx = let e = task.Exception if e.InnerExceptions.Count = 1 then - econt e.InnerExceptions.[0] + onError e.InnerExceptions.[0] else - econt e + onError e elif task.IsCanceled then - ccont (TaskCanceledException(task)) + onCancel (TaskCanceledException(task)) else - cont task.Result + onNext task.Result ), TaskContinuationOptions.ExecuteSynchronously ) @@ -103,11 +153,16 @@ type AsyncEx = ) #if NETSTANDARD2_1 + /// - /// Return an asynchronous computation that will check if ValueTask is completed or wait for - /// the given task to complete and return its result. + /// Return an asynchronous computation that will wait for the given ValueTask to complete and return + /// its result. /// - /// The task to await. + /// The Awaitable to await + /// + /// + /// This is based on Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// static member inline AwaitValueTask(vTask: ValueTask<_>) : Async<_> = // https://github.com/dotnet/runtime/issues/31503#issuecomment-554415966 if vTask.IsCompletedSuccessfully then @@ -117,10 +172,14 @@ type AsyncEx = /// - /// Return an asynchronous computation that will check if ValueTask is completed or wait for - /// the given task to complete and return its result. + /// Return an asynchronous computation that will wait for the given Task to complete and return + /// its result. /// - /// The task to await. + /// The Awaitable to await + /// + /// + /// This is based on Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// static member inline AwaitValueTask(vTask: ValueTask) : Async = // https://github.com/dotnet/runtime/issues/31503#issuecomment-554415966 if vTask.IsCompletedSuccessfully then @@ -135,36 +194,48 @@ module AsyncExtensions = type Microsoft.FSharp.Control.Async with - static member inline TryFinallyAsync(comp: Async<'T>, deferred) : Async<'T> = - - let finish (compResult, deferredResult) (cont, (econt: exn -> unit), ccont) = + /// Creates an Async that runs computation. The action compensation is executed + /// after computation completes, whether computation exits normally or by an exception. If compensation raises an exception itself + /// the original exception is discarded and the new exception becomes the overall result of the computation. + /// The input computation. + /// The action to be run after computation completes or raises an + /// exception (including cancellation). + /// See this F# gist + /// An async with the result of the computation. + static member inline TryFinallyAsync + ( + computation: Async<'T>, + compensation: Async + ) : Async<'T> = + + let finish (compResult, deferredResult) (onNext, (onError: exn -> unit), onCancel) = match (compResult, deferredResult) with - | (Choice1Of3 x, Choice1Of3()) -> cont x - | (Choice2Of3 compExn, Choice1Of3()) -> econt compExn - | (Choice3Of3 compExn, Choice1Of3()) -> ccont compExn - | (Choice1Of3 _, Choice2Of3 deferredExn) -> econt deferredExn + | (Choice1Of3 x, Choice1Of3()) -> onNext x + | (Choice2Of3 compExn, Choice1Of3()) -> onError compExn + | (Choice3Of3 compExn, Choice1Of3()) -> onCancel compExn + | (Choice1Of3 _, Choice2Of3 deferredExn) -> onError deferredExn | (Choice2Of3 compExn, Choice2Of3 deferredExn) -> - econt + onError <| new AggregateException(compExn, deferredExn) - | (Choice3Of3 compExn, Choice2Of3 deferredExn) -> econt deferredExn + | (Choice3Of3 compExn, Choice2Of3 deferredExn) -> onError deferredExn | (_, Choice3Of3 deferredExn) -> - econt + onError <| new Exception("Unexpected cancellation.", deferredExn) - let startDeferred compResult (cont, econt, ccont) = + let startDeferred compResult (onNext, onError, onCancel) = Async.StartWithContinuations( - deferred, - (fun () -> finish (compResult, Choice1Of3()) (cont, econt, ccont)), - (fun exn -> finish (compResult, Choice2Of3 exn) (cont, econt, ccont)), - (fun exn -> finish (compResult, Choice3Of3 exn) (cont, econt, ccont)) + compensation, + (fun () -> finish (compResult, Choice1Of3()) (onNext, onError, onCancel)), + (fun exn -> finish (compResult, Choice2Of3 exn) (onNext, onError, onCancel)), + (fun exn -> finish (compResult, Choice3Of3 exn) (onNext, onError, onCancel)) ) - let startComp ct (cont, econt, ccont) = + let startComp ct (onNext, onError, onCancel) = Async.StartWithContinuations( - comp, - (fun x -> startDeferred (Choice1Of3(x)) (cont, econt, ccont)), - (fun exn -> startDeferred (Choice2Of3 exn) (cont, econt, ccont)), - (fun exn -> startDeferred (Choice3Of3 exn) (cont, econt, ccont)), + computation, + (fun x -> startDeferred (Choice1Of3(x)) (onNext, onError, onCancel)), + (fun exn -> startDeferred (Choice2Of3 exn) (onNext, onError, onCancel)), + (fun exn -> startDeferred (Choice3Of3 exn) (onNext, onError, onCancel)), ct ) @@ -174,9 +245,19 @@ module AsyncExtensions = } -/// Class for AsyncEx functionality +/// Builds an asynchronous workflow using computation expression syntax. +/// +/// The difference between the AsyncBuilder and AsyncExBuilder is follows: +/// +/// Allows use on System.IAsyncDisposable +/// Allows let! for Tasks, ValueTasks, and any Awaitable Type +/// When Tasks throw exceptions they will use the behavior described in Async.Await overload (esp. AwaitTask without throwing AggregateException) +/// +/// +/// type AsyncExBuilder() = + member inline _.Zero() = async.Zero() member inline _.Delay([] generator: unit -> Async<'j>) = async.Delay generator @@ -252,8 +333,15 @@ type AsyncExBuilder() = // - Task.GetAwaiter() : Runtime.CompilerServices.TaskAwaiterF# Compiler43 member inline _.Source(task: Task<_>) = AsyncEx.AwaitTask task + member inline _.Source(task: Task) = AsyncEx.AwaitTask task + +#if NETSTANDARD2_1 + member inline _.Source(vtask: ValueTask<_>) = AsyncEx.AwaitValueTask vtask + + member inline _.Source(vtask: ValueTask) = AsyncEx.AwaitValueTask vtask +#endif [] -module Extensions = +module AsyncExExtensions = open FSharp.Core.CompilerServices type AsyncExBuilder with @@ -275,59 +363,16 @@ module Extensions = (task: 'Awaitable) = task - |> Awaitable.GetAwaiter - |> AsyncEx.AwaitAwaiter - + |> AsyncEx.AwaitAwaitable + + /// Builds an asynchronous workflow using computation expression syntax. + /// + /// The difference between the AsyncBuilder and AsyncExBuilder is follows: + /// + /// Allows use on System.IAsyncDisposable + /// Allows let! for Tasks, ValueTasks, and any Awaitable Type + /// When Tasks throw exceptions they will use the behavior described in Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// + /// + /// let asyncEx = new AsyncExBuilder() - - -module Tests = -#if NETSTANDARD2_1 - type DisposableAsync() = - interface IAsyncDisposable with - member this.DisposeAsync() = ValueTask() - - let disposeAsyncTest = asyncEx { - use foo = new DisposableAsync() - return () - } - - let valueTaskTest = asyncEx { - let! ct = ValueTask "LOL" - return ct - } - - let valueTaskTest2 = asyncEx { - let! ct = ValueTask() - return ct - } -#endif - - type Disposable() = - interface IDisposable with - member this.Dispose() = () - - let disposeTest = asyncEx { - use foo = new Disposable() - return () - } - - let taskTest = asyncEx { - let! ct = Task.FromResult "LOL" - return ct - } - - let taskTest2 = asyncEx { - let! ct = (Task.FromResult() :> Task) - return ct - } - - let yieldTasktest = asyncEx { - let! ct = Task.Yield() - return ct - } - - let awaiterTest = asyncEx { - let! ct = (Task.FromResult "LOL").GetAwaiter() - return ct - } diff --git a/tests/IcedTasks.Tests/AsyncExTests.fs b/tests/IcedTasks.Tests/AsyncExTests.fs new file mode 100644 index 0000000..e7e1a8f --- /dev/null +++ b/tests/IcedTasks.Tests/AsyncExTests.fs @@ -0,0 +1,635 @@ +namespace IcedTasks.Tests + +open System +open Expecto +open System.Threading +open System.Threading.Tasks +open IcedTasks + + +module AsyncExTests = + + let builderTests = + testList "AsyncExBuilder" [ + testList "Return" [ + testCaseAsync "Simple return" + <| async { + let data = "foo" + let! result = asyncEx { return data } + Expect.equal result data "Should return the data" + } + ] + testList "ReturnFrom" [ + testCaseAsync "Can ReturnFrom an AsyncEx" + <| async { + let data = "foo" + let inner = asyncEx { return data } + let outer = asyncEx { return! inner } + let! result = outer + Expect.equal result data "Should return the data" + } + testCaseAsync "Can ReturnFrom an Async" + <| async { + let data = "foo" + let inner = async { return data } + let outer = asyncEx { return! inner } + let! result = outer + Expect.equal result data "Should return the data" + } + + testCaseAsync "Can ReturnFrom an Task" + <| async { + let data = "foo" + let inner = task { return data } + let outer = asyncEx { return! inner } + let! result = outer + Expect.equal result data "Should return the data" + } + testCaseAsync "Can ReturnFrom an Task" + <| async { + let inner: Task = Task.CompletedTask + let outer = asyncEx { return! inner } + let! result = outer + Expect.equal result () "Should return the data" + } +#if NETSTANDARD2_1 + testCaseAsync "Can ReturnFrom an ValueTask" + <| async { + let data = "foo" + let inner = valueTask { return data } + let outer = asyncEx { return! inner } + let! result = outer + Expect.equal result data "Should return the data" + } + testCaseAsync "Can ReturnFrom an ValueTask" + <| async { + let inner: ValueTask = ValueTask.CompletedTask + let outer = asyncEx { return! inner } + let! result = outer + Expect.equal result () "Should return the data" + } +#endif + testCaseAsync "Can ReturnFrom an TaskLike" + <| async { + let inner = Task.Yield() + let outer = asyncEx { return! inner } + let! result = outer + Expect.equal result () "Should return the data" + } + ] + testList "Bind" [ + testCaseAsync "Can bind an AsyncEx" + <| async { + let data = "foo" + let inner = asyncEx { return data } + + let outer = asyncEx { + let! result = inner + return result + } + + let! result = outer + Expect.equal result data "Should return the data" + } + testCaseAsync "Can bind an Async" + <| async { + let data = "foo" + let inner = async { return data } + + let outer = asyncEx { + let! result = inner + return result + } + + let! result = outer + Expect.equal result data "Should return the data" + } + testCaseAsync "Can bind an Task" + <| async { + let data = "foo" + let inner = task { return data } + + let outer = asyncEx { + let! result = inner + return result + } + + let! result = outer + Expect.equal result data "Should return the data" + } + testCaseAsync "Can bind an Task" + <| async { + let inner: Task = Task.CompletedTask + + let outer = asyncEx { + let! result = inner + return result + } + + let! result = outer + Expect.equal result () "Should return the data" + } +#if NET7_0_OR_GREATER + testCaseAsync "Can bind an ValueTask" + <| async { + let data = "foo" + let inner = valueTask { return data } + + let outer = asyncEx { + let! result = inner + return result + } + + let! result = outer + Expect.equal result data "Should return the data" + } + testCaseAsync "Can bind an ValueTask" + <| async { + let inner: ValueTask = ValueTask.CompletedTask + + let outer = asyncEx { + let! result = inner + return result + } + + let! result = outer + Expect.equal result () "Should return the data" + } +#endif + testCaseAsync "Can bind an TaskLike" + <| async { + let inner = Task.Yield() + + let outer = asyncEx { + let! result = inner + return result + } + + let! result = outer + Expect.equal result () "Should return the data" + } + ] + testList "Zero/Combine/Delay" [ + testCaseAsync "if statement" + <| async { + let data = "foo" + let inner = asyncEx { return data } + + let outer = asyncEx { + let! result = inner + + if true then + () + + return result + } + + let! result = outer + Expect.equal result data "Should return the data" + } + ] + testList "TryWith" [ + testCaseAsync "simple try with" + <| async { + let data = "foo" + let inner = asyncEx { return data } + + let outer = asyncEx { + try + let! result = inner + return result + with ex -> + return failwith "Should not throw" + } + + let! result = outer + Expect.equal result data "Should return the data" + } + testCaseAsync + "Awaiting Failed Task<'T> should only contain one exception and not aggregation" + <| async { + let data = "lol" + + let inner = asyncEx { + let! result = task { + do! Task.Yield() + raise (ArgumentException "foo") + return data + } + + return result + } + + let outer = asyncEx { + try + let! result = inner + return () + with + | :? ArgumentException -> + // Should be this exception and not AggregationException + return () + | ex -> + return raise (Exception("Should not throw this type of exception", ex)) + } + + let! result = outer + Expect.equal result () "Should return the data" + } + + testCaseAsync + "Awaiting Failed Task should only contain one exception and not aggregation" + <| async { + let data = "lol" + + let inner = asyncEx { + do! + task { + do! Task.Yield() + raise (ArgumentException "foo") + return data + } + :> Task + } + + let outer = asyncEx { + try + do! inner + return () + with + | :? ArgumentException -> + // Should be this exception and not AggregationException + return () + | ex -> + return raise (Exception("Should not throw this type of exception", ex)) + } + + let! result = outer + Expect.equal result () "Should return the data" + } +#if NET7_0_OR_GREATER + testCaseAsync + "Awaiting Failed ValueTask<'T> should only contain one exception and not aggregation" + <| async { + let data = "lol" + + let inner = asyncEx { + let! result = valueTask { + do! Task.Yield() + raise (ArgumentException "foo") + return data + } + + return result + } + + let outer = asyncEx { + try + let! result = inner + return () + with + | :? ArgumentException -> + // Should be this exception and not AggregationException + return () + | ex -> + return raise (Exception("Should not throw this type of exception", ex)) + } + + let! result = outer + Expect.equal result () "Should return the data" + } +#endif + testCaseAsync + "Awaiting Failed CustomAwaiter should only contain one exception and not aggregation" + <| async { + let data = "lol" + + let inner = asyncEx { + let awaiter = + CustomAwaiter.CustomAwaiter( + (fun () -> raise (ArgumentException "foo")), + (fun () -> true) + ) + + let! result = awaiter + + return result + } + + let outer = asyncEx { + try + let! result = inner + return () + with + | :? ArgumentException -> + // Should be this exception and not AggregationException + return () + | ex -> + return + raise ( + Exception( + $"Should not throw this type of exception {ex.GetType()}", + ex + ) + ) + } + + let! result = outer + Expect.equal result () "Should return the data" + } + ] + testList "TryFinally" [ + testCaseAsync "simple try finally" + <| async { + let data = "foo" + let inner = asyncEx { return data } + + let outer = asyncEx { + try + let! result = inner + return result + finally + () + } + + let! result = outer + Expect.equal result data "Should return the data" + } + ] + testList "Using" [ + testCaseAsync "use IDisposable" + <| async { + let data = 42 + let mutable wasDisposed = false + let doDispose () = wasDisposed <- true + + let! actual = asyncEx { + use d = TestHelpers.makeDisposable (doDispose) + return data + } + + Expect.equal actual data "Should be able to use use" + Expect.isTrue wasDisposed "" + } + testCaseAsync "use! using" + <| async { + let data = 42 + let mutable wasDisposed = false + let doDispose () = wasDisposed <- true + + let! actual = asyncEx { + use! d = + TestHelpers.makeDisposable (doDispose) + |> async.Return + + return data + } + + Expect.equal actual data "Should be able to use use" + Expect.isTrue wasDisposed "" + } +#if NET7_0_OR_GREATER + testCaseAsync "use IAsyncDisposable sync" + <| async { + let data = 42 + let mutable wasDisposed = false + + let doDispose () = + wasDisposed <- true + ValueTask.CompletedTask + + let! actual = asyncEx { + use d = TestHelpers.makeAsyncDisposable (doDispose) + return data + } + + Expect.equal actual data "Should be able to use use" + Expect.isTrue wasDisposed "" + } + + testCaseAsync "use! IAsyncDisposable sync" + <| async { + let data = 42 + let mutable wasDisposed = false + + let doDispose () = + wasDisposed <- true + ValueTask.CompletedTask + + let! actual = asyncEx { + use! d = + TestHelpers.makeAsyncDisposable (doDispose) + |> async.Return + + return data + } + + Expect.equal actual data "Should be able to use use" + Expect.isTrue wasDisposed "" + } + + + testCaseAsync "use IAsyncDisposable async" + <| async { + let data = 42 + let mutable wasDisposed = false + + let doDispose () = + task { + do! Task.Yield() + wasDisposed <- true + } + |> ValueTask + + let! actual = asyncEx { + use d = TestHelpers.makeAsyncDisposable (doDispose) + return data + } + + Expect.equal actual data "Should be able to use use" + Expect.isTrue wasDisposed "" + } + + testCaseAsync "use! IAsyncDisposable async" + <| async { + let data = 42 + let mutable wasDisposed = false + + let doDispose () = + task { + do! Task.Yield() + wasDisposed <- true + } + |> ValueTask + + let! actual = asyncEx { + use! d = + TestHelpers.makeAsyncDisposable (doDispose) + |> async.Return + + return data + } + + Expect.equal actual data "Should be able to use use" + Expect.isTrue wasDisposed "" + } +#endif + testCaseAsync "null" + <| async { + let data = 42 + + let! actual = asyncEx { + use d = null + return data + } + + Expect.equal actual data "Should be able to use use" + } + ] + testList "While" [ + + yield! + [ + 10 + 10000 + 1000000 + ] + |> List.map (fun loops -> + testCaseAsync $"while to {loops}" + <| async { + let mutable index = 0 + + let! actual = asyncEx { + while index < loops do + index <- index + 1 + + return index + } + + Expect.equal actual loops "Should be ok" + } + ) + + + yield! + [ + 10 + 10000 + 1000000 + ] + |> List.map (fun loops -> + testCaseAsync $"while bind to {loops}" + <| async { + let mutable index = 0 + + let! actual = asyncEx { + while index < loops do + do! Task.Yield() + index <- index + 1 + + return index + } + + Expect.equal actual loops "Should be ok" + } + ) + ] + + testList "For" [ + + yield! + [ + 10 + 10000 + 1000000 + ] + |> List.map (fun loops -> + testCaseAsync $"for in {loops}" + <| async { + let mutable index = 0 + + let! actual = asyncEx { + for i in [ 1..10 ] do + index <- i + i + + return index + } + + Expect.equal actual index "Should be ok" + } + ) + + + yield! + [ + 10 + 10000 + 1000000 + ] + |> List.map (fun loops -> + testCaseAsync $"for to {loops}" + <| async { + let mutable index = 0 + + let! actual = asyncEx { + for i = 1 to loops do + index <- i + i + + return index + } + + Expect.equal actual index "Should be ok" + } + ) + + yield! + [ + 10 + 10000 + 1000000 + ] + |> List.map (fun loops -> + testCaseAsync $"for bind in {loops}" + <| async { + let mutable index = 0 + + let! actual = asyncEx { + for i in [ 1..10 ] do + do! Task.Yield() + index <- i + i + + return index + } + + Expect.equal actual index "Should be ok" + } + ) + + + yield! + [ + 10 + 10000 + 1000000 + ] + |> List.map (fun loops -> + testCaseAsync $"for bind to {loops}" + <| async { + let mutable index = 0 + + let! actual = asyncEx { + for i = 1 to loops do + do! Task.Yield() + index <- i + i + + return index + } + + Expect.equal actual index "Should be ok" + } + ) + ] + ] + + + [] + let asyncExTests = testList "IcedTasks.AsyncEx" [ builderTests ] diff --git a/tests/IcedTasks.Tests/CancellableTaskTests.fs b/tests/IcedTasks.Tests/CancellableTaskTests.fs index 7961060..45d26ab 100644 --- a/tests/IcedTasks.Tests/CancellableTaskTests.fs +++ b/tests/IcedTasks.Tests/CancellableTaskTests.fs @@ -5,8 +5,9 @@ open Expecto open System.Threading open System.Threading.Tasks open IcedTasks +#if NET7_0_OR_GREATER open IcedTasks.ValueTaskExtensions - +#endif module CancellableTaskTests = open System.Collections.Concurrent open TimeProviderExtensions @@ -180,7 +181,7 @@ module CancellableTaskTests = Expect.equal actual expected "" } - +#if NET7_0_OR_GREATER testCaseAsync "Can Bind Cancellable TaskLike" <| async { let fooTask = fun (ct: CancellationToken) -> Task.Yield() @@ -197,7 +198,7 @@ module CancellableTaskTests = |> Async.AwaitValueTask // Compiling is sufficient expect } - +#endif testCaseAsync "Can Bind Task" <| async { let outerTask = cancellableTask { do! Task.CompletedTask } @@ -395,6 +396,7 @@ module CancellableTaskTests = } +#if NET7_0_OR_GREATER testCaseAsync "use IAsyncDisposable sync" <| async { let data = 42 @@ -486,7 +488,7 @@ module CancellableTaskTests = Expect.equal actual data "Should be able to use use" Expect.isTrue wasDisposed "" } - +#endif testCaseAsync "null" <| async { let data = 42 @@ -749,11 +751,11 @@ module CancellableTaskTests = TimeSpan.FromMilliseconds(100) ) - let t = fooTask cts.Token + let runningTask = fooTask cts.Token do! timeProvider.ForwardTimeAsync(TimeSpan.FromMilliseconds(50)) - Expect.equal t.IsCanceled false "" + Expect.isFalse runningTask.IsCanceled "" do! timeProvider.ForwardTimeAsync(TimeSpan.FromMilliseconds(50)) - do! t + do! runningTask } ) diff --git a/tests/IcedTasks.Tests/CancellableValueTaskTests.fs b/tests/IcedTasks.Tests/CancellableValueTaskTests.fs index d7c3210..d2f86e1 100644 --- a/tests/IcedTasks.Tests/CancellableValueTaskTests.fs +++ b/tests/IcedTasks.Tests/CancellableValueTaskTests.fs @@ -6,7 +6,9 @@ open System.Threading open System.Threading.Tasks open IcedTasks +#if NET7_0_OR_GREATER module CancellableValueTaskTests = + open TimeProviderExtensions let builderTests = testList "CancellableValueTaskBuilder" [ @@ -759,6 +761,8 @@ module CancellableValueTaskTests = testCaseAsync "Can extract context's CancellationToken via CancellableValueTask.getCancellationToken in a deeply nested CE" <| async { + let timeProvider = ManualTimeProvider() + do! Expect.CancellationRequested( cancellableValueTask { @@ -766,14 +770,26 @@ module CancellableValueTaskTests = return! cancellableValueTask { do! cancellableValueTask { let! ct = CancellableValueTask.getCancellationToken () - do! Task.Delay(1000, ct) + + do! + timeProvider.Delay( + TimeSpan.FromMilliseconds(1000), + ct + ) } } } - use cts = new CancellationTokenSource() - cts.CancelAfter(100) - do! fooTask cts.Token + use cts = + timeProvider.CreateCancellationTokenSource( + TimeSpan.FromMilliseconds(100) + ) + + let runningTask = fooTask cts.Token + do! timeProvider.ForwardTimeAsync(TimeSpan.FromMilliseconds(50)) + Expect.isFalse runningTask.IsCanceled "" + do! timeProvider.ForwardTimeAsync(TimeSpan.FromMilliseconds(50)) + do! runningTask } ) @@ -1016,3 +1032,4 @@ module CancellableValueTaskTests = asyncBuilderTests functionTests ] +#endif diff --git a/tests/IcedTasks.Tests/ColdTaskTests.fs b/tests/IcedTasks.Tests/ColdTaskTests.fs index e322231..5f22dd2 100644 --- a/tests/IcedTasks.Tests/ColdTaskTests.fs +++ b/tests/IcedTasks.Tests/ColdTaskTests.fs @@ -407,6 +407,7 @@ module ColdTaskTests = } +#if NET7_0_OR_GREATER testCaseAsync "use IAsyncDisposable sync" <| async { let data = 42 @@ -498,7 +499,7 @@ module ColdTaskTests = Expect.equal actual data "Should be able to use use" Expect.isTrue wasDisposed "" } - +#endif testCaseAsync "null" <| async { let data = 42 diff --git a/tests/IcedTasks.Tests/Expect.fs b/tests/IcedTasks.Tests/Expect.fs index b14ec31..bf96858 100644 --- a/tests/IcedTasks.Tests/Expect.fs +++ b/tests/IcedTasks.Tests/Expect.fs @@ -48,10 +48,11 @@ type Expect = (fun () -> operation) "Should have been cancelled" +#if NET7_0_OR_GREATER static member CancellationRequested(operation: ValueTask) = Expect.CancellationRequested(Async.AwaitValueTask operation) |> Async.AsValueTask - +#endif static member CancellationRequested(operation: Task<_>) = Expect.CancellationRequested(Async.AwaitTask operation) |> Async.StartImmediateAsTask @@ -64,10 +65,11 @@ type Expect = Expect.CancellationRequested(Async.AwaitCancellableTask operation) |> Async.AsCancellableTask +#if NET7_0_OR_GREATER static member CancellationRequested(operation: CancellableValueTask<_>) = Expect.CancellationRequested(Async.AwaitCancellableValueTask operation) |> Async.AsCancellableTask - +#endif open TimeProviderExtensions open System.Runtime.CompilerServices @@ -82,3 +84,15 @@ type ManualTimeProviderExtensions = do! Task.Yield() do! Task.Delay(5) } + + +module CustomAwaiter = + + type CustomAwaiter<'T>(onGetResult, onIsCompleted) = + + member this.GetResult() : 'T = onGetResult () + member this.IsCompleted: bool = onIsCompleted () + + interface ICriticalNotifyCompletion with + member this.UnsafeOnCompleted(continuation) = failwith "Not Implemented" + member this.OnCompleted(continuation: Action) : unit = failwith "Not Implemented" diff --git a/tests/IcedTasks.Tests/IcedTasks.Tests.fsproj b/tests/IcedTasks.Tests/IcedTasks.Tests.fsproj index 8e3a544..55d1123 100644 --- a/tests/IcedTasks.Tests/IcedTasks.Tests.fsproj +++ b/tests/IcedTasks.Tests/IcedTasks.Tests.fsproj @@ -5,12 +5,16 @@ net7.0;net6.0 false - - - + + + + + + + diff --git a/tests/IcedTasks.Tests/ValueTaskTests.fs b/tests/IcedTasks.Tests/ValueTaskTests.fs index 32af631..9ea4f0d 100644 --- a/tests/IcedTasks.Tests/ValueTaskTests.fs +++ b/tests/IcedTasks.Tests/ValueTaskTests.fs @@ -5,7 +5,7 @@ open Expecto open System.Threading open System.Threading.Tasks open IcedTasks - +#if NET7_0 module ValueTaskTests = @@ -720,3 +720,5 @@ module ValueTaskTests = // asyncBuilderTests functionTests ] + +#endif