303 lines
9.3 KiB
C#
303 lines
9.3 KiB
C#
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();
|
|
}
|
|
} |