From 0ad2ee1cdf4104d7fd56ee0db440b9aba8f4b0fb Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 18 Aug 2026 15:14:41 +0200 Subject: [PATCH] test: replace streaming source freezes --- .../2026-08-18-r3-test-truth-ledger.md | 72 +++- .../Architecture/CompiledCallGraph.cs | 84 ++++- .../Streaming/LandblockBuildOriginTests.cs | 311 +++++++++--------- .../LandblockPhysicsPublisherTests.cs | 102 ++++-- .../LandblockRenderPublisherTests.cs | 44 ++- .../GameWindowLiveEntityCompositionTests.cs | 108 +++--- 6 files changed, 436 insertions(+), 285 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 d3b65536..16c1e4b0 100644 --- a/docs/reviews/2026-08-18-r3-test-truth-ledger.md +++ b/docs/reviews/2026-08-18-r3-test-truth-ledger.md @@ -38,12 +38,12 @@ pwsh ./tools/audit-test-inventory.ps1 The generated JSON lives under ignored `artifacts/test-audit/`; it is not a second 6.8 MB checked-in source of truth. The script and this reviewed ledger are durable, while paths and line numbers regenerate after every batch. The -inventory refreshed through Batch W reports: +inventory refreshed through Batch X reports: | Syntax signal | Current count | |---|---:| | Tracked/parsed C# test files | 1,254 / 1,254 | -| Attributed test methods (not expanded theory cases) | 11,416 | +| Attributed test methods (not expanded theory cases) | 11,417 | | Exact duplicate-data rows | 0 | | Reviewed body-equivalent groups / methods | 11 / 27 | | Methods containing at least one empty `return;` | 138 | @@ -56,7 +56,7 @@ inventory refreshed through Batch W reports: | Methods directly using `Thread.Sleep` / `Task.Delay` | 15 / 14 | | Cancellable-infinite-only / elapsed-time methods | 5 / 24 | | Methods directly reading environment variables | 47 | -| Methods directly / through same-file helpers reading `.cs` source text | 63 / 98 | +| Methods directly / through same-file helpers reading `.cs` source text | 57 / 92 | The three remaining prerequisite-return candidates are all reviewed branch false positives: the two Windows/Linux factory assertions and the launcher's @@ -140,7 +140,7 @@ identifiable in the lane report. | T-009 wall-clock double-click tests | resolved in batch E | Four sleeps were replaced by a deterministic test clock behind an internal factory overload. The production factory still reads `Environment.TickCount64` exactly as before. | | T-010 two useless cases | high-confidence cleanup batch A | Delete `SmokeTest.TestProject_IsWired` and `ChaseCameraTests.ImplementsICamera`; compilation already proves both claims. | | T-011 diagnostic-only methods | resolved in batches C, L, and R | The reviewed current set is 82 methods / 103 cases. All carry `Purpose=Diagnostic`, preserving the apparatus while removing it from release pass totals. Batch R catches investigations whose only assertion validates fixture/DAT availability, which the original mechanical output-only scan could not distinguish from an oracle. | -| T-012 source-text freezes | direct-read map completed in batch I, helper-mediated gap corrected in batch U, and staged replacement active | Seventeen whole-tree architecture rules and five cross-artifact contracts stay. Batch W retires the first nine literal implementation freezes beside stronger behavior or compiled-metadata evidence; 76 staged replacements remain. | +| T-012 source-text freezes | direct-read map completed in batch I, helper-mediated gap corrected in batch U, and staged replacement active | Seventeen whole-tree architecture rules and five cross-artifact contracts stay. Batches W/X retire the first 15 literal implementation freezes beside stronger behavior or compiled-metadata evidence; 70 staged replacements remain. | | T-013 controller self-comparison | high-confidence cleanup batch A | Capture the first controller next to the first body and compare every retry with that reference. | | T-014 seven load-sensitive tests | six mechanisms repaired in batch F; one product defect classified | Virtual/wall-clock mixing, tiered-JIT allocation noise, a live controller clock, and a ThreadPool-start timing oracle were removed without weakening behavioral contracts. `DatSoundCache` #321 is a real in-flight-entry race and now runs as `Status=KnownFailure` pending a product fix. | | T-015 four non-prerequisite skips | resolved in batches A/B | PVS scaffold deleted with rationale preserved; redundant chat/radar generators deleted in favor of the comprehensive Manual lane; tower oracle is `Status=KnownFailure`. | @@ -1085,3 +1085,67 @@ Verification: source readers from 67 to 63 and total direct/helper readers from 107 to 98. The remaining 98 reconcile to the 22 approved retained policies/contracts and 76 staged replacements. + +## Batch X streaming publication and live-entity source-freeze replacement + +Batch X converts the four approved streaming-publication freezes and both +live-entity composition freezes. No product source changes. The tests now +inspect the built dependency graph, execute the real origin publishers, or +assert the typed owner/reset calls instead of matching C# fragments. + +The conversion found one materially misleading old assertion. The physics +publisher test rejected the literal strings +`_physicsDataCache.RemoveCellsForLandblock` and +`_physicsDataCache.RemoveBuildingsForLandblock`. Those fragments were absent, +but the real publisher correctly performs both removals through +`publication.StagingCache`; the test therefore passed without establishing its +intended generation-isolation rule. The replacement verifies the compiled +`AdvanceBeginOne` path obtains the staging cache before each removal, commits +through `RuntimePhysicsState.CommitCollisionGeneration`, does not restart the +retained-owner capture, and keeps the old publication bodies out of +`GameWindow`. + +The other exact decisions are: + +- the render publisher boundary now scans every compiled GameWindow method and + constructor for forbidden terrain, cell-visibility, and EnvCell publication + calls, and reflects the removed mesh-preparation/body ownership; +- the former umbrella origin test is narrowed to the claim its fixture could + not observe: production `LivePresentationCompositionPhase` constructs the + render, physics, and static publishers before the one presentation pipeline, + while GameWindow retains only that pipeline. Captured origin itself remains + covered by the real `LandblockBuildFactory.BuildFar` and + `LandblockRenderPublisher.BeginPublication` behavior tests; +- recenter ordering is now its own named compiled-call contract: old-window + retirement completion precedes `LiveWorldOriginState.Recenter`, which + precedes destination commit. The extensive recenter transaction suite still + owns retry, replacement, session-reset, and retirement behavior; +- the shutdown test reads the compiled operation manifest, including delegate + targets, to prove Runtime session stop precedes streamer disposal. It also + proves `GameRuntime.StopSession` disposes the live session before checking + its completion barrier and retains the re-entrant diagnostic message; +- live animation/projectile composition now reflects the exact typed fields + and proves `CompletePresentation` constructs `DatProjectileSetupResolver` + before `ProjectileController`, with no window closure or remote-motion-view + owner; and +- the session reset test follows compiled calls through + `LiveSessionRuntimeFactory`, `PlayerModeController`, the network-update + owner, and `LiveWorldOriginState`, rather than pinning four statement strings. + +`CompiledCallGraph` gained reusable readers for every declared method, +delegate method targets, and compiled string literals. These are test-only +metadata facilities used when invoking the real process/window graph would +mutate external resources. + +Verification: + +- the four directly affected owner suites pass 111/111; +- the complete locked Release build covers all 44 projects with zero warnings + and zero errors; +- the no-retry complete hermetic Release gate passes 14,349/14,349 with zero + skips or failures across all 12 test assemblies. The one-case increase is the + newly separated recenter-order contract; and +- the regenerated 1,254-file inventory parses every file, reports 11,417 + attributed methods, and reduces direct/total source readers from 63/98 to + 57/92. The remaining 92 reconcile to 22 approved retained + policies/contracts and 70 staged replacements. diff --git a/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs b/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs index 29e10362..d97e5cad 100644 --- a/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs +++ b/tests/AcDream.App.Tests/Architecture/CompiledCallGraph.cs @@ -21,6 +21,69 @@ internal static class CompiledCallGraph .ToDictionary(opCode => opCode.Value); public static IReadOnlyList Read(MethodBase method) + => ReadMethodReferences(method, includeDelegateTargets: false); + + /// + /// Reads calls, object construction, and method references used to build + /// delegates. The latter lets lifetime tests inspect an operation manifest + /// without invoking its real process/window resources. + /// + public static IReadOnlyList ReadMethodReferences(MethodBase method) => + ReadMethodReferences(method, includeDelegateTargets: true); + + public static IReadOnlyList ReadDeclared(Type type) + { + ArgumentNullException.ThrowIfNull(type); + const BindingFlags flags = BindingFlags.Instance + | BindingFlags.Static + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly; + return type.GetMethods(flags) + .Cast() + .Concat(type.GetConstructors(flags)) + .Where(method => method.GetMethodBody() is not null) + .SelectMany(Read) + .ToArray(); + } + + public static IReadOnlyList ReadStringLiterals(MethodBase method) + { + ArgumentNullException.ThrowIfNull(method); + byte[] il = method.GetMethodBody()?.GetILAsByteArray() + ?? throw new InvalidOperationException( + $"{method.DeclaringType?.FullName}.{method.Name} has no compiled body."); + var literals = new List(); + + for (int cursor = 0; cursor < il.Length;) + { + OpCode opCode = ReadOpCode(il, ref cursor); + if (opCode == OpCodes.Ldstr) + { + int token = BitConverter.ToInt32(il, cursor); + literals.Add(method.Module.ResolveString(token)); + } + + cursor += OperandSize(opCode.OperandType, il, cursor); + } + + return literals; + } + + public static int IndexOf( + IReadOnlyList calls, + Type declaringType, + string methodName, + int startIndex = 0) => + Enumerable.Range(startIndex, calls.Count - startIndex) + .FirstOrDefault( + index => calls[index].Target.DeclaringType == declaringType + && calls[index].Target.Name == methodName, + -1); + + private static IReadOnlyList ReadMethodReferences( + MethodBase method, + bool includeDelegateTargets) { ArgumentNullException.ThrowIfNull(method); byte[] il = method.GetMethodBody()?.GetILAsByteArray() @@ -45,11 +108,13 @@ internal static class CompiledCallGraph token, declaringArguments, methodArguments); - if (target is not null && opCode is { Value: var value } - && value is 0x28 or 0x6F or 0x73) - { + bool invocation = opCode == OpCodes.Call + || opCode == OpCodes.Callvirt + || opCode == OpCodes.Newobj; + bool delegateTarget = includeDelegateTargets + && (opCode == OpCodes.Ldftn || opCode == OpCodes.Ldvirtftn); + if (target is not null && (invocation || delegateTarget)) calls.Add(new CompiledCall(instructionOffset, target)); - } } cursor += OperandSize(opCode.OperandType, il, cursor); @@ -58,17 +123,6 @@ internal static class CompiledCallGraph return calls; } - public static int IndexOf( - IReadOnlyList calls, - Type declaringType, - string methodName, - int startIndex = 0) => - Enumerable.Range(startIndex, calls.Count - startIndex) - .FirstOrDefault( - index => calls[index].Target.DeclaringType == declaringType - && calls[index].Target.Name == methodName, - -1); - private static OpCode ReadOpCode(byte[] il, ref int cursor) { byte first = il[cursor++]; diff --git a/tests/AcDream.App.Tests/Streaming/LandblockBuildOriginTests.cs b/tests/AcDream.App.Tests/Streaming/LandblockBuildOriginTests.cs index 4684e93a..8c6e8d63 100644 --- a/tests/AcDream.App.Tests/Streaming/LandblockBuildOriginTests.cs +++ b/tests/AcDream.App.Tests/Streaming/LandblockBuildOriginTests.cs @@ -1,7 +1,14 @@ +using System.Reflection; +using AcDream.App.Composition; +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.Terrain; using AcDream.Core.World; +using AcDream.Runtime; +using AcDream.Runtime.Session; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.Streaming; @@ -252,179 +259,136 @@ public sealed class LandblockBuildOriginTests } [Fact] - public void BuildFactoryAndRenderPublisher_UseCapturedOriginWithoutGameWindowFacade() + public void ProductionCompositionOwnsPublishersWithoutGameWindowFacade() { - string root = FindRepoRoot(); - string gameWindowSource = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); - string livePresentationSource = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Composition", - "LivePresentationComposition.cs")); - string buildSource = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Streaming", - "LandblockBuildFactory.cs")); - string renderPublisherSource = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Streaming", - "LandblockRenderPublisher.cs")); - string recenterSource = File.ReadAllText(Path.Combine( - root, - "src", - "AcDream.App", - "Streaming", - "StreamingOriginRecenterCoordinator.cs")); - Assert.Contains("request.Origin", buildSource, StringComparison.Ordinal); - Assert.DoesNotContain("_liveCenterX", buildSource, StringComparison.Ordinal); - Assert.DoesNotContain("_liveCenterY", buildSource, StringComparison.Ordinal); - Assert.DoesNotContain( + MethodInfo[] windowMethods = typeof(GameWindow).GetMethods( + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); + string[] extractedMethodNames = + [ "BuildLandblockForStreaming", - gameWindowSource, - StringComparison.Ordinal); - Assert.DoesNotContain( "BuildSceneryEntitiesForStreaming", - gameWindowSource, - StringComparison.Ordinal); - Assert.DoesNotContain( "BuildInteriorEntitiesForStreaming", - gameWindowSource, - StringComparison.Ordinal); - Assert.DoesNotContain( "BuildPhysicsDatBundle", - gameWindowSource, - StringComparison.Ordinal); - Assert.DoesNotContain( "ApplyLoadedTerrain", - gameWindowSource, - StringComparison.Ordinal); - Assert.DoesNotContain( "PublishLandblockStaticLightingBeforeCollision", - gameWindowSource, - StringComparison.Ordinal); + ]; Assert.DoesNotContain( - "_landblockPhysicsPublisher!.RemoveLandblock", - gameWindowSource, - StringComparison.Ordinal); + windowMethods, + method => extractedMethodNames.Contains(method.Name, StringComparer.Ordinal)); + + FieldInfo[] windowFields = typeof(GameWindow).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.Single( + windowFields, + field => field.FieldType == typeof(LandblockPresentationPipeline)); Assert.DoesNotContain( - "_landblockRenderPublisher", - gameWindowSource, - StringComparison.Ordinal); + windowFields, + field => field.FieldType == typeof(LandblockRenderPublisher) + || field.FieldType == typeof(LandblockPhysicsPublisher) + || field.FieldType == typeof(LandblockStaticPresentationPublisher)); + + IReadOnlyList windowCalls = + CompiledCallGraph.ReadDeclared(typeof(GameWindow)); Assert.DoesNotContain( - "_landblockPhysicsPublisher", - gameWindowSource, - StringComparison.Ordinal); - Assert.DoesNotContain( - "_landblockStaticPresentationPublisher", - gameWindowSource, - StringComparison.Ordinal); - Assert.Contains( - "_landblockPresentationPipeline", - gameWindowSource, - StringComparison.Ordinal); - Assert.Contains( - "new LandblockRenderPublisher(", - livePresentationSource, - StringComparison.Ordinal); - Assert.Contains( - "new LandblockPhysicsPublisher(", - livePresentationSource, - StringComparison.Ordinal); - Assert.Contains( - "new LandblockStaticPresentationPublisher(", - livePresentationSource, - StringComparison.Ordinal); - Assert.DoesNotContain("applyTerrain:", gameWindowSource, StringComparison.Ordinal); - Assert.DoesNotContain("demoteNearLayer:", gameWindowSource, StringComparison.Ordinal); - Assert.DoesNotContain("retirementCoordinator:", gameWindowSource, StringComparison.Ordinal); - Assert.DoesNotContain( - "_liveWorldOrigin.Recenter(lbX, lbY)", - gameWindowSource, - StringComparison.Ordinal); - int retirementBarrier = recenterSource.IndexOf( - "IsOriginRecenterRetirementComplete()", - StringComparison.Ordinal); - int originCommit = recenterSource.IndexOf( - "_origin.Recenter(", - StringComparison.Ordinal); - int destinationCommit = recenterSource.IndexOf( - "_streaming.TryCommitOriginRecenter(", - StringComparison.Ordinal); - Assert.True(retirementBarrier >= 0); - Assert.True(originCommit > retirementBarrier); - Assert.True(destinationCommit > originCommit); - Assert.Contains("ComputeOrigin(landblockId, build.Origin)", renderPublisherSource, StringComparison.Ordinal); - Assert.DoesNotContain("_liveCenterX", renderPublisherSource, StringComparison.Ordinal); - Assert.DoesNotContain("_liveCenterY", renderPublisherSource, StringComparison.Ordinal); + windowCalls, + call => call.Target.DeclaringType == typeof(LiveWorldOriginState) + && call.Target.Name == nameof(LiveWorldOriginState.Recenter)); + + MethodInfo compose = typeof(LivePresentationCompositionPhase).GetMethod( + "CompletePresentation", + BindingFlags.Instance | BindingFlags.NonPublic)!; + IReadOnlyList composeCalls = CompiledCallGraph.Read(compose); + int renderPublisher = RequiredCallIndex( + composeCalls, + typeof(LandblockRenderPublisher), + ".ctor"); + int physicsPublisher = RequiredCallIndex( + composeCalls, + typeof(LandblockPhysicsPublisher), + ".ctor"); + int staticPublisher = RequiredCallIndex( + composeCalls, + typeof(LandblockStaticPresentationPublisher), + ".ctor"); + int pipeline = RequiredCallIndex( + composeCalls, + typeof(LandblockPresentationPipeline), + ".ctor"); + Assert.True(renderPublisher < physicsPublisher); + Assert.True(physicsPublisher < staticPublisher); + Assert.True(staticPublisher < pipeline); + + // Captured-origin flow itself is behavioral: BuildFar exercises the + // real LandblockBuildFactory and BeginPublication exercises the real + // render publisher. This metadata check protects only production + // ownership and composition, which those fixtures cannot observe. + } + + [Fact] + public void OriginRecenterWaitsForRetirementBeforeOriginAndDestinationCommits() + { + MethodInfo advance = typeof(StreamingOriginRecenterCoordinator).GetMethod( + nameof(StreamingOriginRecenterCoordinator.Advance))!; + IReadOnlyList calls = CompiledCallGraph.Read(advance); + int retirement = RequiredCallIndex( + calls, + typeof(StreamingController), + "IsOriginRecenterRetirementComplete"); + int origin = RequiredCallIndex( + calls, + typeof(LiveWorldOriginState), + nameof(LiveWorldOriginState.Recenter)); + int destination = RequiredCallIndex( + calls, + typeof(StreamingController), + "TryCommitOriginRecenter"); + + Assert.True(retirement < origin); + Assert.True(origin < destination); } [Fact] public void GameWindowShutdownKeepsStreamerAliveUntilSessionResetConverges() { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindowLifetime.cs")); - int sessionStage = source.IndexOf( - "new ResourceShutdownStage(\"host and session barriers\"", - StringComparison.Ordinal); - Assert.True(sessionStage >= 0); + MethodInfo manifest = typeof(GameWindowShutdownManifest).GetMethod( + nameof(GameWindowShutdownManifest.Create))!; + IReadOnlyList labels = CompiledCallGraph.ReadStringLiterals(manifest); + AssertAppearsInOrder( + labels, + "host and session barriers", + "game runtime session", + "session dependents", + "streamer"); - int sessionOperation = source.IndexOf( - "Hard(\"game runtime session\", ingress.Runtime.StopSession)", - sessionStage, - StringComparison.Ordinal); - Assert.True(sessionOperation > sessionStage); - - int dependentStage = source.IndexOf( - "new ResourceShutdownStage(\"session dependents\"", - sessionOperation, - StringComparison.Ordinal); - Assert.True(dependentStage > sessionOperation); - - int streamerDispose = source.IndexOf( - "Hard(\"streamer\", () => live.Streamer?.Dispose())", - dependentStage, - StringComparison.Ordinal); - Assert.True(streamerDispose > dependentStage); - - string runtime = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.Runtime", - "GameRuntime.cs")); - int helper = runtime.IndexOf( - "public void StopSession()", - StringComparison.Ordinal); - Assert.True(helper >= 0); - int sessionDispose = runtime.IndexOf( - "Session.Dispose();", - helper, - StringComparison.Ordinal); - Assert.True(sessionDispose > helper); - - int disposalCompletionBarrier = runtime.IndexOf( - "if (!Session.IsDisposalComplete)", - sessionDispose, - StringComparison.Ordinal); - Assert.True(disposalCompletionBarrier > sessionDispose); + IReadOnlyList references = + CompiledCallGraph.ReadMethodReferences(manifest); + int stopSession = RequiredCallIndex( + references, + typeof(GameRuntime), + nameof(GameRuntime.StopSession)); + int streamerDispose = Enumerable.Range(stopSession + 1, references.Count - stopSession - 1) + .FirstOrDefault( + index => Calls( + references[index].Target, + typeof(LandblockStreamer), + nameof(LandblockStreamer.Dispose)), + -1); + Assert.True(streamerDispose > stopSession, "Missing later streamer-disposal operation."); + MethodInfo runtimeStop = typeof(GameRuntime).GetMethod(nameof(GameRuntime.StopSession))!; + IReadOnlyList runtimeCalls = CompiledCallGraph.Read(runtimeStop); + int sessionDispose = RequiredCallIndex( + runtimeCalls, + typeof(LiveSessionController), + nameof(LiveSessionController.Dispose)); + int completionBarrier = RequiredCallIndex( + runtimeCalls, + typeof(LiveSessionController), + "get_IsDisposalComplete"); + Assert.True(sessionDispose < completionBarrier); Assert.Contains( "The Runtime session shutdown was deferred by a re-entrant callback.", - runtime[disposalCompletionBarrier..], - StringComparison.Ordinal); + CompiledCallGraph.ReadStringLiterals(runtimeStop)); } private static LandblockBuild EmptyBuild(uint landblockId, LandblockBuildOrigin origin) => @@ -458,16 +422,37 @@ public sealed class LandblockBuildOriginTests return results; } - private static string FindRepoRoot() + private static int RequiredCallIndex( + IReadOnlyList calls, + Type declaringType, + string methodName) { - string? dir = AppContext.BaseDirectory; - while (dir is not null) - { - if (File.Exists(Path.Combine(dir, "AcDream.slnx"))) - return dir; - dir = Directory.GetParent(dir)?.FullName; - } + int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName); + Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}"); + return index; + } - throw new DirectoryNotFoundException("Could not locate AcDream.slnx."); + private static bool Calls( + MethodBase method, + Type declaringType, + string methodName) => + method.GetMethodBody() is not null + && CompiledCallGraph.IndexOf( + CompiledCallGraph.Read(method), + declaringType, + methodName) >= 0; + + private static void AssertAppearsInOrder( + IReadOnlyList values, + params string[] expected) + { + int cursor = -1; + foreach (string value in expected) + { + int next = Enumerable.Range(cursor + 1, values.Count - cursor - 1) + .FirstOrDefault(index => values[index] == value, -1); + Assert.True(next > cursor, $"Missing or out-of-order metadata string: {value}"); + cursor = next; + } } } diff --git a/tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs b/tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs index b39e81cb..ca117968 100644 --- a/tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs +++ b/tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs @@ -1,7 +1,9 @@ using System.Collections.Immutable; using System.Numerics; using System.Reflection; +using AcDream.App.Rendering; using AcDream.App.Streaming; +using AcDream.App.Tests.Architecture; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; @@ -1047,34 +1049,63 @@ public sealed class LandblockPhysicsPublisherTests [Fact] public void GameWindow_HasNoLandblockPhysicsPublicationBodies() { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); + IReadOnlyList windowCalls = + CompiledCallGraph.ReadDeclared(typeof(GameWindow)); + AssertNoCall(windowCalls, typeof(PhysicsDataCache), nameof(PhysicsDataCache.CacheCellStruct)); + AssertNoCall(windowCalls, typeof(PhysicsDataCache), nameof(PhysicsDataCache.CacheBuilding)); + AssertNoCall( + windowCalls, + typeof(ShadowShapeBuilder), + nameof(ShadowShapeBuilder.FromLandblockBspParts)); + AssertNoCall( + windowCalls, + typeof(ShadowObjectRegistry), + nameof(ShadowObjectRegistry.RefloodLandblock)); + AssertNoCall( + windowCalls, + typeof(PhysicsEngine), + "DemoteLandblockToTerrain"); + AssertNoCall(windowCalls, typeof(PhysicsEngine), "RemoveLandblock"); - Assert.DoesNotContain("_physicsDataCache.CacheCellStruct", source, StringComparison.Ordinal); - Assert.DoesNotContain("_physicsDataCache.CacheBuilding", source, StringComparison.Ordinal); - Assert.DoesNotContain("ShadowShapeBuilder.FromLandblockBspParts", source, StringComparison.Ordinal); - Assert.DoesNotContain("ShadowObjects.RefloodLandblock", source, StringComparison.Ordinal); - Assert.DoesNotContain("_physicsEngine.DemoteLandblockToTerrain", source, StringComparison.Ordinal); - Assert.DoesNotContain("_physicsEngine.RemoveLandblock", source, StringComparison.Ordinal); - - string publisherSource = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Streaming", - "LandblockPhysicsPublisher.cs")); - Assert.DoesNotContain("_physicsDataCache.RemoveCellsForLandblock", publisherSource, StringComparison.Ordinal); - Assert.DoesNotContain("_physicsDataCache.RemoveBuildingsForLandblock", publisherSource, StringComparison.Ordinal); - Assert.DoesNotContain("_physicsEngine.ShadowObjects.Reflood", publisherSource, StringComparison.Ordinal); - Assert.Contains("CommitCollisionGeneration(", publisherSource, StringComparison.Ordinal); - Assert.DoesNotContain( - "RestartCollisionRetainedOwnerCapture(", - publisherSource, - StringComparison.Ordinal); + IReadOnlyList publisherCalls = + CompiledCallGraph.ReadDeclared(typeof(LandblockPhysicsPublisher)); + MethodInfo advance = typeof(LandblockPhysicsPublisher).GetMethod( + "AdvanceBeginOne", + BindingFlags.Instance | BindingFlags.NonPublic)!; + IReadOnlyList advanceCalls = CompiledCallGraph.Read(advance); + int stagedCache = CompiledCallGraph.IndexOf( + advanceCalls, + typeof(LandblockPhysicsPublication), + "get_StagingCache"); + int removeCells = CompiledCallGraph.IndexOf( + advanceCalls, + typeof(PhysicsDataCache), + nameof(PhysicsDataCache.RemoveCellsForLandblock)); + int removeBuildings = CompiledCallGraph.IndexOf( + advanceCalls, + typeof(PhysicsDataCache), + nameof(PhysicsDataCache.RemoveBuildingsForLandblock)); + int secondStagedCache = CompiledCallGraph.IndexOf( + advanceCalls, + typeof(LandblockPhysicsPublication), + "get_StagingCache", + removeCells + 1); + Assert.True(stagedCache >= 0); + Assert.True(stagedCache < removeCells); + Assert.True(removeCells < secondStagedCache); + Assert.True(secondStagedCache < removeBuildings); + AssertNoCall( + publisherCalls, + typeof(ShadowObjectRegistry), + nameof(ShadowObjectRegistry.RefloodLandblock)); + Assert.Contains( + publisherCalls, + call => call.Target.DeclaringType == typeof(RuntimePhysicsState) + && call.Target.Name == "CommitCollisionGeneration"); + AssertNoCall( + publisherCalls, + typeof(RuntimePhysicsState), + "RestartCollisionRetainedOwnerCapture"); } private static void Publish( @@ -1500,15 +1531,14 @@ public sealed class LandblockPhysicsPublisherTests Height = new byte[81], }; - private static string FindRepoRoot() + private static void AssertNoCall( + IReadOnlyList calls, + Type declaringType, + string methodName) { - string? directory = AppContext.BaseDirectory; - while (directory is not null) - { - if (File.Exists(Path.Combine(directory, "AcDream.slnx"))) - return directory; - directory = Directory.GetParent(directory)?.FullName; - } - throw new DirectoryNotFoundException("Could not locate repository root."); + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType == declaringType + && call.Target.Name == methodName); } } diff --git a/tests/AcDream.App.Tests/Streaming/LandblockRenderPublisherTests.cs b/tests/AcDream.App.Tests/Streaming/LandblockRenderPublisherTests.cs index bf7a6309..ec089f6d 100644 --- a/tests/AcDream.App.Tests/Streaming/LandblockRenderPublisherTests.cs +++ b/tests/AcDream.App.Tests/Streaming/LandblockRenderPublisherTests.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using AcDream.App.Rendering; using AcDream.App.Rendering.Wb; using AcDream.App.Streaming; +using AcDream.App.Tests.Architecture; using AcDream.Core.Terrain; using AcDream.Core.World; using DatReaderWriter.DBObjs; @@ -306,18 +307,27 @@ public sealed class LandblockRenderPublisherTests [Fact] public void GameWindow_HasNoDirectRenderPublicationBodies() { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Rendering", - "GameWindow.cs")); + IReadOnlyList calls = + CompiledCallGraph.ReadDeclared(typeof(GameWindow)); + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType == typeof(TerrainModernRenderer) + && call.Target.Name == nameof(TerrainModernRenderer.AddLandblockWithMesh)); + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType == typeof(CellVisibility) + && call.Target.Name == nameof(CellVisibility.CommitLandblock)); + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType == typeof(EnvCellRenderer) + && call.Target.Name == nameof(EnvCellRenderer.CommitLandblock)); - Assert.DoesNotContain("_terrain.AddLandblockWithMesh", source, StringComparison.Ordinal); - Assert.DoesNotContain("_cellVisibility.CommitLandblock", source, StringComparison.Ordinal); - Assert.DoesNotContain("_buildingRegistries", source, StringComparison.Ordinal); - Assert.DoesNotContain("_envCellRenderer?.CommitLandblock", source, StringComparison.Ordinal); - Assert.DoesNotContain("EnsureEnvCellMeshesAfterPin", source, StringComparison.Ordinal); + Assert.Null(typeof(GameWindow).GetMethod( + "EnsureEnvCellMeshesAfterPin", + BindingFlags.Instance | BindingFlags.NonPublic)); + Assert.DoesNotContain( + typeof(GameWindow).GetFields(BindingFlags.Instance | BindingFlags.NonPublic), + field => field.Name == "_buildingRegistries"); } private static LandblockRenderPublisher Publisher( @@ -406,18 +416,6 @@ public sealed class LandblockRenderPublisherTests private static LandblockMeshData EmptyMesh() => new(Array.Empty(), Array.Empty()); - private static string FindRepoRoot() - { - string? directory = AppContext.BaseDirectory; - while (directory is not null) - { - if (File.Exists(Path.Combine(directory, "AcDream.slnx"))) - return directory; - directory = Directory.GetParent(directory)?.FullName; - } - throw new DirectoryNotFoundException("Could not locate repository root."); - } - private sealed class RecordingEnvCellPublisher : IEnvCellLandblockPublisher { diff --git a/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs b/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs index 066d7ab5..8cd22520 100644 --- a/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs +++ b/tests/AcDream.App.Tests/World/GameWindowLiveEntityCompositionTests.cs @@ -1,7 +1,11 @@ using System.Collections; using System.Reflection; +using AcDream.App.Composition; +using AcDream.App.Input; +using AcDream.App.Net; using AcDream.App.Physics; using AcDream.App.Rendering; +using AcDream.App.Tests.Architecture; using AcDream.App.World; using AcDream.Core.Net; using AcDream.Core.Physics; @@ -213,58 +217,74 @@ public sealed class GameWindowLiveEntityCompositionTests field => typeof(Delegate).IsAssignableFrom(field.FieldType) && !field.Name.Contains("DiagnosticSink", StringComparison.Ordinal)); - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs")); - string livePresentation = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Composition", - "LivePresentationComposition.cs")); + FieldInfo[] windowFields = typeof(GameWindow).GetFields( + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.Equal( + typeof(LiveEntityAnimationRuntimeView), + Assert.Single(windowFields, field => field.Name == "_animatedEntities").FieldType); Assert.DoesNotContain( - "new LiveEntityAnimationRuntimeView(() =>", - source, - StringComparison.Ordinal); - Assert.DoesNotContain( - "LiveEntityRemoteMotionRuntimeView", - source, - StringComparison.Ordinal); - Assert.Contains("new DatProjectileSetupResolver", livePresentation); + windowFields, + field => field.FieldType.Name.Contains( + "LiveEntityRemoteMotionRuntimeView", + StringComparison.Ordinal)); + + MethodInfo compose = typeof(LivePresentationCompositionPhase).GetMethod( + "ComposeCore", + BindingFlags.Instance | BindingFlags.NonPublic)!; + IReadOnlyList calls = CompiledCallGraph.Read(compose); + int resolver = RequiredCallIndex( + calls, + typeof(DatProjectileSetupResolver), + ".ctor"); + int controller = RequiredCallIndex( + calls, + typeof(ProjectileController), + ".ctor"); + Assert.True(resolver < controller); } [Fact] - public void SessionReset_ClosesEveryStreamingReadinessOwner() + public void SessionReset_CallsPlayerModeNetworkAndWorldOriginOwners() { - string source = File.ReadAllText(Path.Combine( - FindRepoRoot(), "src", "AcDream.App", "Net", "LiveSessionRuntimeFactory.cs")); + MethodInfo resetPlayer = typeof(LiveSessionRuntimeFactory).GetMethod( + "ResetPlayerPresentation", + BindingFlags.Instance | BindingFlags.NonPublic)!; + IReadOnlyList playerCalls = CompiledCallGraph.Read(resetPlayer); + Assert.True(RequiredCallIndex( + playerCalls, + typeof(PlayerModeController), + nameof(PlayerModeController.ResetSession)) >= 0); - Assert.Contains("_interaction.PlayerMode.ResetSession();", source, - StringComparison.Ordinal); - string playerModeSource = File.ReadAllText(Path.Combine( - FindRepoRoot(), - "src", - "AcDream.App", - "Input", - "PlayerModeController.cs")); - Assert.Contains("_mode.ResetSession();", playerModeSource, - StringComparison.Ordinal); - Assert.Contains( - "_world.NetworkUpdates.ResetSessionState();", - source, - StringComparison.Ordinal); - Assert.Contains("_player.WorldOrigin.Reset();", source, StringComparison.Ordinal); + MethodInfo resetMode = typeof(PlayerModeController).GetMethod( + nameof(PlayerModeController.ResetSession))!; + IReadOnlyList modeCalls = CompiledCallGraph.Read(resetMode); + Assert.True(RequiredCallIndex( + modeCalls, + typeof(LocalPlayerModeState), + nameof(LocalPlayerModeState.ResetSession)) >= 0); + + MethodInfo resetIdentity = typeof(LiveSessionRuntimeFactory).GetMethod( + "ResetIdentityPresentation", + BindingFlags.Instance | BindingFlags.NonPublic)!; + IReadOnlyList identityCalls = CompiledCallGraph.Read(resetIdentity); + int network = RequiredCallIndex( + identityCalls, + typeof(LiveEntityNetworkUpdateController), + nameof(LiveEntityNetworkUpdateController.ResetSessionState)); + int origin = RequiredCallIndex( + identityCalls, + typeof(LiveWorldOriginState), + nameof(LiveWorldOriginState.Reset)); + Assert.True(network < origin); } - private static string FindRepoRoot() + private static int RequiredCallIndex( + IReadOnlyList calls, + Type declaringType, + string methodName) { - 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); + Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}"); + return index; } }