Skip to content

Commit

Permalink
Merge pull request #3 from AshleighAdams/exception-handler
Browse files Browse the repository at this point in the history
Added support to handle exceptions
  • Loading branch information
AshleighAdams authored May 8, 2022
2 parents ee685ed + e49376b commit f06d9b9
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 3 deletions.
7 changes: 7 additions & 0 deletions src/CoroutineScheduler/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@ CoroutineScheduler.Scheduler
CoroutineScheduler.Scheduler.Resume() -> void
CoroutineScheduler.Scheduler.Scheduler() -> void
CoroutineScheduler.Scheduler.SpawnTask(System.Func<System.Threading.Tasks.Task!>! func) -> System.Threading.Tasks.Task!
CoroutineScheduler.Scheduler.UnhandledException -> System.EventHandler<CoroutineScheduler.UnhandledExceptionEventArgs!>?
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
46 changes: 43 additions & 3 deletions src/CoroutineScheduler/Scheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,40 @@
using System.Diagnostics;
#endif

using System.Diagnostics;
using System.Runtime.CompilerServices;

[assembly: CLSCompliant(true)]

namespace CoroutineScheduler;

/// <summary>
/// Represents the data and response for when an unhandled exception occurs.
/// </summary>
public class UnhandledExceptionEventArgs : EventArgs
{
/// <summary>
/// Create an event from the exception <paramref name="ex"/>.
/// </summary>
internal UnhandledExceptionEventArgs(Exception ex)
{
Exception = ex;
}

/// <summary>
/// The exception that was caught.
/// </summary>
public Exception Exception { get; }
/// <summary>
/// Whether or not the <see cref="Task"/> returned by <see cref="Scheduler.SpawnTask(Func{Task})"/> completes the promise with an exception.
/// </summary>
public bool RethrowAsync { get; set; } = true;
/// <summary>
/// Whether or not the exception should be rethrown, resulting in an instant runtime crash.
/// </summary>
public bool Rethrow { get; set; } = true;
}

/// <summary>
/// A manually driven scheduler suitible for tight update loops such as game frames. <br />
/// Will use a <see cref="SynchronizationContext"/> to marshal continuations back onto the expected execution environment.
Expand Down Expand Up @@ -62,7 +90,7 @@ public YieldTask Yield()
#endif
public Task SpawnTask(Func<Task> func)
{
TaskCompletionSource<object?> proxy = new();
var proxy = new TaskCompletionSource<object?>();
SpawnTaskInternal();
return proxy.Task;

Expand All @@ -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;
}
}

}

/// <summary>
/// An event fired when an unhandled exception occurs within a spawned task.
/// </summary>
public event EventHandler<UnhandledExceptionEventArgs>? UnhandledException;

#if !DEBUG_ASYNC
[DebuggerNonUserCode]
#endif
Expand Down
96 changes: 96 additions & 0 deletions tests/UnitTests/SchedulerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestException>();
};

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<TestException>();
};

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<TestException>();
};

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<TestException>(() => scheduler.Resume());

threw.Should().BeFalse();
}
}

0 comments on commit f06d9b9

Please sign in to comment.