using System.Collections.Concurrent; using System.Runtime.CompilerServices; namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes; /// /// Collects s raised after some code abandons work, with a /// positive control (Gitea #583): after the abandon, a task is deliberately faulted and dropped, and garbage is /// collected until THAT task's exception has been reported. Only then is the collection returned — so "nothing /// of ours was reported" means the finalizer pass that would have reported it really ran, not merely that a /// fixed number of GC cycles went by before the runtime got round to it. /// internal static class UnobservedTaskProbe { public static async Task> CollectAsync(Func abandon, TimeSpan timeout) { var marker = $"unobserved-task-probe-control-{Guid.NewGuid():N}"; var seen = new ConcurrentQueue(); var controlReported = 0; EventHandler handler = (_, e) => { foreach (var ex in e.Exception.Flatten().InnerExceptions) { if (ex.Message == marker) Volatile.Write(ref controlReported, 1); else seen.Enqueue(ex); } }; TaskScheduler.UnobservedTaskException += handler; try { await abandon(); AbandonControl(marker); var giveUp = DateTime.UtcNow + timeout; while (Volatile.Read(ref controlReported) == 0) { if (DateTime.UtcNow > giveUp) throw new TimeoutException("The positive control's unobserved exception was never reported."); GC.Collect(); GC.WaitForPendingFinalizers(); await Task.Delay(10); } // One more full pass, so anything that became garbage in the same cycle as the control is reported too. GC.Collect(); GC.WaitForPendingFinalizers(); } finally { TaskScheduler.UnobservedTaskException -= handler; } return [.. seen]; } [MethodImpl(MethodImplOptions.NoInlining)] private static void AbandonControl(string marker) => _ = Task.FromException(new InvalidOperationException(marker)); }