test: replace composition source freezes
This commit is contained in:
parent
caa5eb8b2b
commit
80c7b44457
8 changed files with 471 additions and 406 deletions
|
|
@ -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<CompiledCall> 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<FieldInfo> 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<CompiledCall> 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<MethodBase> 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<CompiledCall> 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<CompiledCall> 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<CompiledCall> 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<CompiledCall> 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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue