test: replace streaming source freezes

This commit is contained in:
Erik 2026-08-18 15:14:41 +02:00
parent 5a33369074
commit 0ad2ee1cdf
6 changed files with 436 additions and 285 deletions

View file

@ -38,12 +38,12 @@ pwsh ./tools/audit-test-inventory.ps1
The generated JSON lives under ignored `artifacts/test-audit/`; it is not a 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 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 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 | | Syntax signal | Current count |
|---|---:| |---|---:|
| Tracked/parsed C# test files | 1,254 / 1,254 | | 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 | | Exact duplicate-data rows | 0 |
| Reviewed body-equivalent groups / methods | 11 / 27 | | Reviewed body-equivalent groups / methods | 11 / 27 |
| Methods containing at least one empty `return;` | 138 | | 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 | | Methods directly using `Thread.Sleep` / `Task.Delay` | 15 / 14 |
| Cancellable-infinite-only / elapsed-time methods | 5 / 24 | | Cancellable-infinite-only / elapsed-time methods | 5 / 24 |
| Methods directly reading environment variables | 47 | | 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 The three remaining prerequisite-return candidates are all reviewed branch
false positives: the two Windows/Linux factory assertions and the launcher's 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-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-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-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-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-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`. | | 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. 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 The remaining 98 reconcile to the 22 approved retained policies/contracts
and 76 staged replacements. 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.

View file

