107 lines
3.9 KiB
C#
107 lines
3.9 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
|
|
|
|
/// <summary>
|
|
/// Deterministic <see cref="TimeProvider"/>: time moves only on <see cref="Advance"/>, 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.)
|
|
/// </summary>
|
|
internal sealed class ManualTimeProvider : TimeProvider
|
|
{
|
|
private readonly object _lock = new();
|
|
private readonly List<ManualTimer> _timers = [];
|
|
private DateTimeOffset _now;
|
|
|
|
public ManualTimeProvider() => Start = _now = new DateTimeOffset(2026, 10, 7, 0, 0, 0, TimeSpan.Zero);
|
|
|
|
public DateTimeOffset Start { get; }
|
|
|
|
/// <summary>Steps the wall clock (<see cref="GetUtcNow"/>) without moving monotonic time
|
|
/// (<see cref="GetTimestamp"/>) or any timer, as an NTP or operator clock change does.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Armed timers (not disposed, with a due time).</summary>
|
|
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); }
|
|
|
|
/// <summary>Waits (in real time) until some code under test has armed a timer due at <paramref name="at"/>,
|
|
/// i.e. is genuinely parked on the fake clock rather than merely not yet scheduled.</summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|