diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 673aefef71..4e3264d4da 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -847,6 +847,8 @@ let tryEntityIdent (com: Compiler) entFullName = | Types.matchFail -> makeImportLib com Any "MatchFailureException" "Types" |> Some | Types.exception_ -> makeImportLib com Any "Exception" "Util" |> Some | "System.OperationCanceledException" -> makeImportLib com Any "OperationCanceledException" "AsyncBuilder" |> Some + | "System.Threading.Tasks.TaskCanceledException" -> + makeImportLib com Any "TaskCanceledException" "AsyncBuilder" |> Some | "System.Collections.Generic.KeyNotFoundException" -> makeImportLib com Any "KeyNotFoundException" "System.Collections.Generic" |> Some @@ -3292,6 +3294,19 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr match i.DeclaringEntityFullName with | "System.Collections.Generic.KeyNotFoundException" | BuiltinSystemException _ -> bclType com ctx r t i thisArg args + // The library classes have no CancellationToken or Task property + | "System.OperationCanceledException" + | "System.Threading.Tasks.TaskCanceledException" as entName -> + let args = + match List.tryLast i.SignatureArgTypes with + | Some(DeclaredTypeFullName(Types.cancellationToken | Types.task)) -> + List.truncate (List.length args - 1) args + | _ -> args + + let className = entName.Substring(entName.LastIndexOf('.') + 1) + + Helper.LibCall(com, "AsyncBuilder", className, t, args, isConstructor = true, ?loc = r) + |> Some | _ -> Helper.LibCall(com, "Util", "Exception", Any, args, isConstructor = true) |> Some @@ -4038,12 +4053,15 @@ let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti | _ -> None let cancels (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let isRegistration = + i.DeclaringEntityFullName = "System.Threading.CancellationTokenRegistration" + match i.CompiledName with | "get_None" // TODO: implement as non-cancellable token | ".ctor" -> Helper.LibCall(com, "Async", "createCancellationToken", t, args, i.SignatureArgTypes) |> Some - | "get_Token" -> thisArg + | "get_Token" when not isRegistration -> thisArg | "Cancel" | "CancelAfter" | "get_IsCancellationRequested" @@ -4063,8 +4081,21 @@ let cancels (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt ?loc = r ) |> Some + | "CreateLinkedTokenSource" -> + let tokens = + match args with + | [ ExprType(Array _) as tokens ] -> tokens + | tokens -> makeArray (List.head i.SignatureArgTypes) tokens + + Helper.LibCall(com, "Async", "createLinkedTokenSource", t, [ tokens ], ?loc = r) + |> Some + // A default registration is null, see disposables + | "Dispose" when isRegistration -> + Helper.LibCall(com, "Util", "disposeSafe", t, [ thisArg.Value ], ?loc = r) + |> Some // TODO: Add check so CancellationTokenSource cannot be cancelled after disposed? - | "Dispose" -> Null Type.Unit |> makeValue r |> Some + | "Dispose" + | "Unregister" -> Helper.InstanceCall(thisArg.Value, i.CompiledName, t, [], ?loc = r) |> Some | "Register" -> Helper.InstanceCall(thisArg.Value, "register", t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -4354,7 +4385,7 @@ let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr lis | "RunSynchronously" -> None // Task is Promise in JS/TS | "AwaitTask" -> - Helper.LibCall(com, "Async", "awaitPromise", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + Helper.LibCall(com, "Async", "awaitTask", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some // Fable.Core extensions | meth -> @@ -4447,6 +4478,14 @@ let tasks (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio ?loc = r ) |> Some + // Like awaiting on .NET, awaiters see the first exception of the seq overloads + | Some x, ("SetException" | "TrySetException" as meth) when + (match i.SignatureArgTypes with + | [ IEnumerable ] -> true + | _ -> false) + -> + Helper.InstanceCall(x, meth + "s", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | Some x, meth -> Helper.InstanceCall(x, meth, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some @@ -4976,6 +5015,7 @@ let private replacedModules = Types.random, random "System.Threading.CancellationToken", cancels "System.Threading.CancellationTokenSource", cancels + "System.Threading.CancellationTokenRegistration", cancels "System.Threading.Monitor", monitor "System.Activator", activator "System.Text.Encoding", encoding @@ -4999,6 +5039,7 @@ let private replacedModules = "Microsoft.FSharp.Control.AsyncPrimitives", asyncs Types.task, tasks Types.taskGeneric, tasks + "System.Threading.Tasks.TaskCompletionSource", tasks "System.Threading.Tasks.TaskCompletionSource`1", tasks "System.Runtime.CompilerServices.TaskAwaiter`1", tasks "Microsoft.FSharp.Control.TaskBuilder", taskBuilder diff --git a/src/fable-library-ts/Async.ts b/src/fable-library-ts/Async.ts index c668bd85c0..d225d29b48 100644 --- a/src/fable-library-ts/Async.ts +++ b/src/fable-library-ts/Async.ts @@ -1,12 +1,13 @@ -import { OperationCanceledException, Trampoline } from "./AsyncBuilder.ts"; +import { OperationCanceledException, TaskCanceledException, Trampoline } from "./AsyncBuilder.ts"; import { Continuation, Continuations } from "./AsyncBuilder.ts"; import { Async, IAsyncContext, CancellationToken } from "./AsyncBuilder.ts"; import { protectedCont, protectedBind, protectedReturn } from "./AsyncBuilder.ts"; import type { IEvent$2 } from "./Event.ts"; import { FSharpChoice$2_$union, Choice_makeChoice1Of2, Choice_makeChoice2Of2 } from "./Choice.ts"; -import { ArgumentException_$ctor_Z384F8060, InvalidOperationException_$ctor_Z721C83C5, TimeoutException_$ctor } from "./System.ts"; +import { ArgumentException_$ctor_Z384F8060, ArgumentException_$ctor_Z721C83C5, InvalidOperationException_$ctor_Z721C83C5, TimeoutException_$ctor } from "./System.ts"; import { Exception, ensureErrorOrException, IDisposable } from "./Util.ts"; import type { Option } from "./Option.ts"; +import { markObserved } from "./Task.ts"; function emptyContinuation(_x: T) { // NOP @@ -51,15 +52,35 @@ export function isCancellationRequested(token: CancellationToken) { export function throwIfCancellationRequested(token: CancellationToken) { if (token != null && token.isCancelled) { - throw new Exception("Operation is cancelled"); + throw new OperationCanceledException(); + } +} + +// Like .NET, the parents keep their listener until the linked token is disposed +class LinkedCancellationToken extends CancellationToken { + private readonly _unlink: () => void; + constructor(tokens: CancellationToken[]) { + super(); + // A default token is null and never cancelled + const parents = tokens.filter((parent) => parent != null); + const ids = parents.map((parent) => parent.addListener(() => this.cancel())); + this._unlink = () => { parents.forEach((parent, i) => parent.removeListener(ids[i])); }; + } + public Dispose() { + this._unlink(); } } -// The parent keeps the listener until unlink is called function createLinkedToken(parent: CancellationToken): [token: CancellationToken, unlink: () => void] { - const token = new CancellationToken(); - const id = parent.addListener(() => token.cancel()); - return [token, () => { parent.removeListener(id); }]; + const token = new LinkedCancellationToken([parent]); + return [token, () => { token.Dispose(); }]; +} + +export function createLinkedTokenSource(tokens: CancellationToken[]): CancellationToken { + if (tokens.length === 0) { + throw ArgumentException_$ctor_Z721C83C5("No tokens were supplied."); + } + return new LinkedCancellationToken(tokens); } export function startChild(computation: Async, ms?: number): Async> { @@ -104,6 +125,13 @@ export function awaitPromise(p: Promise) { ? conts[2] : conts[1])(err))); } +// Like .NET, a cancelled task raises TaskCanceledException in the awaiting workflow instead of cancelling it +export function awaitTask(task: Promise) { + return fromContinuations((conts: Continuations) => + task.then(conts[0], (err) => + conts[1](err instanceof OperationCanceledException ? new TaskCanceledException() : err))); +} + export function awaitEvent(event: IEvent$2, cancelAction?: () => void): Async { return protectedCont((ctx: IAsyncContext) => { let tokenId: number; @@ -370,9 +398,12 @@ export function startAsPromise(computation: Async, cancellationToken?: Can } export function startAsTask(computation: Async, _taskCreationOptions?: number, cancellationToken?: CancellationToken) { - return startAsPromise(computation, cancellationToken); + return startImmediateAsTask(computation, cancellationToken); } +// Like .NET, cancelling the computation cancels the task, and awaiting that raises TaskCanceledException export function startImmediateAsTask(computation: Async, cancellationToken?: CancellationToken) { - return startAsPromise(computation, cancellationToken); + return markObserved(new Promise((resolve: Continuation, reject: Continuation) => + startWithContinuations(computation, resolve, reject, () => reject(new TaskCanceledException()), + cancellationToken ? cancellationToken : defaultCancellationToken))); } diff --git a/src/fable-library-ts/AsyncBuilder.ts b/src/fable-library-ts/AsyncBuilder.ts index 0a89c59e18..68b08b106e 100644 --- a/src/fable-library-ts/AsyncBuilder.ts +++ b/src/fable-library-ts/AsyncBuilder.ts @@ -62,7 +62,10 @@ export class CancellationToken implements IDisposable { public register(f: (state?: any) => void, state?: any) { const $ = this; const id = this.addListener(state == null ? f : () => f(state)); - return { Dispose() { $.removeListener(id); } }; + return { + Dispose() { $.removeListener(id); }, + Unregister() { return $.removeListener(id); }, + }; } public Dispose() { // Implement IDisposable for compatibility but do nothing @@ -72,12 +75,18 @@ export class CancellationToken implements IDisposable { } export class OperationCanceledException extends Exception { - constructor(msg?: string) { - super(msg ?? "The operation was canceled"); + constructor(msg?: string, innerException?: Exception) { + super(msg ?? "The operation was canceled.", innerException); // Object.setPrototypeOf(this, OperationCanceledException.prototype); } } +export class TaskCanceledException extends OperationCanceledException { + constructor(msg?: string, innerException?: Exception) { + super(msg ?? "A task was canceled.", innerException); + } +} + export class Trampoline { static get maxTrampolineCallCount() { return 2000; diff --git a/src/fable-library-ts/Task.ts b/src/fable-library-ts/Task.ts index 6e1cb1c369..8691f162ca 100644 --- a/src/fable-library-ts/Task.ts +++ b/src/fable-library-ts/Task.ts @@ -1,36 +1,84 @@ -import { CancellationToken, OperationCanceledException } from "./AsyncBuilder.ts"; +import { CancellationToken, TaskCanceledException } from "./AsyncBuilder.ts"; +import { ArgumentException_$ctor_Z384F8060, InvalidOperationException_$ctor_Z721C83C5 } from "./System.ts"; export class TaskCompletionSource { private _resolve!: (value: T) => void; private _reject!: (reason?: unknown) => void; + private _completed = false; public task: Promise; constructor() { - this.task = new Promise((resolve, reject) => { + this.task = markObserved(new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; - }); + })); } - SetResult(value: T): void { this._resolve(value); } - SetException(error: unknown): void { this._reject(error); } - SetCancelled(): void { this._reject(new OperationCanceledException()); } + private trySettle(settle: () => void): boolean { + if (this._completed) { return false; } + this._completed = true; + settle(); + return true; + } + + private settle(settle: () => void): void { + if (!this.trySettle(settle)) { + throw InvalidOperationException_$ctor_Z721C83C5("An attempt was made to transition a task to a final state when it had already completed."); + } + } + + SetResult(value: T): void { this.settle(() => this._resolve(value)); } + TrySetResult(value: T): boolean { return this.trySettle(() => this._resolve(value)); } + SetException(error: unknown): void { this.settle(() => this._reject(error)); } + TrySetException(error: unknown): boolean { return this.trySettle(() => this._reject(error)); } + SetExceptions(errors: Iterable): void { this.SetException(firstException(errors)); } + TrySetExceptions(errors: Iterable): boolean { return this.TrySetException(firstException(errors)); } + SetCanceled(): void { this.settle(() => this._reject(new TaskCanceledException())); } + TrySetCanceled(): boolean { return this.trySettle(() => this._reject(new TaskCanceledException())); } + // Former spelling of SetCanceled + SetCancelled(): void { this.SetCanceled(); } get_Task(): Promise { return this.task; } } +// Awaiting a task faulted with several exceptions throws the first one on .NET +function firstException(errors: Iterable): unknown { + const all = Array.from(errors); + if (all.length === 0) { + throw ArgumentException_$ctor_Z384F8060("The exceptions collection was empty.", "exceptions"); + } + if (all.some((error) => error == null)) { + throw ArgumentException_$ctor_Z384F8060("The exceptions collection included at least one null element.", "exceptions"); + } + return all[0]; +} + export function fromResult(value: T): Promise { return Promise.resolve(value); } +// Like .NET, a failed task that nothing awaits is not an error, while awaiters still see the failure +export function markObserved(task: Promise): Promise { + task.then(undefined, () => { /* NOP */ }); + return task; +} + +// Like .NET, the function runs after the caller continues and not at all if the token is cancelled by then +export function run(f: () => T | PromiseLike, cancellationToken?: CancellationToken): Promise { + return markObserved(Promise.resolve().then(() => { + if (cancellationToken?.isCancelled) { throw new TaskCanceledException(); } + return f(); + })); +} + export function zero(): Promise { return Promise.resolve(); } // Like .NET, -1 waits until the token is cancelled export function delay(millisecondsDelay: number, cancellationToken?: CancellationToken): Promise { - return new Promise((resolve, reject) => { + return markObserved(new Promise((resolve, reject) => { if (cancellationToken?.isCancelled) { - reject(new OperationCanceledException()); + reject(new TaskCanceledException()); return; } let tokenId: number | undefined; @@ -41,10 +89,10 @@ export function delay(millisecondsDelay: number, cancellationToken?: Cancellatio if (cancellationToken != null) { tokenId = cancellationToken.addListener(() => { clearTimeout(timeoutId); - reject(new OperationCanceledException()); + reject(new TaskCanceledException()); }); } - }); + })); } // Task = Promise in JS/TS. GetAwaiter/GetResult return the Promise itself; diff --git a/tests/Js/Main/AsyncTests.fs b/tests/Js/Main/AsyncTests.fs index bcf3274a14..8bc4618dc7 100644 --- a/tests/Js/Main/AsyncTests.fs +++ b/tests/Js/Main/AsyncTests.fs @@ -1633,6 +1633,97 @@ let tests = equal false interrupted } + testCase "CancellationToken.ThrowIfCancellationRequested raises OperationCanceledException" <| fun () -> + let cts = new CancellationTokenSource() + cts.Token.ThrowIfCancellationRequested() + cts.Cancel() + let result = + try + cts.Token.ThrowIfCancellationRequested() + "returned" + with :? OperationCanceledException as e -> e.Message + equal "The operation was canceled." result + + testCase "OperationCanceledException constructors work" <| fun () -> + let describe (e: exn) = + match e with + | :? OperationCanceledException when isNull e.InnerException -> e.Message + | :? OperationCanceledException -> e.Message + " " + e.InnerException.Message + | _ -> "other" + let cts = new CancellationTokenSource() + equal "The operation was canceled." (describe (OperationCanceledException())) + equal "m" (describe (OperationCanceledException "m")) + equal "m inner" (describe (OperationCanceledException("m", Exception "inner"))) + equal "The operation was canceled." (describe (OperationCanceledException cts.Token)) + equal "m" (describe (OperationCanceledException("m", cts.Token))) + equal "m inner" (describe (OperationCanceledException("m", Exception "inner", cts.Token))) + + testCase "CancellationTokenSource.CreateLinkedTokenSource works" <| fun () -> + let parent = new CancellationTokenSource() + let linked = CancellationTokenSource.CreateLinkedTokenSource(parent.Token) + equal false linked.IsCancellationRequested + parent.Cancel() + equal true linked.IsCancellationRequested + let alreadyCancelled = CancellationTokenSource.CreateLinkedTokenSource(parent.Token) + equal true alreadyCancelled.IsCancellationRequested + let a = new CancellationTokenSource() + let b = new CancellationTokenSource() + let c = new CancellationTokenSource() + let two = CancellationTokenSource.CreateLinkedTokenSource(a.Token, b.Token) + let many = CancellationTokenSource.CreateLinkedTokenSource(a.Token, b.Token, c.Token) + let array = CancellationTokenSource.CreateLinkedTokenSource([| a.Token; c.Token |]) + c.Cancel() + equal false two.IsCancellationRequested + equal true many.IsCancellationRequested + equal true array.IsCancellationRequested + b.Cancel() + equal true two.IsCancellationRequested + equal false a.IsCancellationRequested + + testCase "CancellationTokenSource.CreateLinkedTokenSource handles default and missing tokens" <| fun () -> + let parent = new CancellationTokenSource() + let linked = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken(), parent.Token) + let alone = CancellationTokenSource.CreateLinkedTokenSource(Unchecked.defaultof) + parent.Cancel() + equal true linked.IsCancellationRequested + equal false alone.IsCancellationRequested + raisedExceptionType (fun () -> CancellationTokenSource.CreateLinkedTokenSource(Array.empty)) + |> equal "ArgumentException" + + testCase "Cancelling a linked CancellationTokenSource does not cancel its parents" <| fun () -> + let parent = new CancellationTokenSource() + let linked = CancellationTokenSource.CreateLinkedTokenSource(parent.Token) + linked.Cancel() + equal true linked.IsCancellationRequested + equal false parent.IsCancellationRequested + + testCase "Disposing a linked CancellationTokenSource unlinks it from its parents" <| fun () -> + let parent = new CancellationTokenSource() + let mutable fired = 0 + let linked = CancellationTokenSource.CreateLinkedTokenSource(parent.Token) + linked.Token.Register(fun () -> fired <- fired + 1) |> ignore + linked.Dispose() + do + use scoped = CancellationTokenSource.CreateLinkedTokenSource(parent.Token) + scoped.Token.Register(fun () -> fired <- fired + 10) |> ignore + parent.Cancel() + equal 0 fired + + testCase "CancellationTokenRegistration.Dispose and Unregister work" <| fun () -> + let cts = new CancellationTokenSource() + let mutable fired = 0 + let disposed = cts.Token.Register(fun () -> fired <- fired + 1) + let unregistered = cts.Token.Register(fun () -> fired <- fired + 10) + let kept = cts.Token.Register(fun () -> fired <- fired + 100) + disposed.Dispose() + equal true (unregistered.Unregister()) + equal false (unregistered.Unregister()) + cts.Cancel() + equal 100 fired + equal false (kept.Unregister()) + let unassigned = CancellationTokenRegistration() + unassigned.Dispose() + testCaseAsync "Async.StartAsTask works" <| fun () -> async { let t = Async.StartAsTask(async { @@ -1653,17 +1744,70 @@ let tests = }, cancellationToken = cts.Token) cts.Cancel() let mutable outcome = "pending" - // AwaitTask reports a cancelled task as an exception on .NET and as a cancellation in JS Async.StartWithContinuations( Async.AwaitTask t, (fun () -> outcome <- "completed"), - (fun _ -> outcome <- "cancelled"), + (function + | :? System.Threading.Tasks.TaskCanceledException -> outcome <- "TaskCanceledException" + | _ -> outcome <- "other error"), (fun _ -> outcome <- "cancelled")) do! Async.Sleep 300 - equal "cancelled" outcome + equal "TaskCanceledException" outcome equal false completed } + testCaseAsync "Async.AwaitTask raises TaskCanceledException for a cancelled task" <| fun () -> + async { + let cancelled = new CancellationTokenSource() + cancelled.Cancel() + let! result = Async.Catch(Async.AwaitTask(System.Threading.Tasks.Task.Delay(1000, cancelled.Token))) + match result with + | Choice2Of2 (:? System.Threading.Tasks.TaskCanceledException as e) -> equal "A task was canceled." e.Message + | _ -> failwith "Expected TaskCanceledException through the error continuation" + let cts = new CancellationTokenSource() + let t = Async.StartAsTask(Async.Sleep 1000, cancellationToken = cts.Token) + cts.Cancel() + let! caught = + async { + try + do! Async.AwaitTask t + return "completed" + with :? OperationCanceledException -> return "caught" + } + equal "caught" caught + } + + testCase "TaskCanceledException constructors work" <| fun () -> + let inner = Exception "inner" + let error: exn = System.Threading.Tasks.TaskCanceledException("task failure", inner) + match error with + | :? System.Threading.Tasks.TaskCanceledException -> + equal "task failure" error.Message + equal true (obj.ReferenceEquals(inner, error.InnerException)) + | _ -> failwith "Expected TaskCanceledException" + let error: exn = System.Threading.Tasks.TaskCanceledException() + match error with + | :? OperationCanceledException -> equal "A task was canceled." error.Message + | _ -> failwith "Expected an OperationCanceledException" + + testCaseAsync "Async.StartAsTask tasks that fail while nothing awaits them are not errors" <| fun () -> + async { + let failing = Async.StartAsTask(async { + do! Async.Sleep 5 + return failwith "unobserved" + }) + Async.StartImmediateAsTask(async { return failwith "unobserved" }) |> ignore + let cts = new CancellationTokenSource() + Async.StartAsTask(Async.Sleep 1000, cancellationToken = cts.Token) |> ignore + cts.Cancel() + do! Async.Sleep 30 + // .NET wraps the failure in an AggregateException + let! result = Async.Catch(Async.AwaitTask failing) + match result with + | Choice2Of2 e -> equal true (e.Message.Contains "unobserved") + | Choice1Of2 _ -> failwith "Expected the task to fail" + } + testCaseAsync "Async.StartImmediateAsTask works" <| fun () -> async { let t = Async.StartImmediateAsTask(async { return 42 }) diff --git a/tests/Js/Main/TaskTests.fs b/tests/Js/Main/TaskTests.fs index 63e60d4006..36120ef3fd 100644 --- a/tests/Js/Main/TaskTests.fs +++ b/tests/Js/Main/TaskTests.fs @@ -3,6 +3,7 @@ module Fable.Tests.Task open System open System.Threading.Tasks open Util.Testing +open Fable.Tests.Util type DisposableAction(f) = interface IDisposable with @@ -11,6 +12,14 @@ type DisposableAction(f) = // Bridge pattern: works on .NET (Async.AwaitTask wraps Task) and JS (awaitPromise wraps Promise) let awaitTask (t: Task<'T>) = Async.AwaitTask t +let cancellationMessage (t: Task) = + task { + try + do! t + return "completed" + with :? TaskCanceledException as e -> return e.Message + } |> Async.AwaitTask + let tests = testList "Task" [ testCaseAsync "Simple task translates without exception" <| fun () -> @@ -175,6 +184,165 @@ let tests = equal 42 result } + testCaseAsync "TaskCompletionSource.SetCanceled works" <| fun () -> + async { + let tcs = TaskCompletionSource() + tcs.SetCanceled() + let! result = awaitTask (task { + try + let! _ = tcs.Task + return "completed" + with :? OperationCanceledException -> return "cancelled" + }) + equal "cancelled" result + let! awaited = Async.Catch(awaitTask tcs.Task) + match awaited with + | Choice2Of2 (:? TaskCanceledException) -> () + | _ -> failwith "Expected TaskCanceledException through the error continuation" + } + + testCaseAsync "TaskCompletionSource.TrySet methods return whether they completed the task" <| fun () -> + async { + let tcs = TaskCompletionSource() + equal true (tcs.TrySetResult 1) + equal false (tcs.TrySetResult 2) + equal false (tcs.TrySetException(Exception "late")) + equal false (tcs.TrySetCanceled()) + let! result = awaitTask tcs.Task + equal 1 result + let failed = TaskCompletionSource() + equal true (failed.TrySetException(Exception "bad")) + equal false (failed.TrySetResult 3) + let cancelled = TaskCompletionSource() + equal true (cancelled.TrySetCanceled()) + equal false (cancelled.TrySetCanceled()) + } + + testCase "TaskCompletionSource raises InvalidOperationException when completed twice" <| fun () -> + let message = "An attempt was made to transition a task to a final state when it had already completed." + let tcs = TaskCompletionSource() + tcs.SetResult 1 + raisedExceptionType (fun () -> tcs.SetResult 2) |> equal "InvalidOperationException" + throwsError message (fun () -> tcs.SetException(Exception "x")) + throwsError message (fun () -> tcs.SetCanceled()) + let tried = TaskCompletionSource() + tried.TrySetCanceled() |> ignore + throwsError message (fun () -> tried.SetResult 1) + + testCaseAsync "TaskCompletionSource.SetException with a sequence faults with the first exception" <| fun () -> + async { + let tcs = TaskCompletionSource() + tcs.SetException([ Exception "a"; Exception "b" ]) + equal false (tcs.TrySetException([ Exception "c" ])) + let! result = awaitTask (task { + try + let! _ = tcs.Task + return "completed" + with e -> return e.Message + }) + equal "a" result + let empty = TaskCompletionSource() + raisedExceptionType (fun () -> empty.SetException(List.empty)) |> equal "ArgumentException" + raisedExceptionType (fun () -> empty.TrySetException([ Exception "a"; null ])) |> equal "ArgumentException" + equal true (empty.TrySetResult 1) + } + + testCaseAsync "Non-generic TaskCompletionSource works" <| fun () -> + async { + let tcs = TaskCompletionSource() + equal true (tcs.TrySetResult()) + equal false (tcs.TrySetResult()) + do! Async.AwaitTask tcs.Task + let cancelled = TaskCompletionSource() + cancelled.SetCanceled() + let! result = awaitTask (task { + try + do! cancelled.Task + return "completed" + with :? OperationCanceledException -> return "cancelled" + }) + equal "cancelled" result + } + + testCaseAsync "Task.Run works" <| fun () -> + async { + let! a = awaitTask (Task.Run(fun () -> 41 + 1)) + equal 42 a + let! b = awaitTask (Task.Run(Func>(fun () -> task { return 7 }))) + equal 7 b + let mutable ran = 0 + do! Async.AwaitTask(Task.Run(fun () -> ran <- ran + 1)) + do! Async.AwaitTask(Task.Run(fun () -> (task { ran <- ran + 1 } : Task))) + equal 2 ran + let cts = new System.Threading.CancellationTokenSource() + let! c = awaitTask (Task.Run((fun () -> 5), cts.Token)) + equal 5 c + } + + testCaseAsync "Task.Run with a cancelled token gives a cancelled task" <| fun () -> + async { + let cts = new System.Threading.CancellationTokenSource() + cts.Cancel() + let mutable ran = false + let t = Task.Run((fun () -> ran <- true; 1), cts.Token) + let! result = awaitTask (task { + try + let! _ = t + return "completed" + with :? OperationCanceledException -> return "cancelled" + }) + equal "cancelled" result + equal false ran + } + + testCaseAsync "Task.Run faults with the exception of the function" <| fun () -> + async { + let t = Task.Run(fun () -> failwith "boom"; 1) + let! result = awaitTask (task { + try + let! _ = t + return "completed" + with e -> return e.Message + }) + equal "boom" result + } + + testCaseAsync "Awaiting a cancelled task in a task raises TaskCanceledException" <| fun () -> + async { + let tcs = TaskCompletionSource() + tcs.SetCanceled() + let cancelled = new System.Threading.CancellationTokenSource() + cancelled.Cancel() + let cts = new System.Threading.CancellationTokenSource() + let started = Async.StartAsTask(Async.Sleep 1000, cancellationToken = cts.Token) + cts.Cancel() + let! a = cancellationMessage tcs.Task + let! b = cancellationMessage (Task.Run((fun () -> ()), cancelled.Token)) + let! c = cancellationMessage (Task.Delay(1000, cancelled.Token)) + let! d = cancellationMessage started + equal [ "A task was canceled."; "A task was canceled."; "A task was canceled."; "A task was canceled." ] [ a; b; c; d ] + } + + testCaseAsync "Faulted and cancelled tasks that nothing awaits are not errors" <| fun () -> + async { + let failed = TaskCompletionSource() + failed.SetException(Exception "unobserved") + let cancelled = TaskCompletionSource() + cancelled.SetCanceled() + let cts = new System.Threading.CancellationTokenSource() + Task.Delay(1000, cts.Token) |> ignore + cts.Cancel() + Task.Run(fun () -> failwith "unobserved"; 1) |> ignore + do! Async.Sleep 20 + let! result = awaitTask (task { + try + let! _ = failed.Task + return "completed" + with e -> return e.Message + }) + equal "unobserved" result + } + testCaseAsync "Task.Delay works" <| fun () -> async { let mutable steps = []