Files
Project-M/Assets/_Project/Tests/EditMode/TestWorld.cs
T
kronic be7aa8affd Hygiene B6: test coverage (fixture + missing coverage + guards)
- TestWorld: shared plain-Entities fixture (Make/Make<T>/SetTick reconciling the two SetServerTick variants + Player/Enemy builders). Additive; new tests consume it (40-file migration of existing tests deliberately deferred as pure churn).
- TestAttributeGuardTests: scans *Tests.cs and fails on any parameterless public-void method missing a runner attribute — guards the swallowed-[Test] bug (B0). Confirms the suite has no other dead tests.
- PrepPurchaseSystemTests (6): the previously-untested RPC economy — afford, reject-when-broke, once-per-run, non-Staging reject, unknown-id drop, and DR-014 same-tick atomicity.
- SystemOrderingCycleTests: registers the real run/cycle/combat system set and asserts SortSystems() has no circular dependency (invisible to single-system fixtures; only throws at Play) — also de-risks the B5 splits.

459/459 EditMode tests pass. (BossAISystemTests remains queued — the most complex to author faithfully; the boss stays Play-validated meanwhile.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 23:50:07 -07:00

76 lines
3.8 KiB
C#

using ProjectM.Simulation;
using Unity.Core;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Tests
{
/// <summary>
/// Plain-Entities EditMode harness. Behaviourally identical to the hand-rolled MakeWorld/SetServerTick idioms
/// scattered across the suite (a NetworkTime-seeded SimulationSystemGroup world), consolidated so the boilerplate
/// has one home. Named NOT to end in "Tests" so the attribute-guard meta-test skips it.
/// </summary>
static class TestWorld
{
/// <summary>World + SimulationSystemGroup, NetworkTime seeded to <paramref name="tick"/>, TimeData = 1/60.
/// <paramref name="server"/> sets WorldFlags.GameServer (the IsServer() gate some systems read).</summary>
public static (World world, SimulationSystemGroup group) Make(string name = "TestWorld", uint tick = 1, bool server = false)
{
var world = server ? new World(name, WorldFlags.Game | WorldFlags.GameServer) : new World(name);
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
SetTick(world, tick);
return (world, group);
}
/// <summary>As <see cref="Make"/>, plus registers ONE ISystem and sorts. Drop-in for the per-file MakeWorld&lt;T&gt;.</summary>
public static (World world, SimulationSystemGroup group) Make<T>(string name = "TestWorld", uint tick = 1, bool server = false)
where T : unmanaged, ISystem
{
var (world, group) = Make(name, tick, server);
group.AddSystemToUpdateList(world.GetOrCreateSystem<T>());
group.SortSystems();
return (world, group);
}
/// <summary>Register an extra ISystem and re-sort (idempotent — final order is what matters).</summary>
public static void Add<T>(World world, SimulationSystemGroup group) where T : unmanaged, ISystem
{
group.AddSystemToUpdateList(world.GetOrCreateSystem<T>());
group.SortSystems();
}
/// <summary>The reconciled tick API: create NetworkTime if absent, else set it (a strict superset of both
/// hand-rolled variants — behaves like GetSingletonEntity() when present, and tolerates absence).</summary>
public static void SetTick(World world, uint tick)
{
var em = world.EntityManager;
using var q = em.CreateEntityQuery(typeof(NetworkTime));
Entity e = q.IsEmpty ? em.CreateEntity(typeof(NetworkTime)) : q.GetSingletonEntity();
em.SetComponentData(e, new NetworkTime { ServerTick = new NetworkTick(tick) });
}
// ---- domain builders (only for shapes that match exactly; per-file builders with extra components stay local) ----
public static Entity Player(EntityManager em, int netId = 1, byte region = RegionId.Base, float3 pos = default)
{
var e = em.CreateEntity(typeof(PlayerTag), typeof(GhostOwner), typeof(RegionTag), typeof(LocalTransform));
em.SetComponentData(e, new GhostOwner { NetworkId = netId });
em.SetComponentData(e, new RegionTag { Region = region });
em.SetComponentData(e, LocalTransform.FromPosition(pos));
return e;
}
public static Entity Enemy(EntityManager em, float3 pos, float hp = 50f, byte region = RegionId.Base)
{
var e = em.CreateEntity(typeof(EnemyTag), typeof(LocalTransform), typeof(RegionTag), typeof(Health));
em.SetComponentData(e, LocalTransform.FromPosition(pos));
em.SetComponentData(e, new RegionTag { Region = region });
em.SetComponentData(e, new Health { Current = hp, Max = hp });
return e;
}
}
}