using FluentAssertions; using Xunit; namespace OECS.Tests; public class QueryTests { private struct Position { public float X; public float Y; } private struct Velocity { public float X; public float Y; } private struct Health { public int Value; } private struct Frozen { } [Fact] public void Query_WithSingleComponent_ReturnsMatchingEntities() { var world = new World(); var a = world.CreateEntity(); var b = world.CreateEntity(); var c = world.CreateEntity(); world.AddComponent(a, new Position { X = 1, Y = 2 }); world.AddComponent(b, new Position { X = 3, Y = 4 }); // c has no Position var query = world.Query().With().Build(); var results = new List(); world.ForEach(query, (Entity entity, ref Position pos) => { results.Add(entity); }); results.Should().BeEquivalentTo([a, b]); } [Fact] public void Query_WithMultipleComponents_ReturnsIntersection() { var world = new World(); var a = world.CreateEntity(); var b = world.CreateEntity(); var c = world.CreateEntity(); world.AddComponent(a, new Position { X = 1, Y = 2 }); world.AddComponent(a, new Velocity { X = 1, Y = 0 }); world.AddComponent(b, new Position { X = 3, Y = 4 }); // b has no Velocity world.AddComponent(c, new Velocity { X = 0, Y = 1 }); // c has no Position var query = world.Query().With().With().Build(); var results = new List(); world.ForEach(query, (Entity entity, ref Position pos, ref Velocity vel) => { results.Add(entity); }); results.Should().BeEquivalentTo([a]); } [Fact] public void Query_Without_ExcludesEntities() { var world = new World(); var a = world.CreateEntity(); var b = world.CreateEntity(); world.AddComponent(a, new Position { X = 1, Y = 2 }); world.AddComponent(b, new Position { X = 3, Y = 4 }); world.AddComponent(b, new Frozen()); var query = world.Query().With().Without().Build(); var results = new List(); world.ForEach(query, (Entity entity, ref Position pos) => { results.Add(entity); }); results.Should().BeEquivalentTo([a]); } [Fact] public void Query_EmptyResult_WhenNoEntitiesMatch() { var world = new World(); var query = world.Query().With().Build(); var count = 0; world.ForEach(query, (Entity entity, ref Position pos) => { count++; }); count.Should().Be(0); } [Fact] public void Query_RefMutation_IsVisible() { var world = new World(); var entity = world.CreateEntity(); world.AddComponent(entity, new Position { X = 0, Y = 0 }); var query = world.Query().With().Build(); world.ForEach(query, (Entity e, ref Position pos) => { pos.X = 42; }); ref var pos = ref world.GetComponent(entity); pos.X.Should().Be(42); } [Fact] public void Query_DestroyedEntities_DoNotAppear() { var world = new World(); var a = world.CreateEntity(); var b = world.CreateEntity(); world.AddComponent(a, new Position { X = 1, Y = 2 }); world.AddComponent(b, new Position { X = 3, Y = 4 }); world.DestroyEntity(a); var query = world.Query().With().Build(); var results = new List(); world.ForEach(query, (Entity entity, ref Position pos) => { results.Add(entity); }); results.Should().BeEquivalentTo([b]); } [Fact] public void Query_ThreeComponents_Works() { var world = new World(); var entity = world.CreateEntity(); world.AddComponent(entity, new Position { X = 1, Y = 2 }); world.AddComponent(entity, new Velocity { X = 3, Y = 4 }); world.AddComponent(entity, new Health { Value = 100 }); var query = world.Query() .With() .With() .With() .Build(); var count = 0; world.ForEach(query, (Entity e, ref Position pos, ref Velocity vel, ref Health hp) => { count++; pos.X.Should().Be(1); vel.Y.Should().Be(4); hp.Value.Should().Be(100); }); count.Should().Be(1); } } public class SystemTests { private struct Position { public float X; public float Y; } private struct Velocity { public float X; public float Y; } private class MovementSystem : ITickedSystem { private readonly QueryDescriptor _query; public MovementSystem(World world) { _query = world.Query().With().With().Build(); } public void Run(World world) => Run(world, Tick.Logical()); public void Run(World world, Tick tick) { float dt = tick.DeltaTime; world.ForEach(_query, (Entity entity, ref Position pos, ref Velocity vel) => { pos.X += vel.X * dt; pos.Y += vel.Y * dt; }); } } [Fact] public void System_RunTimed_UpdatesComponents() { var world = new World(); var group = new SystemGroup(world); var system = new MovementSystem(world); group.Add(system); var entity = world.CreateEntity(); world.AddComponent(entity, new Position { X = 0, Y = 0 }); world.AddComponent(entity, new Velocity { X = 1, Y = 2 }); group.RunTimed(0.5f); ref var pos = ref world.GetComponent(entity); pos.X.Should().Be(0.5f); pos.Y.Should().Be(1.0f); } [Fact] public void System_RunLogical_DoesNotUseDeltaTime() { var world = new World(); var group = new SystemGroup(world); var system = new TestLogicalSystem(world); group.Add(system); group.RunLogical(); system.WasCalled.Should().BeTrue(); system.ReceivedTick.Type.Should().Be(TickType.Logical); system.ReceivedTick.DeltaTime.Should().Be(0); } private class TestLogicalSystem : ITickedSystem { public bool WasCalled { get; private set; } public Tick ReceivedTick { get; private set; } public TestLogicalSystem(World world) { } public void Run(World world) => Run(world, Tick.Logical()); public void Run(World world, Tick tick) { WasCalled = true; ReceivedTick = tick; } } [Fact] public void Systems_RunInRegistrationOrder() { var world = new World(); var group = new SystemGroup(world); var order = new List(); group.Add(new OrderTrackingSystem(world, 1, order)); group.Add(new OrderTrackingSystem(world, 2, order)); group.Add(new OrderTrackingSystem(world, 3, order)); group.RunLogical(); order.Should().BeInAscendingOrder(); order.Should().BeEquivalentTo([1, 2, 3]); } private class OrderTrackingSystem : ISystem { private readonly int _id; private readonly List _order; public OrderTrackingSystem(World world, int id, List order) { _id = id; _order = order; } public void Run(World world) { _order.Add(_id); } } [Fact] public void System_CanBeRemoved() { var world = new World(); var group = new SystemGroup(world); var system = new TestLogicalSystem(world); group.Add(system); group.Count.Should().Be(1); group.Remove(system); group.Count.Should().Be(0); group.RunLogical(); system.WasCalled.Should().BeFalse(); } }