acdream/tests/AcDream.App.Tests/Streaming/LandblockBuildOriginTests.cs
Erik 2fd99c4265 test: FarLoad strip test asserts each build config's designed outcome
LandblockStreamer.HandleJob's near-payload check is "fail loud in Debug
builds and strip in Release" (its own comment, with a Debug.Assert at the
check). FarLoad_StripsEnvCellsAndPhysicsEvenWhenEntityListIsAlreadyEmpty
feeds a deliberately-buggy far factory to verify the Release strip — so
under Debug the assert fires, the test host's listener turns it into an
exception, and the job publishes Failed BY DESIGN. The test asserted the
Release outcome unconditionally and therefore failed on every full Debug
App run (found 2026-08-13 during the #385 session; every campaign gate
runs Release, which is why it never surfaced). It now asserts the Failed
result + assert message under DEBUG and the strip under Release. Verified
green in both configs; full Debug App suite 4,937/3 skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 08:52:12 +02:00

469 lines
17 KiB
C#

using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.Core.Terrain;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Streaming;
public sealed class LandblockBuildOriginTests
{
private const int SpinTimeoutMs = 2000;
private const int SpinStepMs = 10;
[Fact]
public void ExplicitMapZeroOrigin_IsDistinctFromUnspecifiedCompatibilityOrigin()
{
var mapZero = new LandblockBuildOrigin(0, 0);
Assert.True(mapZero.IsSpecified);
Assert.False(default(LandblockBuildOrigin).IsSpecified);
Assert.NotEqual(default, mapZero);
}
[Fact]
public async Task RequestAwareLoad_CarriesExplicitMapZeroOriginThroughFactoryAndCompletion()
{
const uint landblockId = 0x0000FFFFu;
var capturedOrigin = new LandblockBuildOrigin(0, 0);
LandblockBuildRequest? observedRequest = null;
using var streamer = LandblockStreamer.CreateForRequests(
loadLandblock: request =>
{
observedRequest = request;
return EmptyBuild(request.LandblockId, request.Origin);
},
buildMeshOrNull: (_, _) => EmptyMesh());
streamer.EnqueueLoad(new LandblockBuildRequest(
landblockId,
LandblockStreamJobKind.LoadNear,
Generation: 77,
capturedOrigin));
streamer.Start();
var loaded = Assert.IsType<LandblockStreamResult.Loaded>(
await DrainFirstAsync(streamer));
Assert.Equal(
new LandblockBuildRequest(
landblockId,
LandblockStreamJobKind.LoadNear,
77,
capturedOrigin),
observedRequest);
Assert.Equal(capturedOrigin, loaded.Build.Origin);
Assert.Equal(77ul, loaded.Generation);
}
[Fact]
public async Task RequestAwareLoad_WhenFactoryReturnsUnspecifiedOrigin_FailsAtMapZero()
{
using var streamer = LandblockStreamer.CreateForRequests(
loadLandblock: request => EmptyBuild(request.LandblockId, default));
streamer.EnqueueLoad(new LandblockBuildRequest(
0x0000FFFFu,
LandblockStreamJobKind.LoadNear,
Generation: 78,
new LandblockBuildOrigin(0, 0)));
streamer.Start();
var failed = Assert.IsType<LandblockStreamResult.Failed>(
await DrainFirstAsync(streamer));
Assert.Contains("origin", failed.Error, StringComparison.OrdinalIgnoreCase);
Assert.Equal(78ul, failed.Generation);
}
[Fact]
public void CompatibilityLoader_RejectsEvenExplicitMapZeroOriginBeforeQueueing()
{
using var streamer = new LandblockStreamer(loadLandblock: _ => null);
Assert.Throws<InvalidOperationException>(() => streamer.EnqueueLoad(
new LandblockBuildRequest(
0x0000FFFFu,
LandblockStreamJobKind.LoadNear,
Generation: 79,
new LandblockBuildOrigin(0, 0))));
}
[Fact]
public void RequestAwareLoader_RejectsOriginlessCompatibilityEnqueue()
{
using var streamer = LandblockStreamer.CreateForRequests(
loadLandblock: request => EmptyBuild(request.LandblockId, request.Origin));
Assert.Throws<InvalidOperationException>(() =>
streamer.EnqueueLoad(0xA9B4FFFFu));
}
[Fact]
public void RequestAwareLoader_RejectsDirectRequestWithUnspecifiedOrigin()
{
using var streamer = LandblockStreamer.CreateForRequests(
loadLandblock: request => EmptyBuild(request.LandblockId, request.Origin));
Assert.Throws<InvalidOperationException>(() => streamer.EnqueueLoad(
new LandblockBuildRequest(
0xA9B4FFFFu,
LandblockStreamJobKind.LoadNear,
Generation: 80,
Origin: default)));
}
[Fact]
public async Task TwoQueuedLoads_RetainTheirDistinctOriginAndGeneration()
{
var observed = new List<LandblockBuildRequest>();
var firstOrigin = new LandblockBuildOrigin(0xA9, 0xB4);
var secondOrigin = new LandblockBuildOrigin(0x71, 0xEC);
using var streamer = LandblockStreamer.CreateForRequests(
loadLandblock: request =>
{
observed.Add(request);
return EmptyBuild(request.LandblockId, request.Origin);
},
buildMeshOrNull: (_, _) => EmptyMesh());
streamer.EnqueueLoad(new LandblockBuildRequest(
0xA9B4FFFFu,
LandblockStreamJobKind.LoadNear,
Generation: 80,
firstOrigin));
streamer.EnqueueLoad(new LandblockBuildRequest(
0x71ECFFFFu,
LandblockStreamJobKind.LoadNear,
Generation: 81,
secondOrigin));
streamer.Start();
IReadOnlyList<LandblockStreamResult> completions =
await DrainCountAsync(streamer, 2);
Assert.Equal([firstOrigin, secondOrigin], observed.Select(request => request.Origin));
Assert.Equal([80ul, 81ul], completions.Select(result => result.Generation));
Assert.Equal(
[firstOrigin, secondOrigin],
completions
.Cast<LandblockStreamResult.Loaded>()
.Select(result => result.Build.Origin));
}
[Fact]
public async Task QueuedPromotion_SupersedesOldFarWithItsOwnOriginAndGeneration()
{
var observed = new List<LandblockBuildRequest>();
var oldOrigin = new LandblockBuildOrigin(0xA9, 0xB4);
var newOrigin = new LandblockBuildOrigin(0x71, 0xEC);
const uint landblockId = 0x71ECFFFFu;
using var streamer = LandblockStreamer.CreateForRequests(
loadLandblock: request =>
{
observed.Add(request);
return EmptyBuild(request.LandblockId, request.Origin);
},
buildMeshOrNull: (_, _) => EmptyMesh());
streamer.EnqueueLoad(new LandblockBuildRequest(
landblockId,
LandblockStreamJobKind.LoadFar,
Generation: 82,
oldOrigin));
streamer.EnqueueLoad(new LandblockBuildRequest(
landblockId,
LandblockStreamJobKind.PromoteToNear,
Generation: 83,
newOrigin));
streamer.Start();
var promoted = Assert.IsType<LandblockStreamResult.Promoted>(
await DrainFirstAsync(streamer));
Assert.Equal([new LandblockBuildRequest(
landblockId,
LandblockStreamJobKind.PromoteToNear,
83,
newOrigin)], observed);
Assert.Equal(newOrigin, promoted.Build.Origin);
Assert.Equal(83ul, promoted.Generation);
}
[Fact]
public async Task FarLoad_StripsEnvCellsAndPhysicsEvenWhenEntityListIsAlreadyEmpty()
{
const uint landblockId = 0xA9B4FFFFu;
var origin = new LandblockBuildOrigin(0xA9, 0xB4);
var physics = new PhysicsDatBundle(
new LandBlockInfo(),
new Dictionary<uint, EnvCell>(),
new Dictionary<uint, DatReaderWriter.DBObjs.Environment>(),
new Dictionary<uint, Setup>(),
new Dictionary<uint, GfxObj>());
var envCells = new EnvCellLandblockBuild(
landblockId,
Array.Empty<AcDream.App.Rendering.LoadedCell>(),
Array.Empty<EnvCellShellPlacement>());
using var streamer = LandblockStreamer.CreateForRequests(
loadLandblock: request => new LandblockBuild(
new LoadedLandblock(
request.LandblockId,
new LandBlock(),
Array.Empty<WorldEntity>(),
physics),
envCells,
request.Origin),
buildMeshOrNull: (_, _) => EmptyMesh());
streamer.EnqueueLoad(new LandblockBuildRequest(
landblockId,
LandblockStreamJobKind.LoadFar,
Generation: 84,
origin));
streamer.Start();
#if DEBUG
// The near-payload tripwire is config-divergent by design ("fail loud
// in Debug builds and strip in Release" — LandblockStreamer.HandleJob):
// in Debug the Debug.Assert fires and the VSTest host translates it
// into a thrown DebugAssertException, which the worker's catch folds
// into a Failed completion (#351).
var failed = Assert.IsType<LandblockStreamResult.Failed>(
await DrainFirstAsync(streamer));
Assert.Contains(
"Far-tier factory returned Near payload",
failed.Error,
StringComparison.Ordinal);
Assert.Equal(84ul, failed.Generation);
#else
var loaded = Assert.IsType<LandblockStreamResult.Loaded>(
await DrainFirstAsync(streamer));
Assert.Equal(LandblockStreamTier.Far, loaded.Tier);
Assert.Empty(loaded.Landblock.Entities);
Assert.Same(PhysicsDatBundle.Empty, loaded.Landblock.PhysicsDats);
Assert.Null(loaded.Build.EnvCells);
Assert.Equal(origin, loaded.Build.Origin);
#endif
}
[Fact]
public void BuildFactoryAndRenderPublisher_UseCapturedOriginWithoutGameWindowFacade()
{
string root = FindRepoRoot();
string gameWindowSource = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
string livePresentationSource = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Composition",
"LivePresentationComposition.cs"));
string buildSource = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Streaming",
"LandblockBuildFactory.cs"));
string renderPublisherSource = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Streaming",
"LandblockRenderPublisher.cs"));
string recenterSource = File.ReadAllText(Path.Combine(
root,
"src",
"AcDream.App",
"Streaming",
"StreamingOriginRecenterCoordinator.cs"));
Assert.Contains("request.Origin", buildSource, StringComparison.Ordinal);
Assert.DoesNotContain("_liveCenterX", buildSource, StringComparison.Ordinal);
Assert.DoesNotContain("_liveCenterY", buildSource, StringComparison.Ordinal);
Assert.DoesNotContain(
"BuildLandblockForStreaming",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"BuildSceneryEntitiesForStreaming",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"BuildInteriorEntitiesForStreaming",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"BuildPhysicsDatBundle",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"ApplyLoadedTerrain",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"PublishLandblockStaticLightingBeforeCollision",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"_landblockPhysicsPublisher!.RemoveLandblock",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"_landblockRenderPublisher",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"_landblockPhysicsPublisher",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"_landblockStaticPresentationPublisher",
gameWindowSource,
StringComparison.Ordinal);
Assert.Contains(
"_landblockPresentationPipeline",
gameWindowSource,
StringComparison.Ordinal);
Assert.Contains(
"new LandblockRenderPublisher(",
livePresentationSource,
StringComparison.Ordinal);
Assert.Contains(
"new LandblockPhysicsPublisher(",
livePresentationSource,
StringComparison.Ordinal);
Assert.Contains(
"new LandblockStaticPresentationPublisher(",
livePresentationSource,
StringComparison.Ordinal);
Assert.DoesNotContain("applyTerrain:", gameWindowSource, StringComparison.Ordinal);
Assert.DoesNotContain("demoteNearLayer:", gameWindowSource, StringComparison.Ordinal);
Assert.DoesNotContain("retirementCoordinator:", gameWindowSource, StringComparison.Ordinal);
Assert.DoesNotContain(
"_liveWorldOrigin.Recenter(lbX, lbY)",
gameWindowSource,
StringComparison.Ordinal);
int retirementBarrier = recenterSource.IndexOf(
"IsOriginRecenterRetirementComplete()",
StringComparison.Ordinal);
int originCommit = recenterSource.IndexOf(
"_origin.Recenter(",
StringComparison.Ordinal);
int destinationCommit = recenterSource.IndexOf(
"_streaming.TryCommitOriginRecenter(",
StringComparison.Ordinal);
Assert.True(retirementBarrier >= 0);
Assert.True(originCommit > retirementBarrier);
Assert.True(destinationCommit > originCommit);
Assert.Contains("ComputeOrigin(landblockId, build.Origin)", renderPublisherSource, StringComparison.Ordinal);
Assert.DoesNotContain("_liveCenterX", renderPublisherSource, StringComparison.Ordinal);
Assert.DoesNotContain("_liveCenterY", renderPublisherSource, StringComparison.Ordinal);
}
[Fact]
public void GameWindowShutdownKeepsStreamerAliveUntilSessionResetConverges()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindowLifetime.cs"));
int sessionStage = source.IndexOf(
"new ResourceShutdownStage(\"host and session barriers\"",
StringComparison.Ordinal);
Assert.True(sessionStage >= 0);
int sessionOperation = source.IndexOf(
"Hard(\"game runtime session\", ingress.Runtime.StopSession)",
sessionStage,
StringComparison.Ordinal);
Assert.True(sessionOperation > sessionStage);
int dependentStage = source.IndexOf(
"new ResourceShutdownStage(\"session dependents\"",
sessionOperation,
StringComparison.Ordinal);
Assert.True(dependentStage > sessionOperation);
int streamerDispose = source.IndexOf(
"Hard(\"streamer\", () => live.Streamer?.Dispose())",
dependentStage,
StringComparison.Ordinal);
Assert.True(streamerDispose > dependentStage);
string runtime = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.Runtime",
"GameRuntime.cs"));
int helper = runtime.IndexOf(
"public void StopSession()",
StringComparison.Ordinal);
Assert.True(helper >= 0);
int sessionDispose = runtime.IndexOf(
"Session.Dispose();",
helper,
StringComparison.Ordinal);
Assert.True(sessionDispose > helper);
int disposalCompletionBarrier = runtime.IndexOf(
"if (!Session.IsDisposalComplete)",
sessionDispose,
StringComparison.Ordinal);
Assert.True(disposalCompletionBarrier > sessionDispose);
Assert.Contains(
"The Runtime session shutdown was deferred by a re-entrant callback.",
runtime[disposalCompletionBarrier..],
StringComparison.Ordinal);
}
private static LandblockBuild EmptyBuild(uint landblockId, LandblockBuildOrigin origin) =>
new(
new LoadedLandblock(
landblockId,
new LandBlock(),
Array.Empty<WorldEntity>()),
Origin: origin);
private static LandblockMeshData EmptyMesh() =>
new(Array.Empty<TerrainVertex>(), Array.Empty<uint>());
private static async Task<LandblockStreamResult> DrainFirstAsync(
LandblockStreamer streamer) =>
(await DrainCountAsync(streamer, 1))[0];
private static async Task<IReadOnlyList<LandblockStreamResult>> DrainCountAsync(
LandblockStreamer streamer,
int count)
{
var results = new List<LandblockStreamResult>(count);
for (int i = 0; i < SpinTimeoutMs / SpinStepMs && results.Count < count; i++)
{
results.AddRange(streamer.DrainCompletions(count - results.Count));
if (results.Count < count)
await Task.Delay(SpinStepMs);
}
Assert.Equal(count, results.Count);
return results;
}
private static string FindRepoRoot()
{
string? dir = AppContext.BaseDirectory;
while (dir is not null)
{
if (File.Exists(Path.Combine(dir, "AcDream.slnx")))
return dir;
dir = Directory.GetParent(dir)?.FullName;
}
throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
}
}