using System;
using System.Reflection;
namespace Server.Tests;
///
/// Gives a test its own simulated clock and an empty timer wheel, and puts the process-wide ones
/// back on dispose. Core's tick, Core.Now and the wheel are statics shared by every test in the
/// host, so a test that resets them without restoring hands its clock to whatever runs next.
///
///
/// Timers started inside the scope are stopped on dispose; timers that were scheduled before it
/// resume on the restored wheel.
///
internal sealed class TimerWheelScope : IDisposable
{
private const BindingFlags PrivateStatic = BindingFlags.NonPublic | BindingFlags.Static;
private static readonly FieldInfo _ringsField = GetTimerField("_rings");
private static readonly FieldInfo _ringIndexesField = GetTimerField("_ringIndexes");
private static readonly FieldInfo _lastTickTurnedField = GetTimerField("_lastTickTurned");
private readonly long _savedTick;
private readonly DateTime _savedNow;
private readonly Timer[][] _savedRings;
private readonly int[] _savedRingIndexes;
private readonly long _savedLastTickTurned;
private bool _disposed;
public TimerWheelScope(long startTick = 0)
{
_savedTick = Core._tickCount;
_savedNow = Core._now;
_savedRings = (Timer[][])Rings.Clone();
_savedRingIndexes = (int[])RingIndexes.Clone();
_savedLastTickTurned = (long)_lastTickTurnedField.GetValue(null)!;
Core._tickCount = startTick;
Timer.Init(startTick);
}
private static Timer[][] Rings => (Timer[][])_ringsField.GetValue(null)!;
private static int[] RingIndexes => (int[])_ringIndexesField.GetValue(null)!;
/// The wheel's tick rate: one turn per call keeps it in lockstep with Core.TickCount.
public static int Step => Timer.TickRate;
/// Advances the clock by , turning the wheel every step.
public static void RunFor(long ms)
{
var deadline = Core._tickCount + ms;
while (Core._tickCount - deadline < 0)
{
Core._tickCount += Step;
Timer.Slice(Core._tickCount);
}
}
///
/// Advances the clock step by step until holds or
/// has passed. Returns the condition's final value.
///
public static bool RunUntil(Func condition, long maxMs)
{
var deadline = Core._tickCount + maxMs;
while (Core._tickCount - deadline < 0)
{
if (condition())
{
return true;
}
Core._tickCount += Step;
Timer.Slice(Core._tickCount);
}
return condition();
}
/// Jumps the tick and Core.Now forward together, then fires everything that came due.
public static void Advance(TimeSpan time)
{
Core._now += time;
Core._tickCount += (long)time.TotalMilliseconds;
Timer.Slice(Core._tickCount);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
StopAll(Rings);
var rings = Rings;
for (var i = 0; i < rings.Length; i++)
{
rings[i] = _savedRings[i];
}
Array.Copy(_savedRingIndexes, RingIndexes, _savedRingIndexes.Length);
_lastTickTurnedField.SetValue(null, _savedLastTickTurned);
Core._tickCount = _savedTick;
Core._now = _savedNow;
}
// Stop() unlinks the slot's head and promotes the next timer, so draining heads empties a slot.
private static void StopAll(Timer[][] rings)
{
for (var i = 0; i < rings.Length; i++)
{
var ring = rings[i];
for (var slot = 0; slot < ring.Length; slot++)
{
Timer head;
while ((head = ring[slot]) != null)
{
head.Stop();
if (ring[slot] == head)
{
// A head that was already stopped never unlinks itself.
ring[slot] = null;
}
}
}
}
}
private static FieldInfo GetTimerField(string name) =>
typeof(Timer).GetField(name, PrivateStatic)
?? throw new InvalidOperationException($"Timer.{name} not found via reflection; the timer wheel was refactored.");
}