test(runtime): add deterministic world gate artifacts
This commit is contained in:
parent
db45b81f75
commit
354c2adc2e
11 changed files with 703 additions and 10 deletions
|
|
@ -0,0 +1,130 @@
|
|||
using System.Text.Json;
|
||||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Streaming;
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace AcDream.App.Tests.Diagnostics;
|
||||
|
||||
public sealed class WorldLifecycleAutomationControllerTests
|
||||
{
|
||||
[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(
|
||||
() => (2, 2),
|
||||
(_, _) => rgba,
|
||||
directory,
|
||||
logs.Add);
|
||||
|
||||
try
|
||||
{
|
||||
Assert.True(controller.TryRequest("login_stable", out string error), error);
|
||||
Assert.False(controller.IsComplete("login_stable"));
|
||||
|
||||
controller.CapturePending();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
[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,
|
||||
TrackedGpuBytes = 1234,
|
||||
};
|
||||
var screenshots = new FrameScreenshotController(
|
||||
() => (1, 1),
|
||||
(_, _) => [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.TryWriteCheckpoint("dungeon", out string error), error);
|
||||
|
||||
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(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32());
|
||||
Assert.True(File.Exists(Path.Combine(directory, "checkpoint-dungeon.json")));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0L, 0, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L, 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -33,12 +33,14 @@ public class RuntimeOptionsRetailUiTests
|
|||
{
|
||||
["ACDREAM_UI_PROBE_DUMP"] = "1",
|
||||
["ACDREAM_UI_PROBE_SCRIPT"] = @"C:\tmp\ui-probe.txt",
|
||||
["ACDREAM_AUTOMATION_ARTIFACT_DIR"] = @"C:\tmp\world-gate",
|
||||
};
|
||||
|
||||
var opts = RuntimeOptions.Parse("dats", k => env.GetValueOrDefault(k));
|
||||
|
||||
Assert.True(opts.UiProbeDump);
|
||||
Assert.Equal(@"C:\tmp\ui-probe.txt", opts.UiProbeScript);
|
||||
Assert.Equal(@"C:\tmp\world-gate", opts.AutomationArtifactDirectory);
|
||||
Assert.True(opts.UiProbeEnabled);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ public sealed class RuntimeOptionsTests
|
|||
Assert.Null(opts.LegacyStreamRadius);
|
||||
Assert.False(opts.UiProbeDump);
|
||||
Assert.Null(opts.UiProbeScript);
|
||||
Assert.Null(opts.AutomationArtifactDirectory);
|
||||
Assert.False(opts.UiProbeEnabled);
|
||||
Assert.False(opts.HasLiveCredentials);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,32 @@ public sealed class RetailUiAutomationProbeTests
|
|||
=> LastDrop = (targetList, targetCell, payload);
|
||||
}
|
||||
|
||||
private sealed class FakeRuntime : IRetailUiAutomationRuntime
|
||||
{
|
||||
public bool IsWorldReady { get; set; }
|
||||
public bool IsWorldViewportVisible { get; set; }
|
||||
public int PortalMaterializationCount { get; set; }
|
||||
public List<string> Checkpoints { get; } = new();
|
||||
public HashSet<string> ScreenshotRequests { get; } = new();
|
||||
public HashSet<string> CompletedScreenshots { get; } = new();
|
||||
|
||||
public bool TryWriteCheckpoint(string name, out string error)
|
||||
{
|
||||
Checkpoints.Add(name);
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryRequestScreenshot(string name, out string error)
|
||||
{
|
||||
ScreenshotRequests.Add(name);
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsScreenshotComplete(string name) => CompletedScreenshots.Contains(name);
|
||||
}
|
||||
|
||||
private static (UiRoot root, UiItemList source, UiItemList target, SpyHandler handler, ClientObjectTable objects)
|
||||
RootWithTwoItemLists()
|
||||
{
|
||||
|
|
@ -527,4 +553,86 @@ public sealed class RetailUiAutomationProbeTests
|
|||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptRunner_worldLifecycleCommands_waitForCanonicalRuntimeState()
|
||||
{
|
||||
var (root, _, _, _, objects) = RootWithTwoItemLists();
|
||||
var runtime = new FakeRuntime();
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
|
||||
File.WriteAllLines(path,
|
||||
[
|
||||
"wait world-ready 1000",
|
||||
"wait world-visible 1000",
|
||||
"wait materialized 2 1000",
|
||||
"checkpoint stable",
|
||||
"screenshot stable 1000",
|
||||
]);
|
||||
|
||||
try
|
||||
{
|
||||
using var runner = new RetailUiAutomationScriptRunner(
|
||||
probe,
|
||||
path,
|
||||
dumpOnStart: false,
|
||||
runtime: runtime);
|
||||
|
||||
runner.Tick(0d);
|
||||
Assert.False(runner.Completed);
|
||||
|
||||
runtime.IsWorldReady = true;
|
||||
runner.Tick(0.001d);
|
||||
Assert.False(runner.Completed);
|
||||
|
||||
runtime.IsWorldViewportVisible = true;
|
||||
runtime.PortalMaterializationCount = 2;
|
||||
runner.Tick(0.001d);
|
||||
|
||||
Assert.Equal(["stable"], runtime.Checkpoints);
|
||||
Assert.Contains("stable", runtime.ScreenshotRequests);
|
||||
Assert.False(runner.Completed);
|
||||
|
||||
runtime.CompletedScreenshots.Add("stable");
|
||||
runner.Tick(0.001d);
|
||||
|
||||
Assert.True(runner.Completed);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptRunner_worldReadyTimeout_stopsWithActionableLine()
|
||||
{
|
||||
var (root, _, _, _, objects) = RootWithTwoItemLists();
|
||||
var runtime = new FakeRuntime();
|
||||
var probe = new RetailUiAutomationProbe(root, objects);
|
||||
var logs = new List<string>();
|
||||
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".ui-probe.txt");
|
||||
File.WriteAllText(path, "wait world-ready 10");
|
||||
|
||||
try
|
||||
{
|
||||
using var runner = new RetailUiAutomationScriptRunner(
|
||||
probe,
|
||||
path,
|
||||
dumpOnStart: false,
|
||||
log: logs.Add,
|
||||
runtime: runtime);
|
||||
|
||||
runner.Tick(0d);
|
||||
runner.Tick(0.011d);
|
||||
|
||||
Assert.True(runner.Completed);
|
||||
Assert.Contains(logs, line =>
|
||||
line.Contains("timed out waiting for world readiness", StringComparison.Ordinal));
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue