Files
lingua-engine/tests/Engine.ConformanceHarness/PluginSystemTests.cs
T
EmilandClaude Sonnet 5 17f66c4de5 Implement Scheduler: stage execution, conflict batching, debug enforcement
Schedule gains real execution on top of the registration bookkeeping
from the PluginHost pass: RunStage(stage, world) runs every system
registered for a stage, and Reads<>/Writes<>() declarations are
enforced live via SystemAccessScope — a system touching a component it
didn't declare throws immediately, with a message naming the
violation, from GameWorld.Query<T>() and GameObject.GetComponent<T>()/
AddComponent<T>()/RemoveComponent<T>(). Outside of a running system
(editor code, tests, scene construction) nothing is enforced.

Scope cut made deliberately, not by accident: systems are grouped into
conflict-free batches by declared access (ComputeBatches, tested
directly), but batches run sequentially rather than on real threads.
Actually parallelizing them needs GameWorld's structural changes
(Create/Destroy/AddComponent/RemoveComponent) deferred to a command
buffer first — without that, two systems with disjoint *declared*
types can still race on shared storage, since AddComponent<T>() on a
GameObject mutates that object's own component list regardless of T.
Building real concurrency on top of a known thread-safety hole would
be worse than not building it yet. Noted as a TODO on RunStage.

Two real bugs found and fixed while wiring this up, not designed in
from the start:

- ISchedule.Add took `Delegate`, and a lambda passed there doesn't
  reliably compile down to `Action<IWorld>` at runtime — the
  compiler's natural-type inference for lambdas (as opposed to method
  groups, which do work this way) can synthesize a different, private
  delegate type instead, so `is Action<IWorld>` silently failed for
  every lambda-registered system. Changed Add's parameter type to
  Action<IWorld> directly, which sidesteps the inference question
  entirely — found by ScheduleTests actually using lambdas, which
  EchoPlugin's method-group-based Tick had been masking.
- AlcUnloadTests started failing intermittently ("ALC survived unload
  cycle 3") once PluginSystemTests existed alongside it — xUnit
  parallelizes across test classes by default, and ALC-unload tests
  are sensitive to any concurrent activity in the process. Added
  [CollectionBehavior(DisableTestParallelization = true)] to the
  harness assembly; stable across 5+ repeated runs since.

EchoPlugin.Tick is no longer a stub — it increments every Ping.Count
in World, which two new integration tests in
Engine.ConformanceHarness/PluginSystemTests.cs exercise end to end: a
plugin loaded from a real collectible ALC registers a system, Schedule
actually invokes that cross-ALC delegate, and it correctly mutates a
component owned by the Default-ALC World. This only works because
PluginLoadContext resolves Sandbox.Echo.Contracts to the copy this
test project references directly, rather than loading a second,
type-incompatible one — the harness's new normal ProjectReference to
Sandbox.Echo.Contracts.csproj makes that a live assertion, not just an
implementation detail no test would notice breaking.

27 tests total now (21 in Engine.Kernel.Tests, 6 in
Engine.ConformanceHarness), all green on a clean build, harness
verified stable across repeated runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
2026-09-02 00:59:02 +03:00

60 lines
2.1 KiB
C#

using Engine.Kernel.Events;
using Engine.Kernel.Plugins;
using Engine.Kernel.Scheduling;
using Engine.Kernel.Services;
using Engine.Kernel.World;
using Sandbox.Echo.Contracts;
namespace Engine.ConformanceHarness;
/// <summary>
/// Closes the loop this whole exercise was for: a plugin loaded from a real
/// collectible ALC registers a system with Schedule, Schedule actually
/// invokes that cross-ALC delegate, and it correctly mutates a component
/// living in the Default-ALC-owned World. If PluginLoadContext resolved
/// Sandbox.Echo.Contracts incorrectly (a second, distinct copy instead of
/// deferring to the one this project references directly — see the note on
/// that ProjectReference in the .csproj), the Ping instance this test
/// constructs wouldn't be a Ping as far as EchoPlugin's Tick sees it, and
/// world.Query&lt;Ping&gt;() inside the plugin would find nothing.
/// </summary>
public class PluginSystemTests
{
private static string PluginDirectory =>
Path.Combine(AppContext.BaseDirectory, "plugins", "sandbox.echo");
[Fact]
public void Loaded_Plugin_System_Runs_And_Mutates_A_Component_It_Declared()
{
var world = new GameWorld();
var schedule = new Schedule();
var host = new PluginHost(world, new ServiceRegistry(), schedule, new NullEventBus());
var ping = world.CreateGameObject("Pinger").AddComponent<Ping>();
var id = host.Load(PluginDirectory);
schedule.RunStage(Stage.Update, world);
schedule.RunStage(Stage.Update, world);
Assert.Equal(2, ping.Count);
host.Unload(id);
}
[Fact]
public void Unloading_The_Plugin_Stops_Its_System_From_Running()
{
var world = new GameWorld();
var schedule = new Schedule();
var host = new PluginHost(world, new ServiceRegistry(), schedule, new NullEventBus());
var ping = world.CreateGameObject("Pinger").AddComponent<Ping>();
var id = host.Load(PluginDirectory);
host.Unload(id);
schedule.RunStage(Stage.Update, world);
Assert.Equal(0, ping.Count);
}
}