using FluentAssertions; using Xunit; namespace OECS.Tests; public class EntityTests { [Fact] public void CreateEntity_ReturnsUniqueIds() { var world = new World(); var a = world.CreateEntity(); var b = world.CreateEntity(); a.Should().NotBe(b); a.Id.Should().NotBe(b.Id); } [Fact] public void Null_IsSentinel() { Entity.Null.IsNull.Should().BeTrue(); Entity.Null.Id.Should().Be(0); Entity.Null.Version.Should().Be(0); } [Fact] public void DestroyEntity_RecyclesId_WithIncrementedVersion() { var world = new World(); var original = world.CreateEntity(); uint originalId = original.Id; uint originalVersion = original.Version; world.DestroyEntity(original); world.IsAlive(original).Should().BeFalse(); var recycled = world.CreateEntity(); recycled.Id.Should().Be(originalId); recycled.Version.Should().BeGreaterThan(originalVersion); } [Fact] public void IsAlive_ReturnsFalse_ForDestroyedEntity() { var world = new World(); var entity = world.CreateEntity(); world.IsAlive(entity).Should().BeTrue(); world.DestroyEntity(entity); world.IsAlive(entity).Should().BeFalse(); } [Fact] public void IsAlive_ReturnsFalse_ForNull() { var world = new World(); world.IsAlive(Entity.Null).Should().BeFalse(); } [Fact] public void ToString_IncludesIdAndVersion() { var world = new World(); var entity = world.CreateEntity(); var str = entity.ToString(); str.Should().Contain(entity.Id.ToString()); str.Should().Contain(entity.Version.ToString()); } [Fact] public void Equality_ChecksBothIdAndVersion() { var world = new World(); var a = world.CreateEntity(); world.DestroyEntity(a); var b = world.CreateEntity(); // Same ID, different version a.Should().NotBe(b); (a == b).Should().BeFalse(); } } public class ComponentTests { private struct Position { public float X; public float Y; } private struct Velocity { public float X; public float Y; } private struct Health { public int Value; } [Fact] public void AddAndGetComponent_RoundTrips() { var world = new World(); var entity = world.CreateEntity(); world.AddComponent(entity, new Position { X = 1, Y = 2 }); ref var pos = ref world.GetComponent(entity); pos.X.Should().Be(1); pos.Y.Should().Be(2); } [Fact] public void AddComponent_ReplacesExisting() { var world = new World(); var entity = world.CreateEntity(); world.AddComponent(entity, new Position { X = 1, Y = 2 }); world.AddComponent(entity, new Position { X = 3, Y = 4 }); ref var pos = ref world.GetComponent(entity); pos.X.Should().Be(3); pos.Y.Should().Be(4); } [Fact] public void HasComponent_ReturnsCorrectly() { var world = new World(); var entity = world.CreateEntity(); world.HasComponent(entity).Should().BeFalse(); world.AddComponent(entity, new Position()); world.HasComponent(entity).Should().BeTrue(); } [Fact] public void RemoveComponent_RemovesIt() { var world = new World(); var entity = world.CreateEntity(); world.AddComponent(entity, new Position { X = 1, Y = 2 }); world.RemoveComponent(entity); world.HasComponent(entity).Should().BeFalse(); } [Fact] public void RemoveComponent_NoOp_WhenNotPresent() { var world = new World(); var entity = world.CreateEntity(); // Should not throw. world.RemoveComponent(entity); } [Fact] public void MultipleComponents_OnSameEntity() { 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 }); world.HasComponent(entity).Should().BeTrue(); world.HasComponent(entity).Should().BeTrue(); world.HasComponent(entity).Should().BeTrue(); ref var pos = ref world.GetComponent(entity); ref var vel = ref world.GetComponent(entity); ref var hp = ref world.GetComponent(entity); pos.X.Should().Be(1); vel.Y.Should().Be(4); hp.Value.Should().Be(100); } [Fact] public void DestroyEntity_RemovesAllComponents() { var world = new World(); var entity = world.CreateEntity(); world.AddComponent(entity, new Position()); world.AddComponent(entity, new Velocity()); world.DestroyEntity(entity); // After destroy, the entity is not alive, so component queries // on the stale handle are irrelevant. But a new entity with the // same ID should not have the old components. var newEntity = world.CreateEntity(); world.HasComponent(newEntity).Should().BeFalse(); world.HasComponent(newEntity).Should().BeFalse(); } [Fact] public void GetComponent_Throws_WhenNotPresent() { var world = new World(); var entity = world.CreateEntity(); var act = () => world.GetComponent(entity); act.Should().Throw() .WithMessage("*does not have*Position*"); } [Fact] public void GetComponent_Throws_WhenEntityIdOutOfRange() { var world = new World(); // Create an entity with a very large ID by exhausting the allocator. // This is hard to do without internals, so we test via a destroyed // entity that has a valid ID but no component. var entity = world.CreateEntity(); var act = () => world.GetComponent(entity); act.Should().Throw() .WithMessage("*does not have*Position*"); } [Fact] public void TryGetComponent_ReturnsTrue_WhenPresent() { var world = new World(); var entity = world.CreateEntity(); world.AddComponent(entity, new Position { X = 1, Y = 2 }); bool found = world.TryGetComponent(entity, out var pos); found.Should().BeTrue(); pos.X.Should().Be(1); pos.Y.Should().Be(2); } [Fact] public void TryGetComponent_ReturnsFalse_WhenNotPresent() { var world = new World(); var entity = world.CreateEntity(); bool found = world.TryGetComponent(entity, out var pos); found.Should().BeFalse(); pos.Should().Be(default(Position)); } [Fact] public void AddComponent_Throws_WhenEntityNotAlive() { var world = new World(); var entity = world.CreateEntity(); world.DestroyEntity(entity); var act = () => world.AddComponent(entity, new Position()); act.Should().Throw(); } [Fact] public void Mutation_ViaRef_IsVisible() { var world = new World(); var entity = world.CreateEntity(); world.AddComponent(entity, new Position { X = 0, Y = 0 }); ref var pos = ref world.GetComponent(entity); pos.X = 42; ref var pos2 = ref world.GetComponent(entity); pos2.X.Should().Be(42); } }