namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes; /// /// Deterministic : time moves only on , which fires every /// timer falling due on the way, in due order, synchronously on the calling thread. /// (Microsoft.Extensions.TimeProvider.Testing is not pinned in this repo and no package may be added.) /// internal sealed class ManualTimeProvider : TimeProvider { private readonly object _lock = new(); private readonly List _timers = []; private DateTimeOffset _now; public ManualTimeProvider() => Start = _now = new DateTimeOffset(2026, 10, 7, 0, 0, 0, TimeSpan.Zero); public DateTimeOffset Start { get; } /// Steps the wall clock () without moving monotonic time /// () or any timer, as an NTP or operator clock change does. public TimeSpan WallClockOffset { get; set; } public override DateTimeOffset GetUtcNow() { lock (_lock) return _now + WallClockOffset; } public override long GetTimestamp() { lock (_lock) return _now.UtcTicks; } public override long TimestampFrequency => TimeSpan.TicksPerSecond; public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) { var t = new ManualTimer(this, callback, state); lock (_lock) _timers.Add(t); t.Change(dueTime, period); return t; } /// Armed timers (not disposed, with a due time). public int ArmedTimers { get { lock (_lock) return _timers.Count(t => t.Due is not null); } } public bool HasTimerDueAt(DateTimeOffset at) { lock (_lock) return _timers.Any(t => t.Due == at); } /// Waits (in real time) until some code under test has armed a timer due at , /// i.e. is genuinely parked on the fake clock rather than merely not yet scheduled. public async Task WaitForTimerDueAtAsync(DateTimeOffset at, TimeSpan timeout) { var giveUp = DateTime.UtcNow + timeout; while (!HasTimerDueAt(at)) { if (DateTime.UtcNow > giveUp) throw new TimeoutException($"No timer armed for {at:O} (now {GetUtcNow():O})."); await Task.Delay(5); } } public void Advance(TimeSpan by) { DateTimeOffset target; lock (_lock) target = _now + by; while (true) { ManualTimer? next; lock (_lock) { next = _timers.Where(t => t.Due <= target).MinBy(t => t.Due); if (next is null) { _now = target; return; } _now = next.Due!.Value; next.Due = next.Period is { } p ? _now + p : null; } next.Fire(); } } private sealed class ManualTimer(ManualTimeProvider owner, TimerCallback callback, object? state) : ITimer { public DateTimeOffset? Due; // guarded by owner._lock public TimeSpan? Period; // guarded by owner._lock private bool _disposed; public void Fire() => callback(state); public bool Change(TimeSpan dueTime, TimeSpan period) { lock (owner._lock) { if (_disposed) return false; Due = dueTime == Timeout.InfiniteTimeSpan ? null : owner._now + dueTime; Period = period == Timeout.InfiniteTimeSpan || period == TimeSpan.Zero ? null : period; return true; } } public void Dispose() { lock (owner._lock) { _disposed = true; Due = null; owner._timers.Remove(this); } } public ValueTask DisposeAsync() { Dispose(); return ValueTask.CompletedTask; } } }