oecs-sharp/OECS.Tests/RelationshipTests.cs

303 lines
9.3 KiB
C#
Raw Normal View History

using FluentAssertions;
using Xunit;
namespace OECS.Tests;
// ── Phantom types for relationship differentiation ───────────────────
public struct ChildOf { }
public struct ParentOf { }
public struct OwnedBy { }
public struct MemberOf { }
public struct Thing { }
// ── Tests ────────────────────────────────────────────────────────────
public class RelationshipTests
{
[Fact]
public void AddRelationship_UpdatesReverseIndex()
{
var world = new World();
var child = world.CreateEntity();
var parent = world.CreateEntity();
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
{
Target = parent
});
var sources = world.GetSources<Relationship<ChildOf, ParentOf>>(parent);
sources.Should().BeEquivalentTo([child]);
}
[Fact]
public void RemoveRelationship_UpdatesReverseIndex()
{
var world = new World();
var child = world.CreateEntity();
var parent = world.CreateEntity();
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
{
Target = parent
});
world.RemoveComponent<Relationship<ChildOf, ParentOf>>(child);
var sources = world.GetSources<Relationship<ChildOf, ParentOf>>(parent);
sources.Should().BeEmpty();
}
[Fact]
public void ReplaceRelationship_UpdatesReverseIndex()
{
var world = new World();
var child = world.CreateEntity();
var parent1 = world.CreateEntity();
var parent2 = world.CreateEntity();
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
{
Target = parent1
});
// Replace with a new target.
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
{
Target = parent2
});
world.GetSources<Relationship<ChildOf, ParentOf>>(parent1).Should().BeEmpty();
world.GetSources<Relationship<ChildOf, ParentOf>>(parent2).Should().BeEquivalentTo([child]);
}
[Fact]
public void DestroySourceEntity_CleansUpRelationships()
{
var world = new World();
var child = world.CreateEntity();
var parent = world.CreateEntity();
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
{
Target = parent
});
world.DestroyEntity(child);
// The relationship should be gone from the reverse index.
world.GetSources<Relationship<ChildOf, ParentOf>>(parent).Should().BeEmpty();
// The destroyed entity should not be alive.
world.IsAlive(child).Should().BeFalse();
}
[Fact]
public void DestroyTargetEntity_CleansUpIncomingRelationships()
{
var world = new World();
var child = world.CreateEntity();
var parent = world.CreateEntity();
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
{
Target = parent
});
world.DestroyEntity(parent);
// The relationship component should be removed from the source entity.
world.HasComponent<Relationship<ChildOf, ParentOf>>(child).Should().BeFalse();
// The reverse index should be empty for the destroyed target.
world.GetSources<Relationship<ChildOf, ParentOf>>(parent).Should().BeEmpty();
}
[Fact]
public void MultipleSources_ForSameTarget()
{
var world = new World();
var child1 = world.CreateEntity();
var child2 = world.CreateEntity();
var child3 = world.CreateEntity();
var parent = world.CreateEntity();
world.AddComponent(child1, new Relationship<ChildOf, ParentOf>
{
Target = parent
});
world.AddComponent(child2, new Relationship<ChildOf, ParentOf>
{
Target = parent
});
world.AddComponent(child3, new Relationship<ChildOf, ParentOf>
{
Target = parent
});
var sources = world.GetSources<Relationship<ChildOf, ParentOf>>(parent);
sources.Should().BeEquivalentTo([child1, child2, child3]);
}
[Fact]
public void DifferentRelationshipTypes_AreIndependent()
{
var world = new World();
var entity = world.CreateEntity();
var owner = world.CreateEntity();
var group = world.CreateEntity();
world.AddComponent(entity, new Relationship<OwnedBy, Thing>
{
Target = owner
});
world.AddComponent(entity, new Relationship<MemberOf, Thing>
{
Target = group
});
world.GetSources<Relationship<OwnedBy, Thing>>(owner).Should().BeEquivalentTo([entity]);
world.GetSources<Relationship<MemberOf, Thing>>(group).Should().BeEquivalentTo([entity]);
// Cross-check: OwnedBy sources should not appear in MemberOf lookups.
world.GetSources<Relationship<MemberOf, Thing>>(owner).Should().BeEmpty();
}
[Fact]
public void ReverseLookup_ReturnsEmpty_WhenNoRelationships()
{
var world = new World();
var entity = world.CreateEntity();
var sources = world.GetSources<Relationship<ChildOf, ParentOf>>(entity);
sources.Should().BeEmpty();
}
[Fact]
public void Relationship_CanBeQueried_LikeNormalComponent()
{
var world = new World();
var child = world.CreateEntity();
var parent = world.CreateEntity();
world.AddComponent(child, new Relationship<ChildOf, ParentOf>
{
Target = parent
});
var results = new List<Entity>();
foreach (var it in world.Select<Relationship<ChildOf, ParentOf>>())
{
results.Add(it.Entity);
it.Val1.Target.Should().Be(parent);
}
results.Should().BeEquivalentTo([child]);
}
[Fact]
public void GetSources_PreservesInsertionOrder()
{
var world = new World();
var parent = world.CreateEntity();
// Add sources in a specific order.
var sources = new Entity[5];
for (int i = 0; i < 5; i++)
{
sources[i] = world.CreateEntity();
world.AddComponent(sources[i], new Relationship<ChildOf, ParentOf>
{
Target = parent
});
}
var result = world.GetSources<Relationship<ChildOf, ParentOf>>(parent);
result.Should().Equal(sources);
}
[Fact]
public void GetSources_OrderIsStableAfterMiddleRemove()
{
var world = new World();
var parent = world.CreateEntity();
var a = world.CreateEntity();
var b = world.CreateEntity();
var c = world.CreateEntity();
world.AddComponent(a, new Relationship<ChildOf, ParentOf> { Target = parent });
world.AddComponent(b, new Relationship<ChildOf, ParentOf> { Target = parent });
world.AddComponent(c, new Relationship<ChildOf, ParentOf> { Target = parent });
// Remove the middle source.
world.RemoveComponent<Relationship<ChildOf, ParentOf>>(b);
// Remaining sources should preserve insertion order.
var result = world.GetSources<Relationship<ChildOf, ParentOf>>(parent);
result.Should().Equal([a, c]);
}
[Fact]
public void ReorderSources_ChangesIterationOrder()
{
var world = new World();
var parent = world.CreateEntity();
var a = world.CreateEntity();
var b = world.CreateEntity();
var c = world.CreateEntity();
world.AddComponent(a, new Relationship<ChildOf, ParentOf> { Target = parent });
world.AddComponent(b, new Relationship<ChildOf, ParentOf> { Target = parent });
world.AddComponent(c, new Relationship<ChildOf, ParentOf> { Target = parent });
// Reverse the order.
world.ReorderSources<Relationship<ChildOf, ParentOf>>(parent, [c, b, a]);
var result = world.GetSources<Relationship<ChildOf, ParentOf>>(parent);
result.Should().Equal([c, b, a]);
}
[Fact]
public void ReorderSources_NoopsWhenTargetHasNoRelationships()
{
var world = new World();
var entity = world.CreateEntity();
// Should not throw.
world.ReorderSources<Relationship<ChildOf, ParentOf>>(entity, []);
}
[Fact]
public void DestroyEntity_WithBothIncomingAndOutgoingRelationships()
{
var world = new World();
var a = world.CreateEntity();
var b = world.CreateEntity();
var c = world.CreateEntity();
// a → b (a is child of b)
world.AddComponent(a, new Relationship<ChildOf, ParentOf>
{
Target = b
});
// c → a (c is child of a)
world.AddComponent(c, new Relationship<ChildOf, ParentOf>
{
Target = a
});
// Destroy a. This should:
// 1. Remove the a→b relationship (a is source).
// 2. Remove the c→a relationship (a is target, so c's component is removed).
world.DestroyEntity(a);
world.IsAlive(a).Should().BeFalse();
world.HasComponent<Relationship<ChildOf, ParentOf>>(c).Should().BeFalse();
world.GetSources<Relationship<ChildOf, ParentOf>>(b).Should().BeEmpty();
}
}