test: run tile-dependent tests on synthetic terrain instead of client files (#2725)
## Summary About 114 tests skipped on CI because they needed the official client files (`tiledata.mul`, map and multi files). They gave CI no signal, and locally they ran against whichever client a developer had installed. The test hosts now build their own world instead, so every test runs on CI and runs the same on every machine. CI simulation: Server.Tests 837 passed, UOContent.Tests 1131 passed, **0 skipped**. - **Generated tile data.** The fixtures write a pre-HS `tiledata.mul` from a small invented tile kit (`Server.Tests/Terrain/TestTiles`) and load it through the real `TileData.Load()`. The test hosts no longer read any client directory (`MODERNUO_CLIENT_PATH`, `MODERNUO_TEST_DATA_DIR` and `C:\Ultima Online Classic` are gone from test code). The file is written to `SyntheticData` in each test project's output, overwritten every run and removed by `dotnet clean`. - **Injected terrain.** `TestTerrain` stamps land and statics into Felucca and Trammel, which share one terrain. Writes after startup must go in a `TestTerrain.Scope()`, which puts back every block it touched and keeps the step cache fresh. - **Prefab scenes.** Seven named scenes (town, causeway, sewer walkway, coastline, deep sea, map-edge sea, an upstairs room with an L-shaped counter) replace the Britain coordinates the pathing, AI and fishing tests relied on. Each scene-based test asserts its scene is present first, so a missing scene fails instead of passing on flat ground. Stamping all seven takes 3-5 ms. - **Multi footprints.** Contrived footprints for the three multi IDs content looks up (0x74, 0x7A, 0x0) are seeded through a new internal `MultiData.SetComponents`. - **Fingerprint.** `StepCacheFile.HashTileDataFile` is extracted so a test can show that a one-flag change to the file changes the fingerprint, while runtime table mutation does not. ### Engine changes - `TileMatrix.GetLandBlock`/`GetStaticBlock`: on a map without its files, return the block injected with `SetLandBlock`/`SetStaticBlock`, or the default when nothing was injected. Maps with files take the same path as before. - `MultiData.SetComponents` (internal): registers a footprint without multi files. ### Test changes worth a look - Values that were properties of real Britain terrain (probes at Z 10, a pinned plain walk mask of 0xC1) are re-pinned to the synthetic plain (Z 0, 0xFF). - No tests were deleted. The multi walkability and boat-deck tests assert movement behaviour over a footprint, not what a real multi looks like, so they run on the contrived footprints. - Maps 0 and 1 keep file indexes 0 and 1, and every stamp writes both, rather than sharing one file index. On a map without files the getters never consult the shared-file cache, and its patch bits would hide one facet's injection from the other. ## Test plan - [x] `dotnet test -p:SolutionDir=... --filter "Category!=Manual"` with the client env vars pointed at an empty directory: 837 + 1131 passed, 0 skipped, 0 failed - [x] Mutation check: removing the step-cache clear from `TestTerrain` makes the stale-cache tests fail - [x] CI green on all platforms
This commit is contained in:
parent
00134cac33
commit
54c0040ac8
51 changed files with 1517 additions and 601 deletions
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Server.Items;
|
||||
|
|
@ -7,6 +6,7 @@ using Server.Misc;
|
|||
using Server.Movement;
|
||||
using Server.PathAlgorithms;
|
||||
using Server.Tests.Maps;
|
||||
using Server.Tests.Terrain;
|
||||
|
||||
namespace Server.Tests;
|
||||
|
||||
|
|
@ -24,19 +24,15 @@ namespace Server.Tests;
|
|||
/// single time regardless of how many collection fixtures are constructed. The two stateful
|
||||
/// collections use <c>[CollectionDefinition(DisableParallelization = true)]</c> so they never
|
||||
/// overlap; pure tests still run in parallel.
|
||||
///
|
||||
/// The host runs on generated tile data, terrain and multi footprints (see <see cref="SyntheticWorld"/>),
|
||||
/// never on client files, so every machine and CI run the same world.
|
||||
/// </summary>
|
||||
internal static class TestServerInitializer
|
||||
{
|
||||
private static bool _initialized;
|
||||
private static readonly Lock _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// True if the UO client tile data was found and loaded. When false (e.g. CI, where the
|
||||
/// copyrighted client files are absent), tile/map/multi-dependent tests must skip rather than
|
||||
/// fail. Guard such tests with <c>Skip.If(!TestServerInitializer.TileDataLoaded, ...)</c>.
|
||||
/// </summary>
|
||||
public static bool TileDataLoaded { get; private set; }
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
lock (_lock)
|
||||
|
|
@ -53,10 +49,9 @@ internal static class TestServerInitializer
|
|||
ServerConfiguration.Load(true);
|
||||
ServerConfiguration.AssemblyDirectories.Add(Core.BaseDirectory);
|
||||
|
||||
// Required for the pathfinding tests (real .mul tile data). Harmless for the rest.
|
||||
var clientFiles = Environment.GetEnvironmentVariable("MODERNUO_TEST_DATA_DIR")
|
||||
?? @"C:\Ultima Online Classic";
|
||||
ServerConfiguration.DataDirectories.Add(clientFiles);
|
||||
// TileData's static cctor short-circuits under xUnit, so the generated tiledata.mul must load
|
||||
// before anything reads LandTable/ItemTable (the multi footprints, MovementImpl, CheckMovement).
|
||||
SyntheticWorld.LoadData();
|
||||
|
||||
AssemblyHandler.LoadAssemblies(["Server.dll", "UOContent.dll"]);
|
||||
|
||||
|
|
@ -72,16 +67,7 @@ internal static class TestServerInitializer
|
|||
Timer.Init(0);
|
||||
Server.Network.NetState.Configure();
|
||||
TestMapDefinitions.ConfigureTestMapDefinitions();
|
||||
|
||||
// TileData's static cctor short-circuits when running under xUnit
|
||||
// (see Server/TileData.cs:295). Force-load via reflection so LandTable/ItemTable
|
||||
// flags are populated before anything that reads TileData (MultiData, MovementImpl,
|
||||
// CheckMovement). Without this, TileData.MaxItemValue is 0 at MultiData.Configure()
|
||||
// time, causing every MCL tile ID to be masked to 0 and stored as ID=0 in Tiles[x][y].
|
||||
// The copyrighted client files are absent on CI; when tiledata.mul is missing we skip
|
||||
// the tile/map/multi-dependent bootstrap and leave TileDataLoaded false so those tests
|
||||
// skip instead of failing the whole collection from the fixture constructor.
|
||||
TileDataLoaded = TryForceLoadTileData();
|
||||
SyntheticWorld.StampTerrain();
|
||||
|
||||
// Production runs every static Configure() via AssemblyHandler.Invoke("Configure");
|
||||
// the fixture calls a curated subset, so configure the pathfinding singleton here so
|
||||
|
|
@ -89,16 +75,6 @@ internal static class TestServerInitializer
|
|||
// calls Find. ServerConfiguration is already loaded above, so the setting resolves.
|
||||
BitmapAStarAlgorithm.Configure();
|
||||
|
||||
if (TileDataLoaded)
|
||||
{
|
||||
// Multi component lists (multi.mul / MultiCollection.uop). Production invokes this via
|
||||
// AssemblyHandler.Invoke("Configure"); the curated fixture subset must call it so that
|
||||
// BaseMulti.Components (MultiData.GetComponents) returns real footprints instead of
|
||||
// MultiComponentList.Empty. Required by the Multi pathfinding tests. Depends on the
|
||||
// client files, so it only runs when tile data loaded.
|
||||
MultiData.Configure();
|
||||
}
|
||||
|
||||
World.Configure();
|
||||
// Registers the Accounts entity persistence; without it no test can construct an Account.
|
||||
Server.Accounting.Accounts.Configure();
|
||||
|
|
@ -113,55 +89,7 @@ internal static class TestServerInitializer
|
|||
Server.Mobiles.NPCSpeeds.Configure();
|
||||
Server.Engines.Spawners.SpawnerJsonSerializer.Configure();
|
||||
|
||||
if (TileDataLoaded)
|
||||
{
|
||||
VerifyTrammelTileDataLoaded();
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryForceLoadTileData()
|
||||
{
|
||||
var tileDataPath = Core.FindDataFile("tiledata.mul", false);
|
||||
if (string.IsNullOrEmpty(tileDataPath) || !File.Exists(tileDataPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var loadMethod = typeof(TileData).GetMethod(
|
||||
"Load",
|
||||
BindingFlags.Static | BindingFlags.NonPublic
|
||||
);
|
||||
if (loadMethod == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"TileData.Load not found via reflection — engine may have refactored."
|
||||
);
|
||||
}
|
||||
loadMethod.Invoke(null, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void VerifyTrammelTileDataLoaded()
|
||||
{
|
||||
var trammel = Map.Maps[1];
|
||||
if (trammel == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Trammel (mapId=1) was not registered. Check TestMapDefinitions."
|
||||
);
|
||||
}
|
||||
|
||||
var tile = trammel.Tiles.GetLandTile(1500, 1600);
|
||||
if (tile.ID == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Trammel tile data did not load — GetLandTile(1500,1600) returned ID 0. " +
|
||||
$"Verify Distribution/Data/map1*.mul (or map1LegacyMUL.uop) is present at " +
|
||||
$"{Path.Combine(Core.BaseDirectory, "Data")}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue