using System.Diagnostics; using Shouldly; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests; /// /// Shared presence waits for the Galaxy driver tests (#856, #858). /// /// /// /// Every wait here is a PRESENCE wait: it returns the instant the awaited thing happens, so a /// generous ceiling costs nothing on a passing run. Under heavy parallel load (#858: 14 concurrent /// test processes + 14 CPU burners, or a starved thread pool) a background loop started with /// Task.Run can take seconds just to be scheduled — the old 1–3 s ceilings failed there while /// the code under test was correct. /// /// /// On timeout the failure reports the elapsed time and, when given, a describe snapshot of the /// state actually observed, so a flake says which link of the chain never happened. /// /// /// Never assert ABSENCE with a short sleep: follow the event that must not fire with one that MUST /// arrive (a positive control), wait for that, then assert the absence. /// /// internal static class TestWait { /// Ceiling for a presence wait. Only a genuinely broken run ever waits this long. internal static readonly TimeSpan Presence = TimeSpan.FromSeconds(30); /// Polls until it holds or elapses. /// The condition to wait for. /// What the condition means; leads the failure message. /// Optional snapshot of the observed state, appended on timeout. /// Optional ceiling overriding . internal static async Task UntilAsync( Func condition, string because, Func? describe = null, TimeSpan? timeout = null) { var limit = timeout ?? Presence; var sw = Stopwatch.StartNew(); while (!condition()) { if (sw.Elapsed > limit) { throw new ShouldAssertException(Message(because, sw, describe)); } await Task.Delay(10).ConfigureAwait(false); } } /// Waits for to complete, or fails after . /// The task that must complete (e.g. a TaskCompletionSource.Task). /// What completion means; leads the failure message. /// Optional snapshot of the observed state, appended on timeout. /// A task that completes when does (its fault/cancellation propagates). internal static async Task ForAsync(Task task, string because, Func? describe = null) { var sw = Stopwatch.StartNew(); var winner = await Task.WhenAny(task, Task.Delay(Presence)).ConfigureAwait(false); if (winner != task) { throw new ShouldAssertException(Message(because, sw, describe)); } await task.ConfigureAwait(false); } /// Waits for and returns its result, or fails after . /// The result type. /// The task that must complete. /// What completion means; leads the failure message. /// Optional snapshot of the observed state, appended on timeout. /// The task's result. internal static async Task ForAsync(Task task, string because, Func? describe = null) { await ForAsync((Task)task, because, describe).ConfigureAwait(false); return await task.ConfigureAwait(false); } /// /// Drives the production under the presence /// ceiling; a timeout fails with the supervisor's observed state rather than a bare /// . /// /// The supervisor that must return to Healthy. /// What recovery means here; leads the failure message. internal static async Task HealthyAsync(ReconnectSupervisor supervisor, string because) { var sw = Stopwatch.StartNew(); using var cts = new CancellationTokenSource(Presence); try { await supervisor.WaitForHealthyAsync(cts.Token).ConfigureAwait(false); } catch (OperationCanceledException) when (cts.IsCancellationRequested) { throw new ShouldAssertException(Message(because, sw, () => $"state={supervisor.CurrentState} lastError={supervisor.LastError ?? ""}")); } } private static string Message(string because, Stopwatch sw, Func? describe) { var observed = describe is null ? "" : $" Observed: {describe()}"; return $"{because} (not met after {sw.ElapsedMilliseconds} ms).{observed}"; } }