@ -21,6 +21,69 @@ internal static class CompiledCallGraph
.ToDictionary(opCode => opCode.Value); .ToDictionary(opCode => opCode.Value);
public static IReadOnlyList<CompiledCall> Read(MethodBase method) public static IReadOnlyList<CompiledCall> Read(MethodBase method)
=> ReadMethodReferences(method, includeDelegateTargets: false);
/// <summary>
/// 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.
/// </summary>
public static IReadOnlyList<CompiledCall> ReadMethodReferences(MethodBase method) =>
ReadMethodReferences(method, includeDelegateTargets: true);
public static IReadOnlyList<CompiledCall> ReadDeclared(Type type)
{
ArgumentNullException.ThrowIfNull(type);
const BindingFlags flags = BindingFlags.Instance
| BindingFlags.Static
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.DeclaredOnly;
return type.GetMethods(flags)
.Cast<MethodBase>()
.Concat(type.GetConstructors(flags))
.Where(method => method.GetMethodBody() is not null)
.SelectMany(Read)
.ToArray();
}
public static IReadOnlyList<string> 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<string>();
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<CompiledCall> 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<CompiledCall> ReadMethodReferences(
MethodBase method,
bool includeDelegateTargets)
{ {
ArgumentNullException.ThrowIfNull(method); ArgumentNullException.ThrowIfNull(method);
byte[] il = method.GetMethodBody()?.GetILAsByteArray() byte[] il = method.GetMethodBody()?.GetILAsByteArray()
@ -45,11 +108,13 @@ internal static class CompiledCallGraph
token, token,
declaringArguments, declaringArguments,
methodArguments); methodArguments);
if (target is not null && opCode is { Value: var value } bool invocation = opCode == OpCodes.Call
&& value is 0x28 or 0x6F or 0x73) || 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)); calls.Add(new CompiledCall(instructionOffset, target));
}
} }
cursor += OperandSize(opCode.OperandType, il, cursor); cursor += OperandSize(opCode.OperandType, il, cursor);
@ -58,17 +123,6 @@ internal static class CompiledCallGraph
return calls; return calls;
} }
public static int IndexOf(
IReadOnlyList<CompiledCall> 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) private static OpCode ReadOpCode(byte[] il, ref int cursor)
{ {
byte first = il[cursor++]; byte first = il[cursor++];

View file

@ -1,7 +1,14 @@
using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.App.World;
using AcDream.Core.Terrain; using AcDream.Core.Terrain;
using AcDream.Core.World; using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Session;
using DatReaderWriter.DBObjs; using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Streaming; namespace AcDream.App.Tests.Streaming;
@ -252,179 +259,136 @@ public sealed class LandblockBuildOriginTests
} }
[Fact] [Fact]
public void BuildFactoryAndRenderPublisher_UseCapturedOriginWithoutGameWindowFacade() public void ProductionCompositionOwnsPublishersWithoutGameWindowFacade()
{ {
string root = FindRepoRoot(); MethodInfo[] windowMethods = typeof(GameWindow).GetMethods(
string gameWindowSource = File.ReadAllText(Path.Combine( BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
root, string[] extractedMethodNames =
"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(
"BuildLandblockForStreaming", "BuildLandblockForStreaming",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"BuildSceneryEntitiesForStreaming", "BuildSceneryEntitiesForStreaming",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"BuildInteriorEntitiesForStreaming", "BuildInteriorEntitiesForStreaming",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"BuildPhysicsDatBundle", "BuildPhysicsDatBundle",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"ApplyLoadedTerrain", "ApplyLoadedTerrain",
gameWindowSource,
StringComparison.Ordinal);
Assert.DoesNotContain(
"PublishLandblockStaticLightingBeforeCollision", "PublishLandblockStaticLightingBeforeCollision",
gameWindowSource, ];
StringComparison.Ordinal);
Assert.DoesNotContain( Assert.DoesNotContain(
"_landblockPhysicsPublisher!.RemoveLandblock", windowMethods,
gameWindowSource, method => extractedMethodNames.Contains(method.Name, StringComparer.Ordinal));
StringComparison.Ordinal);
FieldInfo[] windowFields = typeof(GameWindow).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.Single(
windowFields,
field => field.FieldType == typeof(LandblockPresentationPipeline));
Assert.DoesNotContain( Assert.DoesNotContain(
"_landblockRenderPublisher", windowFields,
gameWindowSource, field => field.FieldType == typeof(LandblockRenderPublisher)
StringComparison.Ordinal); || field.FieldType == typeof(LandblockPhysicsPublisher)
|| field.FieldType == typeof(LandblockStaticPresentationPublisher));
IReadOnlyList<CompiledCall> windowCalls =
CompiledCallGraph.ReadDeclared(typeof(GameWindow));
Assert.DoesNotContain( Assert.DoesNotContain(
"_landblockPhysicsPublisher", windowCalls,
gameWindowSource, call => call.Target.DeclaringType == typeof(LiveWorldOriginState)
StringComparison.Ordinal); && call.Target.Name == nameof(LiveWorldOriginState.Recenter));
Assert.DoesNotContain(
"_landblockStaticPresentationPublisher", MethodInfo compose = typeof(LivePresentationCompositionPhase).GetMethod(
gameWindowSource, "CompletePresentation",
StringComparison.Ordinal); BindingFlags.Instance | BindingFlags.NonPublic)!;
Assert.Contains( IReadOnlyList<CompiledCall> composeCalls = CompiledCallGraph.Read(compose);
"_landblockPresentationPipeline", int renderPublisher = RequiredCallIndex(
gameWindowSource, composeCalls,
StringComparison.Ordinal); typeof(LandblockRenderPublisher),
Assert.Contains( ".ctor");
"new LandblockRenderPublisher(", int physicsPublisher = RequiredCallIndex(
livePresentationSource, composeCalls,
StringComparison.Ordinal); typeof(LandblockPhysicsPublisher),
Assert.Contains( ".ctor");
"new LandblockPhysicsPublisher(", int staticPublisher = RequiredCallIndex(
livePresentationSource, composeCalls,
StringComparison.Ordinal); typeof(LandblockStaticPresentationPublisher),
Assert.Contains( ".ctor");
"new LandblockStaticPresentationPublisher(", int pipeline = RequiredCallIndex(
livePresentationSource, composeCalls,
StringComparison.Ordinal); typeof(LandblockPresentationPipeline),
Assert.DoesNotContain("applyTerrain:", gameWindowSource, StringComparison.Ordinal); ".ctor");
Assert.DoesNotContain("demoteNearLayer:", gameWindowSource, StringComparison.Ordinal); Assert.True(renderPublisher < physicsPublisher);
Assert.DoesNotContain("retirementCoordinator:", gameWindowSource, StringComparison.Ordinal); Assert.True(physicsPublisher < staticPublisher);
Assert.DoesNotContain( Assert.True(staticPublisher < pipeline);
"_liveWorldOrigin.Recenter(lbX, lbY)",
gameWindowSource, // Captured-origin flow itself is behavioral: BuildFar exercises the
StringComparison.Ordinal); // real LandblockBuildFactory and BeginPublication exercises the real
int retirementBarrier = recenterSource.IndexOf( // render publisher. This metadata check protects only production
"IsOriginRecenterRetirementComplete()", // ownership and composition, which those fixtures cannot observe.
StringComparison.Ordinal); }
int originCommit = recenterSource.IndexOf(
"_origin.Recenter(", [Fact]
StringComparison.Ordinal); public void OriginRecenterWaitsForRetirementBeforeOriginAndDestinationCommits()
int destinationCommit = recenterSource.IndexOf( {
"_streaming.TryCommitOriginRecenter(", MethodInfo advance = typeof(StreamingOriginRecenterCoordinator).GetMethod(
StringComparison.Ordinal); nameof(StreamingOriginRecenterCoordinator.Advance))!;
Assert.True(retirementBarrier >= 0); IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(advance);
Assert.True(originCommit > retirementBarrier); int retirement = RequiredCallIndex(
Assert.True(destinationCommit > originCommit); calls,
Assert.Contains("ComputeOrigin(landblockId, build.Origin)", renderPublisherSource, StringComparison.Ordinal); typeof(StreamingController),
Assert.DoesNotContain("_liveCenterX", renderPublisherSource, StringComparison.Ordinal); "IsOriginRecenterRetirementComplete");
Assert.DoesNotContain("_liveCenterY", renderPublisherSource, StringComparison.Ordinal); 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] [Fact]
public void GameWindowShutdownKeepsStreamerAliveUntilSessionResetConverges() public void GameWindowShutdownKeepsStreamerAliveUntilSessionResetConverges()
{ {
string source = File.ReadAllText(Path.Combine( MethodInfo manifest = typeof(GameWindowShutdownManifest).GetMethod(
FindRepoRoot(), nameof(GameWindowShutdownManifest.Create))!;
"src", IReadOnlyList<string> labels = CompiledCallGraph.ReadStringLiterals(manifest);
"AcDream.App", AssertAppearsInOrder(
"Rendering", labels,
"GameWindowLifetime.cs")); "host and session barriers",
int sessionStage = source.IndexOf( "game runtime session",
"new ResourceShutdownStage(\"host and session barriers\"", "session dependents",
StringComparison.Ordinal); "streamer");
Assert.True(sessionStage >= 0);
int sessionOperation = source.IndexOf( IReadOnlyList<CompiledCall> references =
"Hard(\"game runtime session\", ingress.Runtime.StopSession)", CompiledCallGraph.ReadMethodReferences(manifest);
sessionStage, int stopSession = RequiredCallIndex(
StringComparison.Ordinal); references,
Assert.True(sessionOperation > sessionStage); typeof(GameRuntime),
nameof(GameRuntime.StopSession));
int dependentStage = source.IndexOf( int streamerDispose = Enumerable.Range(stopSession + 1, references.Count - stopSession - 1)
"new ResourceShutdownStage(\"session dependents\"", .FirstOrDefault(
sessionOperation, index => Calls(
StringComparison.Ordinal); references[index].Target,
Assert.True(dependentStage > sessionOperation); typeof(LandblockStreamer),
nameof(LandblockStreamer.Dispose)),
int streamerDispose = source.IndexOf( -1);
"Hard(\"streamer\", () => live.Streamer?.Dispose())", Assert.True(streamerDispose > stopSession, "Missing later streamer-disposal operation.");
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);
MethodInfo runtimeStop = typeof(GameRuntime).GetMethod(nameof(GameRuntime.StopSession))!;
IReadOnlyList<CompiledCall> 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( Assert.Contains(
"The Runtime session shutdown was deferred by a re-entrant callback.", "The Runtime session shutdown was deferred by a re-entrant callback.",
runtime[disposalCompletionBarrier..], CompiledCallGraph.ReadStringLiterals(runtimeStop));
StringComparison.Ordinal);
} }
private static LandblockBuild EmptyBuild(uint landblockId, LandblockBuildOrigin origin) => private static LandblockBuild EmptyBuild(uint landblockId, LandblockBuildOrigin origin) =>
@ -458,16 +422,37 @@ public sealed class LandblockBuildOriginTests
return results; return results;
} }
private static string FindRepoRoot() private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName)
{ {
string? dir = AppContext.BaseDirectory; int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
while (dir is not null) Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}");
{ return index;
if (File.Exists(Path.Combine(dir, "AcDream.slnx"))) }
return dir;
dir = Directory.GetParent(dir)?.FullName;
}
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<string> 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;
}
} }
} }

