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