From c5fd0bea72ef3c6c7fbe3199f74cedc3382100ac Mon Sep 17 00:00:00 2001 From: Jimmy Byrd Date: Tue, 4 Jul 2023 16:16:08 -0400 Subject: [PATCH] Lots of AsyncEx tests and docs --- README.md | 97 ++++++++-- docsSrc/index.md | 165 ++++++++++++++++++ src/IcedTasks/AsyncEx.fs | 3 + src/IcedTasks/CancellableTask.fs | 42 ++++- src/IcedTasks/CancellableValueTask.fs | 39 ++++- src/IcedTasks/ColdTask.fs | 33 +++- tests/IcedTasks.Tests/CancellableTaskTests.fs | 50 ++++++ .../CancellableValueTaskTests.fs | 65 +++++++ tests/IcedTasks.Tests/ColdTaskTests.fs | 49 ++++++ 9 files changed, 528 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5ef6493..0b2d785 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # IcedTasks -## What +## What is IcedTasks? This library contains additional [computation expressions](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions) for the [task CE](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/task-expressions) utilizing the [Resumable Code](https://github.com/fsharp/fslang-design/blob/main/FSharp-6.0/FS-1087-resumable-code.md) introduced [in F# 6.0](https://devblogs.microsoft.com/dotnet/whats-new-in-fsharp-6/#making-f-faster-and-more-interopable-with-task). @@ -14,16 +14,21 @@ This library contains additional [computation expressions](https://docs.microsof - `ParallelAsync<'T>` - Utilizes the [applicative syntax](https://docs.microsoft.com/en-us/dotnet/fsharp/whats-new/fsharp-50#applicative-computation-expressions) to allow parallel execution of [Async<'T> expressions](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/async-expressions). See [this discussion](https://github.com/dotnet/fsharp/discussions/11043) as to why this is a separate computation expression. +- `AsyncEx<'T>` - Slight variation of F# async semantics described further below with examples. -| Computation Expression1 | Library2 | TFM3 | Hot/Cold4 | Multiple-Awaits5 | Multi-start6 | Tailcalls7 | CancellationToken propagation8 | Cancellation checks9 | Parallel when using and!10 | -|------------------------------------|---------------------|-----------------|----------------------|-----------------------------|-------------------------|-----------------------|-------------------------------------------|---------------------------------|--------------------------------------| -| F# Async | FSharp.Core | netstandard2.0 | Cold | multiple | multiple | tailcalls | implicit | implicit | No | -| F# ParallelAsync | IcedTasks | netstandard2.0 | Cold | multiple | multiple | tailcalls | implicit | implicit | Yes | -| F# Task/C# Task | FSharp.Core | netstandard2.0 | Hot | multiple | once-start | no tailcalls | explicit | explicit | No | -| F# ValueTask | IcedTasks | netstandard2.1 | Hot | once | once-start | no tailcalls | explicit | explicit | Yes | -| F# ColdTask | IcedTasks | netstandard2.0 | Cold | multiple | multiple | no tailcalls | explicit | explicit | Yes | -| F# CancellableTask | IcedTasks | netstandard2.0 | Cold | multiple | multiple | no tailcalls | implicit | implicit | Yes | -| F# CancellableValueTask | IcedTasks | netstandard2.1 | Cold | one | multiple | no tailcalls | implicit | implicit | Yes | + +### Differences at a glance + +| Computation Expression1 | Library2 | TFM3 | Hot/Cold4 | Multiple Awaits 5 | Multi-start6 | Tailcalls7 | CancellationToken propagation8 | Cancellation checks9 | Parallel when using and!10 | use IAsyncDisposable 11 | +|------------------------------------|---------------------|-----------------|----------------------|------------------------------|-------------------------|-----------------------|-------------------------------------------|---------------------------------|---------------------------------------|------------------------------------| +| F# Async | FSharp.Core | netstandard2.0 | Cold | Multiple | multiple | tailcalls | implicit | implicit | No | No | +| F# AsyncEx | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | tailcalls | implicit | implicit | No | Yes | +| F# ParallelAsync | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | tailcalls | implicit | implicit | Yes | No | +| F# Task/C# Task | FSharp.Core | netstandard2.0 | Hot | Multiple | once-start | no tailcalls | explicit | explicit | No | Yes | +| F# ValueTask | IcedTasks | netstandard2.1 | Hot | Once | once-start | no tailcalls | explicit | explicit | Yes | Yes | +| F# ColdTask | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | no tailcalls | explicit | explicit | Yes | Yes | +| F# CancellableTask | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | no tailcalls | implicit | implicit | Yes | Yes | +| F# CancellableValueTask | IcedTasks | netstandard2.1 | Cold | Once | multiple | no tailcalls | implicit | implicit | Yes | Yes | - 1 - [Computation Expression](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions) - 2 - Which [Nuget](https://www.nuget.org/) package do they come from @@ -35,11 +40,71 @@ This library contains additional [computation expressions](https://docs.microsof - 8 - `CancellationToken` is propagated to all types the support implicit `CancellatationToken` passing. Calling `cancellableTask { ... }` nested inside `async { ... }` (or any of those combinations) will use the `CancellationToken` from when the code was started. - 9 - Cancellation will be checked before binds and runs. - 10 - Allows parallel execution of the asynchronous code using the [Applicative Syntax](https://docs.microsoft.com/en-us/dotnet/fsharp/whats-new/fsharp-50#applicative-computation-expressions) in computation expressions. +- 11 - Allows `use` of `IAsyncDisposable` with the computation expression. See [IAsyncDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.iasyncdisposable) for more info. + +## Why should I use this? + + +### AsyncEx + +AsyncEx is similar to Async except in the following ways: + +1. Allows `use` for `[IAsyncDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.iasyncdisposable)` + + ```fsharp + open IcedTasks + let fakeDisposable = { new IAsyncDisposable with member __.DisposeAsync() = ValueTask.CompletedTask } + + let myAsyncEx = asyncEx { + use! _ = fakeDisposable + return 42 + } + ```` +2. Allows `let!/do!` against Tasks/ValueTasks/[any Awaitable](https://devblogs.microsoft.com/pfxteam/await-anything/) + ```fsharp + open IcedTasks + let myAsyncEx = asyncEx { + let! _ = task { return 42 } // Task + let! _ = valueTask { return 42 } // ValueTask + let! _ = Task.Yield() // YieldAwaitable + return 42 + } + ``` +3. When Tasks throw exceptions they will use the behavior described in [Async.Await overload (esp. AwaitTask without throwing AggregateException](https://github.com/fsharp/fslang-suggestions/issues/840) + + + ```fsharp + 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)) + } + ``` + + +### For [ValueTasks](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/) -## How +- F# doesn't currently have a `valueTask` computation expression. [Until this PR is merged.](https://github.com/dotnet/fsharp/pull/14755) -### ValueTask ```fsharp open IcedTasks @@ -50,6 +115,12 @@ let myValueTask = task { } ``` +### For Cold & CancellableTasks +- You want control over when your tasks are started +- You want to be able to re-run these executable tasks +- You don't want to pollute your methods/functions with extra CancellationToken parameters +- You want the computation to handle checking cancellation before every bind. + ### ColdTask @@ -125,6 +196,8 @@ let executeWriting = task { ### ParallelAsync +- When you want to execute multiple asyncs in parallel and wait for all of them to complete. + Short example: ```fsharp diff --git a/docsSrc/index.md b/docsSrc/index.md index e877b29..97e5463 100644 --- a/docsSrc/index.md +++ b/docsSrc/index.md @@ -13,12 +13,80 @@ This library contains additional [computation expressions](https://docs.microsof - `ParallelAsync<'T>` - Utilizes the [applicative syntax](https://docs.microsoft.com/en-us/dotnet/fsharp/whats-new/fsharp-50#applicative-computation-expressions) to allow parallel execution of [Async<'T> expressions](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/async-expressions). See [this discussion](https://github.com/dotnet/fsharp/discussions/11043) as to why this is a separate computation expression. +- `AsyncEx<'T>` - Slight variation of F# async semantics described further below with examples. + ## Why should I use IcedTasks? +### AsyncEx + +AsyncEx is similar to Async except in the following ways: + +1. Allows `use` for `[IAsyncDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.iasyncdisposable)` + + ```fsharp + open IcedTasks + let fakeDisposable = { new IAsyncDisposable with member __.DisposeAsync() = ValueTask.CompletedTask } + + let myAsyncEx = asyncEx { + use! _ = fakeDisposable + return 42 + } + ```` +2. Allows `let!/do!` against Tasks/ValueTasks/[any Awaitable](https://devblogs.microsoft.com/pfxteam/await-anything/) + + ```fsharp + open IcedTasks + let myAsyncEx = asyncEx { + let! _ = task { return 42 } // Task + let! _ = valueTask { return 42 } // ValueTask + let! _ = Task.Yield() // YieldAwaitable + return 42 + } + ``` +3. When Tasks throw exceptions they will use the behavior described in [Async.Await overload (esp. AwaitTask without throwing AggregateException](https://github.com/fsharp/fslang-suggestions/issues/840) + + + ```fsharp + 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)) + } + ``` + + ### For [ValueTasks](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/) - F# doesn't currently have a `valueTask` computation expression. [Until this PR is merged.](https://github.com/dotnet/fsharp/pull/14755) + +```fsharp +open IcedTasks + +let myValueTask = task { + let! theAnswer = valueTask { return 42 } + return theAnswer +} +``` + ### For Cold & CancellableTasks - You want control over when your tasks are started - You want to be able to re-run these executable tasks @@ -26,6 +94,103 @@ This library contains additional [computation expressions](https://docs.microsof - You want the computation to handle checking cancellation before every bind. +### ColdTask + +Short example: + +```fsharp +open IcedTasks + +let coldTask_dont_start_immediately = task { + let mutable someValue = null + let fooColdTask = coldTask { someValue <- 42 } + do! Async.Sleep(100) + // ColdTasks will not execute until they are called, similar to how Async works + Expect.equal someValue null "" + // Calling fooColdTask will start to execute it + do! fooColdTask () + Expect.equal someValue 42 "" +} + +``` + +### CancellableTask & CancellableValueTask + +The examples show `cancellableTask` but `cancellableValueTask` can be swapped in. + +Accessing the context's CancellationToken: + +1. Binding against `CancellationToken -> Task<_>` + + ```fsharp + let writeJunkToFile = + let path = Path.GetTempFileName() + + cancellableTask { + let junk = Array.zeroCreate bufferSize + use file = File.Create(path) + + for i = 1 to manyIterations do + // You can do! directly against a function with the signature of `CancellationToken -> Task<_>` to access the context's `CancellationToken`. This is slightly more performant. + do! fun ct -> file.WriteAsync(junk, 0, junk.Length, ct) + } + ``` + +2. Binding against `CancellableTask.getCancellationToken` + + ```fsharp + let writeJunkToFile = + let path = Path.GetTempFileName() + + cancellableTask { + let junk = Array.zeroCreate bufferSize + use file = File.Create(path) + // You can bind against `CancellableTask.getCancellationToken` to get the current context's `CancellationToken`. + let! ct = CancellableTask.getCancellationToken () + for i = 1 to manyIterations do + do! file.WriteAsync(junk, 0, junk.Length, ct) + } + ``` + +Short example: + +```fsharp +let executeWriting = task { + // CancellableTask is an alias for `CancellationToken -> Task<_>` so we'll need to pass in a `CancellationToken`. + // For this example we'll use a `CancellationTokenSource` but if you were using something like ASP.NET, passing in `httpContext.RequestAborted` would be appropriate. + use cts = new CancellationTokenSource() + // call writeJunkToFile from our previous example + do! writeJunkToFile cts.Token +} + + +``` + +### ParallelAsync + +- When you want to execute multiple asyncs in parallel and wait for all of them to complete. + +Short example: + +```fsharp +open IcedTasks + +let exampleHttpCall url = async { + // Pretend we're executing an HttpClient call + return 42 +} + +let getDataFromAFewSites = parallelAsync { + let! result1 = exampleHttpCall "howManyPlantsDoIOwn" + and! result2 = exampleHttpCall "whatsTheTemperature" + and! result3 = exampleHttpCall "whereIsMyPhone" + + // Do something meaningful with results + return () +} + +``` + ## How do I get started dotnet add nuget IcedTasks diff --git a/src/IcedTasks/AsyncEx.fs b/src/IcedTasks/AsyncEx.fs index 7d9cfb2..3d67f2e 100644 --- a/src/IcedTasks/AsyncEx.fs +++ b/src/IcedTasks/AsyncEx.fs @@ -9,6 +9,9 @@ type private Async = static member inline map f x = async.Bind(x, (fun v -> async.Return(f v))) +/// +/// This contains many functions that implement Task throwing semantics differently than the current FSharp.Core. See Async.Await overload (esp. AwaitTask without throwing AggregateException) +/// type AsyncEx = /// diff --git a/src/IcedTasks/CancellableTask.fs b/src/IcedTasks/CancellableTask.fs index b07f5a3..ddd62bf 100644 --- a/src/IcedTasks/CancellableTask.fs +++ b/src/IcedTasks/CancellableTask.fs @@ -766,6 +766,37 @@ module CancellableTasks = /// [] module HighPriority = + + type AsyncEx with + + /// Return an asynchronous computation that will wait for the given task to complete and return + /// its result. + /// + /// + /// This is based on Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// + static member inline AwaitCancellableTask(t: CancellableTask<'T>) = async { + let! ct = Async.CancellationToken + + return! + t ct + |> AsyncEx.AwaitTask + } + + /// Return an asynchronous computation that will wait for the given task to complete and return + /// its result. + /// + /// + /// This is based on Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// + static member inline AwaitCancellableTask(t: CancellableTask) = async { + let! ct = Async.CancellationToken + + return! + t ct + |> AsyncEx.AwaitTask + } + type Microsoft.FSharp.Control.Async with /// Return an asynchronous computation that will wait for the given task to complete and return @@ -846,7 +877,16 @@ module CancellableTasks = /// A set of extension methods making it possible to bind against in async computations. /// [] - module AsyncExtenions = + module AsyncExtensions = + + type AsyncExBuilder with + + member inline this.Source(t: CancellableTask<'T>) : Async<'T> = + AsyncEx.AwaitCancellableTask t + + member inline this.Source(t: CancellableTask) : Async = + AsyncEx.AwaitCancellableTask t + type Microsoft.FSharp.Control.AsyncBuilder with member inline this.Bind(t: CancellableTask<'T>, binder: ('T -> Async<'U>)) : Async<'U> = diff --git a/src/IcedTasks/CancellableValueTask.fs b/src/IcedTasks/CancellableValueTask.fs index 2b4a676..a2c3bb9 100644 --- a/src/IcedTasks/CancellableValueTask.fs +++ b/src/IcedTasks/CancellableValueTask.fs @@ -785,6 +785,35 @@ module CancellableValueTasks = /// [] module HighPriority = + + type AsyncEx with + + /// Return an asynchronous computation that will wait for the given task to complete and return + /// its result. + /// + /// This is based on Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// + static member inline AwaitCancellableValueTask(t: CancellableValueTask<'T>) = async { + let! ct = Async.CancellationToken + + return! + t ct + |> AsyncEx.AwaitValueTask + } + + /// Return an asynchronous computation that will wait for the given task to complete and return + /// its result. + /// + /// This is based on Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// + static member inline AwaitCancellableValueTask(t: CancellableValueTask) = async { + let! ct = Async.CancellationToken + + return! + t ct + |> AsyncEx.AwaitValueTask + } + type Microsoft.FSharp.Control.Async with /// Return an asynchronous computation that will wait for the given task to complete and return @@ -870,7 +899,15 @@ module CancellableValueTasks = /// A set of extension methods making it possible to bind against in async computations. /// [] - module AsyncExtenions = + module AsyncExtensions = + type AsyncExBuilder with + + member inline this.Source(t: CancellableValueTask<'T>) : Async<'T> = + AsyncEx.AwaitCancellableValueTask t + + member inline this.Source(t: CancellableValueTask) : Async = + AsyncEx.AwaitCancellableValueTask t + type Microsoft.FSharp.Control.AsyncBuilder with member inline this.Bind diff --git a/src/IcedTasks/ColdTask.fs b/src/IcedTasks/ColdTask.fs index 7da65dc..7b4fa8d 100644 --- a/src/IcedTasks/ColdTask.fs +++ b/src/IcedTasks/ColdTask.fs @@ -661,6 +661,31 @@ module ColdTasks = [] module HighPriority = // High priority extensions + + type AsyncEx with + + /// Return an asynchronous computation that will wait for the given task to complete and return + /// its result. + /// + /// This is based on Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// + static member inline AwaitColdTask(t: ColdTask<'T>) = + async.Delay(fun () -> + t () + |> AsyncEx.AwaitTask + ) + + /// Return an asynchronous computation that will wait for the given task to complete and return + /// its result. + /// + /// This is based on Async.Await overload (esp. AwaitTask without throwing AggregateException) + /// + static member inline AwaitColdTask(t: ColdTask) = + async.Delay(fun () -> + t () + |> AsyncEx.AwaitTask + ) + type Microsoft.FSharp.Control.Async with /// Return an asynchronous computation that will wait for the given task to complete and return @@ -720,7 +745,13 @@ module ColdTasks = /// A set of extension methods making it possible to bind against in async computations. /// [] - module AsyncExtenions = + module AsyncExtensions = + + + type AsyncExBuilder with + + member inline this.Source(task: ColdTask<'T>) : Async<'T> = AsyncEx.AwaitColdTask task + member inline this.Source(task: ColdTask) : Async = AsyncEx.AwaitColdTask task type Microsoft.FSharp.Control.AsyncBuilder with diff --git a/tests/IcedTasks.Tests/CancellableTaskTests.fs b/tests/IcedTasks.Tests/CancellableTaskTests.fs index 45d26ab..a4c1410 100644 --- a/tests/IcedTasks.Tests/CancellableTaskTests.fs +++ b/tests/IcedTasks.Tests/CancellableTaskTests.fs @@ -869,6 +869,55 @@ module CancellableTaskTests = Expect.equal actual cts.Token "" ] + + let asyncExBuilderTests = + testList "AsyncExBuilder" [ + + testCase "AsyncExBuilder can Bind CancellableTask" + <| fun () -> + let innerTask = cancellableTask { return! CancellableTask.getCancellationToken () } + + let outerAsync = asyncEx { + let! result = innerTask + return result + } + + use cts = new CancellationTokenSource() + let actual = Async.RunSynchronously(outerAsync, cancellationToken = cts.Token) + Expect.equal actual cts.Token "" + + + testCase "AsyncBuilder can ReturnFrom CancellableTask" + <| fun () -> + let innerTask = cancellableTask { return! CancellableTask.getCancellationToken () } + let outerAsync = asyncEx { return! innerTask } + + use cts = new CancellationTokenSource() + let actual = Async.RunSynchronously(outerAsync, cancellationToken = cts.Token) + Expect.equal actual cts.Token "" + + + testCase "AsyncBuilder can Bind CancellableTask" + <| fun () -> + let mutable actual = CancellationToken.None + let innerTask: CancellableTask = fun ct -> task { actual <- ct } :> Task + let outerAsync = asyncEx { do! innerTask } + + use cts = new CancellationTokenSource() + Async.RunSynchronously(outerAsync, cancellationToken = cts.Token) + Expect.equal actual cts.Token "" + + testCase "AsyncBuilder can ReturnFrom CancellableTask" + <| fun () -> + let mutable actual = CancellationToken.None + let innerTask: CancellableTask = fun ct -> task { actual <- ct } :> Task + let outerAsync = asyncEx { return! innerTask } + + use cts = new CancellationTokenSource() + Async.RunSynchronously(outerAsync, cancellationToken = cts.Token) + Expect.equal actual cts.Token "" + ] + let functionTests = testList "functions" [ testList "singleton" [ @@ -1154,5 +1203,6 @@ module CancellableTaskTests = testList "IcedTasks.CancellableTask" [ builderTests asyncBuilderTests + asyncExBuilderTests functionTests ] diff --git a/tests/IcedTasks.Tests/CancellableValueTaskTests.fs b/tests/IcedTasks.Tests/CancellableValueTaskTests.fs index d2f86e1..719aeac 100644 --- a/tests/IcedTasks.Tests/CancellableValueTaskTests.fs +++ b/tests/IcedTasks.Tests/CancellableValueTaskTests.fs @@ -918,6 +918,70 @@ module CancellableValueTaskTests = Expect.equal actual cts.Token "" ] + + let asyncExBuilderTests = + testList "AsyncExBuilder" [ + + testCase "AsyncExBuilder can Bind CancellableValueTask" + <| fun () -> + let innerTask = cancellableValueTask { + return! CancellableValueTask.getCancellationToken () + } + + let outerAsync = asyncEx { + let! result = innerTask + return result + } + + use cts = new CancellationTokenSource() + let actual = Async.RunSynchronously(outerAsync, cancellationToken = cts.Token) + Expect.equal actual cts.Token "" + + + testCase "AsyncBuilder can ReturnFrom CancellableValueTask" + <| fun () -> + let innerTask = cancellableValueTask { + return! CancellableValueTask.getCancellationToken () + } + + let outerAsync = asyncEx { return! innerTask } + + use cts = new CancellationTokenSource() + let actual = Async.RunSynchronously(outerAsync, cancellationToken = cts.Token) + Expect.equal actual cts.Token "" + + + testCase "AsyncBuilder can Bind CancellableValueTask" + <| fun () -> + let mutable actual = CancellationToken.None + + let innerTask: CancellableValueTask = + fun ct -> + valueTask { actual <- ct } + |> ValueTask.toUnit + + let outerAsync = asyncEx { do! innerTask } + + use cts = new CancellationTokenSource() + Async.RunSynchronously(outerAsync, cancellationToken = cts.Token) + Expect.equal actual cts.Token "" + + testCase "AsyncBuilder can ReturnFrom CancellableValueTask" + <| fun () -> + let mutable actual = CancellationToken.None + + let innerTask: CancellableValueTask = + fun ct -> + valueTask { actual <- ct } + |> ValueTask.toUnit + + let outerAsync = asyncEx { return! innerTask } + + use cts = new CancellationTokenSource() + Async.RunSynchronously(outerAsync, cancellationToken = cts.Token) + Expect.equal actual cts.Token "" + ] + let functionTests = testList "functions" [ testList "singleton" [ @@ -1030,6 +1094,7 @@ module CancellableValueTaskTests = testList "IcedTasks.CancellableValueTask" [ builderTests asyncBuilderTests + asyncExBuilderTests functionTests ] #endif diff --git a/tests/IcedTasks.Tests/ColdTaskTests.fs b/tests/IcedTasks.Tests/ColdTaskTests.fs index 5f22dd2..3416e6b 100644 --- a/tests/IcedTasks.Tests/ColdTaskTests.fs +++ b/tests/IcedTasks.Tests/ColdTaskTests.fs @@ -816,6 +816,54 @@ module ColdTaskTests = ] + let asyncExBuilderTests = + testList "AsyncExBuilder" [ + + testCase "AsyncExBuilder can Bind ColdTask" + <| fun () -> + let innerTask = coldTask { return! coldTask { return "lol" } } + + let outerAsync = asyncEx { + let! result = innerTask + return result + } + + let actual = Async.RunSynchronously(outerAsync) + Expect.equal actual "lol" "" + + + testCase "AsyncBuilder can ReturnFrom ColdTask" + <| fun () -> + let innerTask = coldTask { return! coldTask { return "lol" } } + + let outerAsync = asyncEx { return! innerTask } + + let actual = Async.RunSynchronously(outerAsync) + Expect.equal actual "lol" "" + + testCase "AsyncBuilder can Bind ColdTask" + <| fun () -> + let innerTask: ColdTask = fun () -> Task.CompletedTask + + let outerAsync = asyncEx { + let! result = innerTask + return result + } + + let actual = Async.RunSynchronously(outerAsync) + Expect.equal actual () "" + + testCase "AsyncBuilder can ReturnFrom ColdTask" + <| fun () -> + let innerTask: ColdTask = fun () -> Task.CompletedTask + + let outerAsync = asyncEx { return! innerTask } + + let actual = Async.RunSynchronously(outerAsync) + Expect.equal actual () "" + ] + + let taskBuilderTests = testList "TaskBuilder" [ @@ -972,6 +1020,7 @@ module ColdTaskTests = testList "IcedTasks.ColdTask" [ builderTests asyncBuilderTests + asyncExBuilderTests taskBuilderTests functionTests ]