From 35845be93f7aeb4c5f12f4556a63a41e78b48117 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:26:48 +0800 Subject: [PATCH] =?UTF-8?q?test(ecs):=20=E6=B7=BB=E5=8A=A0=E9=AB=98?= =?UTF-8?q?=E7=BA=A7ECS=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95=E5=B9=B6?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增EcsAdvancedTests类,包含完整的ECS系统测试套件 - 添加EcsWorld高级功能测试,包括实体创建、销毁和组件操作 - 实现EcsSystemRunner生命周期控制测试,验证启动停止行为 - 添加多系统交互测试,验证优先级执行顺序 - 完善组件操作测试,涵盖增删改查场景 - 集成ArchitectureContext与ECS的测试用例 - 添加依赖注入容器的ECS系统注册测试 - 补充系统运行器异常处理和资源清理测试 --- GFramework.Core.Tests/ecs/EcsAdvancedTests.cs | 342 ++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 GFramework.Core.Tests/ecs/EcsAdvancedTests.cs diff --git a/GFramework.Core.Tests/ecs/EcsAdvancedTests.cs b/GFramework.Core.Tests/ecs/EcsAdvancedTests.cs new file mode 100644 index 0000000..f223938 --- /dev/null +++ b/GFramework.Core.Tests/ecs/EcsAdvancedTests.cs @@ -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(); + 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); + } + + 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(entity), Is.True); + Assert.That(world.Has(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(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(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() as IReadOnlyList); + + 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(entity); + var xBefore = posBeforeDestroy.X; + var yBefore = posBeforeDestroy.Y; + + runner.Destroy(); + + // 销毁后再更新,位置应该保持不变 + runner.Update(1.0f); + + ref var posAfterDestroy = ref world.Get(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(); + + _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 { systemA, systemB, systemC } as IReadOnlyList); + + 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 { movementSystem } as IReadOnlyList); + + var runner = CreateRunner(); + runner.Start(); + runner.Update(1.0f); + runner.Update(1.0f); + + ref var pos = ref world.Get(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()); + Assert.That(ecsWorld, Is.InstanceOf()); + } + + [Test] + public void Component_AddAfterCreation_Should_Work() + { + _ecsWorld = new EcsWorld(); + var entity = _ecsWorld.CreateEntity(Array.Empty()); + var world = _ecsWorld.InternalWorld; + + world.Add(entity, new Position(5, 10)); + + Assert.That(world.Has(entity), Is.True); + ref var pos = ref world.Get(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(entity); + + Assert.That(world.Has(entity), Is.True); + Assert.That(world.Has(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(entity); + Assert.That(pos.X, Is.EqualTo(100)); + Assert.That(pos.Y, Is.EqualTo(200)); + } +} + +internal class OrderTrackingSystem(string name, int priority, List executionOrder) : EcsSystemBase +{ + public override int Priority { get; } = priority; + + protected override void OnEcsInit() + { + } + + public override void Update(float deltaTime) + { + executionOrder.Add(name); + } +} \ No newline at end of file