268 lines
7.6 KiB
C#
268 lines
7.6 KiB
C#
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<Position>(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<Position>(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<Position>(entity).Should().BeFalse();
|
|
world.AddComponent(entity, new Position());
|
|
world.HasComponent<Position>(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<Position>(entity);
|
|
world.HasComponent<Position>(entity).Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoveComponent_NoOp_WhenNotPresent()
|
|
{
|
|
var world = new World();
|
|
var entity = world.CreateEntity();
|
|
|
|
// Should not throw.
|
|
world.RemoveComponent<Position>(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<Position>(entity).Should().BeTrue();
|
|
world.HasComponent<Velocity>(entity).Should().BeTrue();
|
|
world.HasComponent<Health>(entity).Should().BeTrue();
|
|
|
|
ref var pos = ref world.GetComponent<Position>(entity);
|
|
ref var vel = ref world.GetComponent<Velocity>(entity);
|
|
ref var hp = ref world.GetComponent<Health>(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<Position>(newEntity).Should().BeFalse();
|
|
world.HasComponent<Velocity>(newEntity).Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void GetComponent_Throws_WhenNotPresent()
|
|
{
|
|
var world = new World();
|
|
var entity = world.CreateEntity();
|
|
|
|
var act = () => world.GetComponent<Position>(entity);
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.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<Position>(entity);
|
|
act.Should().Throw<InvalidOperationException>()
|
|
.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<Position>(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<Position>(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<InvalidOperationException>();
|
|
}
|
|
|
|
[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<Position>(entity);
|
|
pos.X = 42;
|
|
|
|
ref var pos2 = ref world.GetComponent<Position>(entity);
|
|
pos2.X.Should().Be(42);
|
|
}
|
|
}
|