ModernUO/Projects/UOContent.Tests/Tests/Mobiles/AI/PetPacingTests.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

288 lines
8.7 KiB
C#

using System;
using System.Collections.Generic;
using Server;
using Server.Mobiles;
using Xunit;
namespace UOContent.Tests.Mobiles.AI;
// Pet order handlers own the speed clocks; combat chases and herding keep their own pacing.
[Collection("Sequential UOContent Tests")]
public class PetPacingTests : IDisposable
{
private readonly List<Mobile> _created = new();
private (PlayerMobile master, PetTestStub pet) Spawn(Point3D masterLoc, Point3D petLoc)
{
var pair = PetTestSetup.SpawnControlledPet(masterLoc, petLoc);
_created.Add(pair.master);
_created.Add(pair.pet);
return pair;
}
public void Dispose()
{
foreach (var m in _created)
{
m?.Delete();
}
_created.Clear();
}
// Movement orders run active, resting orders run passive; the move clock follows.
[Fact]
public void OrderIssue_SetsThinkClock()
{
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
pet.SetMoveSpeed(0.3, 0.9);
pet.SetCurrentSpeedToPassive();
pet.ControlOrder = OrderType.Come;
Assert.Equal(0.2, pet.CurrentSpeed);
Assert.Equal(0.3, pet.CurrentMoveSpeed); // verbatim active -> activeMove
pet.ControlOrder = OrderType.Stay;
Assert.Equal(0.4, pet.CurrentSpeed);
Assert.Equal(0.9, pet.CurrentMoveSpeed);
pet.ControlTarget = master;
pet.ControlOrder = OrderType.Follow;
Assert.Equal(0.2, pet.CurrentSpeed);
pet.ControlOrder = OrderType.Guard;
Assert.Equal(0.2, pet.CurrentSpeed);
}
// The follow pace caps the step delay and leaves the think clock on the active value.
[Fact]
public void FollowMaster_PacesStepsWithoutInflatingTheThinkClock()
{
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
pet.SetMoveSpeed(0.3, 0.9);
pet.ControlTarget = master;
pet.ControlOrder = OrderType.Follow; // fixture era is EJ
Assert.Equal(0.2, pet.CurrentSpeed); // active think, not the follow pace
Assert.Equal(0.1, pet.CurrentMoveSpeed); // capped at the follow pace
}
// A creature configured faster than the follow pace keeps its own.
[Fact]
public void FollowMaster_KeepsAFasterConfiguredPace()
{
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
pet.SetMoveSpeed(0.05, 0.9);
pet.ControlTarget = master;
pet.ControlOrder = OrderType.Follow;
Assert.Equal(0.05, pet.CurrentMoveSpeed);
}
// The move-clock override survives the order: it is capped while following, not overwritten.
[Fact]
public void FollowMaster_LeavesTheConfiguredMoveClockAlone()
{
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
pet.SetMoveSpeed(0.3, 0.9);
pet.ControlTarget = master;
pet.ControlOrder = OrderType.Follow;
pet.AIObject.Obey();
pet.ControlOrder = OrderType.Stay;
Assert.Equal(0.3, pet.ActiveMoveSpeed);
Assert.Equal(0.9, pet.PassiveMoveSpeed);
Assert.Equal(0.9, pet.CurrentMoveSpeed); // resting on its own passive pace again
}
// Pre-AOS pets follow at their own pace; nothing caps them.
private sealed class SprintingPet : PetTestStub
{
public override double FollowMoveSpeed => 0.125;
}
// A shard paces follows in any era by overriding the property, not by patching the AI.
[Fact]
public void FollowMoveSpeedOverride_PacesFollowsInAnyEra()
{
var previous = Core.Expansion;
try
{
Core.Expansion = Expansion.UOR;
var master = new PlayerMobile(World.NewMobile);
master.DefaultMobileInit();
master.MoveToWorld(new Point3D(1000, 1000, 0), Map.Felucca);
_created.Add(master);
var pet = new SprintingPet();
pet.MoveToWorld(new Point3D(1001, 1000, 0), Map.Felucca);
pet.SetControlMaster(master);
_created.Add(pet);
pet.SetMoveSpeed(0.3, 0.9);
pet.ControlTarget = master;
pet.ControlOrder = OrderType.Follow;
Assert.Equal(0.125, pet.CurrentMoveSpeed);
}
finally
{
Core.Expansion = previous;
}
}
// A guarding pet outside guard range closes at the follow pace, thinking on its active clock.
[Fact]
public void GuardReturn_PacesStepsWithoutInflatingTheThinkClock()
{
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1006, 1000, 0));
pet.SetMoveSpeed(0.3, 0.9);
pet.ControlOrder = OrderType.Guard;
Assert.Equal(0.2, pet.CurrentSpeed);
Assert.Equal(0.1, pet.CurrentMoveSpeed);
}
// Obeying the follow order must not write the pace into either clock.
[Fact]
public void FollowMaster_ObeyKeepsTheThinkClockActive()
{
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
pet.SetMoveSpeed(0.3, 0.9);
pet.AIObject.AITimer?.Stop();
pet.ControlTarget = master;
pet.ControlOrder = OrderType.Follow; // fixture era is EJ
pet.AIObject.Obey();
Assert.Equal(0.2, pet.CurrentSpeed);
Assert.Equal(0.1, pet.CurrentMoveSpeed);
}
// At the master's side a guarding pet stays active: no stale-warmode passive, no sprint.
[Fact]
public void GuardAtMastersSide_IsActive()
{
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
pet.SetMoveSpeed(0.3, 0.9);
pet.AIObject.AITimer?.Stop();
pet.SetCurrentSpeedToPassive();
pet.ControlOrder = OrderType.Guard;
pet.AIObject.Obey(); // nothing to guard against, master adjacent
Assert.Equal(0.2, pet.CurrentSpeed);
Assert.Equal(0.3, pet.CurrentMoveSpeed);
}
// A pet chasing a combatant keeps the move table.
// Herding overrides order pacing.
private sealed class ThinkProbe : PetTestStub
{
public int Thinks;
public override void OnThink()
{
Thinks++;
base.OnThink();
}
}
private (PlayerMobile master, ThinkProbe pet) SpawnProbe()
{
var master = new PlayerMobile(World.NewMobile);
master.DefaultMobileInit();
master.MoveToWorld(new Point3D(1000, 1000, 0), Map.Felucca);
_created.Add(master);
var pet = new ThinkProbe();
pet.MoveToWorld(new Point3D(1001, 1000, 0), Map.Felucca);
pet.SetControlMaster(master);
_created.Add(pet);
return (master, pet);
}
// Advances time in 8ms lockstep so the wheel and Core.TickCount stay in sync.
private static void RunFor(long ms)
{
var deadline = Core._tickCount + ms;
while (Core._tickCount < deadline)
{
Core._tickCount += 8;
Timer.Slice(Core._tickCount);
}
}
private static bool RunUntil(Func<bool> condition, long maxMs)
{
var deadline = Core._tickCount + maxMs;
while (Core._tickCount < deadline)
{
if (condition())
{
return true;
}
Core._tickCount += 8;
Timer.Slice(Core._tickCount);
}
return condition();
}
// Runs past the spawn stagger; returns right after a think with the next 0.4s away.
private ThinkProbe SettledProbe(out PlayerMobile master)
{
Core._tickCount = 0;
Timer.Init(0);
var (m, pet) = SpawnProbe();
master = m;
pet.ForceIdle = true; // no wandering; pure cadence
pet.ControlOrder = OrderType.Stay;
var settled = RunUntil(() => pet.Thinks >= 2, 8000);
Assert.True(settled, "the AI must reach a steady think cadence");
return pet;
}
[Fact]
public void OrderChange_WakesStaleThinkTimer()
{
var pet = SettledProbe(out var master);
var thinksBefore = pet.Thinks;
RunFor(200); // mid-wait, next think ~200ms out
Assert.Equal(thinksBefore, pet.Thinks);
pet.ControlTarget = master;
pet.ControlOrder = OrderType.Follow;
RunFor(80);
Assert.True(pet.Thinks > thinksBefore, "a fresh order must wake the AI promptly");
}
[Fact]
public void SpeedUp_ReschedulesPendingWake()
{
var pet = SettledProbe(out _);
var thinksBefore = pet.Thinks;
RunFor(200); // mid-wait, next think ~200ms out
Assert.Equal(thinksBefore, pet.Thinks);
pet.CurrentSpeed = 0.1;
RunFor(120);
Assert.True(pet.Thinks > thinksBefore, "a speed-up must reschedule the pending wake");
}
}