acdream/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs
Erik f9b68f8f2a feat(rendering): complete current-path render referee
Extend the non-drawing oracle through ordered PView routes, dispatcher visibility and final instance payloads, and accepted retail selection parts. Lifecycle artifacts can now referee the later shadow scene without influencing production visibility or draw decisions.
2026-07-24 21:28:12 +02:00

367 lines
14 KiB
C#

using System.Text.Json;
using AcDream.App.Diagnostics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Residency;
using AcDream.App.Rendering.Scene;
using AcDream.App.Streaming;
using AcDream.App.UI.Testing;
using SixLabors.ImageSharp;
namespace AcDream.App.Tests.Diagnostics;
public sealed class WorldLifecycleAutomationControllerTests
{
private static readonly RenderFrameInput FrameInput = new(1d / 60d, 1280, 720);
private static readonly RenderFrameOutcome FrameOutcome = new(
new WorldRenderFrameOutcome(4, 17, NormalWorldDrawn: true),
new PrivatePresentationFrameOutcome(
PortalViewportDrawn: false,
ScreenshotCaptured: true));
[Fact]
public void ScreenshotCapture_FlipsGlRowsAndWritesACompletePng()
{
string directory = NewDirectory();
// GL order: bottom row red/green, top row blue/white.
byte[] rgba =
[
255, 0, 0, 255, 0, 255, 0, 255,
0, 0, 255, 255, 255, 255, 255, 255,
];
var logs = new List<string>();
var controller = new FrameScreenshotController(
(_, _) => rgba,
directory,
logs.Add);
try
{
Assert.True(controller.TryRequest("login_stable", out string error), error);
Assert.False(controller.IsComplete("login_stable"));
Assert.True(controller.CapturePending(2, 2));
Assert.True(controller.IsComplete("login_stable"));
string path = Path.Combine(directory, "login_stable.png");
using Image image = Image.Load(path);
Assert.Equal(2, image.Width);
Assert.Equal(2, image.Height);
Assert.Contains(logs, line => line.Contains("screenshot-complete", StringComparison.Ordinal));
byte[] flipped = FrameScreenshotController.FlipRows(rgba, 2, 2);
Assert.Equal(rgba.AsSpan(8, 8).ToArray(), flipped.AsSpan(0, 8).ToArray());
Assert.Equal(rgba.AsSpan(0, 8).ToArray(), flipped.AsSpan(8, 8).ToArray());
}
finally
{
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void ScreenshotCapture_ReportsNoWorkAndFailedCapture()
{
string directory = NewDirectory();
var controller = new FrameScreenshotController(
(_, _) => [],
directory);
try
{
Assert.False(controller.CapturePending(0, 0));
Assert.True(controller.TryRequest("bad_frame", out string error), error);
Assert.False(controller.CapturePending(0, 0));
Assert.False(controller.IsComplete("bad_frame"));
}
finally
{
if (Directory.Exists(directory))
Directory.Delete(directory, recursive: true);
}
}
[Theory]
[InlineData("")]
[InlineData("../escape")]
[InlineData("has spaces")]
[InlineData("name.png")]
public void ArtifactNames_RejectPathsAndAmbiguousCharacters(string name)
{
Assert.False(AutomationArtifactName.TryValidate(name, out string error));
Assert.NotEmpty(error);
}
[Fact]
public void Checkpoint_WritesCanonicalRevealAndResourceSnapshot()
{
string directory = NewDirectory();
var reveal = new WorldRevealLifecycleSnapshot(
Generation: 7,
Kind: WorldRevealKind.Portal,
Readiness: new WorldRevealReadinessSnapshot(
0x8A020164u,
IsIndoor: true,
IsUnhydratable: false,
RequiredRenderRadius: 0,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true),
Materialized: true,
Completed: true,
Cancelled: false,
WorldViewportObserved: true,
InvariantFailureCount: 0);
var resources = EmptyResources() with
{
LoadedLandblocks = 1,
WorldEntities = 42,
RenderSceneOracle = new CurrentRenderSceneOracleSnapshot(
Enabled: true,
CompletedFrameSequence: 19,
AbortedFrames: 1,
ProjectionCount: 73,
OutdoorStaticCount: 41,
CellStaticCount: 20,
DynamicCount: 12,
CellBucketCount: 4,
Digest: new RenderSceneHash128(
Low: 0x0123456789ABCDEF,
High: 0xFEDCBA9876543210),
CompletedPViewFrameSequence: 19,
AbortedPViewFrames: 0,
PViewCandidateCount: 61,
PViewDigest: new RenderSceneHash128(11, 12),
DispatcherFrameSequence: 19,
AbortedDispatcherFrames: 0,
DispatcherDrawCount: 3,
DispatcherEntityCount: 52,
DispatcherMeshRefCount: 80,
DispatcherInstanceCount: 96,
DispatcherOpaqueGroupCount: 11,
DispatcherTransparentGroupCount: 4,
DispatcherDigest: new RenderSceneHash128(13, 14),
CompletedSelectionFrameSequence: 19,
AbortedSelectionFrames: 0,
SelectionPartCount: 7,
SelectionDigest: new RenderSceneHash128(15, 16)),
TrackedGpuBytes = 1234,
Residency = new ResidencySnapshot(
[
new ResidencyDomainSnapshot(
ResidencyDomain.Animations,
EntryCount: 2,
OwnerCount: 0,
Charges: new ResidencyCharges(
DecodedBytes: 1024),
BudgetBytes: 4096,
Hits: 8,
Misses: 3,
Evictions: 1),
],
new ResidencyCharges(DecodedBytes: 1024)),
};
var screenshots = new FrameScreenshotController(
(_, _) => [0, 0, 0, 255],
Path.Combine(directory, "screenshots"));
var controller = new WorldLifecycleAutomationController(
() => reveal,
() => 3,
_ => resources,
screenshots,
directory);
try
{
Assert.True(controller.IsWorldReady);
Assert.True(controller.IsWorldViewportVisible);
Assert.Equal(3, controller.PortalMaterializationCount);
Assert.True(controller.TryRequestCheckpoint(
"dungeon",
out IRetailUiAutomationCheckpoint? request,
out string error), error);
Assert.NotNull(request);
Assert.Equal(RetailUiAutomationCheckpointStatus.Pending, request.Status);
controller.Process(FrameInput, FrameOutcome);
Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, request.Status);
string line = Assert.Single(File.ReadAllLines(
Path.Combine(directory, "world-lifecycle.checkpoints.jsonl")));
using JsonDocument json = JsonDocument.Parse(line);
Assert.Equal("dungeon", json.RootElement.GetProperty("name").GetString());
Assert.Equal(7, json.RootElement.GetProperty("reveal").GetProperty("generation").GetInt64());
Assert.Equal(4, json.RootElement.GetProperty("render").GetProperty("world")
.GetProperty("visibleLandblocks").GetInt32());
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
.GetProperty("screenshotCaptured").GetBoolean());
Assert.Equal(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32());
JsonElement renderSceneOracle = json.RootElement
.GetProperty("resources")
.GetProperty("renderSceneOracle");
Assert.True(renderSceneOracle.GetProperty("enabled").GetBoolean());
Assert.Equal(
19,
renderSceneOracle.GetProperty("completedFrameSequence").GetInt64());
Assert.Equal(
73,
renderSceneOracle.GetProperty("projectionCount").GetInt32());
Assert.Equal(
0x0123456789ABCDEFuL,
renderSceneOracle.GetProperty("digest").GetProperty("low")
.GetUInt64());
Assert.Equal(
0xFEDCBA9876543210uL,
renderSceneOracle.GetProperty("digest").GetProperty("high")
.GetUInt64());
JsonElement residency = json.RootElement
.GetProperty("resources")
.GetProperty("residency");
Assert.Equal(
4096,
residency.GetProperty("totalBudgetBytes").GetInt64());
Assert.Equal(8, residency.GetProperty("totalHits").GetInt64());
Assert.Equal(
1024,
residency.GetProperty("totalCharges")
.GetProperty("decodedBytes")
.GetInt64());
Assert.True(File.Exists(Path.Combine(directory, "checkpoint-dungeon.json")));
}
finally
{
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void CheckpointRequests_DrainInFifoOrderOnce()
{
string directory = NewDirectory();
int captures = 0;
var controller = CreateController(
directory,
_ =>
{
captures++;
return EmptyResources();
});
try
{
Assert.True(controller.TryRequestCheckpoint(
"first", out IRetailUiAutomationCheckpoint? first, out _));
Assert.True(controller.TryRequestCheckpoint(
"second", out IRetailUiAutomationCheckpoint? second, out _));
controller.Process(FrameInput, FrameOutcome);
controller.Process(FrameInput, FrameOutcome);
Assert.NotNull(first);
Assert.NotNull(second);
Assert.Equal(1, first.Sequence);
Assert.Equal(2, second.Sequence);
Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, first.Status);
Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, second.Status);
Assert.Equal(2, captures);
string[] lines = File.ReadAllLines(
Path.Combine(directory, "world-lifecycle.checkpoints.jsonl"));
Assert.Equal(2, lines.Length);
Assert.Contains("\"name\":\"first\"", lines[0], StringComparison.Ordinal);
Assert.Contains("\"name\":\"second\"", lines[1], StringComparison.Ordinal);
}
finally
{
controller.Dispose();
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void CheckpointWriteFailure_IsReportedOnTokenWithoutThrowingFromFrame()
{
string directory = NewDirectory();
string blockedPath = Path.Combine(directory, "not-a-directory");
File.WriteAllText(blockedPath, "occupied");
var controller = CreateController(blockedPath, _ => EmptyResources());
try
{
Assert.True(controller.TryRequestCheckpoint(
"failed", out IRetailUiAutomationCheckpoint? request, out _));
controller.Process(FrameInput, FrameOutcome);
Assert.NotNull(request);
Assert.Equal(RetailUiAutomationCheckpointStatus.Failed, request.Status);
Assert.Contains("checkpoint 'failed' sequence 1 failed", request.Error);
}
finally
{
controller.Dispose();
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void CheckpointCancellationAndShutdown_AreExplicitAndNeverWritten()
{
string directory = NewDirectory();
var controller = CreateController(directory, _ => EmptyResources());
try
{
Assert.True(controller.TryRequestCheckpoint(
"caller", out IRetailUiAutomationCheckpoint? caller, out _));
Assert.True(controller.TryRequestCheckpoint(
"shutdown", out IRetailUiAutomationCheckpoint? shutdown, out _));
Assert.NotNull(caller);
Assert.NotNull(shutdown);
controller.CancelCheckpoint(caller);
controller.Dispose();
controller.Process(FrameInput, FrameOutcome);
Assert.Equal(RetailUiAutomationCheckpointStatus.Cancelled, caller.Status);
Assert.Contains("cancelled before render-frame capture", caller.Error);
Assert.Equal(RetailUiAutomationCheckpointStatus.Cancelled, shutdown.Status);
Assert.Contains("cancelled during world lifecycle automation shutdown", shutdown.Error);
Assert.False(controller.TryRequestCheckpoint(
"late", out IRetailUiAutomationCheckpoint? late, out string error));
Assert.Null(late);
Assert.Contains("shutting down", error);
Assert.False(File.Exists(Path.Combine(
directory,
"world-lifecycle.checkpoints.jsonl")));
}
finally
{
controller.Dispose();
Directory.Delete(directory, recursive: true);
}
}
private static WorldLifecycleAutomationController CreateController(
string directory,
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> capture) =>
new(
() => default,
() => 0,
capture,
new FrameScreenshotController((_, _) => [], directory),
directory);
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
0, 0, 0, 0, 0, 0, 0, CurrentRenderSceneOracleSnapshot.Disabled,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
default, default, 0d, 0d, null);
private static string NewDirectory()
{
string path = Path.Combine(Path.GetTempPath(), "acdream-world-gate-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(path);
return path;
}
}