From bf1e941e11b149f93bf6a32f3cdf097f1a7140aa Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Tue, 29 Mar 2022 13:08:45 +0100 Subject: [PATCH] Capture ExceptionDispatchInfo in ToAsyncEnumerable --- .../System/Linq/AsyncEnumerableTests.cs | 14 ++---- .../Linq/Operators/ToAsyncEnumerable.cs | 43 +++++++++++++++++++ .../Operators/ToAsyncEnumerable.Observable.cs | 7 +-- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/AsyncEnumerableTests.cs b/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/AsyncEnumerableTests.cs index 01dc488ef..bde04affa 100644 --- a/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/AsyncEnumerableTests.cs +++ b/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/AsyncEnumerableTests.cs @@ -15,22 +15,16 @@ public class AsyncEnumerableTests { protected static readonly IAsyncEnumerable Return42 = new[] { 42 }.ToAsyncEnumerable(); - protected async Task AssertThrowsAsync(Task t) + protected async Task AssertThrowsAsync(Task t) where TException : Exception { - await Assert.ThrowsAsync(() => t); + return await Assert.ThrowsAsync(() => t); } protected async Task AssertThrowsAsync(Task t, Exception e) { - try - { - await t; - } - catch (Exception ex) - { - Assert.Same(e, ex); - } + var ex = await Assert.ThrowsAnyAsync(() => t); + Assert.Same(e, ex); } protected Task AssertThrowsAsync(ValueTask t, Exception e) diff --git a/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/Operators/ToAsyncEnumerable.cs b/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/Operators/ToAsyncEnumerable.cs index d9cf28ef9..e9927344f 100644 --- a/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/Operators/ToAsyncEnumerable.cs +++ b/Ix.NET/Source/System.Linq.Async.Tests/System/Linq/Operators/ToAsyncEnumerable.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -283,6 +284,48 @@ public async Task ToAsyncEnumerable_Observable_Throw() await AssertThrowsAsync(e.MoveNextAsync(), ex); } + [Fact] + public async Task ToAsyncEnumerable_Observable_Throw_ActiveException() + { + // No inlining as we're asserting that this function must be mentioned in the exception stacktrace + [MethodImpl(MethodImplOptions.NoInlining)] + void ThrowsException() + { + throw new Exception("Bang!"); + } + + var subscribed = false; + + var xs = new MyObservable(obs => + { + subscribed = true; + + try + { + ThrowsException(); + } + catch (Exception ex) + { + // `ex` is active at this point as it has been thrown. + // Therefore the stack trace should be captured by ToAsyncEnumerable. + // See 'Remarks' at https://docs.microsoft.com/en-us/dotnet/api/system.runtime.exceptionservices.exceptiondispatchinfo.capture + obs.OnError(ex); + } + + return new MyDisposable(() => { }); + }).ToAsyncEnumerable(); + + Assert.False(subscribed); + + var e = xs.GetAsyncEnumerator(); + + // NB: Breaking change to align with lazy nature of async iterators. + // Assert.True(subscribed); + + var actual = await AssertThrowsAsync(e.MoveNextAsync().AsTask()); + Assert.Contains(nameof(ThrowsException), actual.StackTrace); + } + [Fact] public async Task ToAsyncEnumerable_Observable_Dispose() { diff --git a/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/ToAsyncEnumerable.Observable.cs b/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/ToAsyncEnumerable.Observable.cs index 6fa01ebba..e02437fc3 100644 --- a/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/ToAsyncEnumerable.Observable.cs +++ b/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/ToAsyncEnumerable.Observable.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; @@ -31,7 +32,7 @@ private sealed class ObservableAsyncEnumerable : AsyncIterator private readonly IObservable _source; private ConcurrentQueue? _values = new ConcurrentQueue(); - private Exception? _error; + private ExceptionDispatchInfo? _error; private bool _completed; private TaskCompletionSource? _signal; private IDisposable? _subscription; @@ -95,7 +96,7 @@ protected override async ValueTask MoveNextCore() if (error != null) { - throw error; + error.Throw(); } return false; @@ -120,7 +121,7 @@ public void OnCompleted() public void OnError(Exception error) { - _error = error; + _error = ExceptionDispatchInfo.Capture(error); Volatile.Write(ref _completed, true); DisposeSubscription();