## Summary Many tests proved a specific change when it landed but don't need to run on every CI build forever. This removes **202 test methods and 9 files: 313 test cases, 2,247 → 1,934**. UOContent.Tests drops from about 14s to about 5s locally. Removed: - **Tautologies.** Asserting a constant, a constructor readback or a trivial getter, or testing BCL behavior. - **Exact duplicates** of a surviving test on the same code path. - **Obsolete regressions** whose failure can no longer be expressed. - **Game-content values** shard owners customize (era drop rates, loot and skill tables, single-creature tuning). Each customization would otherwise force a test fix. ## How it was decided 1. Every test was classified, with a reason, by a per-shard sweep over all 287 test files. 2. Each proposed deletion was then checked by a second, adversarial reviewer against the surviving tests and the production code. 3. Anything that was the only coverage of a separate code path was **kept**, even where it looked like a duplicate. Examples: the unsigned `SpanReader`/`SpanWriter` methods, the at-location versus bounds enumerators, null-map enumerables, the security filters, archive-journal state transitions. About 130 proposed deletions were rejected this way. Whole files removed: - `FishingTests`, apart from 3 tests: the map-edge scan, serpent cleanup, and SOS chest consumption. The SOS test now stops at the first chest instead of measuring a catch rate. The multi-million-cast rate simulations are gone. - `LordOaksTests` and `MeerEnragedTests` (single-creature tests). - `AutoArchiveHelperTests`, `JsonDiscoverableTypeAttributeTests`, `DecimalSerializationTests` and `EnumConversionTests` (tautologies, or tests of their own local helpers). - `NetworkStatsTests`. - `MigratedDataLoadTests` (a subset of `AllSpawnFilesLoadTests`). - `SpawnerFixtureCapture`: a capture tool, not a test, and its `version == 12` assert can no longer pass. The `.bin` fixtures it produced stay. ## Tests - Server.Tests: 811 pass. - UOContent.Tests: 1,123 pass. A follow-up PR, stacked on this one, fixes how the remaining tests behave: shared state, CI-vacuous tests, harness duplication.
342 lines
12 KiB
C#
342 lines
12 KiB
C#
using Server.Network;
|
|
using Xunit;
|
|
|
|
namespace Server.Tests.Network;
|
|
|
|
/// <summary>
|
|
/// Unit tests for MovementThrottle rate calculation and detection logic.
|
|
/// These tests are CI/CD safe - they don't depend on real timing.
|
|
/// </summary>
|
|
[Collection("Sequential Server Tests")]
|
|
public class MovementThrottleTests
|
|
{
|
|
/// <summary>
|
|
/// Creates a test NetState with movement history pre-populated.
|
|
/// </summary>
|
|
private static NetState CreateNetStateWithHistory(params (short interval, ushort targetSpeed)[] records)
|
|
{
|
|
var ns = PacketTestUtilities.CreateTestNetState();
|
|
|
|
if (records.Length == 0)
|
|
{
|
|
return ns;
|
|
}
|
|
|
|
// Initialize history buffer
|
|
ns._movementHistory = new NetState.MovementRecord[20];
|
|
ns._movementHistoryIndex = 0;
|
|
ns._movementHistoryFull = false;
|
|
|
|
foreach (var (interval, targetSpeed) in records)
|
|
{
|
|
ns._movementHistory[ns._movementHistoryIndex] = new NetState.MovementRecord
|
|
{
|
|
Interval = interval,
|
|
TargetSpeed = targetSpeed,
|
|
QueueDepth = 0,
|
|
Flags = 0
|
|
};
|
|
ns._movementHistoryIndex++;
|
|
|
|
if (ns._movementHistoryIndex >= 20)
|
|
{
|
|
ns._movementHistoryIndex = 0;
|
|
ns._movementHistoryFull = true;
|
|
}
|
|
}
|
|
|
|
return ns;
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateMovementRate_NoHistory_ReturnsOne()
|
|
{
|
|
var ns = CreateNetStateWithHistory();
|
|
|
|
var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount);
|
|
|
|
Assert.Equal(1.0f, rate);
|
|
Assert.Equal(0, sampleCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateMovementRate_InsufficientSamples_ReturnsOne()
|
|
{
|
|
// Less than 8 samples (minSamplesForRate default)
|
|
var ns = CreateNetStateWithHistory(
|
|
(100, 100),
|
|
(100, 100),
|
|
(100, 100)
|
|
);
|
|
|
|
var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount);
|
|
|
|
Assert.Equal(1.0f, rate);
|
|
// When insufficient samples, method returns early with sampleCount = 0
|
|
Assert.Equal(0, sampleCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateMovementRate_NormalSpeed_ReturnsOne()
|
|
{
|
|
// 10 samples at exactly expected speed (100ms interval, 100ms target)
|
|
var ns = CreateNetStateWithHistory(
|
|
(100, 100), (100, 100), (100, 100), (100, 100), (100, 100),
|
|
(100, 100), (100, 100), (100, 100), (100, 100), (100, 100)
|
|
);
|
|
|
|
var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount);
|
|
|
|
Assert.Equal(1.0f, rate);
|
|
Assert.Equal(10, sampleCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateMovementRate_SpeedHack_ReturnsHighRate()
|
|
{
|
|
// Moving at 50ms intervals when 100ms is expected = 2x speed
|
|
var ns = CreateNetStateWithHistory(
|
|
(50, 100), (50, 100), (50, 100), (50, 100), (50, 100),
|
|
(50, 100), (50, 100), (50, 100), (50, 100), (50, 100)
|
|
);
|
|
|
|
var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount);
|
|
|
|
Assert.Equal(2.0f, rate);
|
|
Assert.Equal(10, sampleCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateMovementRate_SlowMovement_ReturnsLowRate()
|
|
{
|
|
// Moving at 200ms intervals when 100ms is expected = 0.5x speed
|
|
var ns = CreateNetStateWithHistory(
|
|
(200, 100), (200, 100), (200, 100), (200, 100), (200, 100),
|
|
(200, 100), (200, 100), (200, 100), (200, 100), (200, 100)
|
|
);
|
|
|
|
var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount);
|
|
|
|
Assert.Equal(0.5f, rate);
|
|
Assert.Equal(10, sampleCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateMovementRate_MixedSpeeds_ReturnsAverageRate()
|
|
{
|
|
// Mix of mounted running (100ms target) at varying speeds
|
|
// Total target: 1000ms, Total actual: 900ms = 1.11 rate
|
|
var ns = CreateNetStateWithHistory(
|
|
(80, 100), (100, 100), (90, 100), (100, 100), (80, 100),
|
|
(90, 100), (100, 100), (80, 100), (90, 100), (90, 100)
|
|
);
|
|
|
|
var rate = MovementThrottle.CalculateMovementRate(ns, out var sampleCount);
|
|
|
|
// 1000 / 900 = 1.111...
|
|
Assert.True(rate > 1.10f && rate < 1.12f, $"Expected rate ~1.11, got {rate}");
|
|
Assert.Equal(10, sampleCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectRecentBurst_NoBurst_ReturnsZero()
|
|
{
|
|
// Normal intervals, no burst
|
|
var ns = CreateNetStateWithHistory(
|
|
(100, 100), (100, 100), (100, 100), (100, 100), (100, 100),
|
|
(100, 100), (100, 100), (100, 100), (100, 100), (100, 100)
|
|
);
|
|
|
|
var (burstSize, precedingGap) = MovementThrottle.DetectRecentBurst(ns);
|
|
|
|
Assert.Equal(0, burstSize);
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectRecentBurst_BurstDetected_ReturnsBurstSizeAndGap()
|
|
{
|
|
// Last 4 packets arrived in burst (< 15ms intervals), preceded by 500ms gap
|
|
var ns = CreateNetStateWithHistory(
|
|
(100, 100), (100, 100), (100, 100), (100, 100), (100, 100),
|
|
(500, 100), (5, 100), (5, 100), (5, 100), (5, 100)
|
|
);
|
|
|
|
var (burstSize, precedingGap) = MovementThrottle.DetectRecentBurst(ns);
|
|
|
|
Assert.Equal(4, burstSize);
|
|
Assert.Equal(500, precedingGap);
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectRecentBurst_SmallBurst_DetectsCorrectly()
|
|
{
|
|
// 2 packets in burst
|
|
var ns = CreateNetStateWithHistory(
|
|
(100, 100), (100, 100), (100, 100), (100, 100), (100, 100),
|
|
(100, 100), (100, 100), (100, 100), (200, 100), (10, 100)
|
|
);
|
|
|
|
var (burstSize, precedingGap) = MovementThrottle.DetectRecentBurst(ns);
|
|
|
|
Assert.Equal(1, burstSize);
|
|
Assert.Equal(200, precedingGap);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeMovement_NormalMovement_ReturnsNormal()
|
|
{
|
|
var ns = CreateNetStateWithHistory(
|
|
(100, 100), (100, 100), (100, 100), (100, 100), (100, 100),
|
|
(100, 100), (100, 100), (100, 100), (100, 100), (100, 100)
|
|
);
|
|
|
|
var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence);
|
|
|
|
Assert.Equal(MovementThrottle.DetectionVerdict.Normal, verdict);
|
|
Assert.Equal(1.0f, rate);
|
|
Assert.Equal(10, sampleCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeMovement_ClearSpeedHack_ReturnsDefinite()
|
|
{
|
|
// 30% faster than expected - clear speed hack
|
|
var ns = CreateNetStateWithHistory(
|
|
(77, 100), (77, 100), (77, 100), (77, 100), (77, 100),
|
|
(77, 100), (77, 100), (77, 100), (77, 100), (77, 100)
|
|
);
|
|
|
|
var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence);
|
|
|
|
Assert.Equal(MovementThrottle.DetectionVerdict.Definite, verdict);
|
|
Assert.True(rate > 1.25f, $"Expected rate > 1.25, got {rate}");
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeMovement_BurstAfterLargeGap_ReducesConfidence()
|
|
{
|
|
// Simulate lag recovery: large gap followed by burst of packets
|
|
var ns = CreateNetStateWithHistory(
|
|
(100, 100), (100, 100), (100, 100), (100, 100), (100, 100),
|
|
(5, 100), (5, 100), (5, 100), (5, 100), (5, 100)
|
|
);
|
|
|
|
// Set up the gap duration (simulates what RecordMovement does)
|
|
ns._lastGapDuration = 1500; // Gap > maxChainGap/2 (1000)
|
|
|
|
var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence);
|
|
|
|
// Even though rate is high, confidence should be reduced due to burst forgiveness
|
|
// The gap duration should trigger the forgiveness path
|
|
Assert.True(confidence < 0.5f, $"Expected low confidence due to burst forgiveness, got {confidence}");
|
|
|
|
// After analysis, gap duration should be reset
|
|
Assert.Equal(0, ns._lastGapDuration);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeMovement_HighQueue_IncreasesConfidence()
|
|
{
|
|
var ns = CreateNetStateWithHistory(
|
|
(95, 100), (95, 100), (95, 100), (95, 100), (95, 100),
|
|
(95, 100), (95, 100), (95, 100), (95, 100), (95, 100)
|
|
);
|
|
|
|
// Simulate high queue depth (modified client indicator)
|
|
ns._movementQueue = new System.Collections.Generic.Queue<NetState.QueuedMovement>();
|
|
for (var i = 0; i < 5; i++)
|
|
{
|
|
ns._movementQueue.Enqueue(new NetState.QueuedMovement());
|
|
}
|
|
|
|
var verdict = MovementThrottle.AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence);
|
|
|
|
// Queue > 4 indicates modified client - should be Definite
|
|
Assert.Equal(MovementThrottle.DetectionVerdict.Definite, verdict);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnalyzeMovement_UnstableConnection_ReducesConfidence()
|
|
{
|
|
var ns = CreateNetStateWithHistory(
|
|
(90, 100), (90, 100), (90, 100), (90, 100), (90, 100),
|
|
(90, 100), (90, 100), (90, 100), (90, 100), (90, 100)
|
|
);
|
|
|
|
// Set up unstable connection (high variance)
|
|
ns._rttHistory = [50, 200, 80, 350, 100, 250, 90, 300];
|
|
ns._rttSampleCount = 8;
|
|
ns._rttVariance = 15000; // High variance
|
|
|
|
var verdictUnstable = MovementThrottle.AnalyzeMovement(ns, out _, out _, out var confidenceUnstable);
|
|
|
|
// Reset and test with stable connection for comparison
|
|
var nsStable = CreateNetStateWithHistory(
|
|
(90, 100), (90, 100), (90, 100), (90, 100), (90, 100),
|
|
(90, 100), (90, 100), (90, 100), (90, 100), (90, 100)
|
|
);
|
|
nsStable._rttHistory = [50, 52, 48, 51, 49, 50, 51, 50];
|
|
nsStable._rttSampleCount = 8;
|
|
nsStable._rttVariance = 4;
|
|
|
|
var verdictStable = MovementThrottle.AnalyzeMovement(nsStable, out _, out _, out var confidenceStable);
|
|
|
|
// Unstable connection should have lower confidence
|
|
Assert.True(
|
|
confidenceUnstable < confidenceStable,
|
|
$"Expected unstable ({confidenceUnstable}) < stable ({confidenceStable})"
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void HasStableConnection_LowLatencyLowVariance_ReturnsTrue()
|
|
{
|
|
var ns = PacketTestUtilities.CreateTestNetState();
|
|
|
|
// Stable: low variance, enough samples, low latency
|
|
ns._rttHistory = [50, 52, 48, 51, 49, 50, 51, 50];
|
|
ns._rttSampleCount = 8;
|
|
ns._rttVariance = 4; // Low variance < 2500
|
|
|
|
Assert.True(ns.HasStableConnection);
|
|
}
|
|
|
|
[Fact]
|
|
public void HasStableConnection_HighLatency_ReturnsFalse()
|
|
{
|
|
var ns = PacketTestUtilities.CreateTestNetState();
|
|
|
|
// High but consistent latency (500ms) - should NOT be considered stable
|
|
ns._rttHistory = [500, 502, 498, 501, 499, 500, 501, 500];
|
|
ns._rttSampleCount = 8;
|
|
ns._rttVariance = 4; // Low variance, but latency > 200ms
|
|
|
|
Assert.False(ns.HasStableConnection, "High latency connection should not be considered stable");
|
|
}
|
|
|
|
[Fact]
|
|
public void HasStableConnection_HighVariance_ReturnsFalse()
|
|
{
|
|
var ns = PacketTestUtilities.CreateTestNetState();
|
|
|
|
// Low average latency but high variance
|
|
ns._rttHistory = [50, 200, 30, 180, 40, 150, 60, 170];
|
|
ns._rttSampleCount = 8;
|
|
ns._rttVariance = 5000; // High variance > 2500
|
|
|
|
Assert.False(ns.HasStableConnection);
|
|
}
|
|
|
|
[Fact]
|
|
public void HasStableConnection_InsufficientSamples_ReturnsFalse()
|
|
{
|
|
var ns = PacketTestUtilities.CreateTestNetState();
|
|
|
|
// Only 2 samples
|
|
ns._rttHistory = [50, 50, 0, 0, 0, 0, 0, 0];
|
|
ns._rttSampleCount = 2;
|
|
ns._rttVariance = 0;
|
|
|
|
Assert.False(ns.HasStableConnection, "Needs at least 3 samples");
|
|
}
|
|
}
|