using Azimuth; using Blacklight.Core.Drawables; using System; using System.Collections.Generic; using UnitTestSharp; namespace Blacklight.Core.UnitTests { public class EntityContainerTests : TestFixture { /// /// Shared scaffolding for the per-method fixtures below. /// [IgnoreFixture] public class Base : TestFixture { protected class CapturingVisitor : EntityContainer.IVisitor { public readonly List DrawableTypes = new List(); public readonly List Translations = new List(); public void Visit(in Entity entity) where DrawableType : IDrawable { DrawableTypes.Add(typeof(DrawableType)); Translations.Add(entity.modelToWorld.TransformPoint(Vector.Zero)); } } /// /// A recoverable id baked into an entity's transform, so a visitor can tell entities apart. /// protected static AffineMatrix At(Scalar marker) => AffineMatrix.BuildFromTranslation(new Vector(marker, 0)); protected static Vector Marker(Scalar marker) => new Vector(marker, 0); [IgnoreTest] protected void CheckDrawable(EntityContainer container, EntityHandle handle, Type expectedType, Vector expectedTranslation) { var visitor = new CapturingVisitor(); CheckTrue(container.VisitEntity(handle, visitor)); CheckEqual(1, visitor.DrawableTypes.Count); CheckEqual(expectedType, visitor.DrawableTypes[0]); CheckEqual(expectedTranslation, visitor.Translations[0]); } } public class AddEntityTests : Base { public void RoundTripsThroughVisitEntity() { var container = new EntityContainer(); var handle = container.AddEntity(new Ellipse(), At(7)); CheckDrawable(container, handle, typeof(Ellipse), Marker(7)); } public void EntityOverload_StoresTheEntity() { var container = new EntityContainer(); var entity = new Entity(new Rectangle(), At(3)); var handle = container.AddEntity(entity); CheckDrawable(container, handle, typeof(Rectangle), Marker(3)); } public void DrawableOverload_DefaultsToIdentityTransform() { var container = new EntityContainer(); var handle = container.AddEntity(new Ellipse()); CheckDrawable(container, handle, typeof(Ellipse), Vector.Zero); } public void MixedDrawableTypes_ResolveToTheirOwnEntities() { var container = new EntityContainer(); var collage = container.AddEntity(new Collage(), At(1)); var ellipse = container.AddEntity(new Ellipse(), At(2)); var rectangle = container.AddEntity(new Rectangle(), At(3)); CheckDrawable(container, collage, typeof(Collage), Marker(1)); CheckDrawable(container, ellipse, typeof(Ellipse), Marker(2)); CheckDrawable(container, rectangle, typeof(Rectangle), Marker(3)); } public void GrowsPastInitialCapacity() { // Backing array starts at 16 and doubles; add past that to hit the resize + copy path. var container = new EntityContainer(); var handles = new List(); for (int i = 0; i < 40; ++i) { handles.Add(container.AddEntity(new Ellipse(), At(i))); } for (int i = 0; i < 40; ++i) { CheckDrawable(container, handles[i], typeof(Ellipse), Marker(i)); } var visitor = new CapturingVisitor(); container.ForEach(visitor); CheckEqual(40, visitor.Translations.Count); } } public class VisitEntityTests : Base { public void RemovedHandle_ReturnsFalse() { var container = new EntityContainer(); var handle = container.AddEntity(new Ellipse(), At(1)); container.RemoveEntity(handle); var visitor = new CapturingVisitor(); CheckFalse(container.VisitEntity(handle, visitor)); CheckEqual(0, visitor.Translations.Count); } } public class ReplaceEntityTests : Base { public void SameType_UpdatesInPlace() { var container = new EntityContainer(); var handle = container.AddEntity(new Ellipse(), At(1)); var replacement = new Entity(new Ellipse(), At(9)); container.ReplaceEntity(handle, ref replacement); CheckDrawable(container, handle, typeof(Ellipse), Marker(9)); } public void SameType_DoesNotGrowOrAliasStorage() { var container = new EntityContainer(); var first = container.AddEntity(new Ellipse(), At(1)); var replacement = new Entity(new Ellipse(), At(2)); container.ReplaceEntity(first, ref replacement); var second = container.AddEntity(new Ellipse(), At(3)); CheckDrawable(container, first, typeof(Ellipse), Marker(2)); CheckDrawable(container, second, typeof(Ellipse), Marker(3)); var visitor = new CapturingVisitor(); container.ForEach(visitor); CheckEqual(2, visitor.Translations.Count); } public void ChangingType_MovesEntityToNewTypeList() { var container = new EntityContainer(); var handle = container.AddEntity(new Ellipse(), At(1)); var replacement = new Entity(new Rectangle(), At(5)); container.ReplaceEntity(handle, ref replacement); CheckDrawable(container, handle, typeof(Rectangle), Marker(5)); // The released Ellipse slot must not leak into iteration. var visitor = new CapturingVisitor(); container.ForEach(visitor); CheckEqual(1, visitor.Translations.Count); CheckEqual(typeof(Rectangle), visitor.DrawableTypes[0]); } public void ChangingType_AppendsToExistingTypeList() { var container = new EntityContainer(); var ellipse = container.AddEntity(new Ellipse(), At(1)); var rectangle = container.AddEntity(new Rectangle(), At(2)); // The Rectangle list already exists and is non-empty, so the moved entity must append after the // existing Rectangle, not overwrite it at index 0. var replacement = new Entity(new Rectangle(), At(5)); container.ReplaceEntity(ellipse, ref replacement); CheckDrawable(container, ellipse, typeof(Rectangle), Marker(5)); CheckDrawable(container, rectangle, typeof(Rectangle), Marker(2)); } public void RepeatedSameType_StaysBounded() { // The point of the in-place fix: replacing every frame (cross-frame caching) must not grow storage. var container = new EntityContainer(); var handle = container.AddEntity(new Ellipse(), At(0)); for (int i = 1; i <= 20; ++i) { var replacement = new Entity(new Ellipse(), At(i)); container.ReplaceEntity(handle, ref replacement); } CheckDrawable(container, handle, typeof(Ellipse), Marker(20)); var visitor = new CapturingVisitor(); container.ForEach(visitor); CheckEqual(1, visitor.Translations.Count); } } public class RemoveEntityTests : Base { public void LeavesOtherEntitiesReachable() { var container = new EntityContainer(); var doomed = container.AddEntity(new Ellipse(), At(1)); var survivor = container.AddEntity(new Ellipse(), At(2)); container.RemoveEntity(doomed); CheckDrawable(container, survivor, typeof(Ellipse), Marker(2)); var visitor = new CapturingVisitor(); container.ForEach(visitor); CheckEqual(1, visitor.Translations.Count); CheckEqual(Marker(2), visitor.Translations[0]); } public void RemovedHandle_IsNotRecycledByLaterAdds() { // Slots are never reused (EntityHandle is a UID), so a removed handle stays dead even as later adds // grow the container. A tripwire for the slot-recycling TODO on EntityHandle. var container = new EntityContainer(); var doomed = container.AddEntity(new Ellipse(), At(1)); container.RemoveEntity(doomed); container.AddEntity(new Ellipse(), At(2)); container.AddEntity(new Rectangle(), At(3)); var visitor = new CapturingVisitor(); CheckFalse(container.VisitEntity(doomed, visitor)); CheckEqual(0, visitor.Translations.Count); } } public class ForEachTests : Base { public void VisitsEntitiesInInsertionOrder() { var container = new EntityContainer(); container.AddEntity(new Ellipse(), At(1)); container.AddEntity(new Ellipse(), At(2)); container.AddEntity(new Ellipse(), At(3)); var visitor = new CapturingVisitor(); container.ForEach(visitor); CheckEqual(3, visitor.Translations.Count); CheckEqual(Marker(1), visitor.Translations[0]); CheckEqual(Marker(2), visitor.Translations[1]); CheckEqual(Marker(3), visitor.Translations[2]); } public void VisitsMixedTypesInInsertionOrder() { var container = new EntityContainer(); container.AddEntity(new Collage(), At(1)); container.AddEntity(new Ellipse(), At(2)); container.AddEntity(new Rectangle(), At(3)); var visitor = new CapturingVisitor(); container.ForEach(visitor); CheckEqual(3, visitor.Translations.Count); CheckEqual(typeof(Collage), visitor.DrawableTypes[0]); CheckEqual(typeof(Ellipse), visitor.DrawableTypes[1]); CheckEqual(typeof(Rectangle), visitor.DrawableTypes[2]); CheckEqual(Marker(1), visitor.Translations[0]); CheckEqual(Marker(2), visitor.Translations[1]); CheckEqual(Marker(3), visitor.Translations[2]); } public void EmptyContainer_VisitsNothing() { var container = new EntityContainer(); var visitor = new CapturingVisitor(); container.ForEach(visitor); CheckEqual(0, visitor.Translations.Count); } } public class ClearTests : Base { public void RemovesAllEntities() { var container = new EntityContainer(); container.AddEntity(new Ellipse(), At(1)); container.AddEntity(new Rectangle(), At(2)); container.Clear(); var visitor = new CapturingVisitor(); container.ForEach(visitor); CheckEqual(0, visitor.Translations.Count); } public void ThenAddAgain_Works() { var container = new EntityContainer(); container.AddEntity(new Ellipse(), At(1)); container.Clear(); var handle = container.AddEntity(new Ellipse(), At(4)); CheckDrawable(container, handle, typeof(Ellipse), Marker(4)); var visitor = new CapturingVisitor(); container.ForEach(visitor); CheckEqual(1, visitor.Translations.Count); } } } }