test: replace gameplay owner source freezes
This commit is contained in:
parent
9b94050229
commit
84034f732c
6 changed files with 473 additions and 388 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -56,6 +56,57 @@ internal static class CompiledCallGraph
|
|||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<CompiledCall> 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<MethodBase>()
|
||||
.Concat(owner.GetConstructors(flags)))
|
||||
.Where(method => method.GetMethodBody() is not null)
|
||||
.ToArray();
|
||||
var pending = new Queue<MethodBase>(roots);
|
||||
var seen = new HashSet<MethodBase>(roots);
|
||||
var calls = new List<CompiledCall>();
|
||||
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<Type> 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<string> ReadStringLiterals(MethodBase method)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(method);
|
||||
|
|
|
|||
|
|
@ -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<CompiledCall> 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<CompiledCall> 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<string> 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<CompiledCall> 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) =>
|
||||
|
|
|
|||
|
|
@ -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<CompiledCall> 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<CompiledCall> 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<string> 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<string> 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<CompiledCall> 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<CompiledCall> 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<CompiledCall> 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.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string> 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<CompiledCall> 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<CompiledCall> 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<string> 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<IReadOnlyList<ShortcutEntry>>",
|
||||
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<string> 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<CompiledCall> 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()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue