// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Threading.Tasks; namespace CommunityToolkit.Mvvm.UnitTests.Helpers; /// /// A helper class to validate scenarios related to . /// internal static class TaskSchedulerTestHelper { /// /// A custom for callbacks to . /// /// An instance that throws a test exception to track. /// An that signals whenever the test has completed. public delegate void TestCallback(Action throwAction, Action completeAction); /// /// Checks whether a given test exception is correctly bubbled up to . /// /// The instance to use to run the test. /// Whether or not the test exception was correctly bubbled up to . public static async Task IsExceptionBubbledUpToUnobservedTaskExceptionAsync(TestCallback callback) { TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); string guid = Guid.NewGuid().ToString(); bool exceptionFound = false; void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) { e.SetObserved(); foreach (Exception exception in e.Exception!.InnerExceptions) { if (exception is TestException testException && testException.Message == guid) { exceptionFound = true; return; } } } EventHandler handler = TaskScheduler_UnobservedTaskException; TaskScheduler.UnobservedTaskException += handler; try { // Enqueue a continuation that will throw and ignore the returned task. This has // to be a separate method to ensure the returned task isn't kept alive for longer. callback( () => throw new TestException(guid), () => tcs.SetResult(null)); // Await for the continuation to actually run _ = await tcs.Task; // Wait for some additional time to ensure the exception is propagated. This is a bit counterintuitive, but the delay is // not actually for the event to be raised, but to ensure the task that is throwing has had time to be scheduled and fail. // The event is raised only when the exception wrapper inside that task is collected and its finalizer has run (that's where // the logic to raise the event is executed), which is why we're then calling GC.Collect() and GC.WaitForPendingFinalizers(). // That is, we can't use a task completion source from that event, because that event is only guaranteed to actually be raised // when the finalizer for that task run, which is why we're calling the GC after the delay here. await Task.Delay(200); GC.Collect(); GC.WaitForPendingFinalizers(); } finally { TaskScheduler.UnobservedTaskException -= handler; } return exceptionFound; } /// /// A custom exception to support . /// private sealed class TestException : Exception { /// /// Creates a new instance with the specified parameters. /// /// The exception message. public TestException(string message) : base(message) { } } }