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 95b3ec66..e121dca2 100644 --- a/docs/reviews/2026-08-18-r3-test-truth-ledger.md +++ b/docs/reviews/2026-08-18-r3-test-truth-ledger.md @@ -1208,3 +1208,60 @@ Verification: attributed methods, and reduces total direct/helper source readers from 92 to 83 while direct readers remain 57. The remaining 83 reconcile to the 22 approved retained policies/contracts and 61 staged replacements. + +## Batch Z composition-root source-freeze replacement + +Batch Z converts 12 approved source-text readers across the seven graphical +composition-owner suites and removes one stale backend assertion. No product +source changes. The replacement tests use the built call graph, reflected +owner metadata, existing transactional fixtures, and typed composition results +instead of source fragments. + +The exact decisions are: + +- all seven phase-boundary tests now prove that `GameWindow` constructs exactly + one instance of the relevant composition phase and does not directly + construct the leaf owners that belong behind it. The phase and snapshot + types are also checked for the absence of a retained `GameWindow` owner; +- the platform prelude now verifies the compiled `OnLoad` route through + `AcquirePlatform` and `GameWindowCompositionPipeline.Run`, plus the single + `GameWindowPlatformAcquisition.Acquire` and host/input/camera phase edges; +- the prepared-asset contract follows the production factory, world phase, + `ObjectMeshManager`, `WbMeshAdapter`, and compiled shutdown manifest. It + proves one `IPreparedAssetSource` owner, no DAT-reader constructor seam in the + production mesh manager, prepared reads rather than live mesh building, and + mesh/prepared-source/DAT release order; +- the frame-root test follows the compiled construction order from render + resources and comparison state through the world/update roots, graph + publication, window publication, and final ownership transfers. The + lifecycle snapshot contract now verifies its exact `RenderFrameOutcome` + input and its typed live-entity, GPU-memory, profiler, and landblock metrics; +- the session phase follows the compiled streamer/start/reveal route and the + delegate target that constructs the request streamer. Its completion check + follows hydration, inbound routing, input/player mode, portal transfer, + session creation, command bindings, action attachment, and final publication + in order. Character selection now verifies the graphical selector option, + typed connect options, absence of automatic first-character selection, and + retained-UI runtime bindings; +- the interaction, live-presentation, and world-render boundaries retain their + existing behavioral phase fixtures while compiled metadata guards against + construction leaking back into `GameWindow`; and +- `PortalSpaceIsComposedOnBothBackendArms` is removed as stale. Campaign V + deleted the OpenGL backend, so a two-backend-arms source condition is no + longer an architectural contract; its negative assertion named an already + deleted null presentation. The valuable portal ownership rationale remains + in the live/session composition code, the compiled portal transfer order, + and the existing teleport/reveal behavior suites. + +Verification: + +- all 187 composition tests pass with zero skips or failures; +- the complete locked Release build covers all 44 projects with zero warnings + and zero errors; +- the no-retry complete hermetic Release gate passes 14,346/14,346 with zero + skips or failures across all 12 test assemblies. The one-case reduction is + the stale two-backend-arms assertion; and +- the regenerated 1,254-file inventory parses every file, reports 11,414 + attributed methods, and reduces direct/total source readers from 57/83 to + 44/70. The remaining 70 reconcile to the 22 approved retained + policies/contracts and 48 staged replacements. diff --git a/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs b/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs index 730c54ad..3e2bbab9 100644 --- a/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/ContentEffectsAudioCompositionTests.cs @@ -6,12 +6,15 @@ using AcDream.App.Physics; using AcDream.App.Rendering; using AcDream.App.Rendering.Residency; using AcDream.App.Rendering.Vfx; +using AcDream.App.Rendering.Wb; using AcDream.App.Spells; +using AcDream.App.Tests.Architecture; using AcDream.Content; using AcDream.Content.Vfx; using AcDream.Core.Audio; using AcDream.Core.CharGen; using AcDream.Core.Lighting; +using AcDream.Core.Meshing; using AcDream.Core.Physics; using AcDream.Core.Rendering; using AcDream.Core.Spells; @@ -20,6 +23,7 @@ using AcDream.Runtime.Gameplay; using AcDream.Runtime.Physics; using AcDream.Runtime.Session; using DatReaderWriter.DBObjs; +using DatReaderWriter.Lib.IO; using Silk.NET.Input; using Silk.NET.OpenAL; @@ -163,84 +167,73 @@ public sealed class ContentEffectsAudioCompositionTests [Fact] public void GameWindowUsesTheProductionPhaseAndNoLongerConstructsItsBodyInline() { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); + IReadOnlyList calls = + CompiledCallGraph.ReadDeclared(typeof(GameWindow)); - Assert.Contains("new ContentEffectsAudioCompositionPhase(", source, - StringComparison.Ordinal); + Assert.Single( + calls, + call => call.Target.DeclaringType + == typeof(ContentEffectsAudioCompositionPhase) + && call.Target.IsConstructor); Assert.DoesNotContain( - "_dats = RuntimeDatCollectionFactory.OpenReadOnly(_datDir);", - source, - StringComparison.Ordinal); - Assert.DoesNotContain("_hookRouter.Register(_particleSink)", source, - StringComparison.Ordinal); - Assert.DoesNotContain("new AcDream.App.Audio.OpenAlAudioEngine()", source, - StringComparison.Ordinal); + calls, + call => call.Target.DeclaringType + == typeof(RuntimeDatCollectionFactory) + && call.Target.Name == nameof(RuntimeDatCollectionFactory.OpenReadOnly)); + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType == typeof(AnimationHookRouter) + && call.Target.Name == nameof(AnimationHookRouter.Register)); + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType == typeof(OpenAlAudioEngine) + && call.Target.IsConstructor); } [Fact] public void ProductionRendererConsumesOnlyThePublishedPreparedAssetSource() { - string root = FindRepoRoot(); - string contentPhase = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Composition", - "ContentEffectsAudioComposition.cs")); - string worldPhase = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Composition", - "WorldRenderComposition.cs")); - string manager = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Rendering", - "Wb", - "ObjectMeshManager.cs")); - string adapter = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Rendering", - "Wb", - "WbMeshAdapter.cs")); - string lifetime = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Rendering", - "GameWindowLifetime.cs")); + MethodInfo openPrepared = typeof(RetailContentEffectsAudioCompositionFactory) + .GetMethod(nameof( + RetailContentEffectsAudioCompositionFactory.OpenPreparedAssetSource))!; + Assert.Contains( + CompiledCallGraph.Read(openPrepared), + call => call.Target.DeclaringType == typeof(PakPreparedAssetSource) + && call.Target.IsConstructor); - Assert.Contains("new PakPreparedAssetSource(path, dats, diagnostic)", - contentPhase, StringComparison.Ordinal); - Assert.Contains("content.PreparedAssets", worldPhase, - StringComparison.Ordinal); - Assert.Contains("_preparedAssets.Read(request.Asset, ct)", manager, - StringComparison.Ordinal); - Assert.DoesNotContain("MeshExtractor", manager, - StringComparison.Ordinal); - Assert.DoesNotContain("IDatReaderWriter", manager, - StringComparison.Ordinal); - Assert.DoesNotContain("GfxObjMesh.Build", adapter, - StringComparison.Ordinal); + MethodInfo compose = typeof(WorldRenderCompositionPhase) + .GetMethod(nameof(WorldRenderCompositionPhase.Compose))!; + IReadOnlyList worldCalls = CompiledCallGraph.Read(compose); + Assert.Contains( + worldCalls, + call => call.Target.DeclaringType == typeof(ContentEffectsAudioResult) + && call.Target.Name == "get_PreparedAssets"); - int meshStage = lifetime.IndexOf( - "new ResourceShutdownStage(\"mesh adapter\"", - StringComparison.Ordinal); - int preparedRelease = lifetime.IndexOf( - "Hard(\"prepared asset source\"", - StringComparison.Ordinal); - int datRelease = lifetime.IndexOf( - "Hard(\"DAT collection\"", - StringComparison.Ordinal); + FieldInfo preparedAssets = Assert.Single( + typeof(ObjectMeshManager).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic), + field => field.FieldType == typeof(IPreparedAssetSource)); + Assert.Equal("_preparedAssets", preparedAssets.Name); + Assert.DoesNotContain( + typeof(ObjectMeshManager).GetConstructors(), + constructor => constructor.GetParameters().Any(parameter => + parameter.ParameterType == typeof(IDatReaderWriter))); + Assert.Contains( + CompiledCallGraph.ReadDeclared(typeof(ObjectMeshManager)), + call => call.Target.DeclaringType == typeof(IPreparedAssetSource) + && call.Target.Name == nameof(IPreparedAssetSource.Read)); + Assert.DoesNotContain( + CompiledCallGraph.ReadDeclared(typeof(WbMeshAdapter)), + call => call.Target.DeclaringType == typeof(GfxObjMesh) + && call.Target.Name == nameof(GfxObjMesh.Build)); + + MethodInfo shutdown = typeof(GameWindowShutdownManifest).GetMethod( + nameof(GameWindowShutdownManifest.Create))!; + List labels = + CompiledCallGraph.ReadStringLiterals(shutdown).ToList(); + int meshStage = labels.IndexOf("mesh adapter"); + int preparedRelease = labels.IndexOf("prepared asset source"); + int datRelease = labels.IndexOf("DAT collection"); Assert.True(meshStage >= 0); Assert.True(preparedRelease > meshStage); Assert.True(datRelease > preparedRelease); @@ -564,15 +557,4 @@ public sealed class ContentEffectsAudioCompositionTests public void CloseDevice(nint device) { } } - 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."); - } } diff --git a/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs b/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs index e2b5fc2f..991c7b80 100644 --- a/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs @@ -1,6 +1,12 @@ using System.Reflection; using AcDream.App.Composition; +using AcDream.App.Diagnostics; using AcDream.App.Rendering; +using AcDream.App.Rendering.Scene; +using AcDream.App.Rendering.Wb; +using AcDream.App.Tests.Architecture; +using AcDream.App.Update; +using AcDream.App.World; namespace AcDream.App.Tests.Composition; @@ -28,48 +34,77 @@ public sealed class FrameRootCompositionTests [Fact] public void ProductionPhasePublishesOnlyAfterBothRootsExist() { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "FrameRootComposition.cs")); + MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod( + "ComposeCore", + BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new MissingMethodException( + typeof(FrameRootCompositionPhase).FullName, + "ComposeCore"); + IReadOnlyList calls = CompiledCallGraph.Read(compose); - AssertAppearsInOrder( - source, - "new RenderFrameResourceController(", - "new RenderSceneShadowComparisonController(", - "new WorldSceneRenderer(", - "new WorldLifecycleAutomationController(", - "\"world lifecycle automation owner\"", - "\"world lifecycle automation binding\"", - "new SerialRenderFramePostDiagnosticsPhase(", - "new RenderFrameOrchestrator(", - "postDiagnostics,", - "new RetailLiveFrameCoordinator(", - "new UpdateFrameOrchestrator(", - "d.FrameGraphs.PublishOwned(", - "_publication.PublishFrameRoots(result);", - "graphLease.Transfer();", - "bindingsLease.Transfer();"); + int resources = CallIndex(calls, typeof(RenderFrameResourceController), ".ctor"); + int comparison = CallIndex( + calls, + typeof(RenderSceneShadowComparisonController), + ".ctor", + resources + 1); + int scene = CallIndex(calls, typeof(WorldSceneRenderer), ".ctor", comparison + 1); + int automation = CallIndex( + calls, + typeof(WorldLifecycleAutomationController), + ".ctor", + scene + 1); + int diagnostics = CallIndex( + calls, + typeof(SerialRenderFramePostDiagnosticsPhase), + ".ctor", + automation + 1); + int render = CallIndex(calls, typeof(RenderFrameOrchestrator), ".ctor", diagnostics + 1); + int live = CallIndex(calls, typeof(RetailLiveFrameCoordinator), ".ctor", render + 1); + int update = CallIndex(calls, typeof(UpdateFrameOrchestrator), ".ctor", live + 1); + int graph = CallIndex(calls, typeof(GameFrameGraphSlot), "PublishOwned", update + 1); + int publish = CallIndex( + calls, + typeof(IGameWindowFrameRootPublication), + "PublishFrameRoots", + graph + 1); + int firstTransfer = calls + .Select((call, index) => (call, index)) + .First(pair => pair.index > publish + && pair.call.Target.Name == "Transfer") + .index; + int secondTransfer = calls + .Select((call, index) => (call, index)) + .First(pair => pair.index > firstTransfer + && pair.call.Target.Name == "Transfer") + .index; + + Assert.True(secondTransfer > firstTransfer); } [Fact] public void GameWindowRetainsOnlyThePhaseBoundaryAndFrameHandoffs() { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); + IReadOnlyList calls = + CompiledCallGraph.ReadDeclared(typeof(GameWindow)); - Assert.Contains("new FrameRootCompositionPhase(", source); - Assert.DoesNotContain("new AcDream.App.Rendering.WorldSceneRenderer(", source); - Assert.DoesNotContain("new AcDream.App.Update.UpdateFrameOrchestrator(", source); - Assert.DoesNotContain("CaptureWorldLifecycleResourceSnapshot", source); - Assert.DoesNotContain("_worldLifecycleAutomation", source); - Assert.DoesNotContain("_frameGraphs.Publish(", source); + Assert.Single( + calls, + call => call.Target.DeclaringType == typeof(FrameRootCompositionPhase) + && call.Target.IsConstructor); + Assert.DoesNotContain( + calls, + call => call.Target.IsConstructor + && call.Target.DeclaringType is { } type + && (type == typeof(WorldSceneRenderer) + || type == typeof(UpdateFrameOrchestrator) + || type == typeof(WorldLifecycleAutomationController))); + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType + == typeof(WorldLifecycleResourceSnapshotSource) + && call.Target.Name == nameof( + WorldLifecycleResourceSnapshotSource.Capture)); } [Fact] @@ -80,19 +115,37 @@ public sealed class FrameRootCompositionTests BindingFlags.Instance | BindingFlags.NonPublic), field => field.FieldType == typeof(GameWindow)); - string snapshots = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Diagnostics", - "WorldLifecycleResourceSnapshotSource.cs")); - Assert.DoesNotContain("GameWindow", snapshots, StringComparison.Ordinal); - Assert.Contains("_liveEntities.PendingTeardownCount", snapshots); - Assert.Contains("GpuMemoryTracker.AllocatedBytes", snapshots); - Assert.Contains("_frameProfiler.LastReport", snapshots); - Assert.Contains("Capture(RenderFrameOutcome outcome)", snapshots); - Assert.Contains("outcome.World.VisibleLandblocks", snapshots); - Assert.Contains("outcome.World.TotalLandblocks", snapshots); + Assert.DoesNotContain( + typeof(WorldLifecycleResourceSnapshotSource).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic), + field => field.FieldType == typeof(GameWindow)); + + MethodInfo capture = typeof(WorldLifecycleResourceSnapshotSource) + .GetMethod(nameof(WorldLifecycleResourceSnapshotSource.Capture))!; + Assert.Equal( + typeof(RenderFrameOutcome), + Assert.Single(capture.GetParameters()).ParameterType); + IReadOnlyList calls = CompiledCallGraph.Read(capture); + Assert.Contains( + calls, + call => call.Target.DeclaringType == typeof(LiveEntityRuntime) + && call.Target.Name == "get_PendingTeardownCount"); + Assert.Contains( + calls, + call => call.Target.DeclaringType == typeof(GpuMemoryTracker) + && call.Target.Name == "get_AllocatedBytes"); + Assert.Contains( + calls, + call => call.Target.DeclaringType == typeof(FrameProfiler) + && call.Target.Name == "get_LastReport"); + Assert.Contains( + calls, + call => call.Target.DeclaringType == typeof(WorldRenderFrameOutcome) + && call.Target.Name == "get_VisibleLandblocks"); + Assert.Contains( + calls, + call => call.Target.DeclaringType == typeof(WorldRenderFrameOutcome) + && call.Target.Name == "get_TotalLandblocks"); } private sealed class RetryBinding( @@ -110,27 +163,20 @@ public sealed class FrameRootCompositionTests } } - private static void AssertAppearsInOrder(string source, params string[] values) + private static int CallIndex( + IReadOnlyList calls, + Type declaringType, + string methodName, + int startIndex = 0) { - int cursor = 0; - foreach (string value in values) - { - int found = source.IndexOf(value, cursor, StringComparison.Ordinal); - Assert.True(found >= 0, $"Missing expected source fragment: {value}"); - cursor = found + value.Length; - } - } - - 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."); + int index = CompiledCallGraph.IndexOf( + calls, + declaringType, + methodName, + startIndex); + Assert.True( + index >= startIndex, + $"Missing compiled edge {declaringType.FullName}.{methodName}."); + return index; } } diff --git a/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs b/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs index 6c25f115..ea1fb2a9 100644 --- a/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/HostInputCameraCompositionTests.cs @@ -4,6 +4,7 @@ using AcDream.App.Composition; using AcDream.App.Input; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; +using AcDream.App.Tests.Architecture; using AcDream.App.Tests.Rendering.Gpu; using AcDream.UI.Abstractions.Input; using Silk.NET.Input; @@ -67,21 +68,35 @@ public sealed class HostInputCameraCompositionTests [Fact] public void GameWindowUsesTheExactPlatformPreludeAndPhaseOneType() { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); + IReadOnlyList declaredCalls = + CompiledCallGraph.ReadDeclared(typeof(GameWindow)); - Assert.Contains("GameWindowPlatformAcquisition.Acquire(", source, - StringComparison.Ordinal); - Assert.Contains("new HostInputCameraCompositionPhase(", source, - StringComparison.Ordinal); - Assert.DoesNotContain("_gl = GL.GetApi(_window!)", source, - StringComparison.Ordinal); - Assert.DoesNotContain("_input = _window!.CreateInput()", source, - StringComparison.Ordinal); + Assert.Single( + declaredCalls, + call => call.Target.DeclaringType + == typeof(GameWindowPlatformAcquisition) + && call.Target.Name == nameof(GameWindowPlatformAcquisition.Acquire)); + Assert.Single( + declaredCalls, + call => call.Target.DeclaringType + == typeof(HostInputCameraCompositionPhase) + && call.Target.IsConstructor); + + MethodInfo onLoad = typeof(GameWindow).GetMethod( + "OnLoad", + BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new MissingMethodException(typeof(GameWindow).FullName, "OnLoad"); + IReadOnlyList loadCalls = CompiledCallGraph.Read(onLoad); + Assert.Contains( + loadCalls, + call => call.Target.DeclaringType == typeof(GameWindow) + && call.Target.Name == "AcquirePlatform"); + Assert.Contains( + loadCalls, + call => call.Target.DeclaringType == typeof(GameWindowCompositionPipeline) + && call.Target.Name == nameof(GameWindowCompositionPipeline.Run)); + Assert.DoesNotContain(loadCalls, call => call.Target.Name == "GetApi"); + Assert.DoesNotContain(loadCalls, call => call.Target.Name == "CreateInput"); } private sealed class Fixture : IDisposable @@ -450,16 +465,4 @@ public sealed class HostInputCameraCompositionTests public void WriteLine(string message) { } } - 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."); - } } diff --git a/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs b/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs index abdda089..1cf040cb 100644 --- a/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/InteractionRetainedUiCompositionTests.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System.Runtime.CompilerServices; using AcDream.App.Combat; using AcDream.App.Composition; @@ -8,6 +9,7 @@ using AcDream.Content; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.App.World; +using AcDream.App.Tests.Architecture; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Spells; @@ -190,19 +192,33 @@ public sealed class InteractionRetainedUiCompositionTests [Fact] public void GameWindowUsesPhaseAndContainsNoRetainedUiConstructionBody() { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); + IReadOnlyList calls = + CompiledCallGraph.ReadDeclared(typeof(GameWindow)); - Assert.Contains("new InteractionRetainedUiCompositionPhase(", source); - Assert.DoesNotContain("new AcDream.App.UI.ItemInteractionController(", source); - Assert.DoesNotContain("_retailUiLease.AcquireHost(", source); - Assert.DoesNotContain("RetailUiRuntime.CreateUninitialized(", source); - Assert.DoesNotContain("private void UseItemByGuid(", source); - Assert.DoesNotContain("private uint? PickWorldGuidAtCursor(", source); + Assert.Single( + calls, + call => call.Target.DeclaringType + == typeof(InteractionRetainedUiCompositionPhase) + && call.Target.IsConstructor); + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType + == typeof(ItemInteractionController) + && call.Target.IsConstructor); + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType == typeof(RetailUiRuntimeLease) + && call.Target.Name == "AcquireHost"); + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType == typeof(RetailUiRuntime) + && call.Target.Name == nameof(RetailUiRuntime.CreateUninitialized)); + Assert.Null(typeof(GameWindow).GetMethod( + "UseItemByGuid", + BindingFlags.Instance | BindingFlags.NonPublic)); + Assert.Null(typeof(GameWindow).GetMethod( + "PickWorldGuidAtCursor", + BindingFlags.Instance | BindingFlags.NonPublic)); } private sealed class Fixture : IDisposable @@ -422,15 +438,4 @@ public sealed class InteractionRetainedUiCompositionTests private static T Stub() where T : class => (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); - 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."); - } } diff --git a/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs b/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs index ef8e90b8..3eeedfe8 100644 --- a/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/LivePresentationCompositionTests.cs @@ -1,6 +1,10 @@ +using System.Reflection; using AcDream.App.Composition; using AcDream.App.Physics; +using AcDream.App.Rendering; +using AcDream.App.Rendering.Wb; using AcDream.App.Streaming; +using AcDream.App.Tests.Architecture; using AcDream.App.World; using AcDream.Core.Net; using AcDream.Core.Physics.Motion; @@ -125,71 +129,29 @@ public sealed class LivePresentationCompositionTests [Fact] public void GameWindowUsesLivePhaseAndContainsNoPhaseSixConstructionBody() { - string root = FindRepoRoot(); - string window = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); - string phase = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Composition", - "LivePresentationComposition.cs")); + IReadOnlyList windowCalls = + CompiledCallGraph.ReadDeclared(typeof(GameWindow)); - Assert.Contains("new LivePresentationCompositionPhase(", window); - Assert.DoesNotContain("new AcDream.App.World.LiveEntityRuntime(", window); - Assert.DoesNotContain("new AcDream.App.Rendering.Wb.WbDrawDispatcher(", window); - Assert.DoesNotContain("new AcDream.App.Streaming.LandblockRenderPublisher(", window); - Assert.DoesNotContain("_portalTunnelFallback.AcquirePrepared(", window); - Assert.Contains("new LiveEntityRuntime(", phase); - Assert.Contains("new WbDrawDispatcher(", phase); - Assert.Contains("new LandblockRenderPublisher(", phase); - Assert.Contains("d.PortalTunnelFallback.AcquirePrepared(", phase); - } - - /// - /// Campaign V slice V6m: portal space is composed on BOTH arms. It was the - /// last renderer this phase built only when a GL context existed, and the - /// Vulkan arm's portal-less teleport presentation went with the condition — - /// so both the gate and the stand-in are asserted absent here. - /// - [Fact] - public void PortalSpaceIsComposedOnBothBackendArms() - { - string root = FindRepoRoot(); - string phase = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Composition", - "LivePresentationComposition.cs")); - string session = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Composition", - "SessionPlayerComposition.cs")); - - Assert.Contains( - "if (dispatcherLease.Resource is { } portalDispatcher)", - phase); + Assert.Single( + windowCalls, + call => call.Target.DeclaringType + == typeof(LivePresentationCompositionPhase) + && call.Target.IsConstructor); Assert.DoesNotContain( - "if (gl is not null && dispatcherLease.Resource is { } portalDispatcher)", - phase); - Assert.DoesNotContain("NullLocalPlayerTeleportPresentation", session); + windowCalls, + call => call.Target.IsConstructor + && call.Target.DeclaringType is { } type + && (type == typeof(LiveEntityRuntime) + || type == typeof(WbDrawDispatcher) + || type == typeof(LandblockRenderPublisher))); Assert.DoesNotContain( - "NullLocalPlayerTeleportPresentation", - File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Rendering", - "Gpu", - "Vk", - "VulkanCompositionFramePhases.cs"))); + windowCalls, + call => call.Target.Name == "AcquirePrepared"); + + Assert.DoesNotContain( + typeof(LivePresentationCompositionPhase).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic), + field => field.FieldType == typeof(GameWindow)); } private static LiveEntityRuntime Runtime() => LiveEntityRuntimeFixture.Create( @@ -250,15 +212,4 @@ public sealed class LivePresentationCompositionTests } } - 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."); - } } diff --git a/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs b/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs index 319982fa..e1b566d3 100644 --- a/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/SessionPlayerCompositionTests.cs @@ -1,5 +1,17 @@ +using System.Reflection; +using AcDream.App.Combat; using AcDream.App.Composition; +using AcDream.App.Diagnostics; +using AcDream.App.Input; +using AcDream.App.Net; +using AcDream.App.Physics; +using AcDream.App.Rendering; using AcDream.App.Streaming; +using AcDream.App.Tests.Architecture; +using AcDream.App.UI; +using AcDream.App.World; +using AcDream.Core.Net.Messages; +using AcDream.Runtime.Session; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.Composition; @@ -66,115 +78,124 @@ public sealed class SessionPlayerCompositionTests [Fact] public void GameWindowUsesSessionPhaseAndContainsNoPhaseSevenBody() { - string root = FindRepoRoot(); - string window = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); - string phase = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Composition", - "SessionPlayerComposition.cs")); + IReadOnlyList windowCalls = + CompiledCallGraph.ReadDeclared(typeof(GameWindow)); - Assert.Contains("new SessionPlayerCompositionPhase(", window); - Assert.DoesNotContain("LandblockStreamer.CreateForRequests(", window); - Assert.DoesNotContain("new AcDream.App.Net.LiveSessionController()", window); + Assert.Single( + windowCalls, + call => call.Target.DeclaringType + == typeof(SessionPlayerCompositionPhase) + && call.Target.IsConstructor); Assert.DoesNotContain( - "new AcDream.App.Streaming.LocalPlayerTeleportController(", - window); - Assert.DoesNotContain("IsSpawnClaimUnhydratable", window); - Assert.Contains("LandblockStreamer.CreateForRequests(", phase); - Assert.Contains( - "LiveSessionController liveSession = d.Runtime.Session;", - phase); - Assert.DoesNotContain("new LiveSessionController()", phase); - Assert.Contains("new LocalPlayerTeleportController(", phase); - Assert.Contains("new DatSpawnClaimHydrationClassifier(", phase); + windowCalls, + call => call.Target.DeclaringType == typeof(LandblockStreamer) + && call.Target.Name == nameof(LandblockStreamer.CreateForRequests)); + Assert.DoesNotContain( + windowCalls, + call => call.Target.IsConstructor + && call.Target.DeclaringType is { } type + && (type == typeof(LiveSessionController) + || type == typeof(LocalPlayerTeleportController) + || type == typeof(DatSpawnClaimHydrationClassifier))); + + IReadOnlyList phaseFields = + typeof(SessionPlayerCompositionPhase).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.DoesNotContain( + phaseFields, + field => field.FieldType == typeof(GameWindow)); } [Fact] public void ProductionPhaseStartsStreamerBeforeSessionAndTransfersPortalLast() { - string phase = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "SessionPlayerComposition.cs")); + MethodInfo composeCore = RequiredMethod( + typeof(SessionPlayerCompositionPhase), + "ComposeCore"); + IReadOnlyList composeCalls = + CompiledCallGraph.Read(composeCore); + AssertCallOrder( + composeCalls, + (typeof(LandblockStreamer), nameof(LandblockStreamer.Start)), + (typeof(StreamingController), ".ctor"), + (typeof(WorldRevealCoordinator), ".ctor"), + (typeof(SessionPlayerCompositionPhase), "CompleteSessionPlayer")); - AssertAppearsInOrder( - phase, - "LandblockStreamer.CreateForRequests(", - "streamerLease.Resource.Start();", - "new StreamingController(", - "new WorldRevealCoordinator(", - "LiveSessionController liveSession = d.Runtime.Session;", - "new LiveEntityHydrationController(", - "new LiveEntityNetworkUpdateController(", - "new GameplayInputFrameController(", - "new PlayerModeController(", - "d.PortalTunnelFallback.Transfer(", - "d.TeleportSink.BindOwned(localTeleport)", - "LiveSessionHost sessionHost = sessionRuntimeFactory.Create(", - "d.CombatModeCommands.BindOwned(combatCommand)", - "d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)", - "GameplayInputActionRouter.Create(", - "gameplayActions.Attach();", - "_publication.PublishSessionPlayer(result);"); + IEnumerable streamerFactories = + CompiledCallGraph.ReadMethodReferences(composeCore) + .Select(call => call.Target) + .Where(method => method.GetMethodBody() is not null); + MethodBase streamerFactory = Assert.Single( + streamerFactories, + method => CompiledCallGraph.Read(method).Any(call => + call.Target.DeclaringType == typeof(LandblockStreamer) + && call.Target.Name + == nameof(LandblockStreamer.CreateForRequests))); + Assert.Contains( + CompiledCallGraph.Read(streamerFactory), + call => call.Target.DeclaringType == typeof(LandblockStreamer) + && call.Target.Name == nameof(LandblockStreamer.CreateForRequests)); - Assert.DoesNotContain("GameWindow", File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Net", - "LiveSessionRuntimeFactory.cs")), StringComparison.Ordinal); + MethodInfo complete = RequiredMethod( + typeof(SessionPlayerCompositionPhase), + "CompleteSessionPlayer"); + IReadOnlyList completeCalls = + CompiledCallGraph.Read(complete); + AssertCallOrder( + completeCalls, + (typeof(LiveEntityHydrationController), ".ctor"), + (typeof(LiveEntityNetworkUpdateController), ".ctor"), + (typeof(GameplayInputFrameController), ".ctor"), + (typeof(PlayerModeController), ".ctor"), + (typeof(TransferableResourceSlot<>), "Transfer"), + (typeof(DeferredLocalPlayerTeleportNetworkSink), "BindOwned"), + (typeof(LiveSessionRuntimeFactory), nameof(LiveSessionRuntimeFactory.Create)), + (typeof(LiveCombatModeCommandSlot), "BindOwned"), + (typeof(RuntimeDiagnosticCommandSlot), "BindOwned"), + (typeof(GameplayInputActionRouter), nameof(GameplayInputActionRouter.Create)), + (typeof(GameplayInputActionRouter), nameof(GameplayInputActionRouter.Attach)), + (typeof(IGameWindowSessionPlayerPublication), "PublishSessionPlayer")); + + Assert.DoesNotContain( + typeof(LiveSessionRuntimeFactory).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic), + field => field.FieldType == typeof(GameWindow)); } [Fact] public void GraphicalCompositionPausesOnlyWhenCharacterSelectorIsAbsent() { - string root = FindRepoRoot(); - string phase = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Composition", - "SessionPlayerComposition.cs")); - string retainedUi = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Composition", - "InteractionRetainedUiComposition.cs")); - + MethodInfo complete = RequiredMethod( + typeof(SessionPlayerCompositionPhase), + "CompleteSessionPlayer"); + IReadOnlyList sessionCalls = + CompiledCallGraph.Read(complete); Assert.Contains( - "AwaitCharacterSelection:", - phase, - StringComparison.Ordinal); + sessionCalls, + call => call.Target.DeclaringType == typeof(RuntimeOptions) + && call.Target.Name == "get_LiveCharacterSelector"); Assert.Contains( - "d.Options.LiveCharacterSelector is null", - phase, - StringComparison.Ordinal); + sessionCalls, + call => call.Target.DeclaringType == typeof(LiveSessionConnectOptions) + && call.Target.IsConstructor); Assert.DoesNotContain( - "CharacterList.TrySelectFirstAvailable", - phase, - StringComparison.Ordinal); + sessionCalls, + call => call.Target.DeclaringType == typeof(CharacterList) + && call.Target.Name == nameof(CharacterList.TrySelectFirstAvailable)); + + MethodInfo retainedUi = typeof(RetailInteractionRetainedUiCompositionFactory) + .GetMethod(nameof( + RetailInteractionRetainedUiCompositionFactory.CreateRetainedUi))!; + IReadOnlyList uiCalls = CompiledCallGraph.Read(retainedUi); Assert.Contains( - "CharacterSelection: d.Options.LiveCharacterSelector is null", - retainedUi, - StringComparison.Ordinal); + uiCalls, + call => call.Target.DeclaringType == typeof(RuntimeOptions) + && call.Target.Name == "get_LiveCharacterSelector"); Assert.Contains( - "() => late.GameRuntime.CharacterSelection", - retainedUi, - StringComparison.Ordinal); - Assert.Contains( - "late.GameRuntime.CharacterSelectionEnter", - retainedUi, - StringComparison.Ordinal); + uiCalls, + call => call.Target.DeclaringType + == typeof(CharacterSelectionRuntimeBindings) + && call.Target.IsConstructor); } private sealed class RetryBinding( @@ -192,27 +213,39 @@ public sealed class SessionPlayerCompositionTests } } - private static void AssertAppearsInOrder(string source, params string[] values) + 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 void AssertCallOrder( + IReadOnlyList calls, + params (Type DeclaringType, string MethodName)[] expected) { - int cursor = 0; - foreach (string value in values) + int cursor = -1; + foreach ((Type declaringType, string methodName) in expected) { - int found = source.IndexOf(value, cursor, StringComparison.Ordinal); - Assert.True(found >= 0, $"Missing expected source fragment: {value}"); - cursor = found + value.Length; + int found = calls + .Select((call, index) => (call, index)) + .FirstOrDefault( + pair => pair.index > cursor + && MatchesType(pair.call.Target.DeclaringType, declaringType) + && pair.call.Target.Name == methodName, + defaultValue: (default, -1)) + .index; + Assert.True( + found > cursor, + $"Missing compiled edge after index {cursor}: " + + $"{declaringType.FullName}.{methodName}"); + 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."); - } + private static bool MatchesType(Type? actual, Type expected) => + actual == expected + || expected.IsGenericTypeDefinition + && actual?.IsGenericType == true + && actual.GetGenericTypeDefinition() == expected; } diff --git a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs index 386c0c26..364b218a 100644 --- a/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs +++ b/tests/AcDream.App.Tests/Composition/WorldRenderCompositionTests.cs @@ -6,6 +6,7 @@ using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Residency; using AcDream.App.World; +using AcDream.App.Tests.Architecture; using AcDream.App.Tests.Rendering.Gpu; using AcDream.Content; using AcDream.Core.Physics; @@ -121,23 +122,21 @@ public sealed class WorldRenderCompositionTests [Fact] public void GameWindowUsesPhaseAndNoLongerBuildsWorldFoundationInline() { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); + IReadOnlyList calls = + CompiledCallGraph.ReadDeclared(typeof(GameWindow)); - Assert.Contains("new WorldRenderCompositionPhase(", source, - StringComparison.Ordinal); - Assert.DoesNotContain("BindlessSupport.TryCreate(_gl", source, - StringComparison.Ordinal); - Assert.DoesNotContain("_terrainModernShader = new Shader", source, - StringComparison.Ordinal); - Assert.DoesNotContain("_wbMeshAdapter = new", source, - StringComparison.Ordinal); - Assert.DoesNotContain("_textureCache = new TextureCache", source, - StringComparison.Ordinal); + Assert.Single( + calls, + call => call.Target.DeclaringType + == typeof(WorldRenderCompositionPhase) + && call.Target.IsConstructor); + Assert.DoesNotContain( + calls, + call => call.Target.IsConstructor + && call.Target.DeclaringType is { } type + && (type == typeof(TerrainModernRenderer) + || type == typeof(WbMeshAdapter) + || type == typeof(TextureCache))); } private sealed class Fixture @@ -385,15 +384,4 @@ public sealed class WorldRenderCompositionTests private static T Stub() where T : class => (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); - 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."); - } }