From 5e56045077fe4118f6dd2730af450c4a433b75b5 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 18 Aug 2026 16:12:50 +0200 Subject: [PATCH] test: replace render leaf source freezes --- .../2026-08-18-r3-test-truth-ledger.md | 40 ++ .../Architecture/CompiledCallGraph.cs | 38 ++ .../GameWindowRenderLeafCompositionTests.cs | 514 ++++++++++-------- 3 files changed, 378 insertions(+), 214 deletions(-) diff --git a/docs/reviews/2026-08-18-r3-test-truth-ledger.md b/docs/reviews/2026-08-18-r3-test-truth-ledger.md index 80000609..ec0a708f 100644 --- a/docs/reviews/2026-08-18-r3-test-truth-ledger.md +++ b/docs/reviews/2026-08-18-r3-test-truth-ledger.md @@ -1301,3 +1301,43 @@ Verification: attributed methods, and reduces direct/total source readers from 44/70 to 41/64. The remaining 64 reconcile to the 22 approved retained policies/contracts and 42 staged replacements. + +## Batch AB render-leaf and frame-handoff source-freeze replacement + +Batch AB converts all nine approved `GameWindowRenderLeafCompositionTests` +source readers. No product source changes and no test is removed. + +Private presentation, resource preparation, world/private/diagnostic routing, +GPU measurement, and immutable outcome construction now follow compiled typed +calls in their production order. The portal-tunnel check follows the compiled +session transfer/bind/publication path, both local teleport factory targets, +the session-before-frame phase references in `OnLoad`, and the exact frame-root +borrower construction order. `OnRender` now proves its single +`GameFrameGraphSlot.Render` handoff and absence of the five displaced leaf +field reads directly from the built method. + +The broad legacy-owner blacklist is narrowed to what it actually protects: +reflected `GameWindow` fields and methods reject the displaced owners, while +compiled composition metadata positively locates paperdoll, frame-resource, +weather, private-presentation, orchestrator, frame-pacing, and mouse-capture +ownership behind their focused types. Shutdown ordering uses the compiled +operation/stage labels. Terrain diagnostics are constructed with the scene +executor but not driven directly by the frame root. + +The optional paperdoll contract is stronger than the old source-expression +match: compiled branch destinations prove that either a missing viewport or a +missing inventory frame jumps past `PaperdollFramePresenter` construction. +`CompiledCallGraph` gained a reusable branch-target reader for this kind of +optional-dependency guard. + +Verification: + +- all nine focused render-leaf methods pass; +- the complete locked Release build covers all 44 projects with zero warnings + and zero errors; +- the no-retry complete hermetic Release gate remains 14,346/14,346 with zero + skips or failures across all 12 test assemblies; and +- the regenerated 1,254-file inventory parses every file, remains at 11,414 + attributed methods, and reduces direct/total source readers from 41/64 to + 39/55. The remaining 55 reconcile to the 22 approved retained + policies/contracts and 33 staged replacements. diff --git a/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs b/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs index 4228a501..ca563bdf 100644 --- a/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs +++ b/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs @@ -9,6 +9,10 @@ internal readonly record struct CompiledFieldReference( int Offset, OpCode OpCode, FieldInfo Field); +internal readonly record struct CompiledBranch( + int Offset, + OpCode OpCode, + int TargetOffset); /// /// Reads compiled call/new-object edges from a method body. Architecture tests @@ -146,6 +150,40 @@ internal static class CompiledCallGraph return references; } + /// + /// Reads short and long branch destinations. Tests use this to prove that + /// an optional dependency guard jumps around a construction edge without + /// freezing the source expression that produced the branch. + /// + public static IReadOnlyList ReadBranches(MethodBase method) + { + ArgumentNullException.ThrowIfNull(method); + byte[] il = method.GetMethodBody()?.GetILAsByteArray() + ?? throw new InvalidOperationException( + $"{method.DeclaringType?.FullName}.{method.Name} has no compiled body."); + var branches = new List(); + + for (int cursor = 0; cursor < il.Length;) + { + int instructionOffset = cursor; + OpCode opCode = ReadOpCode(il, ref cursor); + if (opCode.OperandType == OperandType.ShortInlineBrTarget) + { + int target = cursor + sizeof(sbyte) + unchecked((sbyte)il[cursor]); + branches.Add(new CompiledBranch(instructionOffset, opCode, target)); + } + else if (opCode.OperandType == OperandType.InlineBrTarget) + { + int target = cursor + sizeof(int) + BitConverter.ToInt32(il, cursor); + branches.Add(new CompiledBranch(instructionOffset, opCode, target)); + } + + cursor += OperandSize(opCode.OperandType, il, cursor); + } + + return branches; + } + /// /// Reads types named by compiled type operands such as casts, boxing, and /// isinst. This lets tests retain an exact type boundary without diff --git a/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs b/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs index 880d4596..151fda1f 100644 --- a/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs +++ b/tests/AcDream.App.Tests/Rendering/GameWindowRenderLeafCompositionTests.cs @@ -1,303 +1,389 @@ +using System.Reflection; +using AcDream.App.Composition; +using AcDream.App.Input; using AcDream.App.Rendering; +using AcDream.App.Streaming; +using AcDream.App.Tests.Architecture; +using AcDream.App.UI; namespace AcDream.App.Tests.Rendering; public sealed class GameWindowRenderLeafCompositionTests { + private const BindingFlags Declared = BindingFlags.Instance + | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly; + [Fact] public void ProductionRender_PreservesPrivatePresentationAndCaptureOrder() { - string presentation = Source("PrivatePresentationRenderer.cs"); - string orchestrator = Source("RenderFrameOrchestrator.cs"); - - AssertAppearsInOrder( + MethodInfo presentation = RequiredMethod( + typeof(PrivatePresentationRenderer), + nameof(PrivatePresentationRenderer.Render)); + AssertCallOrder( presentation, - "_portal.Draw(", - "_entityViewports?.Render();", - "_gameplayUi?.Render(", - "_devTools?.Render(", - "_screenshots?.CapturePending("); - AssertAppearsInOrder( + (typeof(IPrivatePortalViewport), nameof(IPrivatePortalViewport.Draw)), + (typeof(IPrivateEntityViewportFrame), nameof(IPrivateEntityViewportFrame.Render)), + (typeof(IRetainedGameplayUiFrame), nameof(IRetainedGameplayUiFrame.Render)), + (typeof(IDevToolsFrameLifecycle), nameof(IDevToolsFrameLifecycle.Render)), + (typeof(IPrivateFrameScreenshot), nameof(IPrivateFrameScreenshot.CapturePending))); + + MethodInfo orchestrator = RequiredMethod( + typeof(RenderFrameOrchestrator), + nameof(RenderFrameOrchestrator.Render)); + AssertCallOrder( orchestrator, - "_world.Render(input);", - "_presentation.Render(input, world);", - "_diagnostics.Publish(input, outcome);"); + (typeof(IWorldSceneFramePhase), nameof(IWorldSceneFramePhase.Render)), + (typeof(IPrivatePresentationFramePhase), nameof(IPrivatePresentationFramePhase.Render)), + (typeof(IRenderFrameDiagnosticsPhase), nameof(IRenderFrameDiagnosticsPhase.Publish))); } [Fact] public void ProductionRender_Prepares_resources_then_devtools_weather_and_world() { - string source = Source("RenderFramePreparationController.cs"); - - AssertAppearsInOrder( - source, - "_resources.Prepare(input);", - "_devTools?.BeginFrame((float)input.DeltaSeconds);", - "_weather.Tick(input.DeltaSeconds);"); + MethodInfo prepare = RequiredMethod( + typeof(RenderFramePreparationController), + nameof(RenderFramePreparationController.Prepare)); + AssertCallOrder( + prepare, + (typeof(IRenderFrameResourcePhase), nameof(IRenderFrameResourcePhase.Prepare)), + (typeof(IDevToolsFrameLifecycle), nameof(IDevToolsFrameLifecycle.BeginFrame)), + (typeof(IRenderWeatherFramePhase), nameof(IRenderWeatherFramePhase.Tick))); } [Fact] public void Composition_transfers_portal_tunnel_before_constructing_frame_borrowers() { - string phase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "SessionPlayerComposition.cs")); - string source = GameWindowSource(); - string framePhase = FrameRootSource(); + MethodInfo complete = RequiredMethod( + typeof(SessionPlayerCompositionPhase), + "CompleteSessionPlayer"); + AssertCallOrder( + complete, + (typeof(TransferableResourceSlot<>), "Transfer"), + (typeof(DeferredLocalPlayerTeleportNetworkSink), "BindOwned"), + (typeof(IGameWindowSessionPlayerPublication), "PublishSessionPlayer")); - AssertAppearsInOrder( - phase, - "d.PortalTunnelFallback.Transfer(", - "new LocalPlayerTeleportController(", - "new LocalPlayerTeleportPresentation(portalTunnel)", - "d.TeleportSink.BindOwned(localTeleport)", - "_publication.PublishSessionPlayer(result);"); - AssertAppearsInOrder( - source, - "new SessionPlayerCompositionPhase(", - "new FrameRootCompositionPhase("); - AssertAppearsInOrder( - framePhase, - "new LocalPlayerTeleportRenderStateSource(", - "new RenderFrameResourceController(", - "new PrivatePresentationRenderer(", - "new RenderFrameOrchestrator("); + MethodBase tunnelFactory = ReferencedMethodConstructing( + complete, + typeof(LocalPlayerTeleportPresentation)); + Assert.NotNull(ReferencedMethodConstructing( + tunnelFactory, + typeof(LocalPlayerTeleportController))); + + MethodInfo onLoad = RequiredMethod(typeof(GameWindow), "OnLoad"); + IReadOnlyList loadReferences = + CompiledCallGraph.ReadMethodReferences(onLoad); + CompiledCall sessionPhase = Assert.Single( + loadReferences, + reference => Constructs( + reference.Target, + typeof(SessionPlayerCompositionPhase))); + CompiledCall framePhase = Assert.Single( + loadReferences, + reference => Constructs( + reference.Target, + typeof(FrameRootCompositionPhase))); + Assert.True(sessionPhase.Offset < framePhase.Offset); + + MethodInfo composeFrame = RequiredMethod( + typeof(FrameRootCompositionPhase), + "ComposeCore"); + AssertCallOrder( + composeFrame, + (typeof(LocalPlayerTeleportRenderStateSource), ".ctor"), + (typeof(RenderFrameResourceController), ".ctor"), + (typeof(PrivatePresentationRenderer), ".ctor"), + (typeof(RenderFrameOrchestrator), ".ctor")); } [Fact] public void ProductionComposition_RemovesLegacyLeafOwnership() { - string source = GameWindowSource(); - - string[] removed = + FieldInfo[] windowFields = typeof(GameWindow).GetFields(Declared); + HashSet removedFieldNames = [ "_imguiBootstrap", "_panelHost", "_paperdollDollDirty", + "_lastRenderSignature", + "_lastVisibleLandblocks", + "_perfAccum", + "_entityUploadTiming", + "_weatherAccum", + ]; + HashSet removedFieldTypes = + [ + nameof(WorldRenderDiagnostics), + nameof(WorldRenderFrameBuilder), + nameof(SkyPesFrameController), + nameof(RetailPViewRenderer), + nameof(RetailPViewPassExecutor), + nameof(RetailPViewCellSource), + nameof(TerrainDrawDiagnosticsController), + ]; + Assert.DoesNotContain( + windowFields, + field => removedFieldNames.Contains(field.Name) + || removedFieldTypes.Contains(field.FieldType.Name)); + + HashSet removedMethods = + [ "RefreshPaperdollDoll", "ApplyPaperdollPose", "ResolvePaperdollPoseDid", "EnumerateDebugPanel", "ResetPanelLayout", "SetPanelLayout", - "_lastRenderSignature", - "private void EmitRenderSignatureIfChanged(", - "private void EmitRetailPViewDiagnostics(", - "EmitGlStateTripwireIfChanged();", - "EmitClipRouteScissorProbe(scissor", - "_lastVisibleLandblocks", - "_perfAccum", - "_entityUploadTiming", - "_weatherAccum", + "EmitRenderSignatureIfChanged", + "EmitRetailPViewDiagnostics", + "EmitGlStateTripwireIfChanged", + "EmitClipRouteScissorProbe", "TryGetLoginWorldCell", "ApplyFramePacingPreference", "RefreshActiveMonitorFramePacing", - "private void OnFrameRendered(", - "private AcDream.App.Rendering.WorldRenderDiagnostics? _worldRenderDiagnostics", - "private AcDream.App.Rendering.WorldRenderFrameBuilder?", - "private AcDream.App.Rendering.SkyPesFrameController?", - "private AcDream.App.Rendering.RetailPViewRenderer?", - "private AcDream.App.Rendering.RetailPViewPassExecutor?", - "private AcDream.App.Rendering.RetailPViewCellSource?", - "private AcDream.App.Rendering.TerrainDrawDiagnosticsController?", + "OnFrameRendered", ]; - foreach (string identifier in removed) - Assert.DoesNotContain(identifier, source, StringComparison.Ordinal); + Assert.DoesNotContain( + typeof(GameWindow).GetMethods(Declared), + method => removedMethods.Contains(method.Name)); - Assert.Contains("new PaperdollFramePresenter(", LivePresentationSource()); - string framePhase = FrameRootSource(); - Assert.Contains("new RenderFrameResourceController(", framePhase); - Assert.Contains("new RenderWeatherFrameController(", framePhase); - Assert.Contains("new PrivatePresentationRenderer(", framePhase); - Assert.Contains("new RenderFrameOrchestrator(", framePhase); - Assert.Contains("new DisplayFramePacingController(", source); - string pointerSource = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Input", - "CameraPointerInputController.cs")); - Assert.Contains("_capture.WantCaptureMouse", pointerSource); + Assert.Contains( + CompiledCallGraph.ReadDeclared(typeof(LivePresentationCompositionPhase)), + call => call.Target.DeclaringType == typeof(PaperdollFramePresenter) + && call.Target.IsConstructor); + IReadOnlyList frameCalls = + CompiledCallGraph.ReadDeclared(typeof(FrameRootCompositionPhase)); + Assert.All( + new[] + { + typeof(RenderFrameResourceController), + typeof(RenderWeatherFrameController), + typeof(PrivatePresentationRenderer), + typeof(RenderFrameOrchestrator), + }, + type => Assert.Contains( + frameCalls, + call => call.Target.DeclaringType == type + && call.Target.IsConstructor)); + Assert.Contains( + CompiledCallGraph.ReadDeclared(typeof(GameWindow)), + call => call.Target.DeclaringType == typeof(DisplayFramePacingController) + && call.Target.IsConstructor); + Assert.Contains( + CompiledCallGraph.ReadDeclared(typeof(CameraPointerInputController)), + call => call.Target.DeclaringType == typeof(IInputCaptureSource) + && call.Target.Name == "get_WantCaptureMouse"); } [Fact] public void Shutdown_DrainsGpuBeforeFrontendsAndPreservesFrameBorrowerOrder() { - // Campaign V slice V11 removed the ImGui developer-tools frontend and - // its "developer tools" shutdown stage entry along with it; this test - // used to pin that entry's position and is now renamed to pin what - // survives it. - string source = GameWindowLifetimeSource(); + MethodInfo create = RequiredMethod( + typeof(GameWindowShutdownManifest), + nameof(GameWindowShutdownManifest.Create)); + IReadOnlyList labels = CompiledCallGraph.ReadStringLiterals(create); - AssertAppearsInOrder( - source, - "new ResourceShutdownStage(\"submitted GPU work\"", - "new ResourceShutdownStage(\"render frontends\"", - "Hard(\"portal tunnel\"", - "Hard(\"paperdoll viewport\"", - "new ResourceShutdownStage(\"graphics API context\""); - AssertAppearsInOrder( - source, - "new ResourceShutdownStage(\"frame borrowers\"", - "frame.FrameGraphPublication?.Dispose()", - "frame.FrameBindings?.Dispose()", - "new ResourceShutdownStage(\"session dependents\""); - AssertAppearsInOrder( - source, - "Hard(\"frame pacing\", render.FramePacing.Dispose)", - "Hard(\"frame profiler\", render.FrameProfiler.Dispose)"); - AssertAppearsInOrder( - source, - "frame.FrameGraphPublication?.Dispose()", - "new ResourceShutdownStage(\"render frontends\"", - "new ResourceShutdownStage(\"input context\"", - "platform.Input?.Dispose()", - "new ResourceShutdownStage(\"graphics API context\""); + AssertLabelOrder( + labels, + "submitted GPU work", + "render frontends", + "portal tunnel", + "paperdoll viewport", + "graphics API context"); + AssertLabelOrder( + labels, + "frame borrowers", + "world frame composition", + "frame-root bindings", + "session dependents"); + AssertLabelOrder(labels, "frame pacing", "frame profiler"); + AssertLabelOrder( + labels, + "world frame composition", + "render frontends", + "input context", + "graphics API context"); } [Fact] public void ProductionOutcome_UsesObservedWorldAndScreenshotFacts() { - string presentation = Source("PrivatePresentationRenderer.cs"); - string orchestrator = Source("RenderFrameOrchestrator.cs"); - - AssertAppearsInOrder( + MethodInfo presentation = RequiredMethod( + typeof(PrivatePresentationRenderer), + nameof(PrivatePresentationRenderer.Render)); + AssertCallOrder( presentation, - "_foundation.Foundation.PortalViewportVisible;", - "_portal.Draw(", - "bool screenshotCaptured = _screenshots?.CapturePending(", - "portalViewportVisible,", - "screenshotCaptured);"); - AssertAppearsInOrder( + (typeof(RenderFrameFoundation), "get_PortalViewportVisible"), + (typeof(IPrivatePortalViewport), nameof(IPrivatePortalViewport.Draw)), + (typeof(IPrivateFrameScreenshot), nameof(IPrivateFrameScreenshot.CapturePending)), + (typeof(PrivatePresentationFrameOutcome), ".ctor")); + + MethodInfo orchestrator = RequiredMethod( + typeof(RenderFrameOrchestrator), + nameof(RenderFrameOrchestrator.Render)); + AssertCallOrder( orchestrator, - "_gpuMeasurement.BeginFrame();", - "world = _world.Render(input);", - "_presentation.Render(input, world);", - "_gpuMeasurement.EndFrame();", - "new RenderFrameOutcome(world, presentation);", - "_diagnostics.Publish(input, outcome);"); + (typeof(IRenderFrameGpuMeasurement), nameof(IRenderFrameGpuMeasurement.BeginFrame)), + (typeof(IWorldSceneFramePhase), nameof(IWorldSceneFramePhase.Render)), + (typeof(IPrivatePresentationFramePhase), nameof(IPrivatePresentationFramePhase.Render)), + (typeof(IRenderFrameGpuMeasurement), nameof(IRenderFrameGpuMeasurement.EndFrame)), + (typeof(RenderFrameOutcome), ".ctor"), + (typeof(IRenderFrameDiagnosticsPhase), nameof(IRenderFrameDiagnosticsPhase.Publish))); } [Fact] public void GameWindow_OnRenderIsOneImmutableOrchestratorHandoff() { - string source = GameWindowSource(); - int start = source.IndexOf( - "private void OnRender(double deltaSeconds)", - StringComparison.Ordinal); - int end = source.IndexOf( - "private void OnFramebufferResize(", - start, - StringComparison.Ordinal); - Assert.True(start >= 0 && end > start); - string body = source[start..end]; + MethodInfo onRender = RequiredMethod(typeof(GameWindow), "OnRender"); + IReadOnlyList calls = CompiledCallGraph.Read(onRender); + Assert.Single( + calls, + call => call.Target.DeclaringType == typeof(GameFrameGraphSlot) + && call.Target.Name == nameof(GameFrameGraphSlot.Render)); - Assert.Equal(1, CountOccurrences(body, "_frameGraphs.Render(")); - Assert.DoesNotContain("_gpuFrameFlights", body); - Assert.DoesNotContain("_worldScene", body); - Assert.DoesNotContain("_devToolsFramePresenter", body); - Assert.DoesNotContain("_retailUiRuntime", body); - Assert.DoesNotContain("_frameScreenshots", body); + HashSet forbiddenFields = + [ + "_gpuFrameFlights", + "_worldScene", + "_devToolsFramePresenter", + "_retailUiRuntime", + "_frameScreenshots", + ]; + Assert.DoesNotContain( + CompiledCallGraph.ReadFieldReferences(onRender), + reference => forbiddenFields.Contains(reference.Field.Name)); } [Fact] public void PaperdollComposition_SkipsEitherMissingOptionalUiSurface() { - string source = LivePresentationSource(); + MethodBase compose = MethodConstructing( + typeof(LivePresentationCompositionPhase), + typeof(PaperdollFramePresenter)); + IReadOnlyList calls = CompiledCallGraph.Read(compose); + CompiledCall viewport = Assert.Single( + calls, + call => call.Target.DeclaringType == typeof(RetailUiRuntime) + && call.Target.Name == "get_PaperdollViewportWidget"); + CompiledCall inventory = Assert.Single( + calls, + call => call.Target.DeclaringType == typeof(RetailUiRuntime) + && call.Target.Name == "get_InventoryFrame"); + CompiledCall presenter = Assert.Single( + calls, + call => call.Target.DeclaringType == typeof(PaperdollFramePresenter) + && call.Target.IsConstructor); + Assert.True(viewport.Offset < inventory.Offset); + Assert.True(inventory.Offset < presenter.Offset); - AssertAppearsInOrder( - source, - "PaperdollViewportWidget is { } viewport", - "InventoryFrame is { } inventoryFrame", - "new PaperdollFramePresenter("); - Assert.DoesNotContain("Paperdoll inventory frame is required.", source); + IReadOnlyList branches = CompiledCallGraph.ReadBranches(compose); + Assert.Contains( + branches, + branch => branch.OpCode.FlowControl == System.Reflection.Emit.FlowControl.Cond_Branch + && branch.Offset > viewport.Offset + && branch.Offset < inventory.Offset + && branch.TargetOffset > presenter.Offset); + Assert.Contains( + branches, + branch => branch.OpCode.FlowControl == System.Reflection.Emit.FlowControl.Cond_Branch + && branch.Offset > inventory.Offset + && branch.Offset < presenter.Offset + && branch.TargetOffset > presenter.Offset); + Assert.DoesNotContain( + CompiledCallGraph.ReadStringLiterals(compose), + value => value == "Paperdoll inventory frame is required."); } [Fact] public void TerrainAndFrameDiagnostics_AreComposedAsOneFocusedOwner() { - string source = FrameRootSource(); - - Assert.Contains( - "new TerrainDrawDiagnosticsController(", - source); - Assert.Contains("new WorldScenePassExecutor(", source); - Assert.DoesNotContain("_terrainDrawDiagnostics!.Begin();", source); - Assert.DoesNotContain("_terrainDrawDiagnostics.Complete();", source); + MethodInfo compose = RequiredMethod( + typeof(FrameRootCompositionPhase), + "ComposeCore"); + IReadOnlyList calls = CompiledCallGraph.Read(compose); + AssertCallOrder( + compose, + (typeof(TerrainDrawDiagnosticsController), ".ctor"), + (typeof(WorldScenePassExecutor), ".ctor")); + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType == typeof(TerrainDrawDiagnosticsController) + && (call.Target.Name == nameof(TerrainDrawDiagnosticsController.Begin) + || call.Target.Name == nameof(TerrainDrawDiagnosticsController.Complete))); } - private static string GameWindowSource() => File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); + private static MethodInfo RequiredMethod(Type type, string name) => + type.GetMethod( + name, + BindingFlags.Instance | BindingFlags.Static + | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new MissingMethodException(type.FullName, name); - private static string GameWindowLifetimeSource() => File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindowLifetime.cs")); + private static MethodBase MethodConstructing(Type owner, Type constructed) => + Assert.Single( + owner.GetMethods(Declared) + .Cast() + .Concat(owner.GetConstructors(Declared)) + .Where(method => method.GetMethodBody() is not null), + method => Constructs(method, constructed)); - private static string LivePresentationSource() => File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "LivePresentationComposition.cs")); + private static MethodBase ReferencedMethodConstructing( + MethodBase owner, + Type constructed) => + Assert.Single( + CompiledCallGraph.ReadMethodReferences(owner) + .Select(reference => reference.Target) + .Where(method => method.GetMethodBody() is not null) + .Distinct(), + method => Constructs(method, constructed)); - private static string FrameRootSource() => File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "FrameRootComposition.cs")); + private static bool Constructs(MethodBase method, Type type) => + method.GetMethodBody() is not null + && CompiledCallGraph.Read(method).Any(call => + call.Target.DeclaringType == type && call.Target.IsConstructor); - private static string Source(string fileName) => File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - fileName)); - - private static int CountOccurrences(string source, string value) + private static void AssertCallOrder( + MethodBase method, + params (Type Type, string Method)[] expected) { - int count = 0; - int cursor = 0; - while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0) + IReadOnlyList calls = CompiledCallGraph.Read(method); + int cursor = -1; + foreach ((Type type, string name) in expected) { - count++; - cursor += value.Length; + int found = calls + .Select((call, index) => (call, index)) + .Where(pair => pair.index > cursor) + .Select(pair => pair.call) + .Select((call, index) => (call, index: index + cursor + 1)) + .FirstOrDefault( + pair => MatchesType(pair.call.Target.DeclaringType, type) + && pair.call.Target.Name == name, + defaultValue: (default, -1)) + .index; + Assert.True( + found > cursor, + $"Missing compiled edge after index {cursor}: {type.FullName}.{name}."); + cursor = found; } - return count; } - private static void AssertAppearsInOrder(string source, params string[] needles) + private static bool MatchesType(Type? actual, Type expected) => + actual == expected + || expected.IsGenericTypeDefinition + && actual?.IsGenericType == true + && actual.GetGenericTypeDefinition() == expected; + + private static void AssertLabelOrder( + IReadOnlyList labels, + params string[] expected) { int cursor = -1; - foreach (string needle in needles) + foreach (string label in expected) { - int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal); - Assert.True(next >= 0, $"Missing expected source fragment: {needle}"); - Assert.True(next > cursor, $"Out-of-order source fragment: {needle}"); - cursor = next; + int found = Enumerable.Range(cursor + 1, labels.Count - cursor - 1) + .FirstOrDefault(index => labels[index] == label, -1); + Assert.True(found > cursor, $"Missing shutdown label after {cursor}: {label}."); + cursor = found; } } - - 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."); - } }