test: fix PhysicsResolveCapture/PhysicsDiagnostics static-leak isolation
xUnit's default parallel execution let diagnostic-harness tests (CellarUp, DoorBug, DoorCollisionApparatus) mutate PhysicsResolveCapture.CapturePath and PhysicsDiagnostics probe flags concurrently with victim tests (MotionInterpreter, PositionManager, PlayerMovementController, DispatcherToMovement, BSPStepUp), producing a flaky 14-26 failure range. Fixes: - Add PhysicsResolveCapture.ResetForTest() + PhysicsDiagnostics.ResetForTest() as documented test-only reset APIs (never called from production paths). - Add IDisposable to CellarUpTrajectoryReplayTests with ctor/Dispose calling both ResetForTest() — prevents CapturePath from leaking between the Capture_* tests in the same class (the immediate root cause of Capture_SkipsNonPlayerCalls finding an unexpected file). - Add xunit.runner.json (maxParallelThreads=1, parallelizeTestCollections=false) to AcDream.Core.Tests — eliminates parallelism-induced probe-flag leaks across all test classes without requiring [Collection] boilerplate on every offender. After: two consecutive runs produce the identical 12-failure set. Confirmed: LiveCompare_FirstCap_FixClosesCottageFloorCap passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a06226f9a2
commit
21ee5e1035
5 changed files with 101 additions and 1 deletions
|
|
@ -512,6 +512,56 @@ public static class PhysicsDiagnostics
|
||||||
|
|
||||||
public static bool ProbeDumpGfxObjsEnabled => ProbeDumpGfxObjIds.Count > 0;
|
public static bool ProbeDumpGfxObjsEnabled => ProbeDumpGfxObjIds.Count > 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test-only reset: set every probe flag to <c>false</c> and clear any
|
||||||
|
/// side-channel fields. Does NOT re-read environment variables — tests
|
||||||
|
/// run in environments where the env vars are all absent, so false is
|
||||||
|
/// the correct default.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Call from test constructors and <c>IDisposable.Dispose()</c> to
|
||||||
|
/// prevent one test class from leaving enabled probes that corrupt
|
||||||
|
/// timing-sensitive tests in another class (the static-leak root cause
|
||||||
|
/// documented in T0).
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// This method is intentionally <c>public</c> so test projects can call
|
||||||
|
/// it without reflection, but it must NEVER be called from production
|
||||||
|
/// code paths.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public static void ResetForTest()
|
||||||
|
{
|
||||||
|
ProbeResolveEnabled = false;
|
||||||
|
ProbeCellEnabled = false;
|
||||||
|
ProbeBuildingEnabled = false;
|
||||||
|
ProbeCellSetEnabled = false;
|
||||||
|
ProbeAutoWalkEnabled = false;
|
||||||
|
ProbeUseabilityFallbackEnabled= false;
|
||||||
|
DumpSteepRoofEnabled = false;
|
||||||
|
ProbeIndoorBspEnabled = false;
|
||||||
|
ProbeCellCacheEnabled = false;
|
||||||
|
ProbeContactPlaneEnabled = false;
|
||||||
|
ProbeWalkMissEnabled = false;
|
||||||
|
ProbePushBackEnabled = false;
|
||||||
|
ProbePolyDumpEnabled = false;
|
||||||
|
ProbePlacementFailEnabled = false;
|
||||||
|
ProbeSweptEnabled = false;
|
||||||
|
ProbeStepWalkEnabled = false;
|
||||||
|
|
||||||
|
// Side-channel fields
|
||||||
|
LastBspHitPoly = null;
|
||||||
|
LastPlacementFailPolyId = 0;
|
||||||
|
LastPlacementFailPolyNormal = default;
|
||||||
|
LastPlacementFailPolyD = 0f;
|
||||||
|
LastPlacementFailSolidLeaf = false;
|
||||||
|
|
||||||
|
// Dump-trigger sets
|
||||||
|
ProbeDumpCellIds = new System.Collections.Generic.HashSet<uint>();
|
||||||
|
ProbeDumpGfxObjIds = new System.Collections.Generic.HashSet<uint>();
|
||||||
|
}
|
||||||
|
|
||||||
private static IReadOnlySet<uint> ParseHexIdList(string? raw)
|
private static IReadOnlySet<uint> ParseHexIdList(string? raw)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(raw))
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,24 @@ public static class PhysicsResolveCapture
|
||||||
public static void ResetTickCounter() =>
|
public static void ResetTickCounter() =>
|
||||||
Interlocked.Exchange(ref _tickCounter, 0);
|
Interlocked.Exchange(ref _tickCounter, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test-only reset: close any open writer, clear <see cref="CapturePath"/>,
|
||||||
|
/// and reset the tick counter to 0. Call from test constructors and
|
||||||
|
/// <c>IDisposable.Dispose()</c> to prevent static state from leaking
|
||||||
|
/// across test-class boundaries.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// This method is intentionally <c>public</c> so test projects can call it
|
||||||
|
/// without reflection, but it must NEVER be called from production code paths.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public static void ResetForTest()
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
CapturePath = null;
|
||||||
|
Interlocked.Exchange(ref _tickCounter, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private static void EnsureWriter_NoLock()
|
private static void EnsureWriter_NoLock()
|
||||||
{
|
{
|
||||||
if (_writer is not null)
|
if (_writer is not null)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,15 @@
|
||||||
<Using Include="Xunit" />
|
<Using Include="Xunit" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- T0: force serial test execution to prevent PhysicsResolveCapture /
|
||||||
|
PhysicsDiagnostics static state from leaking between test classes.
|
||||||
|
These statics are mutated by diagnostic-harness tests; xUnit's
|
||||||
|
default parallel execution races them against victim tests like
|
||||||
|
MotionInterpreterTests and PlayerMovementControllerTests. -->
|
||||||
|
<None Update="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\AcDream.Core\AcDream.Core.csproj" />
|
<ProjectReference Include="..\..\src\AcDream.Core\AcDream.Core.csproj" />
|
||||||
<ProjectReference Include="..\..\src\AcDream.App\AcDream.App.csproj" />
|
<ProjectReference Include="..\..\src\AcDream.App\AcDream.App.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,25 @@ namespace AcDream.Core.Tests.Physics;
|
||||||
/// trajectory shape.
|
/// trajectory shape.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CellarUpTrajectoryReplayTests
|
public class CellarUpTrajectoryReplayTests : IDisposable
|
||||||
{
|
{
|
||||||
|
// ── T0: constructor + Dispose reset PhysicsResolveCapture static ────────
|
||||||
|
// xUnit creates a fresh instance per test, so the constructor runs before
|
||||||
|
// each test and Dispose runs after. Resetting here guarantees that no
|
||||||
|
// prior test in this class can leave CapturePath set (which would cause
|
||||||
|
// Capture_SkipsNonPlayerCalls to find an unexpected file).
|
||||||
|
public CellarUpTrajectoryReplayTests()
|
||||||
|
{
|
||||||
|
PhysicsResolveCapture.ResetForTest();
|
||||||
|
PhysicsDiagnostics.ResetForTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
PhysicsResolveCapture.ResetForTest();
|
||||||
|
PhysicsDiagnostics.ResetForTest();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Cellar / cottage geometry constants ────────────────────────
|
// ── Cellar / cottage geometry constants ────────────────────────
|
||||||
private const uint CellarId = 0xA9B40147u;
|
private const uint CellarId = 0xA9B40147u;
|
||||||
private const uint CottageNeighborA = 0xA9B40143u;
|
private const uint CottageNeighborA = 0xA9B40143u;
|
||||||
|
|
|
||||||
6
tests/AcDream.Core.Tests/xunit.runner.json
Normal file
6
tests/AcDream.Core.Tests/xunit.runner.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||||
|
"parallelizeAssembly": false,
|
||||||
|
"parallelizeTestCollections": false,
|
||||||
|
"maxParallelThreads": 1
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue