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