From 84034f732c5921eeb4763ec54834443dd854cbbb Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 18 Aug 2026 16:49:38 +0200 Subject: [PATCH] test: replace gameplay owner source freezes --- .../2026-08-18-r3-test-truth-ledger.md | 51 ++++ .../Architecture/CompiledCallGraph.cs | 51 ++++ .../Runtime/RuntimeActionOwnershipTests.cs | 143 +++++---- .../Runtime/RuntimeCharacterOwnershipTests.cs | 222 +++++++------- .../Runtime/RuntimeInventoryOwnershipTests.cs | 281 ++++++++---------- .../Runtime/RuntimeMovementOwnershipTests.cs | 113 +++---- 6 files changed, 473 insertions(+), 388 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 0e2ad557..5a9eefd0 100644 --- a/docs/reviews/2026-08-18-r3-test-truth-ledger.md +++ b/docs/reviews/2026-08-18-r3-test-truth-ledger.md @@ -1447,3 +1447,54 @@ R3 work: - a targeted scan confirms neither owning test file retains a source reader or its source-slicing helper. The final full inventory and twelve-assembly Release gate are intentionally deferred to the one R3 closeout run. + +## Batch AE Runtime gameplay-owner wiring replacement + +Batch AE converts the nine approved consumer-wiring source readers across +`RuntimeActionOwnershipTests`, `RuntimeCharacterOwnershipTests`, +`RuntimeInventoryOwnershipTests`, and `RuntimeMovementOwnershipTests`. No +product source changes and no test is removed. The two approved whole-tree +construction policies—one action-owner policy and one movement-owner policy— +remain source-aware by design and are not part of the 85 staged replacements. + +The replacements use reflected fields, properties, constructor parameters, +compiled child access, and shutdown-root metadata to prove that: + +- retained UI, session composition, graphical command adapters, and item + interaction borrow the canonical action children and the exact Runtime + transaction owner, with no App `InteractionState` or UI-side transaction + construction; +- `GameWindow` owns one `GameRuntime`, constructs no character child root, + and projects its Spellbook/LocalPlayer properties through + `RuntimeCharacterState`; content installation, session routing, retained UI, + options, and movement-skill updates all consume that owner; +- the deleted App character-option and movement-skill owner types remain + absent, while Runtime's option replacement and movement-skill update edges + remain live; +- `GameWindow` constructs none of the displaced inventory/item-mana/container/ + shortcut snapshot owners; retained UI and item interaction use the Runtime + action transaction, session bindings use the Runtime shortcut store and use + completion, and shutdown carries only `GameRuntime`; +- `ToolbarController` retains the exact `ShortcutStore` without constructing a + mirror, provider delegate, or loaded-state latch, and spell UI has no second + local mutation calls for filters, desired components, or favorites; and +- graphical input, both runtime adapters, and both shutdown roots borrow the + exact `RuntimeLocalPlayerMovementState`, including its autorun and typed + command execution edges. + +`CompiledCallGraph.ReadOwned` was added as a test-only metadata facility. It +reads declared owner methods plus compiler-generated acquisition/callback +closures, so a typed dependency carried by a UI factory remains inspectable +without freezing the lambda's source spelling. + +Verification follows the proportionate R3 policy: + +- all 11 focused ownership methods pass, including the two retained whole-tree + policies adjacent to the nine replacements; +- the complete `AcDream.App.Tests` hermetic lane passes 5,381/5,381 with zero + skips or failures; +- the complete locked Release build covers all 44 projects with zero warnings + and zero errors; and +- a targeted scan finds source reads only in the two explicitly retained + whole-tree policy methods. The final inventory and twelve-assembly Release + gate remain deferred to closeout. diff --git a/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs b/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs index ca563bdf..de085749 100644 --- a/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs +++ b/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs @@ -56,6 +56,57 @@ internal static class CompiledCallGraph .ToArray(); } + /// + /// Reads a type's declared methods plus compiler-generated closure methods + /// nested beneath it. Composition tests use this when an owned callback or + /// acquisition factory carries the typed edge under review. + /// + public static IReadOnlyList ReadOwned(Type type) + { + ArgumentNullException.ThrowIfNull(type); + const BindingFlags flags = BindingFlags.Instance + | BindingFlags.Static + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly; + MethodBase[] roots = EnumerateOwnedTypes(type) + .SelectMany(owner => owner.GetMethods(flags) + .Cast() + .Concat(owner.GetConstructors(flags))) + .Where(method => method.GetMethodBody() is not null) + .ToArray(); + var pending = new Queue(roots); + var seen = new HashSet(roots); + var calls = new List(); + while (pending.TryDequeue(out MethodBase? method)) + { + foreach (CompiledCall call in ReadMethodReferences(method)) + { + calls.Add(call); + if (call.Target.Module == type.Module + && call.Target.Name.Contains('<', StringComparison.Ordinal) + && call.Target.GetMethodBody() is not null + && seen.Add(call.Target)) + { + pending.Enqueue(call.Target); + } + } + } + + return calls; + } + + private static IEnumerable EnumerateOwnedTypes(Type root) + { + yield return root; + foreach (Type nested in root.GetNestedTypes( + BindingFlags.Public | BindingFlags.NonPublic)) + { + foreach (Type owned in EnumerateOwnedTypes(nested)) + yield return owned; + } + } + public static IReadOnlyList ReadStringLiterals(MethodBase method) { ArgumentNullException.ThrowIfNull(method); diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs index 0458ecc6..14030ec1 100644 --- a/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Runtime/RuntimeActionOwnershipTests.cs @@ -1,4 +1,13 @@ +using System.Reflection; using System.Text.RegularExpressions; +using AcDream.App.Composition; +using AcDream.App.Net; +using AcDream.App.Rendering; +using AcDream.App.Runtime; +using AcDream.App.Tests.Architecture; +using AcDream.App.UI; +using AcDream.Runtime; +using AcDream.Runtime.Gameplay; namespace AcDream.App.Tests.Runtime; @@ -81,71 +90,83 @@ public sealed class RuntimeActionOwnershipTests [Fact] public void UiSessionRuntimeAndShutdownBorrowTheExactActionChildren() { - string root = FindRepositoryRoot(); - string ui = ReadAppSource( - root, - "Composition", - "InteractionRetainedUiComposition.cs"); - string session = ReadAppSource( - root, - "Composition", - "SessionPlayerComposition.cs"); - string liveSession = ReadAppSource( - root, - "Net", - "LiveSessionRuntimeFactory.cs"); - string commands = ReadAppSource( - root, - "Runtime", - "CurrentGameRuntimeCommandAdapter.cs"); - string itemInteraction = ReadAppSource( - root, - "UI", - "ItemInteractionController.cs"); - string shutdown = ReadAppSource( - root, - "Rendering", - "GameWindowLifetime.cs"); - - Assert.Contains("d.Actions.Interaction,", ui, StringComparison.Ordinal); - Assert.Contains("d.Actions.Transactions,", ui, StringComparison.Ordinal); - Assert.Contains("d.Actions.Selection", ui, StringComparison.Ordinal); - Assert.Contains("d.Actions.Combat", ui, StringComparison.Ordinal); - Assert.Contains( - "_dependencies.Actions.CombatAttack", + IReadOnlyList ui = + CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory)); + AssertActionChildren( ui, - StringComparison.Ordinal); - Assert.Contains("d.Actions.SpellCast", ui, StringComparison.Ordinal); - Assert.Contains("d.Actions.Selection", session, StringComparison.Ordinal); - Assert.Contains("d.Actions.Combat", session, StringComparison.Ordinal); - Assert.Contains("_domain.Actions.Combat", liveSession, StringComparison.Ordinal); + "get_Interaction", + "get_Transactions", + "get_Selection", + "get_Combat", + "get_SpellCast"); + AssertActionChildren( + CompiledCallGraph.ReadOwned(typeof(InteractionRetainedUiCompositionPhase)), + "get_CombatAttack"); + + IReadOnlyList session = + CompiledCallGraph.ReadOwned(typeof(SessionPlayerCompositionPhase)); + AssertActionChildren(session, "get_Selection", "get_Combat"); + + PropertyInfo domainActions = typeof(LiveSessionDomainRuntime).GetProperty( + "Actions", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new MissingMemberException(typeof(LiveSessionDomainRuntime).FullName, "Actions"); + Assert.Equal(typeof(RuntimeActionState), domainActions.PropertyType); + + FieldInfo commandActions = Assert.Single( + typeof(CurrentGameRuntimeCommandAdapter).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic), + field => field.Name == "_actions"); + Assert.Equal(typeof(RuntimeActionState), commandActions.FieldType); Assert.Contains( - "_domain.Actions.Transactions.CompleteUse(error)", - liveSession, - StringComparison.Ordinal); - Assert.Contains("_actions.Selection", commands, StringComparison.Ordinal); - Assert.Contains( - "RuntimeInteractionTransactionState runtimeTransactions,", - itemInteraction, - StringComparison.Ordinal); + CompiledCallGraph.ReadDeclared(typeof(CurrentGameRuntimeCommandAdapter)), + call => call.Target.DeclaringType == typeof(RuntimeActionState) + && call.Target.Name == "get_Selection"); + + FieldInfo transactions = Assert.Single( + typeof(ItemInteractionController).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic), + field => field.Name == "_runtimeTransactions"); + Assert.Equal(typeof(RuntimeInteractionTransactionState), transactions.FieldType); Assert.DoesNotContain( - "new InteractionState", - itemInteraction, - StringComparison.Ordinal); - Assert.Contains( - "new ResourceShutdownStage(\"game runtime root\"", - shutdown, - StringComparison.Ordinal); + CompiledCallGraph.ReadDeclared(typeof(ItemInteractionController)), + call => call.Target.DeclaringType == typeof(InteractionState) + && call.Target.IsConstructor); + + IReadOnlyList labels = CompiledCallGraph.ReadStringLiterals( + typeof(GameWindowShutdownManifest).GetMethod( + nameof(GameWindowShutdownManifest.Create), + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new MissingMethodException()); + Assert.Contains("game runtime root", labels); Assert.DoesNotContain( - "RuntimeActionState Actions", - shutdown, - StringComparison.Ordinal); - Assert.False(File.Exists(Path.Combine( - root, - "src", - "AcDream.App", - "UI", - "InteractionState.cs"))); + typeof(IngressShutdownRoots).GetProperties(), + property => property.PropertyType == typeof(RuntimeActionState)); + Assert.DoesNotContain( + typeof(LiveShutdownRoots).GetProperties(), + property => property.PropertyType == typeof(RuntimeActionState)); + + Assert.DoesNotContain( + typeof(GameWindow).Assembly.GetTypes(), + type => type.FullName == "AcDream.App.UI.InteractionState"); + } + + private static void AssertActionChildren( + IReadOnlyList calls, + params string[] getters) + { + foreach (string getter in getters) + { + Assert.True( + calls.Any(call => call.Target.DeclaringType == typeof(RuntimeActionState) + && call.Target.Name == getter), + $"Missing RuntimeActionState.{getter}. Runtime calls: " + + string.Join(", ", calls + .Where(call => call.Target.DeclaringType?.Namespace?.StartsWith( + "AcDream.Runtime", StringComparison.Ordinal) == true) + .Select(call => $"{call.Target.DeclaringType?.Name}.{call.Target.Name}") + .Distinct())); + } } private static string ReadAppSource(string root, params string[] relative) => diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs index 77e2a1bb..cd5d924f 100644 --- a/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Runtime/RuntimeCharacterOwnershipTests.cs @@ -1,149 +1,145 @@ +using System.Reflection; +using AcDream.App.Composition; +using AcDream.App.Net; +using AcDream.App.Rendering; +using AcDream.App.Tests.Architecture; +using AcDream.Core.Player; +using AcDream.Core.Spells; +using AcDream.Runtime; +using AcDream.Runtime.Gameplay; +using AcDream.Runtime.Session; + namespace AcDream.App.Tests.Runtime; public sealed class RuntimeCharacterOwnershipTests { + private const BindingFlags Declared = BindingFlags.Instance + | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly; + [Fact] public void ProductionConstructsOnlyTheRuntimeCharacterOwner() { - string source = ReadSource("Rendering", "GameWindow.cs"); + FieldInfo runtime = typeof(GameWindow).GetField("_runtime", Declared) + ?? throw new MissingFieldException(typeof(GameWindow).FullName, "_runtime"); + Assert.Equal(typeof(GameRuntime), runtime.FieldType); + Assert.Single( + CompiledCallGraph.ReadDeclared(typeof(GameWindow)), + call => call.Target.DeclaringType == typeof(GameRuntime) + && call.Target.IsConstructor); - Assert.Contains( - "private readonly GameRuntime _runtime;", - source, - StringComparison.Ordinal); - Assert.Contains( - "_runtime = new GameRuntime(new GameRuntimeDependencies(", - source, - StringComparison.Ordinal); + Type[] childOwners = + [ + typeof(RuntimeCharacterState), + typeof(Spellbook), + typeof(LocalPlayerState), + ]; Assert.DoesNotContain( - "new RuntimeCharacterState(", - source, - StringComparison.Ordinal); - Assert.DoesNotContain( - "new AcDream.Core.Spells.Spellbook", - source, - StringComparison.Ordinal); - Assert.DoesNotContain( - "new AcDream.Core.Player.LocalPlayerState", - source, - StringComparison.Ordinal); - Assert.Contains( - "SpellBook =>\n _runtimeCharacter.Spellbook;", - Normalize(source), - StringComparison.Ordinal); - Assert.Contains( - "LocalPlayer =>\n _runtimeCharacter.LocalPlayer;", - Normalize(source), - StringComparison.Ordinal); + CompiledCallGraph.ReadDeclared(typeof(GameWindow)), + call => call.Target.IsConstructor + && childOwners.Contains(call.Target.DeclaringType)); + + AssertGetterRoutesToCharacterChild("SpellBook", "get_Spellbook"); + AssertGetterRoutesToCharacterChild("LocalPlayer", "get_LocalPlayer"); } [Fact] public void SessionRoutingAndShutdownBorrowTheExactRuntimeOwner() { - string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs"); - string router = ReadRuntimeSource( - "Session", - "LiveSessionEventRouter.cs"); - string content = ReadSource( - "Composition", - "ContentEffectsAudioComposition.cs"); - string ui = ReadSource( - "Composition", - "InteractionRetainedUiComposition.cs"); - string inventory = ReadRuntimeSource( - "Gameplay", - "RuntimeInventoryState.cs"); - string shutdown = ReadSource("Rendering", "GameWindowLifetime.cs"); + PropertyInfo character = typeof(LiveSessionDomainRuntime).GetProperty( + "Character", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new MissingMemberException(typeof(LiveSessionDomainRuntime).FullName, + "Character"); + Assert.Equal(typeof(RuntimeCharacterState), character.PropertyType); + IReadOnlyList router = + CompiledCallGraph.ReadOwned(typeof(LiveSessionEventRouter)); + AssertCharacterChildren(router, "get_Spellbook", "get_LocalPlayer"); + + MethodInfo install = typeof(RetailContentEffectsAudioCompositionFactory).GetMethod( + nameof(RetailContentEffectsAudioCompositionFactory.InstallSpellMetadata), + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new MissingMethodException(); Assert.Contains( - "RuntimeCharacterState Character", - session, - StringComparison.Ordinal); - Assert.Contains( - "character.Character.Spellbook", - router, - StringComparison.Ordinal); - Assert.Contains( - "character.Character.LocalPlayer", - router, - StringComparison.Ordinal); - Assert.Contains( - "_factory.InstallSpellMetadata(_dependencies.Character, magic)", - content, - StringComparison.Ordinal); - Assert.Contains( - "d.Character.Spellbook", - ui, - StringComparison.Ordinal); - Assert.Contains( - "d.Character.LocalPlayer", - ui, - StringComparison.Ordinal); + CompiledCallGraph.Read(install), + call => call.Target.DeclaringType == typeof(RuntimeCharacterState) + && call.Target.Name == nameof(RuntimeCharacterState.InstallSpellMetadata)); + + IReadOnlyList ui = + CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory)); + AssertCharacterChildren(ui, "get_Spellbook", "get_LocalPlayer"); + Assert.DoesNotContain( - "DesiredComponentState", - inventory, - StringComparison.Ordinal); - Assert.Contains( - "new ResourceShutdownStage(\"game runtime root\"", - shutdown, - StringComparison.Ordinal); + typeof(RuntimeInventoryState).GetFields(Declared), + field => field.FieldType.Name == "DesiredComponentState"); + + IReadOnlyList labels = CompiledCallGraph.ReadStringLiterals( + typeof(GameWindowShutdownManifest).GetMethod( + nameof(GameWindowShutdownManifest.Create), + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new MissingMethodException()); + Assert.Contains("game runtime root", labels); Assert.DoesNotContain( - "RuntimeCharacterState Character", - shutdown, - StringComparison.Ordinal); + typeof(IngressShutdownRoots).GetProperties(), + property => property.PropertyType == typeof(RuntimeCharacterState)); + Assert.DoesNotContain( + typeof(LiveShutdownRoots).GetProperties(), + property => property.PropertyType == typeof(RuntimeCharacterState)); } [Fact] public void AppCharacterOptionAndMovementSkillOwnersWereDeleted() { - string gameWindow = ReadSource("Rendering", "GameWindow.cs"); - string sources = ReadSource( - "Composition", - "InteractionUiRuntimeSources.cs"); - string playerState = ReadSource("Input", "LocalPlayerRuntimeState.cs"); - string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs"); - string router = ReadRuntimeSource( - "Session", - "LiveSessionEventRouter.cs"); - - Assert.DoesNotContain( + HashSet removed = + [ "PlayerCharacterOptionsState", - gameWindow + sources + session, - StringComparison.Ordinal); - Assert.DoesNotContain( "LocalPlayerSkillState", - gameWindow + playerState + session, - StringComparison.Ordinal); + ]; + Assert.DoesNotContain( + typeof(GameWindow).Assembly.GetTypes(), + type => type.Name is not null && removed.Contains(type.Name)); + + IReadOnlyList router = + CompiledCallGraph.ReadOwned(typeof(LiveSessionEventRouter)); Assert.Contains( - "character.Character.Options.Replace", router, - StringComparison.Ordinal); + call => call.Target.DeclaringType == typeof(RuntimeCharacterOptionsState) + && call.Target.Name == nameof(RuntimeCharacterOptionsState.Replace)); Assert.Contains( - "character.Character.MovementSkills.Update", router, - StringComparison.Ordinal); + call => call.Target.DeclaringType == typeof(RuntimeMovementSkillState) + && call.Target.Name.StartsWith("Update", StringComparison.Ordinal)); } - private static string ReadSource(params string[] relative) => - File.ReadAllText(Path.Combine( - [FindRepositoryRoot(), "src", "AcDream.App", .. relative])); - - private static string ReadRuntimeSource(params string[] relative) => - File.ReadAllText(Path.Combine( - [FindRepositoryRoot(), "src", "AcDream.Runtime", .. relative])); - - private static string Normalize(string source) => - source.Replace("\r\n", "\n", StringComparison.Ordinal); - - private static string FindRepositoryRoot() + private static void AssertGetterRoutesToCharacterChild( + string property, + string childGetter) { - var current = new DirectoryInfo(AppContext.BaseDirectory); - while (current is not null) + MethodInfo getter = typeof(GameWindow).GetProperty(property, Declared)?.GetMethod + ?? throw new MissingMemberException(typeof(GameWindow).FullName, property); + IReadOnlyList calls = CompiledCallGraph.Read(getter); + Assert.Contains(calls, call => call.Target.DeclaringType == typeof(GameWindow) + && call.Target.Name == "get__runtimeCharacter"); + Assert.Contains(calls, call => call.Target.DeclaringType == typeof(RuntimeCharacterState) + && call.Target.Name == childGetter); + } + + private static void AssertCharacterChildren( + IReadOnlyList calls, + params string[] getters) + { + foreach (string getter in getters) { - if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx"))) - return current.FullName; - current = current.Parent; + Assert.True( + calls.Any(call => call.Target.DeclaringType == typeof(RuntimeCharacterState) + && call.Target.Name == getter), + $"Missing RuntimeCharacterState.{getter}. Runtime calls: " + + string.Join(", ", calls + .Where(call => call.Target.DeclaringType?.Namespace?.StartsWith( + "AcDream.Runtime", StringComparison.Ordinal) == true) + .Select(call => $"{call.Target.DeclaringType?.Name}.{call.Target.Name}") + .Distinct())); } - throw new DirectoryNotFoundException("AcDream.slnx was not found."); } } diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs index fc3a0357..fff4823f 100644 --- a/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Runtime/RuntimeInventoryOwnershipTests.cs @@ -1,199 +1,156 @@ +using System.Reflection; +using AcDream.App.Composition; +using AcDream.App.Net; +using AcDream.App.Rendering; +using AcDream.App.Tests.Architecture; +using AcDream.App.UI; +using AcDream.App.UI.Layout; +using AcDream.Core.Items; +using AcDream.Runtime; +using AcDream.Runtime.Gameplay; + namespace AcDream.App.Tests.Runtime; public sealed class RuntimeInventoryOwnershipTests { + private const BindingFlags Declared = BindingFlags.Instance + | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly; + [Fact] public void ProductionConstructsOnlyTheRuntimeInventoryOwner() { - string source = ReadSource("Rendering", "GameWindow.cs"); + FieldInfo runtime = typeof(GameWindow).GetField("_runtime", Declared) + ?? throw new MissingFieldException(typeof(GameWindow).FullName, "_runtime"); + Assert.Equal(typeof(GameRuntime), runtime.FieldType); - Assert.Contains( - "private readonly GameRuntime _runtime;", - source, - StringComparison.Ordinal); - Assert.DoesNotContain( - "new RuntimeInventoryState(", - source, - StringComparison.Ordinal); - Assert.DoesNotContain( - "new AcDream.Core.Items.ItemManaState", - source, - StringComparison.Ordinal); - Assert.DoesNotContain( - "new DesiredComponentSnapshotState", - source, - StringComparison.Ordinal); - Assert.DoesNotContain( - "new ShortcutSnapshotState", - source, - StringComparison.Ordinal); - Assert.DoesNotContain( - "new AcDream.Core.Items.ExternalContainerState", - source, - StringComparison.Ordinal); - Assert.DoesNotContain( + HashSet displacedConstructors = + [ + nameof(RuntimeInventoryState), + nameof(ItemManaState), + "DesiredComponentSnapshotState", + "ShortcutSnapshotState", + nameof(ExternalContainerState), "DesiredComponentState", - source, - StringComparison.Ordinal); + ]; + Assert.DoesNotContain( + CompiledCallGraph.ReadDeclared(typeof(GameWindow)), + call => call.Target.IsConstructor + && call.Target.DeclaringType?.Name is { } name + && displacedConstructors.Contains(name)); + Assert.DoesNotContain( + typeof(GameWindow).GetFields(Declared), + field => displacedConstructors.Contains(field.FieldType.Name)); } [Fact] public void UiSessionAndShutdownBorrowTheExactRuntimeOwner() { - string ui = ReadSource( - "Composition", - "InteractionRetainedUiComposition.cs"); - string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs"); - string shutdown = ReadSource("Rendering", "GameWindowLifetime.cs"); - string itemInteraction = ReadSource( - "UI", - "ItemInteractionController.cs"); + IReadOnlyList ui = + CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory)); + AssertRuntimeCall(ui, typeof(RuntimeActionState), "get_Transactions"); + Assert.DoesNotContain( + ui, + call => call.Target.DeclaringType == typeof(InventoryTransactionState) + && call.Target.IsConstructor); - Assert.Contains( - "d.Actions.Transactions", - ui, - StringComparison.Ordinal); + FieldInfo transactions = Assert.Single( + typeof(ItemInteractionController).GetFields(Declared), + field => field.Name == "_runtimeTransactions"); + Assert.Equal(typeof(RuntimeInteractionTransactionState), transactions.FieldType); Assert.DoesNotContain( - "new InventoryTransactionState(d.Inventory.Objects)", - ui, - StringComparison.Ordinal); + typeof(ItemInteractionController).GetFields(Declared), + field => field.Name == "_ownsTransactions"); Assert.DoesNotContain( - "new InventoryTransactionState", - itemInteraction, - StringComparison.Ordinal); + CompiledCallGraph.ReadDeclared(typeof(ItemInteractionController)), + call => call.Target.DeclaringType == typeof(InventoryTransactionState) + && call.Target.IsConstructor); + + IReadOnlyList bindings = + CompiledCallGraph.ReadOwned(typeof(LiveSessionRuntimeFactory)); + Assert.Contains(bindings, call => call.Target.DeclaringType == typeof(ShortcutStore) + && call.Target.Name == nameof(ShortcutStore.Load)); + Assert.Contains(bindings, call => + call.Target.DeclaringType == typeof(RuntimeInteractionTransactionState) + && call.Target.Name == nameof(RuntimeInteractionTransactionState.CompleteUse)); + Assert.DoesNotContain(bindings, call => + call.Target.DeclaringType == typeof(RuntimeInventoryState) + && call.Target.Name == "get_DesiredComponents"); + + IReadOnlyList labels = CompiledCallGraph.ReadStringLiterals( + typeof(GameWindowShutdownManifest).GetMethod( + nameof(GameWindowShutdownManifest.Create), + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new MissingMethodException()); + Assert.Contains("game runtime root", labels); + Assert.Contains("game runtime", labels); Assert.DoesNotContain( - "_ownsTransactions", - itemInteraction, - StringComparison.Ordinal); - Assert.Contains( - "RuntimeInteractionTransactionState runtimeTransactions,", - itemInteraction, - StringComparison.Ordinal); - Assert.Contains( - "OnShortcuts: _domain.Inventory.Shortcuts.Load", - session, - StringComparison.Ordinal); - Assert.Contains( - "_domain.Actions.Transactions.CompleteUse(error)", - session, - StringComparison.Ordinal); + typeof(IngressShutdownRoots).GetProperties(), + property => property.PropertyType == typeof(RuntimeInventoryState)); Assert.DoesNotContain( - "_domain.Inventory.DesiredComponents", - session, - StringComparison.Ordinal); - Assert.Contains( - "new ResourceShutdownStage(\"game runtime root\"", - shutdown, - StringComparison.Ordinal); - Assert.Contains( - "Hard(\"game runtime\", () => DisposeGameRuntime(live.Runtime))", - shutdown, - StringComparison.Ordinal); - Assert.DoesNotContain( - "RuntimeInventoryState Inventory", - shutdown, - StringComparison.Ordinal); + typeof(LiveShutdownRoots).GetProperties(), + property => property.PropertyType == typeof(RuntimeInventoryState)); } [Fact] public void ToolbarBorrowsRuntimeShortcutManagerWithoutAProviderOrMirror() { - string toolbar = ReadSource( - "UI", - "Layout", - "ToolbarController.cs"); - string ui = ReadSource( - "Composition", - "InteractionRetainedUiComposition.cs"); - - Assert.Contains( - "private readonly ShortcutStore _store;", - toolbar, - StringComparison.Ordinal); - Assert.Contains( - "_store = shortcuts ?? throw", - toolbar, - StringComparison.Ordinal); + FieldInfo store = Assert.Single( + typeof(ToolbarController).GetFields(Declared), + field => field.Name == "_store"); + Assert.Equal(typeof(ShortcutStore), store.FieldType); Assert.DoesNotContain( - "new ShortcutStore()", - toolbar, - StringComparison.Ordinal); + CompiledCallGraph.ReadDeclared(typeof(ToolbarController)), + call => call.Target.DeclaringType == typeof(ShortcutStore) + && call.Target.IsConstructor); Assert.DoesNotContain( - "Func>", - toolbar, - StringComparison.Ordinal); + typeof(ToolbarController).GetFields(Declared), + field => field.Name == "_storeLoaded" + || IsShortcutProvider(field.FieldType)); Assert.DoesNotContain( - "_storeLoaded", - toolbar, - StringComparison.Ordinal); - Assert.Contains( - "d.Inventory.Shortcuts,", - ui, - StringComparison.Ordinal); + typeof(ToolbarController).GetConstructors(Declared) + .SelectMany(constructor => constructor.GetParameters()), + parameter => IsShortcutProvider(parameter.ParameterType)); + AssertRuntimeCall( + CompiledCallGraph.ReadOwned(typeof(RetailInteractionRetainedUiCompositionFactory)), + typeof(RuntimeInventoryState), + "get_Shortcuts"); } [Fact] public void SpellUiDoesNotApplyASecondLocalCommandMutation() { - string spellbook = ReadSource( - "UI", - "Layout", - "SpellbookWindowController.cs"); - string spellcasting = ReadSource( - "UI", - "Layout", - "SpellcastingUiController.cs"); - + HashSet duplicateMutations = + [ + "SetSpellbookFilters", + "SetDesiredComponent", + "SetFavorite", + "RemoveFavorite", + ]; Assert.DoesNotContain( - "_spellbook.SetSpellbookFilters(filters);", - spellbook, - StringComparison.Ordinal); - Assert.DoesNotContain( - "_spellbook.SetDesiredComponent(componentId, parsed);", - spellbook, - StringComparison.Ordinal); - Assert.DoesNotContain( - "_spellbook.SetFavorite(", - spellcasting, - StringComparison.Ordinal); - Assert.DoesNotContain( - "_spellbook.RemoveFavorite(", - spellcasting, - StringComparison.Ordinal); + CompiledCallGraph.ReadOwned(typeof(SpellbookWindowController)) + .Concat(CompiledCallGraph.ReadOwned(typeof(SpellcastingUiController))), + call => duplicateMutations.Contains(call.Target.Name)); } - private static string ReadSource(params string[] relative) - { - string root = FindRepositoryRoot(); - return File.ReadAllText(Path.Combine( - [root, "src", "AcDream.App", .. relative])); - } + private static bool IsShortcutProvider(Type type) => + type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(Func<>) + && type.GenericTypeArguments[0].IsGenericType + && type.GenericTypeArguments[0].GetGenericTypeDefinition() + == typeof(IReadOnlyList<>); - private static string FindRepositoryRoot() - { - var current = new DirectoryInfo(AppContext.BaseDirectory); - while (current is not null) - { - if (File.Exists(Path.Combine(current.FullName, "AcDream.slnx"))) - return current.FullName; - current = current.Parent; - } - throw new DirectoryNotFoundException("AcDream.slnx was not found."); - } - - private static void AssertAppearsInOrder( - string source, - params string[] fragments) - { - int cursor = 0; - foreach (string fragment in fragments) - { - int index = source.IndexOf( - fragment, - cursor, - StringComparison.Ordinal); - Assert.True(index >= 0, $"Missing source fragment: {fragment}"); - cursor = index + fragment.Length; - } - } + private static void AssertRuntimeCall( + IReadOnlyList calls, + Type owner, + string name) => + Assert.True( + calls.Any(call => call.Target.DeclaringType == owner + && call.Target.Name == name), + $"Missing {owner.Name}.{name}. Runtime calls: " + + string.Join(", ", calls + .Where(call => call.Target.DeclaringType?.Namespace?.StartsWith( + "AcDream.Runtime", StringComparison.Ordinal) == true) + .Select(call => $"{call.Target.DeclaringType?.Name}.{call.Target.Name}") + .Distinct())); } diff --git a/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs b/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs index 376cea1a..d752ff7e 100644 --- a/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs +++ b/tests/AcDream.App.Tests/Runtime/RuntimeMovementOwnershipTests.cs @@ -1,4 +1,11 @@ +using System.Reflection; using System.Text.RegularExpressions; +using AcDream.App.Input; +using AcDream.App.Rendering; +using AcDream.App.Runtime; +using AcDream.App.Tests.Architecture; +using AcDream.Runtime; +using AcDream.Runtime.Gameplay; namespace AcDream.App.Tests.Runtime; @@ -57,65 +64,67 @@ public sealed class RuntimeMovementOwnershipTests [Fact] public void GraphicalInputRuntimeViewsAndShutdownBorrowTheExactOwner() { - string root = FindRepositoryRoot(); - string gameWindow = ReadAppSource(root, "Rendering", "GameWindow.cs"); - string input = ReadAppSource( - root, - "Input", - "DispatcherMovementInputSource.cs"); - string adapter = ReadAppSource( - root, - "Runtime", - "CurrentGameRuntimeAdapter.cs"); - string commands = ReadAppSource( - root, - "Runtime", - "CurrentGameRuntimeCommandAdapter.cs"); - string lifetime = ReadAppSource( - root, - "Rendering", - "GameWindowLifetime.cs"); + PropertyInfo movementProperty = typeof(GameWindow).GetProperty( + "_playerControllerSlot", + BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new MissingMemberException(typeof(GameWindow).FullName, + "_playerControllerSlot"); + Assert.Equal(typeof(RuntimeLocalPlayerMovementState), + movementProperty.PropertyType); + Assert.Contains( + CompiledCallGraph.Read(movementProperty.GetMethod!), + call => call.Target.DeclaringType == typeof(GameRuntime) + && call.Target.Name == "get_MovementOwner"); + ConstructorInfo window = Assert.Single( + typeof(GameWindow).GetConstructors( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic), + constructor => CompiledCallGraph.Read(constructor).Any(call => + call.Target.DeclaringType == typeof(DispatcherMovementInputSource) + && call.Target.IsConstructor)); Assert.Contains( - "RuntimeLocalPlayerMovementState _playerControllerSlot =>", - gameWindow, - StringComparison.Ordinal); + CompiledCallGraph.Read(window), + call => call.Target.DeclaringType == typeof(GameWindow) + && call.Target.Name == "get__playerControllerSlot"); + + FieldInfo inputMovement = Assert.Single( + typeof(DispatcherMovementInputSource).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic), + field => field.Name == "_movement"); + Assert.Equal(typeof(RuntimeLocalPlayerMovementState), inputMovement.FieldType); Assert.Contains( - "_runtime.MovementOwner;", - gameWindow, - StringComparison.Ordinal); + CompiledCallGraph.ReadDeclared(typeof(DispatcherMovementInputSource)), + call => call.Target.DeclaringType == typeof(RuntimeLocalPlayerMovementState) + && call.Target.Name == "get_AutoRunActive"); + + FieldInfo adapterRuntime = Assert.Single( + typeof(CurrentGameRuntimeAdapter).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic), + field => field.Name == "_runtime"); + Assert.Equal(typeof(GameRuntime), adapterRuntime.FieldType); + FieldInfo commandMovement = Assert.Single( + typeof(CurrentGameRuntimeCommandAdapter).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic), + field => field.Name == "_movement"); + Assert.Equal(typeof(RuntimeLocalPlayerMovementState), commandMovement.FieldType); Assert.Contains( - "new AcDream.App.Input.DispatcherMovementInputSource(", - gameWindow, - StringComparison.Ordinal); + CompiledCallGraph.ReadDeclared(typeof(CurrentGameRuntimeCommandAdapter)), + call => call.Target.DeclaringType == typeof(RuntimeLocalPlayerMovementState) + && call.Target.Name == nameof(RuntimeLocalPlayerMovementState.Execute)); + + Assert.Equal(typeof(GameRuntime), + typeof(IngressShutdownRoots).GetProperty("Runtime")?.PropertyType); + Assert.Equal(typeof(GameRuntime), + typeof(LiveShutdownRoots).GetProperty("Runtime")?.PropertyType); Assert.Contains( - "_playerControllerSlot,", - gameWindow, - StringComparison.Ordinal); - Assert.Contains( - "RuntimeLocalPlayerMovementState movement,", - input, - StringComparison.Ordinal); - Assert.Contains("_movement.AutoRunActive", input, StringComparison.Ordinal); - Assert.Contains( - "private readonly GameRuntime _runtime;", - adapter, - StringComparison.Ordinal); - Assert.Contains("_movement.Execute(command)", commands, StringComparison.Ordinal); - Assert.Contains( - "GameRuntime Runtime", - lifetime, - StringComparison.Ordinal); - Assert.Contains( - "\"game runtime root\"", - lifetime, - StringComparison.Ordinal); + "game runtime root", + CompiledCallGraph.ReadStringLiterals( + typeof(GameWindowShutdownManifest).GetMethod( + nameof(GameWindowShutdownManifest.Create), + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + ?? throw new MissingMethodException())); } - private static string ReadAppSource(string root, params string[] relative) => - File.ReadAllText(Path.Combine( - [root, "src", "AcDream.App", .. relative])); - private static string FindRepositoryRoot() { var current = new DirectoryInfo(AppContext.BaseDirectory);