// 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.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit; namespace Tests.System.Reactive { /// /// Verifies behavior around unobserved exceptions from tasks. /// /// /// Testing whether unhandled exceptions emerge from is not /// entirely straightforward. A few tests need to do this because we have some historical behaviour described in /// https://github.com/dotnet/reactive/issues/1256 that needs to be preserved for backwards compatibility, along /// with some new functionality enabling optional different behavior regarding unobserved exceptions. This provides /// common mechanism to enable such testing. /// internal class TaskErrorObservation : IDisposable { private ManualResetEventSlim _exceptionReportedAsUnobserved; private WeakReference _taskWeakReference; public TaskErrorObservation() { _exceptionReportedAsUnobserved = new(false); TaskScheduler.UnobservedTaskException += HandleUnobservedException; } public Exception Exception { get; } = new(); public void Dispose() { if (_exceptionReportedAsUnobserved is not null) { _exceptionReportedAsUnobserved.Dispose(); _exceptionReportedAsUnobserved = null; TaskScheduler.UnobservedTaskException -= HandleUnobservedException; } } [MethodImpl(MethodImplOptions.NoInlining)] public IDisposable SuscribeWithoutKeepingSourceReachable( Func, Exception, IDisposable> subscribe) { // We provide nested function because the temporary storage location that // holds the value returned by a call to, say, Observable.StartAsync can end up keeping it reachable // for GC purposes, which in turn keeps the task reachable. That stops the // finalization-driven unobserved exception detection from working. // By calling Subscribe in a method whose stack frame is then immediately torn // down, we ensure that we don't hang onto anything other than the IDisposable // it returns. return subscribe( t => { _taskWeakReference = new(t); return t; }, Exception); } public IDisposable SuscribeWithoutKeepingSourceReachable( Func, Task>, Exception, IDisposable> subscribe) { return SuscribeWithoutKeepingSourceReachable( (Func setTask, Exception ex) => subscribe( t => { setTask(t); return t; }, ex)); } public void AssertExceptionReportedAsUnobserved() { var start = Environment.TickCount; var firstIteration = true; while (!_exceptionReportedAsUnobserved.Wait(TimeSpan.FromSeconds(firstIteration ? 0 : 0.001)) && ((Environment.TickCount - start) < 5000)) { firstIteration = false; GC.Collect(); GC.WaitForPendingFinalizers(); } Assert.True(_exceptionReportedAsUnobserved.Wait(TimeSpan.FromSeconds(0.01))); } /// /// Waits for the task to become unreachable, and then verifies that this did not result in /// reporting the failure. /// /// public void AssertExceptionNotReportedAsUnobserved() { if (_taskWeakReference is null) { throw new InvalidOperationException("Test did not supply task to " + nameof(TaskErrorObservation)); } var start = Environment.TickCount; var firstIteration = true; do { // We try to get away without sleeping, to enable tests to run as quickly as // possible, but if the object remains reachable after the initial attempt to // force a GC and then immediately run finalizers, there's probably some deferred // work waiting to happen somewhere, so we are better off backing off and giving // that a chance to run. if (firstIteration) { firstIteration = false; } else { Thread.Sleep(1); } GC.Collect(); GC.WaitForPendingFinalizers(); } while (IsTaskStillReachable() && ((Environment.TickCount - start) < 5000)); // The task is now unreachable, but it's possible that this happened in between our // last call to GC.WaitForPendingFinalizers and our test for reachability, in which // case it might still be awaiting finalization, so we need one more of these to ensure // it gets flushed through: GC.WaitForPendingFinalizers(); Assert.False(_exceptionReportedAsUnobserved.IsSet); } // This needs to be done in a separate method to ensure that when the weak reference returns a task, we // immediately destroy the stack frame containing the temporary variable into which it was returned, // to avoid keeping the task reachable by accident. [MethodImpl(MethodImplOptions.NoInlining)] private bool IsTaskStillReachable() { return _taskWeakReference.TryGetTarget(out _); } private void HandleUnobservedException(object sender, UnobservedTaskExceptionEventArgs e) { if (e.Exception.InnerException == Exception) { e.SetObserved(); _exceptionReportedAsUnobserved.Set(); } } } }