View file

@ -1,7 +1,9 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using AcDream.App.Rendering;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.Core.Net; using AcDream.Core.Net;
using AcDream.Core.Net.Messages; using AcDream.Core.Net.Messages;
using AcDream.Core.Physics; using AcDream.Core.Physics;
@ -1047,34 +1049,63 @@ public sealed class LandblockPhysicsPublisherTests
[Fact] [Fact]
public void GameWindow_HasNoLandblockPhysicsPublicationBodies() public void GameWindow_HasNoLandblockPhysicsPublicationBodies()
{ {
string source = File.ReadAllText(Path.Combine( IReadOnlyList<CompiledCall> windowCalls =
FindRepoRoot(), CompiledCallGraph.ReadDeclared(typeof(GameWindow));
"src", AssertNoCall(windowCalls, typeof(PhysicsDataCache), nameof(PhysicsDataCache.CacheCellStruct));
"AcDream.App", AssertNoCall(windowCalls, typeof(PhysicsDataCache), nameof(PhysicsDataCache.CacheBuilding));
"Rendering", AssertNoCall(
"GameWindow.cs")); 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); IReadOnlyList<CompiledCall> publisherCalls =
Assert.DoesNotContain("_physicsDataCache.CacheBuilding", source, StringComparison.Ordinal); CompiledCallGraph.ReadDeclared(typeof(LandblockPhysicsPublisher));
Assert.DoesNotContain("ShadowShapeBuilder.FromLandblockBspParts", source, StringComparison.Ordinal); MethodInfo advance = typeof(LandblockPhysicsPublisher).GetMethod(
Assert.DoesNotContain("ShadowObjects.RefloodLandblock", source, StringComparison.Ordinal); "AdvanceBeginOne",
Assert.DoesNotContain("_physicsEngine.DemoteLandblockToTerrain", source, StringComparison.Ordinal); BindingFlags.Instance | BindingFlags.NonPublic)!;
Assert.DoesNotContain("_physicsEngine.RemoveLandblock", source, StringComparison.Ordinal); IReadOnlyList<CompiledCall> advanceCalls = CompiledCallGraph.Read(advance);
int stagedCache = CompiledCallGraph.IndexOf(
string publisherSource = File.ReadAllText(Path.Combine( advanceCalls,
FindRepoRoot(), typeof(LandblockPhysicsPublication),
"src", "get_StagingCache");
"AcDream.App", int removeCells = CompiledCallGraph.IndexOf(
"Streaming", advanceCalls,
"LandblockPhysicsPublisher.cs")); typeof(PhysicsDataCache),
Assert.DoesNotContain("_physicsDataCache.RemoveCellsForLandblock", publisherSource, StringComparison.Ordinal); nameof(PhysicsDataCache.RemoveCellsForLandblock));
Assert.DoesNotContain("_physicsDataCache.RemoveBuildingsForLandblock", publisherSource, StringComparison.Ordinal); int removeBuildings = CompiledCallGraph.IndexOf(
Assert.DoesNotContain("_physicsEngine.ShadowObjects.Reflood", publisherSource, StringComparison.Ordinal); advanceCalls,
Assert.Contains("CommitCollisionGeneration(", publisherSource, StringComparison.Ordinal); typeof(PhysicsDataCache),
Assert.DoesNotContain( nameof(PhysicsDataCache.RemoveBuildingsForLandblock));
"RestartCollisionRetainedOwnerCapture(", int secondStagedCache = CompiledCallGraph.IndexOf(
publisherSource, advanceCalls,
StringComparison.Ordinal); 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( private static void Publish(
@ -1500,15 +1531,14 @@ public sealed class LandblockPhysicsPublisherTests
Height = new byte[81], Height = new byte[81],
}; };
private static string FindRepoRoot() private static void AssertNoCall(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName)
{ {
string? directory = AppContext.BaseDirectory; Assert.DoesNotContain(
while (directory is not null) calls,
{ call => call.Target.DeclaringType == declaringType
if (File.Exists(Path.Combine(directory, "AcDream.slnx"))) && call.Target.Name == methodName);
return directory;
directory = Directory.GetParent(directory)?.FullName;
}
throw new DirectoryNotFoundException("Could not locate repository root.");
} }
} }

View file

@ -4,6 +4,7 @@ using System.Collections.Immutable;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming; using AcDream.App.Streaming;
using AcDream.App.Tests.Architecture;
using AcDream.Core.Terrain; using AcDream.Core.Terrain;
using AcDream.Core.World; using AcDream.Core.World;
using DatReaderWriter.DBObjs; using DatReaderWriter.DBObjs;
@ -306,18 +307,27 @@ public sealed class LandblockRenderPublisherTests
[Fact] [Fact]
public void GameWindow_HasNoDirectRenderPublicationBodies() public void GameWindow_HasNoDirectRenderPublicationBodies()
{ {
string source = File.ReadAllText(Path.Combine( IReadOnlyList<CompiledCall> calls =
FindRepoRoot(), CompiledCallGraph.ReadDeclared(typeof(GameWindow));
"src", Assert.DoesNotContain(
"AcDream.App", calls,
"Rendering", call => call.Target.DeclaringType == typeof(TerrainModernRenderer)
"GameWindow.cs")); && 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.Null(typeof(GameWindow).GetMethod(
Assert.DoesNotContain("_cellVisibility.CommitLandblock", source, StringComparison.Ordinal); "EnsureEnvCellMeshesAfterPin",
Assert.DoesNotContain("_buildingRegistries", source, StringComparison.Ordinal); BindingFlags.Instance | BindingFlags.NonPublic));
Assert.DoesNotContain("_envCellRenderer?.CommitLandblock", source, StringComparison.Ordinal); Assert.DoesNotContain(
Assert.DoesNotContain("EnsureEnvCellMeshesAfterPin", source, StringComparison.Ordinal); typeof(GameWindow).GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
field => field.Name == "_buildingRegistries");
} }
private static LandblockRenderPublisher Publisher( private static LandblockRenderPublisher Publisher(
@ -406,18 +416,6 @@ public sealed class LandblockRenderPublisherTests
private static LandblockMeshData EmptyMesh() => private static LandblockMeshData EmptyMesh() =>
new(Array.Empty<TerrainVertex>(), Array.Empty<uint>()); new(Array.Empty<TerrainVertex>(), Array.Empty<uint>());
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 : private sealed class RecordingEnvCellPublisher :
IEnvCellLandblockPublisher IEnvCellLandblockPublisher
{ {

View file

@ -1,7 +1,11 @@
using System.Collections; using System.Collections;
using System.Reflection; using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Input;
using AcDream.App.Net;
using AcDream.App.Physics; using AcDream.App.Physics;
using AcDream.App.Rendering; using AcDream.App.Rendering;
using AcDream.App.Tests.Architecture;
using AcDream.App.World; using AcDream.App.World;
using AcDream.Core.Net; using AcDream.Core.Net;
using AcDream.Core.Physics; using AcDream.Core.Physics;
@ -213,58 +217,74 @@ public sealed class GameWindowLiveEntityCompositionTests
field => typeof(Delegate).IsAssignableFrom(field.FieldType) field => typeof(Delegate).IsAssignableFrom(field.FieldType)
&& !field.Name.Contains("DiagnosticSink", StringComparison.Ordinal)); && !field.Name.Contains("DiagnosticSink", StringComparison.Ordinal));
string source = File.ReadAllText(Path.Combine( FieldInfo[] windowFields = typeof(GameWindow).GetFields(
FindRepoRoot(), "src", "AcDream.App", "Rendering", "GameWindow.cs")); BindingFlags.Instance | BindingFlags.NonPublic);
string livePresentation = File.ReadAllText(Path.Combine( Assert.Equal(
FindRepoRoot(), typeof(LiveEntityAnimationRuntimeView<LiveEntityAnimationState>),
"src", Assert.Single(windowFields, field => field.Name == "_animatedEntities").FieldType);
"AcDream.App",
"Composition",
"LivePresentationComposition.cs"));
Assert.DoesNotContain( Assert.DoesNotContain(
"new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(() =>", windowFields,
source, field => field.FieldType.Name.Contains(
StringComparison.Ordinal); "LiveEntityRemoteMotionRuntimeView",
Assert.DoesNotContain( StringComparison.Ordinal));
"LiveEntityRemoteMotionRuntimeView",
source, MethodInfo compose = typeof(LivePresentationCompositionPhase).GetMethod(
StringComparison.Ordinal); "ComposeCore",
Assert.Contains("new DatProjectileSetupResolver", livePresentation); BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
int resolver = RequiredCallIndex(
calls,
typeof(DatProjectileSetupResolver),
".ctor");
int controller = RequiredCallIndex(
calls,
typeof(ProjectileController),
".ctor");
Assert.True(resolver < controller);
} }
[Fact] [Fact]
public void SessionReset_ClosesEveryStreamingReadinessOwner() public void SessionReset_CallsPlayerModeNetworkAndWorldOriginOwners()
{ {
string source = File.ReadAllText(Path.Combine( MethodInfo resetPlayer = typeof(LiveSessionRuntimeFactory).GetMethod(
FindRepoRoot(), "src", "AcDream.App", "Net", "LiveSessionRuntimeFactory.cs")); "ResetPlayerPresentation",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> playerCalls = CompiledCallGraph.Read(resetPlayer);
Assert.True(RequiredCallIndex(
playerCalls,
typeof(PlayerModeController),
nameof(PlayerModeController.ResetSession)) >= 0);
Assert.Contains("_interaction.PlayerMode.ResetSession();", source, MethodInfo resetMode = typeof(PlayerModeController).GetMethod(
StringComparison.Ordinal); nameof(PlayerModeController.ResetSession))!;
string playerModeSource = File.ReadAllText(Path.Combine( IReadOnlyList<CompiledCall> modeCalls = CompiledCallGraph.Read(resetMode);
FindRepoRoot(), Assert.True(RequiredCallIndex(
"src", modeCalls,
"AcDream.App", typeof(LocalPlayerModeState),
"Input", nameof(LocalPlayerModeState.ResetSession)) >= 0);
"PlayerModeController.cs"));
Assert.Contains("_mode.ResetSession();", playerModeSource, MethodInfo resetIdentity = typeof(LiveSessionRuntimeFactory).GetMethod(
StringComparison.Ordinal); "ResetIdentityPresentation",
Assert.Contains( BindingFlags.Instance | BindingFlags.NonPublic)!;
"_world.NetworkUpdates.ResetSessionState();", IReadOnlyList<CompiledCall> identityCalls = CompiledCallGraph.Read(resetIdentity);
source, int network = RequiredCallIndex(
StringComparison.Ordinal); identityCalls,
Assert.Contains("_player.WorldOrigin.Reset();", source, StringComparison.Ordinal); 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<CompiledCall> calls,
Type declaringType,
string methodName)
{ {
DirectoryInfo? directory = new(AppContext.BaseDirectory); int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
while (directory is not null) Assert.True(index >= 0, $"Missing compiled call: {declaringType.Name}.{methodName}");
{ return index;
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
} }
} }