test(app): freeze GameWindow lifecycle boundaries
Pin the accepted startup, input, frame, resize, shutdown, and native-window order before Slice 8 moves those edges, while deleting only unread duplicate state and test-only GameWindow facades. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
20ebf217e2
commit
476c2e6de1
7 changed files with 380 additions and 56 deletions
|
|
@ -1,6 +1,5 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Net.Packets;
|
||||
using AcDream.Core.Physics;
|
||||
|
|
@ -21,7 +20,7 @@ public sealed class LocalPlayerOutboundCombatStyleTests
|
|||
|
||||
MovementResult movement = controller.CaptureMovementResult(
|
||||
mouseLookEvent: false);
|
||||
RawMotionState outbound = GameWindow.BuildOutboundRawMotionState(
|
||||
RawMotionState outbound = LocalPlayerOutboundController.BuildRawMotionState(
|
||||
movement);
|
||||
|
||||
Assert.Equal(combatStyle, movement.CurrentStyle);
|
||||
|
|
@ -43,7 +42,7 @@ public sealed class LocalPlayerOutboundCombatStyleTests
|
|||
{
|
||||
var controller = new PlayerMovementController(new PhysicsEngine());
|
||||
|
||||
RawMotionState outbound = GameWindow.BuildOutboundRawMotionState(
|
||||
RawMotionState outbound = LocalPlayerOutboundController.BuildRawMotionState(
|
||||
controller.CaptureMovementResult(mouseLookEvent: false));
|
||||
|
||||
Assert.Equal(RawMotionState.Default.CurrentStyle, outbound.CurrentStyle);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using Xunit;
|
||||
|
|
@ -259,7 +258,7 @@ public sealed class PlayerMouseLookMovementTests
|
|||
|
||||
controller.BeginMouseLook(new MovementInput(Run: true));
|
||||
MovementResult press = controller.CaptureMovementResult(mouseLookEvent: false);
|
||||
RawMotionState wire = GameWindow.BuildOutboundRawMotionState(press);
|
||||
RawMotionState wire = LocalPlayerOutboundController.BuildRawMotionState(press);
|
||||
|
||||
Assert.Equal(HoldKey.Run, controller.Motion.RawState.CurrentHoldKey);
|
||||
Assert.True(press.IsRunning);
|
||||
|
|
@ -325,7 +324,7 @@ public sealed class PlayerMouseLookMovementTests
|
|||
|
||||
controller.BeginMouseLook(held);
|
||||
MovementResult press = controller.CaptureMovementResult(mouseLookEvent: false);
|
||||
RawMotionState wire = GameWindow.BuildOutboundRawMotionState(press);
|
||||
RawMotionState wire = LocalPlayerOutboundController.BuildRawMotionState(press);
|
||||
|
||||
Assert.Equal(MotionCommand.SideStepRight, press.SidestepCommand);
|
||||
Assert.True(press.SidestepUsesRunHold);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,277 @@
|
|||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary source-shape freeze for the Slice 8 cutover. Each later checkpoint
|
||||
/// replaces the corresponding assertion with functional tests on the extracted
|
||||
/// owner; this file prevents a mechanical move from silently reordering host
|
||||
/// behavior before that owner exists.
|
||||
/// </summary>
|
||||
public sealed class GameWindowSlice8BoundaryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Run_PreservesNativeAttributeAndCallbackOrder()
|
||||
{
|
||||
string body = MethodBody("public void Run()", "private void OnLoad()");
|
||||
|
||||
AssertAppearsInOrder(
|
||||
body,
|
||||
"startupStore.LoadDisplay()",
|
||||
"QualitySettings.WithEnvOverrides(startupBase)",
|
||||
"_displayFramePacing.InitializeStartup(startupDisplay.VSync)",
|
||||
"VSync = startupPacing.UseVSync",
|
||||
"Samples = startupQuality.MsaaSamples",
|
||||
"Window.Create(options)",
|
||||
"_displayFramePacing.BindSurface(",
|
||||
"_window.Load += OnLoad;",
|
||||
"_window.Update += OnUpdate;",
|
||||
"_window.Render += OnRender;",
|
||||
"_window.Render += _displayFramePacing.OnFrameRendered;",
|
||||
"_window.Closing += OnClosing;",
|
||||
"_window.FocusChanged += OnFocusChanged;",
|
||||
"_window.Move += _displayFramePacing.OnWindowMoved;",
|
||||
"_window.StateChanged += _displayFramePacing.OnWindowStateChanged;",
|
||||
"_window.FramebufferResize += OnFramebufferResize;",
|
||||
"_window.Run();");
|
||||
|
||||
string[] registrations =
|
||||
[
|
||||
"_window.Load += OnLoad;",
|
||||
"_window.Update += OnUpdate;",
|
||||
"_window.Render += OnRender;",
|
||||
"_window.Render += _displayFramePacing.OnFrameRendered;",
|
||||
"_window.Closing += OnClosing;",
|
||||
"_window.FocusChanged += OnFocusChanged;",
|
||||
"_window.Move += _displayFramePacing.OnWindowMoved;",
|
||||
"_window.StateChanged += _displayFramePacing.OnWindowStateChanged;",
|
||||
"_window.FramebufferResize += OnFramebufferResize;",
|
||||
];
|
||||
foreach (string registration in registrations)
|
||||
Assert.Equal(1, CountOccurrences(body, registration));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionStart_FollowsFrameGraphPublication()
|
||||
{
|
||||
string body = MethodBody(
|
||||
"private void OnLoad()",
|
||||
"private AcDream.App.Net.LiveSessionLifecycleHost");
|
||||
|
||||
AssertAppearsInOrder(
|
||||
body,
|
||||
"_gl = GL.GetApi(_window!);",
|
||||
"_input = _window!.CreateInput();",
|
||||
"_kbSource = new AcDream.App.Input.SilkKeyboardSource(firstKb);",
|
||||
"_mouseSource = new AcDream.App.Input.SilkMouseSource(",
|
||||
"_inputDispatcher = new AcDream.UI.Abstractions.Input.InputDispatcher(",
|
||||
"_movementInput.Bind(_inputDispatcher);",
|
||||
"_cameraInput.Bind(_inputDispatcher);",
|
||||
"_inputDispatcher.Fired += OnInputAction;",
|
||||
"mouse.MouseMove +=",
|
||||
"_cameraController = new CameraController(orbit, fly);",
|
||||
"_dats = RuntimeDatCollectionFactory.OpenReadOnly(_datDir);",
|
||||
"LoadAndApplyPersistedSettings();",
|
||||
"_uiHost = new AcDream.App.UI.UiHost(",
|
||||
"_uiHost.WireMouse(m)",
|
||||
"_uiHost.WireKeyboard(kb)",
|
||||
"_retailUiRuntime = AcDream.App.UI.RetailUiRuntime.Mount(",
|
||||
"_liveEntities = new AcDream.App.World.LiveEntityRuntime(",
|
||||
"_renderFrameOrchestrator =",
|
||||
"_updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(",
|
||||
"_liveSessionController.Start(");
|
||||
Assert.Equal(1, CountOccurrences(body, "_liveSessionController.Start("));
|
||||
int start = body.IndexOf("_liveSessionController.Start(", StringComparison.Ordinal);
|
||||
string postStart = body[start..];
|
||||
Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal);
|
||||
Assert.Empty(System.Text.RegularExpressions.Regex.Matches(
|
||||
postStart,
|
||||
@"(?m)^\s+_[A-Za-z]\w*\s*(?:\?\?=|=)"));
|
||||
Assert.DoesNotContain("new ", postStart, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(".Mount(", postStart, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(".Bind(", postStart, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(".Attach(", postStart, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(".Compose(", postStart, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("PrepareResources(", postStart, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InputAction_PreservesRetailAcceptedPriority()
|
||||
{
|
||||
string body = MethodBody(
|
||||
"private void OnInputAction(",
|
||||
"private void ToggleLiveCombatMode()");
|
||||
|
||||
AssertAppearsInOrder(
|
||||
body,
|
||||
"HandlePointerAction(action, activation)",
|
||||
"action == AcDream.UI.Abstractions.Input.InputAction.ScrollUp",
|
||||
"HandleScrollAction(action);",
|
||||
"HandleCombatAction(action, activation)",
|
||||
"activation != AcDream.UI.Abstractions.Input.ActivationType.Press",
|
||||
"_retailUiRuntime?.HandleInputAction(action)",
|
||||
"_selectionInteractions?.HandleInputAction(action)",
|
||||
"HandlePressedMovementAction(action)",
|
||||
"switch (action)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FramebufferResize_PreservesViewportCameraAndDevtoolsOrder()
|
||||
{
|
||||
string body = MethodBody(
|
||||
"private void OnFramebufferResize(",
|
||||
"private void ApplyDisplayWindowState(");
|
||||
|
||||
AssertAppearsInOrder(
|
||||
body,
|
||||
"if (newSize.X <= 0 || newSize.Y <= 0) return;",
|
||||
"_gl?.Viewport(",
|
||||
"_viewportAspect.Update(",
|
||||
"_cameraController?.SetAspect(",
|
||||
"_devToolsFramePresenter?.ResetLayout(");
|
||||
Assert.DoesNotContain("_uiHost", body, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateRenderFocusAndCloseRemainNarrowHostEdges()
|
||||
{
|
||||
string source = GameWindowSource();
|
||||
string update = Slice(
|
||||
source,
|
||||
"private void OnUpdate(double dt)",
|
||||
"private void OnCameraModeChanged(");
|
||||
string render = Slice(
|
||||
source,
|
||||
"private void OnRender(double deltaSeconds)",
|
||||
"private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot");
|
||||
string focus = Slice(
|
||||
source,
|
||||
"private void OnFocusChanged(bool focused)",
|
||||
"public void Dispose()");
|
||||
string close = Slice(
|
||||
source,
|
||||
"private void OnClosing()",
|
||||
"private ResourceShutdownTransaction CreateShutdownTransaction()");
|
||||
|
||||
Assert.Equal(1, CountOccurrences(update, "_updateFrameOrchestrator.Tick("));
|
||||
Assert.Equal(1, CountOccurrences(render, "_renderFrameOrchestrator!.Render("));
|
||||
AssertAppearsInOrder(
|
||||
render,
|
||||
"Vector2D<int> size = _window!.Size;",
|
||||
"_renderFrameOrchestrator!.Render(",
|
||||
"new AcDream.App.Rendering.RenderFrameInput(");
|
||||
Assert.DoesNotContain("FramebufferSize", render, StringComparison.Ordinal);
|
||||
AssertAppearsInOrder(focus, "if (!focused)", "_gameplayInputFrame?.EndMouseLook();");
|
||||
AssertAppearsInOrder(close, "=> CompleteShutdown();", "_shutdown.CompleteOrThrow();");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shutdown_PreservesDependencyStagesAndNativeWindowLast()
|
||||
{
|
||||
string source = GameWindowSource();
|
||||
string manifest = Slice(
|
||||
source,
|
||||
"private ResourceShutdownTransaction CreateShutdownTransaction()",
|
||||
"private void OnFocusChanged(bool focused)");
|
||||
string dispose = Slice(
|
||||
source,
|
||||
"public void Dispose()",
|
||||
"private sealed class NullAnimLoader");
|
||||
|
||||
string[] stages =
|
||||
[
|
||||
"new ResourceShutdownStage(\"session lifetime\"",
|
||||
"new ResourceShutdownStage(\"frame borrowers\"",
|
||||
"new ResourceShutdownStage(\"session dependents\"",
|
||||
"new ResourceShutdownStage(\"live entities\"",
|
||||
"new ResourceShutdownStage(\"live entity dependents\"",
|
||||
"new ResourceShutdownStage(\"submitted GPU work\"",
|
||||
"new ResourceShutdownStage(\"render frontends\"",
|
||||
"new ResourceShutdownStage(\"shared texture owners\"",
|
||||
"new ResourceShutdownStage(\"mesh adapter\"",
|
||||
"new ResourceShutdownStage(\"remaining render owners\"",
|
||||
"new ResourceShutdownStage(\"frame flight owner\"",
|
||||
"new ResourceShutdownStage(\"content mappings\"",
|
||||
"new ResourceShutdownStage(\"input\"",
|
||||
"new ResourceShutdownStage(\"OpenGL context\"",
|
||||
];
|
||||
AssertAppearsInOrder(manifest, stages);
|
||||
Assert.Equal(stages.Length, CountOccurrences(manifest, "new ResourceShutdownStage("));
|
||||
foreach (string stage in stages)
|
||||
Assert.Equal(1, CountOccurrences(manifest, stage));
|
||||
AssertAppearsInOrder(
|
||||
dispose,
|
||||
"CompleteShutdown();",
|
||||
"_window?.Dispose();",
|
||||
"_window = null;");
|
||||
Assert.Equal(1, CountOccurrences(dispose, "CompleteShutdown();"));
|
||||
Assert.Equal(1, CountOccurrences(dispose, "_window?.Dispose();"));
|
||||
Assert.Equal(1, CountOccurrences(dispose, "_window = null;"));
|
||||
int nativeReleased = dispose.IndexOf("_window = null;", StringComparison.Ordinal)
|
||||
+ "_window = null;".Length;
|
||||
string afterNativeRelease = dispose[nativeReleased..];
|
||||
int fallbackDocumentation = afterNativeRelease.IndexOf(
|
||||
"/// <summary>",
|
||||
StringComparison.Ordinal);
|
||||
if (fallbackDocumentation >= 0)
|
||||
afterNativeRelease = afterNativeRelease[..fallbackDocumentation];
|
||||
Assert.All(
|
||||
afterNativeRelease,
|
||||
character => Assert.True(char.IsWhiteSpace(character) || character == '}'));
|
||||
}
|
||||
|
||||
private static string MethodBody(string start, string end) =>
|
||||
Slice(GameWindowSource(), start, end);
|
||||
|
||||
private static string Slice(string source, string start, string end)
|
||||
{
|
||||
int first = source.IndexOf(start, StringComparison.Ordinal);
|
||||
int last = source.IndexOf(end, first + 1, StringComparison.Ordinal);
|
||||
Assert.True(first >= 0, $"Missing source boundary: {start}");
|
||||
Assert.True(last > first, $"Missing source boundary: {end}");
|
||||
return source[first..last];
|
||||
}
|
||||
|
||||
private static int CountOccurrences(string source, string value)
|
||||
{
|
||||
int count = 0;
|
||||
int cursor = 0;
|
||||
while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0)
|
||||
{
|
||||
count++;
|
||||
cursor += value.Length;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void AssertAppearsInOrder(string source, params string[] fragments)
|
||||
{
|
||||
int cursor = -1;
|
||||
foreach (string fragment in fragments)
|
||||
{
|
||||
int next = source.IndexOf(fragment, cursor + 1, StringComparison.Ordinal);
|
||||
Assert.True(next >= 0, $"Missing expected source fragment: {fragment}");
|
||||
Assert.True(next > cursor, $"Out-of-order source fragment: {fragment}");
|
||||
cursor = next;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GameWindowSource() => File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||
return directory.FullName;
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||
}
|
||||
}
|
||||
|
|
@ -78,4 +78,85 @@ public sealed class ResourceShutdownTransactionTests
|
|||
Assert.Equal(1, calls);
|
||||
Assert.True(transaction.IsComplete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PersistentFailuresInOneStageAreAllAttemptedAndNamed()
|
||||
{
|
||||
int firstCalls = 0;
|
||||
int secondCalls = 0;
|
||||
var transaction = new ResourceShutdownTransaction(
|
||||
new ResourceShutdownStage("owners",
|
||||
[
|
||||
new("first", () =>
|
||||
{
|
||||
firstCalls++;
|
||||
throw new InvalidOperationException("first failure");
|
||||
}),
|
||||
new("second", () =>
|
||||
{
|
||||
secondCalls++;
|
||||
throw new InvalidOperationException("second failure");
|
||||
}),
|
||||
]));
|
||||
|
||||
AggregateException error = Assert.Throws<AggregateException>(
|
||||
transaction.CompleteOrThrow);
|
||||
|
||||
Assert.True(firstCalls > 0);
|
||||
Assert.Equal(firstCalls, secondCalls);
|
||||
Assert.Contains(
|
||||
error.InnerExceptions,
|
||||
exception => exception.Message.Contains("first", StringComparison.Ordinal));
|
||||
Assert.Contains(
|
||||
error.InnerExceptions,
|
||||
exception => exception.Message.Contains("second", StringComparison.Ordinal));
|
||||
Assert.False(transaction.IsComplete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitRetryAfterFailedCompletionDoesNotReplaySuccessfulOperations()
|
||||
{
|
||||
int completeCalls = 0;
|
||||
int pendingCalls = 0;
|
||||
bool allowPending = false;
|
||||
var transaction = new ResourceShutdownTransaction(
|
||||
new ResourceShutdownStage("owners",
|
||||
[
|
||||
new("complete", () => completeCalls++),
|
||||
new("pending", () =>
|
||||
{
|
||||
pendingCalls++;
|
||||
if (!allowPending)
|
||||
throw new InvalidOperationException("not yet");
|
||||
}),
|
||||
]));
|
||||
|
||||
Assert.Throws<AggregateException>(transaction.CompleteOrThrow);
|
||||
int pendingCallsBeforeRetry = pendingCalls;
|
||||
allowPending = true;
|
||||
|
||||
transaction.CompleteOrThrow();
|
||||
|
||||
Assert.Equal(1, completeCalls);
|
||||
Assert.Equal(pendingCallsBeforeRetry + 1, pendingCalls);
|
||||
Assert.True(transaction.IsComplete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyStagesConvergeWithoutSkippingLaterOperations()
|
||||
{
|
||||
int calls = 0;
|
||||
var transaction = new ResourceShutdownTransaction(
|
||||
new ResourceShutdownStage("empty", []),
|
||||
new ResourceShutdownStage("owner",
|
||||
[
|
||||
new("owner", () => calls++),
|
||||
]),
|
||||
new ResourceShutdownStage("empty tail", []));
|
||||
|
||||
transaction.CompleteOrThrow();
|
||||
|
||||
Assert.Equal(1, calls);
|
||||
Assert.True(transaction.IsComplete);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Vfx;
|
||||
|
|
@ -472,7 +471,6 @@ public sealed class EntityScriptActivatorTests
|
|||
|
||||
Assert.Contains("globally unique", error.Message, StringComparison.Ordinal);
|
||||
Assert.Equal(1, p.Runner.ActiveOwnerCount);
|
||||
Assert.Equal(first.Id, GameWindow.ParticleEntityKey(first));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -569,8 +567,13 @@ public sealed class EntityScriptActivatorTests
|
|||
[Fact]
|
||||
public void LiveParticleFilterUsesCanonicalLocalEffectOwnerNotServerGuid()
|
||||
{
|
||||
var p = BuildPipeline();
|
||||
var activator = new EntityScriptActivator(p.Runner, p.Sink, p.Poses, _ => null);
|
||||
var p = BuildPipeline(
|
||||
(0xAAu, BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100 }))));
|
||||
var activator = new EntityScriptActivator(
|
||||
p.Runner,
|
||||
p.Sink,
|
||||
p.Poses,
|
||||
StaticResolver(0xAAu));
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 1_000_123u,
|
||||
|
|
@ -582,9 +585,11 @@ public sealed class EntityScriptActivatorTests
|
|||
};
|
||||
|
||||
activator.OnCreate(entity);
|
||||
p.Runner.Tick(0.001f);
|
||||
|
||||
Assert.Equal(entity.Id, GameWindow.ParticleEntityKey(entity));
|
||||
Assert.NotEqual(entity.ServerGuid, GameWindow.ParticleEntityKey(entity));
|
||||
Assert.Single(p.Recording.Calls);
|
||||
Assert.Equal(entity.Id, p.Recording.Calls[0].EntityId);
|
||||
Assert.NotEqual(entity.ServerGuid, p.Recording.Calls[0].EntityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue