From e49376be40f186ca48933bb0fe156ca6f9cbd239 Mon Sep 17 00:00:00 2001 From: Ashleigh Adams Date: Fri, 6 May 2022 00:25:58 +0100 Subject: [PATCH] Added support to handle exceptions --- .../PublicAPI.Unshipped.txt | 7 ++ src/CoroutineScheduler/Scheduler.cs | 46 ++++++++- tests/UnitTests/SchedulerTests.cs | 96 +++++++++++++++++++ 3 files changed, 146 insertions(+), 3 deletions(-) diff --git a/src/CoroutineScheduler/PublicAPI.Unshipped.txt b/src/CoroutineScheduler/PublicAPI.Unshipped.txt index 67629a8..9124d89 100644 --- a/src/CoroutineScheduler/PublicAPI.Unshipped.txt +++ b/src/CoroutineScheduler/PublicAPI.Unshipped.txt @@ -7,4 +7,11 @@ CoroutineScheduler.Scheduler CoroutineScheduler.Scheduler.Resume() -> void CoroutineScheduler.Scheduler.Scheduler() -> void CoroutineScheduler.Scheduler.SpawnTask(System.Func! func) -> System.Threading.Tasks.Task! +CoroutineScheduler.Scheduler.UnhandledException -> System.EventHandler? CoroutineScheduler.Scheduler.Yield() -> CoroutineScheduler.YieldTask +CoroutineScheduler.UnhandledExceptionEventArgs +CoroutineScheduler.UnhandledExceptionEventArgs.Exception.get -> System.Exception! +CoroutineScheduler.UnhandledExceptionEventArgs.Rethrow.get -> bool +CoroutineScheduler.UnhandledExceptionEventArgs.Rethrow.set -> void +CoroutineScheduler.UnhandledExceptionEventArgs.RethrowAsync.get -> bool +CoroutineScheduler.UnhandledExceptionEventArgs.RethrowAsync.set -> void diff --git a/src/CoroutineScheduler/Scheduler.cs b/src/CoroutineScheduler/Scheduler.cs index 2c194fc..2467c89 100644 --- a/src/CoroutineScheduler/Scheduler.cs +++ b/src/CoroutineScheduler/Scheduler.cs @@ -6,12 +6,40 @@ using System.Diagnostics; #endif +using System.Diagnostics; using System.Runtime.CompilerServices; [assembly: CLSCompliant(true)] namespace CoroutineScheduler; +/// +/// Represents the data and response for when an unhandled exception occurs. +/// +public class UnhandledExceptionEventArgs : EventArgs +{ + /// + /// Create an event from the exception . + /// + internal UnhandledExceptionEventArgs(Exception ex) + { + Exception = ex; + } + + /// + /// The exception that was caught. + /// + public Exception Exception { get; } + /// + /// Whether or not the returned by completes the promise with an exception. + /// + public bool RethrowAsync { get; set; } = true; + /// + /// Whether or not the exception should be rethrown, resulting in an instant runtime crash. + /// + public bool Rethrow { get; set; } = true; +} + /// /// A manually driven scheduler suitible for tight update loops such as game frames.
/// Will use a to marshal continuations back onto the expected execution environment. @@ -62,7 +90,7 @@ public YieldTask Yield() #endif public Task SpawnTask(Func func) { - TaskCompletionSource proxy = new(); + var proxy = new TaskCompletionSource(); SpawnTaskInternal(); return proxy.Task; @@ -79,13 +107,25 @@ async void SpawnTaskInternal() } catch (Exception ex) { - proxy.SetException(ex); - throw; + var e = new UnhandledExceptionEventArgs(ex); + UnhandledException?.Invoke(this, e); + + if (e.RethrowAsync) + proxy.SetException(ex); + else + proxy.SetResult(null); + if (e.Rethrow) + throw; } } } + /// + /// An event fired when an unhandled exception occurs within a spawned task. + /// + public event EventHandler? UnhandledException; + #if !DEBUG_ASYNC [DebuggerNonUserCode] #endif diff --git a/tests/UnitTests/SchedulerTests.cs b/tests/UnitTests/SchedulerTests.cs index 074a805..c2733a2 100644 --- a/tests/UnitTests/SchedulerTests.cs +++ b/tests/UnitTests/SchedulerTests.cs @@ -162,4 +162,100 @@ async Task taskFunc() } done.Should().BeTrue(); } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1064:Exceptions should be public", Justification = "For test only")] + internal class TestException : Exception + { + } + + [Fact] + public void InstantAsyncExceptionsFireEventHandler() + { +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + static async Task taskFunc() => throw new TestException(); +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + var scheduler = new Scheduler(); + + var threw = false; + scheduler.UnhandledException += (sender, e) => + { + threw.Should().BeFalse(); + threw = true; + e.Rethrow = e.RethrowAsync = false; + e.Exception.Should().BeOfType(); + }; + + scheduler.SpawnTask(taskFunc); + + threw.Should().BeTrue(); + } + + [Fact] + public void InstantSyncExceptionsFireEventHandler() + { + static Task taskFunc() => throw new TestException(); + + var scheduler = new Scheduler(); + + var threw = false; + scheduler.UnhandledException += (sender, e) => + { + threw.Should().BeFalse(); + threw = true; + e.Rethrow = e.RethrowAsync = false; + e.Exception.Should().BeOfType(); + }; + + scheduler.SpawnTask(taskFunc); + + threw.Should().BeTrue(); + } + + [Fact] + public void DeferredAsyncExceptionsFireEventHandler() + { + var scheduler = new Scheduler(); + + async Task taskFunc() + { + await scheduler.Yield(); + throw new TestException(); + } + + var threw = false; + scheduler.UnhandledException += (sender, e) => + { + threw.Should().BeFalse(); + threw = true; + e.Rethrow = e.RethrowAsync = false; + e.Exception.Should().BeOfType(); + }; + + scheduler.SpawnTask(taskFunc); + + threw.Should().BeFalse(); + + scheduler.Resume(); + + threw.Should().BeTrue(); + } + + [Fact] + public void UnflowedExceptionsThrowOnResume() + { + var scheduler = new Scheduler(); + + + var threw = false; + scheduler.UnhandledException += (_, _) => threw = true; + + scheduler.Yield().GetAwaiter().UnsafeOnCompleted(() => throw new TestException()); + + threw.Should().BeFalse(); + + Assert.Throws(() => scheduler.Resume()); + + threw.Should().BeFalse(); + } }