test(ecs): 添加高级ECS集成测试并完善测试覆盖

- 新增EcsAdvancedTests类,包含完整的ECS系统测试套件
- 添加EcsWorld高级功能测试,包括实体创建、销毁和组件操作
- 实现EcsSystemRunner生命周期控制测试,验证启动停止行为
- 添加多系统交互测试,验证优先级执行顺序
- 完善组件操作测试,涵盖增删改查场景
- 集成ArchitectureContext与ECS的测试用例
- 添加依赖注入容器的ECS系统注册测试
- 补充系统运行器异常处理和资源清理测试
This commit is contained in:
GeWuYou 2026-02-23 10:26:48 +08:00 committed by gewuyou
parent 6ee7a52326
commit 35845be93f

View File

@ -0,0 +1,342 @@
using System.Reflection;
using Arch.Core;
using GFramework.Core.Abstractions.ecs;
using GFramework.Core.Abstractions.rule;
using GFramework.Core.architecture;
using GFramework.Core.ecs;
using GFramework.Core.ecs.components;
using GFramework.Core.ecs.systems;
using GFramework.Core.ioc;
using GFramework.Core.logging;
using NUnit.Framework;
namespace GFramework.Core.Tests.ecs;
[TestFixture]
public class EcsAdvancedTests
{
[SetUp]
public void Setup()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
_container = new MicrosoftDiContainer();
var loggerField = typeof(MicrosoftDiContainer).GetField("_logger",
BindingFlags.NonPublic | BindingFlags.Instance);
loggerField?.SetValue(_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(EcsAdvancedTests)));
_context = new ArchitectureContext(_container);
}
[TearDown]
public void TearDown()
{
_ecsWorld?.Dispose();
_ecsWorld = null;
_container?.Clear();
_context = null;
}
private MicrosoftDiContainer? _container;
private ArchitectureContext? _context;
private EcsWorld? _ecsWorld;
private void InitializeEcsWithSystems(params Type[] systemTypes)
{
_ecsWorld = new EcsWorld();
_container!.Register(_ecsWorld);
_container.Register(_ecsWorld as IEcsWorld);
var systems = new List<IEcsSystem>();
foreach (var systemType in systemTypes)
{
var system = (IEcsSystem)Activator.CreateInstance(systemType)!;
((IContextAware)system).SetContext(_context!);
system.Init();
systems.Add(system);
_container.RegisterPlurality(system);
}
_container.Register(systems as IReadOnlyList<IEcsSystem>);
}
private EcsSystemRunner CreateRunner()
{
var runner = new EcsSystemRunner();
((IContextAware)runner).SetContext(_context!);
runner.Init();
return runner;
}
[Test]
public void EcsWorld_Dispose_Should_Be_Idempotent()
{
_ecsWorld = new EcsWorld();
_ecsWorld.CreateEntity(typeof(Position));
Assert.DoesNotThrow(() =>
{
_ecsWorld.Dispose();
_ecsWorld.Dispose();
});
_ecsWorld = null;
}
[Test]
public void EcsWorld_CreateEntity_WithNoComponents_Should_Work()
{
_ecsWorld = new EcsWorld();
var entity = _ecsWorld.CreateEntity();
Assert.That(_ecsWorld.EntityCount, Is.EqualTo(1));
Assert.That(_ecsWorld.IsAlive(entity), Is.True);
}
[Test]
public void EcsWorld_CreateEntity_WithMultipleComponents_Should_Work()
{
_ecsWorld = new EcsWorld();
var entity = _ecsWorld.CreateEntity(typeof(Position), typeof(Velocity));
var world = _ecsWorld.InternalWorld;
Assert.That(world.Has<Position>(entity), Is.True);
Assert.That(world.Has<Velocity>(entity), Is.True);
}
[Test]
public void EcsWorld_IsAlive_AfterDestroy_Should_ReturnFalse()
{
_ecsWorld = new EcsWorld();
var entity = _ecsWorld.CreateEntity(typeof(Position));
Assert.That(_ecsWorld.IsAlive(entity), Is.True);
_ecsWorld.DestroyEntity(entity);
Assert.That(_ecsWorld.IsAlive(entity), Is.False);
}
[Test]
public void EcsSystemRunner_Update_WithoutStart_Should_NotUpdate()
{
InitializeEcsWithSystems(typeof(MovementSystem));
var entity = _ecsWorld!.CreateEntity(typeof(Position), typeof(Velocity));
var world = _ecsWorld.InternalWorld;
world.Set(entity, new Position(0, 0));
world.Set(entity, new Velocity(10, 5));
var runner = CreateRunner();
runner.Update(1.0f);
ref var pos = ref world.Get<Position>(entity);
Assert.That(pos.X, Is.EqualTo(0), "Position should not change without Start()");
Assert.That(pos.Y, Is.EqualTo(0), "Position should not change without Start()");
}
[Test]
public void EcsSystemRunner_StartStop_Should_ControlUpdates()
{
InitializeEcsWithSystems(typeof(MovementSystem));
var entity = _ecsWorld!.CreateEntity(typeof(Position), typeof(Velocity));
var world = _ecsWorld.InternalWorld;
world.Set(entity, new Position(0, 0));
world.Set(entity, new Velocity(10, 5));
var runner = CreateRunner();
runner.Start();
runner.Update(1.0f);
runner.Stop();
runner.Update(1.0f);
ref var pos = ref world.Get<Position>(entity);
Assert.That(pos.X, Is.EqualTo(10).Within(0.001f), "Only first update should apply");
Assert.That(pos.Y, Is.EqualTo(5).Within(0.001f), "Only first update should apply");
}
[Test]
public void EcsSystemRunner_WithNoSystems_Should_NotThrow()
{
_ecsWorld = new EcsWorld();
_container!.Register(_ecsWorld);
_container.Register(new List<IEcsSystem>() as IReadOnlyList<IEcsSystem>);
var runner = CreateRunner();
Assert.DoesNotThrow(() =>
{
runner.Start();
runner.Update(1.0f);
runner.Stop();
});
}
[Test]
public void EcsSystemRunner_OnDestroy_Should_ClearSystems()
{
InitializeEcsWithSystems(typeof(MovementSystem));
var entity = _ecsWorld!.CreateEntity(typeof(Position), typeof(Velocity));
var world = _ecsWorld.InternalWorld;
world.Set(entity, new Position(0, 0));
world.Set(entity, new Velocity(10, 5));
var runner = CreateRunner();
runner.Start();
// 销毁前先更新一次,记录初始位置
runner.Update(1.0f);
ref var posBeforeDestroy = ref world.Get<Position>(entity);
var xBefore = posBeforeDestroy.X;
var yBefore = posBeforeDestroy.Y;
runner.Destroy();
// 销毁后再更新,位置应该保持不变
runner.Update(1.0f);
ref var posAfterDestroy = ref world.Get<Position>(entity);
Assert.That(posAfterDestroy.X, Is.EqualTo(xBefore), "Position should not change after Destroy()");
Assert.That(posAfterDestroy.Y, Is.EqualTo(yBefore), "Position should not change after Destroy()");
}
[Test]
public void MultipleSystems_Should_ExecuteInPriorityOrder()
{
var executionOrder = new List<string>();
_ecsWorld = new EcsWorld();
_container!.Register(_ecsWorld);
var systemA = new OrderTrackingSystem("A", 10, executionOrder);
var systemB = new OrderTrackingSystem("B", -10, executionOrder);
var systemC = new OrderTrackingSystem("C", 0, executionOrder);
foreach (var system in new[] { systemA, systemB, systemC })
{
((IContextAware)system).SetContext(_context!);
system.Init();
_container.RegisterPlurality(system);
}
_container.Register(new List<IEcsSystem> { systemA, systemB, systemC } as IReadOnlyList<IEcsSystem>);
var runner = CreateRunner();
runner.Start();
runner.Update(1.0f);
Assert.That(executionOrder, Is.EqualTo(["B", "C", "A"]),
"Systems should execute in priority order (B=-10, C=0, A=10)");
}
[Test]
public void ChainedSystems_Should_PassDataBetweenSystems()
{
_ecsWorld = new EcsWorld();
_container!.Register(_ecsWorld);
var entity = _ecsWorld.CreateEntity(typeof(Position), typeof(Velocity));
var world = _ecsWorld.InternalWorld;
world.Set(entity, new Position(0, 0));
world.Set(entity, new Velocity(10, 0));
var movementSystem = new MovementSystem();
((IContextAware)movementSystem).SetContext(_context!);
movementSystem.Init();
_container.RegisterPlurality(movementSystem);
_container.Register(new List<IEcsSystem> { movementSystem } as IReadOnlyList<IEcsSystem>);
var runner = CreateRunner();
runner.Start();
runner.Update(1.0f);
runner.Update(1.0f);
ref var pos = ref world.Get<Position>(entity);
Assert.That(pos.X, Is.EqualTo(20).Within(0.001f), "Position should accumulate over multiple updates");
}
[Test]
public void InitializeEcs_CalledTwice_Should_BeIdempotent()
{
_context!.InitializeEcs();
var ecsWorld1 = _context.GetEcsWorld();
Assert.DoesNotThrow(() => _context.InitializeEcs());
var ecsWorld2 = _context.GetEcsWorld();
Assert.That(ecsWorld2, Is.SameAs(ecsWorld1), "Should return same world instance");
}
[Test]
public void GetEcsWorld_Should_ReturnIEcsWorld()
{
_context!.InitializeEcs();
var ecsWorld = _context.GetEcsWorld();
Assert.That(ecsWorld, Is.InstanceOf<IEcsWorld>());
Assert.That(ecsWorld, Is.InstanceOf<EcsWorld>());
}
[Test]
public void Component_AddAfterCreation_Should_Work()
{
_ecsWorld = new EcsWorld();
var entity = _ecsWorld.CreateEntity(Array.Empty<ComponentType>());
var world = _ecsWorld.InternalWorld;
world.Add(entity, new Position(5, 10));
Assert.That(world.Has<Position>(entity), Is.True);
ref var pos = ref world.Get<Position>(entity);
Assert.That(pos.X, Is.EqualTo(5));
Assert.That(pos.Y, Is.EqualTo(10));
}
[Test]
public void Component_Remove_Should_Work()
{
_ecsWorld = new EcsWorld();
var entity = _ecsWorld.CreateEntity(typeof(Position), typeof(Velocity));
var world = _ecsWorld.InternalWorld;
world.Remove<Velocity>(entity);
Assert.That(world.Has<Position>(entity), Is.True);
Assert.That(world.Has<Velocity>(entity), Is.False);
}
[Test]
public void Component_Replace_Should_Work()
{
_ecsWorld = new EcsWorld();
var entity = _ecsWorld.CreateEntity(typeof(Position));
var world = _ecsWorld.InternalWorld;
world.Set(entity, new Position(1, 1));
world.Set(entity, new Position(100, 200));
ref var pos = ref world.Get<Position>(entity);
Assert.That(pos.X, Is.EqualTo(100));
Assert.That(pos.Y, Is.EqualTo(200));
}
}
internal class OrderTrackingSystem(string name, int priority, List<string> executionOrder) : EcsSystemBase
{
public override int Priority { get; } = priority;
protected override void OnEcsInit()
{
}
public override void Update(float deltaTime)
{
executionOrder.Add(name);
}
}