ModernUO/Projects/Server.Tests/Tests/Timer/TimerTests.cs
Kamron Batman 47f972ec9d
fix: ClearAllTimers stops every timer in a slot, not just the first (#2723)
## Summary
`Timer.ClearAllTimers` walked each wheel slot with a `do/while` that read `node._nextTimer` but never advanced `node`. `Stop()` unlinks the head from its slot, so the second pass read a null successor from the same, now-detached node and exited. Only the first timer in every slot was stopped; the rest kept running.

The loop now reads the successor, stops the node, and advances.

Nothing in the repo calls `ClearAllTimers` today, so no running server is affected. It is public API for shards and tests.

## Tests
- `TimerTests.ClearAllTimers_StopsEveryTimerInASlot` puts three timers in the same slot, clears them, asserts none is `Running`, and slices past their due time. It fails without the fix.
- Server.Tests: all pass.
2026-10-18 20:10:19 -07:00

133 lines
3.4 KiB
C#

using System;
using Xunit;
namespace Server.Tests;
[Collection("Sequential Server Tests")]
public class TimerTests
{
[Theory]
[InlineData(0L, 8L)]
[InlineData(80L, 80L)]
[InlineData(65L, 72L)]
[InlineData(32767L, 32768L)]
[InlineData(32768L, 32768L)]
[InlineData(32833L, 32840L)]
[InlineData(134217729L, 134217736L)]
public void TestVariousTimes(long ticks, long expectedTicks)
{
var timerTicks = new TimerTicks();
Timer.Init(timerTicks.Ticks);
Timer.StartTimer(TimeSpan.FromMilliseconds(ticks), action);
var tickCount = expectedTicks / 8;
for (var i = 1; i <= tickCount; i++)
{
timerTicks.Ticks = i * 8;
Timer.Slice(timerTicks.Ticks);
}
Assert.Equal(1, timerTicks.ExecutedCount);
return;
void action()
{
Assert.Equal(expectedTicks, timerTicks.Ticks);
timerTicks.ExecutedCount++;
}
}
[Theory]
[InlineData(1000L, 1000L, 30000L, 30000L, 2)]
public void TestIntervals(long delay, long expectedDelayTicks, long interval, long expectedIntervalTicks, int count)
{
var timerTicks = new TimerTicks();
Timer.Init(timerTicks.Ticks);
Timer.StartTimer(TimeSpan.FromMilliseconds(delay), TimeSpan.FromMilliseconds(interval), count, action);
var tickCount = (expectedDelayTicks + (expectedIntervalTicks * count - 1)) / 8;
for (var i = 1; i <= tickCount; i++)
{
timerTicks.Ticks = i * 8;
Timer.Slice(timerTicks.Ticks);
}
Assert.Equal(count, timerTicks.ExecutedCount);
return;
void action()
{
timerTicks.ExpectedTicks += timerTicks.ExecutedCount++ == 0 ? expectedDelayTicks : expectedIntervalTicks;
Assert.Equal(timerTicks.ExpectedTicks, timerTicks.Ticks);
}
}
[Fact]
public void TestTimerStartedOnTick()
{
var timerTicks = new TimerTicks();
Timer.Init(timerTicks.Ticks);
var timer = new SelfRunningTimer(timerTicks);
timer.Start();
Timer.Slice(128);
Assert.Equal(1, timerTicks.ExecutedCount);
Timer.Slice(256);
Assert.Equal(2, timerTicks.ExecutedCount);
}
[Fact]
public void ClearAllTimers_StopsEveryTimerInASlot()
{
Timer.Init(0);
var timers = new Timer[3];
for (var i = 0; i < timers.Length; i++)
{
timers[i] = Timer.DelayCall(TimeSpan.FromMilliseconds(80), () => Assert.Fail("cleared timer fired"));
}
Timer.ClearAllTimers(0);
for (var i = 0; i < timers.Length; i++)
{
Assert.False(timers[i].Running);
}
for (var tick = 8; tick <= 160; tick += 8)
{
Timer.Slice(tick);
}
}
private class TimerTicks
{
public long ExpectedTicks;
public long Ticks;
public int ExecutedCount;
}
private class SelfRunningTimer : Timer
{
private readonly TimerTicks _timerTicks;
public SelfRunningTimer(TimerTicks ticks) : base(TimeSpan.FromMilliseconds(100)) => _timerTicks = ticks;
protected override void OnTick()
{
if (_timerTicks.ExecutedCount++ == 0)
{
Delay = TimeSpan.FromMilliseconds(100);
Start();
}
}
}
}