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