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);
}
}
}
}