ModernUO/Projects/Server.Tests/Tests/Network/MovementThrottleTests.cs
Kamron Batman 9a25054809
test: remove trivial, duplicate and content-value tests (#2721)
## 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.
2026-10-18 20:08:19 -07:00

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");
}
}