diff options
Diffstat (limited to 'core/src/test')
10 files changed, 985 insertions, 0 deletions
diff --git a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystemTest.java b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystemTest.java new file mode 100644 index 0000000..3743f3e --- /dev/null +++ b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystemTest.java @@ -0,0 +1,188 @@ +package coffee.liz.abstractionengine.grid.system; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import coffee.liz.abstractionengine.grid.component.GridCollidable; +import coffee.liz.abstractionengine.grid.component.GridCollidable.CollisionBehavior; +import coffee.liz.abstractionengine.grid.component.GridCollidable.CollisionBehavior.CollisionBehaviorType; +import coffee.liz.abstractionengine.grid.component.GridInputState; +import coffee.liz.abstractionengine.grid.component.GridMomentum; +import coffee.liz.abstractionengine.grid.component.GridPosition; +import coffee.liz.ecs.math.Vec2i; +import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.World; + +import lombok.Getter; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +final class GridCollisionPropagatationSystemTest { + private static final Duration FRAME = Duration.ZERO; + private static final GridCollidable WALL_COLLIDABLE = (me, them) -> CollisionBehavior.builder() + .collisionBehaviorType(CollisionBehaviorType.WALL).priority(10).build(); + private static final GridCollidable PROPAGATE_COLLIDABLE = (me, them) -> CollisionBehavior.builder() + .collisionBehaviorType(CollisionBehaviorType.PROPAGATE).priority(0).build(); + + @Getter + private static class SwallowCollidable implements GridCollidable { + private final Set<Entity> swallowed = new HashSet<>(); + @Override + public <T> void onSwallow(final Entity them, final World<T> world) { + swallowed.add(them); + } + + @Override + public CollisionBehavior getCollisionBehaviorBetween(final Entity me, final Entity them) { + return CollisionBehavior.builder().collisionBehaviorType(CollisionBehaviorType.SWALLOW).priority(0).build(); + } + } + + @Test + public void testPrioritization() { + final World<GridInputState> world = mockWorld(); + final GridIndexSystem indexSystem = mock(GridIndexSystem.class); + when(indexSystem.inBounds(any())).thenReturn(true); + when(world.getSystem(GridIndexSystem.class)).thenReturn(indexSystem); + + final Entity pusher = Entity.builder().id(1).build().add(PROPAGATE_COLLIDABLE).add(new GridMomentum(Vec2i.EAST)) + .add(new GridPosition(Vec2i.ZERO)); + final Entity toPropagate = Entity.builder().id(2).build().add(PROPAGATE_COLLIDABLE) + .add(new GridPosition(Vec2i.EAST)); + final Entity wall = Entity.builder().id(3).build().add(WALL_COLLIDABLE).add(new GridPosition(Vec2i.EAST)); + + // Propagation takes priority because priority(0) is lower + when(world.query(Set.of(GridMomentum.class, GridPosition.class, GridCollidable.class))) + .thenReturn(Set.of(pusher)); + + when(indexSystem.entitiesAt(Vec2i.ZERO)).thenReturn(List.of(pusher)); + when(indexSystem.entitiesAt(Vec2i.EAST)).thenReturn(List.of(toPropagate, wall)); + + final GridCollisionPropagatationSystem system = new GridCollisionPropagatationSystem(); + system.update(world, mock(GridInputState.class), FRAME); + + assertEquals(Vec2i.EAST, pusher.get(GridMomentum.class).getVelocity()); + assertEquals(Vec2i.EAST, toPropagate.get(GridMomentum.class).getVelocity()); + } + + @Test + public void testWallCollisionHaltsRayMomentum() { + final World<GridInputState> world = mockWorld(); + final GridIndexSystem indexSystem = mock(GridIndexSystem.class); + when(indexSystem.inBounds(any())).thenReturn(true); + when(world.getSystem(GridIndexSystem.class)).thenReturn(indexSystem); + + final Entity pusher = Entity.builder().id(1).build().add(PROPAGATE_COLLIDABLE).add(new GridMomentum(Vec2i.EAST)) + .add(new GridPosition(Vec2i.ZERO)); + final Entity toPropagate = Entity.builder().id(2).build().add(PROPAGATE_COLLIDABLE) + .add(new GridMomentum(Vec2i.EAST)).add(new GridPosition(Vec2i.EAST)); + final Entity wall = Entity.builder().id(3).build().add(WALL_COLLIDABLE) + .add(new GridPosition(Vec2i.EAST.scale(2, 0))); + + when(world.query(Set.of(GridMomentum.class, GridPosition.class, GridCollidable.class))) + .thenReturn(Set.of(pusher, toPropagate)); + + when(indexSystem.entitiesAt(Vec2i.ZERO)).thenReturn(List.of(pusher)); + when(indexSystem.entitiesAt(Vec2i.EAST)).thenReturn(List.of(toPropagate)); + when(indexSystem.entitiesAt(Vec2i.EAST.scale(2, 0))).thenReturn(List.of(wall)); + + final GridCollisionPropagatationSystem system = new GridCollisionPropagatationSystem(); + system.update(world, GridInputState.builder().build(), FRAME); + + assertEquals(Vec2i.ZERO, pusher.get(GridMomentum.class).getVelocity()); + assertEquals(Vec2i.ZERO, toPropagate.get(GridMomentum.class).getVelocity()); + } + + @Test + public void testGoingOutOfBoundsHaltsMomentum() { + final World<GridInputState> world = mockWorld(); + final GridIndexSystem indexSystem = mock(GridIndexSystem.class); + + when(indexSystem.inBounds(any())).thenReturn(false); + when(world.getSystem(GridIndexSystem.class)).thenReturn(indexSystem); + + final Entity pusher = Entity.builder().id(1).build().add(PROPAGATE_COLLIDABLE).add(new GridMomentum(Vec2i.EAST)) + .add(new GridPosition(Vec2i.ZERO)); + + when(world.query(Set.of(GridMomentum.class, GridPosition.class, GridCollidable.class))) + .thenReturn(Set.of(pusher)); + + when(indexSystem.entitiesAt(Vec2i.ZERO)).thenReturn(List.of(pusher)); + + final GridCollisionPropagatationSystem system = new GridCollisionPropagatationSystem(); + system.update(world, GridInputState.builder().build(), FRAME); + + assertEquals(Vec2i.ZERO, pusher.get(GridMomentum.class).getVelocity()); + } + + @Test + public void testZeroVelocity() { + final World<GridInputState> world = mockWorld(); + final GridIndexSystem indexSystem = mock(GridIndexSystem.class); + + when(indexSystem.inBounds(any())).thenReturn(true); + when(world.getSystem(GridIndexSystem.class)).thenReturn(indexSystem); + + final Entity pusher = Entity.builder().id(1).build().add(PROPAGATE_COLLIDABLE).add(new GridMomentum(Vec2i.ZERO)) + .add(new GridPosition(Vec2i.ZERO)); + + when(world.query(Set.of(GridMomentum.class, GridPosition.class, GridCollidable.class))) + .thenReturn(Set.of(pusher)); + + when(indexSystem.entitiesAt(Vec2i.ZERO)).thenReturn(List.of(pusher)); + + final GridCollisionPropagatationSystem system = new GridCollisionPropagatationSystem(); + system.update(world, GridInputState.builder().build(), FRAME); + + assertEquals(Vec2i.ZERO, pusher.get(GridMomentum.class).getVelocity()); + } + + @Test + public void testSwallowInteraction() { + final World<GridInputState> world = mockWorld(); + final GridIndexSystem indexSystem = mock(GridIndexSystem.class); + + when(indexSystem.inBounds(any())).thenReturn(true); + when(world.getSystem(GridIndexSystem.class)).thenReturn(indexSystem); + + final SwallowCollidable swallowCollidable = new SwallowCollidable(); + + final Entity pusher = Entity.builder().id(1).build().add(PROPAGATE_COLLIDABLE).add(new GridMomentum(Vec2i.EAST)) + .add(new GridPosition(Vec2i.ZERO)); + + final Entity swallower = Entity.builder().id(2).build().add(swallowCollidable) + .add(new GridPosition(Vec2i.EAST)); + + when(world.query(Set.of(GridMomentum.class, GridPosition.class, GridCollidable.class))) + .thenReturn(Set.of(pusher)); + + when(indexSystem.entitiesAt(Vec2i.ZERO)).thenReturn(List.of(pusher)); + when(indexSystem.entitiesAt(Vec2i.EAST)).thenReturn(List.of(swallower)); + + final GridCollisionPropagatationSystem system = new GridCollisionPropagatationSystem(); + system.update(world, GridInputState.builder().build(), FRAME); + + assertEquals(Vec2i.EAST, pusher.get(GridMomentum.class).getVelocity()); + assertFalse(swallower.has(GridMomentum.class)); + + assertEquals(swallowCollidable.getSwallowed(), Set.of(pusher)); + } + + @Test + public void testDependencies() { + assertEquals(Set.of(GridMovementSystem.class, GridIndexSystem.class), + new GridCollisionPropagatationSystem().getDependencies()); + } + + @SuppressWarnings("unchecked") + private static World<GridInputState> mockWorld() { + return (World<GridInputState>) mock(World.class); + } +} diff --git a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridIndexSystemTest.java b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridIndexSystemTest.java new file mode 100644 index 0000000..d9afde5 --- /dev/null +++ b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridIndexSystemTest.java @@ -0,0 +1,70 @@ +package coffee.liz.abstractionengine.grid.system; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +import coffee.liz.abstractionengine.grid.component.GridInputState; +import coffee.liz.abstractionengine.grid.component.GridPosition; +import coffee.liz.ecs.math.Vec2i; +import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.World; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.time.Duration; +import java.util.Set; + +class GridIndexSystemTest { + @Test + public void testUpdateIndexesEntitiesIntoGridSlots() { + final GridIndexSystem system = new GridIndexSystem(new Vec2i(4, 4)); + final World<GridInputState> world = mockWorld(); + final Entity alpha = Entity.builder().id(11).build(); + alpha.add(new GridPosition(new Vec2i(1, 2))); + final Entity beta = Entity.builder().id(12).build(); + beta.add(new GridPosition(new Vec2i(0, 0))); + when(world.query(Set.of(GridPosition.class))).thenReturn(Set.of(alpha, beta)); + + system.update(world, GridInputState.builder().movement(Vec2i.NORTH).build(), Duration.ZERO); + + assertTrue(system.entitiesAt(new Vec2i(1, 2)).contains(alpha)); + assertTrue(system.entitiesAt(new Vec2i(0, 0)).contains(beta)); + assertTrue(system.entitiesAt(new Vec2i(3, 3)).isEmpty()); + } + + @Test + public void testUpdateClearsPreviousIndexesBeforeRebuilding() { + final GridIndexSystem system = new GridIndexSystem(new Vec2i(2, 2)); + final World<GridInputState> world = mockWorld(); + final Entity moving = Entity.builder().id(77).build(); + moving.add(new GridPosition(Vec2i.ZERO)); + when(world.query(Set.of(GridPosition.class))).thenReturn(Set.of(moving)).thenReturn(Set.of()); + + system.update(world, GridInputState.builder().movement(Vec2i.EAST).build(), Duration.ZERO); + assertTrue(system.entitiesAt(Vec2i.ZERO).contains(moving)); + + system.update(world, GridInputState.builder().movement(Vec2i.NORTH).build(), Duration.ZERO); + assertTrue(system.entitiesAt(Vec2i.ZERO).isEmpty()); + } + + @Test + public void testEntitiesAtReturnsEmptySetForOutOfBoundsQuery() { + final GridIndexSystem system = new GridIndexSystem(new Vec2i(2, 2)); + + assertEquals(Set.of(), system.entitiesAt(new Vec2i(-1, 0))); + assertEquals(Set.of(), system.entitiesAt(new Vec2i(2, 1))); + assertEquals(Set.of(), system.entitiesAt(new Vec2i(1, 2))); + } + + @Test + public void testDependencies() { + assertEquals(Set.of(), new GridIndexSystem(Vec2i.ZERO).getDependencies()); + } + + @SuppressWarnings("unchecked") + private static World<GridInputState> mockWorld() { + return (World<GridInputState>) Mockito.mock(World.class); + } +} diff --git a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridMovementSystemTest.java b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridMovementSystemTest.java new file mode 100644 index 0000000..8bd8ff3 --- /dev/null +++ b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridMovementSystemTest.java @@ -0,0 +1,48 @@ +package coffee.liz.abstractionengine.grid.system; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import coffee.liz.abstractionengine.grid.component.GridControllable; +import coffee.liz.abstractionengine.grid.component.GridInputState; +import coffee.liz.abstractionengine.grid.component.GridMomentum; +import coffee.liz.abstractionengine.grid.component.GridPosition; +import coffee.liz.ecs.math.Vec2i; +import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.World; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.time.Duration; +import java.util.Set; + +final class GridMovementSystemTest { + @Test + public void testUpdateAssignsMomentumToControllableEntities() { + final World<GridInputState> world = mockWorld(); + final Entity subject = Entity.builder().id(1).build(); + subject.add(new GridControllable()); + subject.add(new GridPosition(Vec2i.ZERO)); + final Set<Entity> controllableEntities = Set.of(subject); + when(world.query(Set.of(GridControllable.class, GridPosition.class))).thenReturn(controllableEntities); + + final GridInputState inputState = GridInputState.builder().movement(Vec2i.SOUTH).build(); + final GridMovementSystem system = new GridMovementSystem(); + + system.update(world, inputState, Duration.ofMillis(16)); + + final GridMomentum appliedMomentum = subject.get(GridMomentum.class); + assertEquals(Vec2i.SOUTH, appliedMomentum.getVelocity()); + } + + @Test + public void testDependencies() { + assertEquals(Set.of(), new GridMovementSystem().getDependencies()); + } + + @SuppressWarnings("unchecked") + private static World<GridInputState> mockWorld() { + return (World<GridInputState>) Mockito.mock(World.class); + } +} diff --git a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystemTest.java b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystemTest.java new file mode 100644 index 0000000..c3bb01e --- /dev/null +++ b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystemTest.java @@ -0,0 +1,46 @@ +package coffee.liz.abstractionengine.grid.system; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import coffee.liz.abstractionengine.grid.component.GridInputState; +import coffee.liz.abstractionengine.grid.component.GridMomentum; +import coffee.liz.abstractionengine.grid.component.GridPosition; +import coffee.liz.ecs.math.Vec2; +import coffee.liz.ecs.math.Vec2i; +import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.World; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.time.Duration; +import java.util.Set; + +final class GridPhysicsSystemTest { + @Test + public void testUpdateMovesEntitiesByMomentumAndResetsVelocity() { + final World<GridInputState> world = mockWorld(); + final Entity body = Entity.builder().id(3).build(); + body.add(new GridPosition(Vec2i.ZERO)); + body.add(new GridMomentum(Vec2i.EAST)); + when(world.query(Set.of(GridMomentum.class, GridPosition.class))).thenReturn(Set.of(body)); + + final GridPhysicsSystem system = new GridPhysicsSystem(); + system.update(world, GridInputState.builder().movement(Vec2i.NORTH).build(), Duration.ZERO); + + final Vec2<Integer> newPosition = body.get(GridPosition.class).getPosition(); + assertEquals(Vec2i.EAST, newPosition); + assertEquals(Vec2i.ZERO, body.get(GridMomentum.class).getVelocity()); + } + + @Test + public void testDependencies() { + assertEquals(Set.of(GridCollisionPropagatationSystem.class), new GridPhysicsSystem().getDependencies()); + } + + @SuppressWarnings("unchecked") + private static World<GridInputState> mockWorld() { + return (World<GridInputState>) Mockito.mock(World.class); + } +} diff --git a/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java b/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java new file mode 100644 index 0000000..2f948d0 --- /dev/null +++ b/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java @@ -0,0 +1,164 @@ +package coffee.liz.ecs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import coffee.liz.ecs.model.Component; +import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; + +import lombok.RequiredArgsConstructor; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +final class DAGWorldTest { + @Test + public void queryReturnsEntitiesMatchingAllRequestedComponents() { + final DAGWorld<String> world = new DAGWorld<>(Map.of()); + final Entity matching = world.createEntity(); + matching.add(new PositionComponent()); + matching.add(new VelocityComponent()); + final Entity partial = world.createEntity(); + partial.add(new PositionComponent()); + final Entity nonMatching = world.createEntity(); + nonMatching.add(new VelocityComponent()); + + world.update("state", Duration.ZERO); + + final Set<Entity> results = world.query(Set.of(PositionComponent.class, VelocityComponent.class)); + + assertEquals(Set.of(matching), results); + } + + @Test + public void queryWithoutComponentsReturnsAllEntities() { + final DAGWorld<String> world = new DAGWorld<>(Map.of()); + final Entity entityOne = world.createEntity(); + final Entity entityTwo = world.createEntity(); + + final Set<Entity> results = world.query(List.<Class<? extends Component>>of()); + + assertEquals(Set.of(entityOne, entityTwo), results); + } + + @Test + public void updateExecutesSystemsInTopologicalOrder() { + final CopyOnWriteArrayList<String> executionLog = new CopyOnWriteArrayList<>(); + + final DAGWorld<String> world = new DAGWorld<>(Map.of(SystemC.class, new SystemC(executionLog), SystemA.class, + new SystemA(executionLog), SystemB.class, new SystemB(executionLog))); + world.update("state", Duration.ZERO); + + assertEquals(List.of("A", "B", "C"), executionLog); + } + + @Test + public void updateRefreshesComponentCacheAfterEntityMutations() { + final DAGWorld<String> world = new DAGWorld<>(Map.of()); + final Entity subject = world.createEntity(); + + world.update("state", Duration.ZERO); + assertTrue(world.query(Set.of(PositionComponent.class)).isEmpty()); + + subject.add(new PositionComponent()); + world.update("state", Duration.ZERO); + assertEquals(1, world.query(Set.of(PositionComponent.class)).size()); + + subject.remove(PositionComponent.class); + world.update("state", Duration.ZERO); + assertTrue(world.query(Set.of(PositionComponent.class)).isEmpty()); + } + + @Test + public void circularDependencyDetectionThrowsIllegalStateException() { + final Map<Class<? extends System<String>>, System<String>> systems = new LinkedHashMap<>(); + final SystemCycleA systemA = new SystemCycleA(); + final SystemCycleB systemB = new SystemCycleB(); + systems.put(SystemCycleA.class, systemA); + systems.put(SystemCycleB.class, systemB); + + assertThrows(IllegalStateException.class, () -> new DAGWorld<>(systems)); + } + + private static final class PositionComponent implements Component { + } + + private static final class VelocityComponent implements Component { + } + + @RequiredArgsConstructor + private abstract static class RecordingSystem implements System<String> { + private final List<String> log; + private final String label; + + @Override + public final void update(final World<String> world, final String state, final Duration duration) { + log.add(label); + } + } + + private static final class SystemA extends RecordingSystem { + private SystemA(final List<String> log) { + super(log, "A"); + } + + @Override + public Collection<Class<? extends System<String>>> getDependencies() { + return List.of(); + } + } + + private static final class SystemB extends RecordingSystem { + private SystemB(final List<String> log) { + super(log, "B"); + } + + @Override + public Collection<Class<? extends System<String>>> getDependencies() { + return Set.of(SystemA.class); + } + } + + private static final class SystemC extends RecordingSystem { + private SystemC(final List<String> log) { + super(log, "C"); + } + + @Override + public Collection<Class<? extends System<String>>> getDependencies() { + return Set.of(SystemB.class); + } + } + + private static final class SystemCycleA implements System<String> { + @Override + public Collection<Class<? extends System<String>>> getDependencies() { + return Set.of(SystemCycleB.class); + } + + @Override + public void update(final World<String> world, final String state, final Duration duration) { + } + } + + private static final class SystemCycleB implements System<String> { + @Override + public Collection<Class<? extends System<String>>> getDependencies() { + return Set.of(SystemCycleA.class); + } + + @Override + public void update(final World<String> world, final String state, final Duration duration) { + } + } +} diff --git a/core/src/test/java/coffee/liz/ecs/model/EntityTest.java b/core/src/test/java/coffee/liz/ecs/model/EntityTest.java new file mode 100644 index 0000000..c9ce59a --- /dev/null +++ b/core/src/test/java/coffee/liz/ecs/model/EntityTest.java @@ -0,0 +1,97 @@ +package coffee.liz.ecs.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import lombok.RequiredArgsConstructor; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +final class EntityTest { + @ParameterizedTest + @MethodSource("componentCombinationProvider") + public void hasAllReportsPresenceForComponentSets(final Collection<Class<? extends Component>> query, + final boolean expected) { + final Entity entity = Entity.builder().id(7).build(); + entity.add(new AlphaComponent("first")); + entity.add(new BetaComponent(3)); + entity.add(new GammaKeyedComponent()); + + assertEquals(expected, entity.hasAll(query)); + } + + private static Stream<Arguments> componentCombinationProvider() { + return Stream + .of(Arguments.of(List.of(AlphaComponent.class), true), Arguments.of(List.of(BetaComponent.class), true), + Arguments.of(List.of(AlphaComponent.class, BetaComponent.class, GammaComponent.class), true), + Arguments.of(List.of(AlphaComponent.class, BetaComponent.class, GammaKeyedComponent.class), + false), + Arguments.of(List.of(GammaComponent.class), true), + Arguments.of(List.of(GammaKeyedComponent.class), false)); + } + + @Test + public void getThrowsForMissingComponentsWithHelpfulMessage() { + final Entity entity = Entity.builder().id(99).build(); + + final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> entity.get(AlphaComponent.class)); + + assertTrue(thrown.getMessage().contains("AlphaComponent")); + assertTrue(thrown.getMessage().contains("99")); + } + + @Test + public void addReplacesExistingComponentInstance() { + final Entity entity = Entity.builder().id(17).build(); + final AlphaComponent initial = new AlphaComponent("initial"); + entity.add(initial); + + final AlphaComponent replacement = new AlphaComponent("replacement"); + entity.add(replacement); + + assertSame(replacement, entity.get(AlphaComponent.class)); + } + + @Test + public void removeClearsComponentPresence() { + final Entity entity = Entity.builder().id(45).build(); + entity.add(new BetaComponent(2)); + assertTrue(entity.has(BetaComponent.class)); + + entity.remove(BetaComponent.class); + + assertFalse(entity.has(BetaComponent.class)); + assertTrue(entity.componentTypes().isEmpty()); + } + + private record AlphaComponent(String name) implements Component { + } + + private record BetaComponent(int level) implements Component { + } + + @RequiredArgsConstructor + private class GammaComponent implements Component { + @Override + public Class<? extends Component> getKey() { + return GammaComponent.class; + } + } + + private class GammaKeyedComponent extends GammaComponent { + } + + private record ContextualComponent(int ownerId) implements Component { + } +} diff --git a/core/src/test/java/coffee/liz/lambda/eval/InterpreterTest.java b/core/src/test/java/coffee/liz/lambda/eval/InterpreterTest.java new file mode 100644 index 0000000..4b63782 --- /dev/null +++ b/core/src/test/java/coffee/liz/lambda/eval/InterpreterTest.java @@ -0,0 +1,103 @@ +package coffee.liz.lambda.eval; + +import coffee.liz.lambda.LambdaDriver; +import coffee.liz.lambda.ast.Expression.IdentifierExpression; +import coffee.liz.lambda.ast.LambdaProgram; +import coffee.liz.lambda.ast.SourceCode; +import coffee.liz.lambda.bind.Tick; +import coffee.liz.lambda.bind.ToChurch; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class InterpreterTest { + + @Test + public void identity() { + final Value result = LambdaDriver.interpret(SourceCode.ofArrow("x -> x")); + + final Value.Closure closure = assertInstanceOf(Value.Closure.class, result); + assertEquals("x", closure.parameter()); + assertInstanceOf(IdentifierExpression.class, closure.body()); + } + + @Test + public void identityApplication() { + final Value result = LambdaDriver.interpret(SourceCode.ofArrow("(x -> x)(y)")); + + assertEquals(new Value.Free("y"), result); + } + + @Test + public void nestedApplication() { + final Value result = LambdaDriver.interpret(SourceCode.ofArrow("(f -> g -> x -> f(g)(x))(x -> x)(y -> y)(x)")); + + assertEquals(new Value.Free("x"), result); + } + + @Test + public void cons() { + final Value result = LambdaDriver.interpret(SourceCode.ofArrow(""" + let pair = a -> b -> f -> f(a)(b); + let second = x -> y -> y; + pair(x)(y)(second) + """)); + + assertEquals(new Value.Free("y"), result); + } + + @Test + public void fibonacci() { + final String source = """ + let true = t -> f -> t; + let false = t -> f -> f; + + let pair = x -> y -> f -> f(x)(y); + let fst = p -> p(x -> y -> x); + let snd = p -> p(x -> y -> y); + + let 0 = f -> x -> x; + let 1 = f -> x -> f(x); + + let succ = n -> f -> x -> f(n(f)(x)); + let plus = m -> n -> f -> x -> m(f)(n(f)(x)); + let next = p -> pair(snd(p))(succ(snd(p))); + let pred = n -> fst(n(next)(pair(0)(0))); + + let iszero = n -> n(x -> false)(true); + let isone = n -> iszero(pred(n)); + + let y = f -> (x -> f(x(x)))(x -> f(x(x))); + + let fib = y(fib -> n -> + iszero(n) (0) + (isone(n) (1) + (plus + (fib(pred(n))) + (fib(pred(pred(n))))))); + + fib(ToChurch(13))(Tick)(dummy) + """; + + final Tick ticker = new Tick(); + final Value result = LambdaDriver.interpret(SourceCode.ofArrow(source), List.of(ticker, new ToChurch())); + + assertEquals(new Value.Free("dummy"), result); + assertEquals(233, ticker.getCounter().get()); + } + + @Test + public void omegaCombinatorThrowsDepthExceeded() { + final LambdaProgram program = LambdaDriver.parse(SourceCode.ofArrow("(x -> x(x))(x -> x(x))")); + final Environment env = Environment.from(program.macros(), List.of()); + + final EvaluationDepthExceededException exception = assertThrows(EvaluationDepthExceededException.class, + () -> NormalOrderEvaluator.evaluate(program.expression(), env, 100)); + + assertEquals(100, exception.getMaxDepth()); + } +} diff --git a/core/src/test/java/coffee/liz/lambda/eval/ThunkTest.java b/core/src/test/java/coffee/liz/lambda/eval/ThunkTest.java new file mode 100644 index 0000000..2a1d5e3 --- /dev/null +++ b/core/src/test/java/coffee/liz/lambda/eval/ThunkTest.java @@ -0,0 +1,38 @@ +package coffee.liz.lambda.eval; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +public class ThunkTest { + @Test + public void testThunkNonNull() { + final AtomicInteger invok = new AtomicInteger(0); + final Supplier<Integer> i = () -> { + invok.incrementAndGet(); + return invok.get(); + }; + final Thunk<Integer> thunk = new Thunk<>(i); + Assertions.assertEquals(1, thunk.get()); + Assertions.assertEquals(1, thunk.get()); + Assertions.assertEquals(1, thunk.get()); + Assertions.assertEquals(1, invok.get()); + } + + @Test + public void testThunkNull() { + final AtomicInteger invok = new AtomicInteger(0); + final Supplier<Integer> i = () -> { + invok.incrementAndGet(); + return null; + }; + final Thunk<Integer> thunk = new Thunk<>(i); + Assertions.assertNull(thunk.get()); + Assertions.assertNull(thunk.get()); + Assertions.assertNull(thunk.get()); + Assertions.assertEquals(1, invok.get()); + } +} diff --git a/core/src/test/java/coffee/liz/lambda/format/FormatterTest.java b/core/src/test/java/coffee/liz/lambda/format/FormatterTest.java new file mode 100644 index 0000000..111855f --- /dev/null +++ b/core/src/test/java/coffee/liz/lambda/format/FormatterTest.java @@ -0,0 +1,53 @@ +package coffee.liz.lambda.format; + +import coffee.liz.lambda.LambdaDriver; +import coffee.liz.lambda.ast.SourceCode; +import coffee.liz.lambda.ast.SourceCode.Syntax; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class FormatterTest { + @ParameterizedTest + @MethodSource("provideRoundTrip") + public void testRoundTrip(final String lambda, final String arrow) { + final String formattedLambda = format(SourceCode.ofArrow(arrow), Syntax.LAMBDA); + final String formattedArrow = format(SourceCode.ofLambda(lambda), Syntax.ARROW); + assertEquals(lambda, formattedLambda); + assertEquals(arrow, formattedArrow); + } + + public static Stream<Arguments> provideRoundTrip() { + return Stream.of(Arguments.of("λx.λy.x", "x -> y -> x"), Arguments.of("(λx.x) y", "(x -> x)(y)"), + Arguments.of("f x y z", "f(x)(y)(z)"), Arguments.of("f x y z -- Comment!", "f(x)(y)(z) -- Comment!"), + Arguments.of(""" + let id = λx.x; + let const = λx.λy.x; -- Inline comment! + + -- Test comment + -- Another comment + id""", """ + let id = x -> x; + let const = x -> y -> x; -- Inline comment! + + -- Test comment + -- Another comment + id"""), Arguments.of(""" + -- The identity function + let id = λx.x; + + id""", """ + -- The identity function + let id = x -> x; + + id""")); + } + + private static String format(final SourceCode code, final Syntax syntax) { + return Formatter.emit(LambdaDriver.parse(code), syntax); + } +} diff --git a/core/src/test/java/coffee/liz/lambda/parser/ParserTest.java b/core/src/test/java/coffee/liz/lambda/parser/ParserTest.java new file mode 100644 index 0000000..0158003 --- /dev/null +++ b/core/src/test/java/coffee/liz/lambda/parser/ParserTest.java @@ -0,0 +1,178 @@ +package coffee.liz.lambda.parser; + +import coffee.liz.lambda.LambdaDriver; +import coffee.liz.lambda.ast.Expression; +import coffee.liz.lambda.ast.Expression.AbstractionExpression; +import coffee.liz.lambda.ast.Expression.ApplicationExpression; +import coffee.liz.lambda.ast.Expression.IdentifierExpression; +import coffee.liz.lambda.ast.LambdaProgram; +import coffee.liz.lambda.ast.Macro; +import coffee.liz.lambda.ast.SourceCode; +import coffee.liz.lambda.ast.SourceComment; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +class ParserTest { + + @Test + public void testTrivial() { + final LambdaProgram lambda = LambdaDriver.parse(SourceCode.ofLambda("λx.x")); + final LambdaProgram arrow = LambdaDriver.parse(SourceCode.ofArrow("x -> x")); + + assertStructurallyEqual(lambda, arrow); + assertEquals(0, lambda.macros().size()); + assertInstanceOf(AbstractionExpression.class, lambda.expression()); + assertEquals("x", ((AbstractionExpression) lambda.expression()).parameter()); + } + + @Test + public void testApplication() { + final LambdaProgram lambda = LambdaDriver.parse(SourceCode.ofLambda("(λx.x) y")); + final LambdaProgram arrow = LambdaDriver.parse(SourceCode.ofArrow("(x -> x)(y)")); + + assertStructurallyEqual(lambda, arrow); + + final ApplicationExpression app = (ApplicationExpression) lambda.expression(); + assertInstanceOf(AbstractionExpression.class, app.applicable()); + assertInstanceOf(IdentifierExpression.class, app.argument()); + } + + @Test + public void testChainedLambdas() { + final LambdaProgram lambda = LambdaDriver.parse(SourceCode.ofLambda("λx.λy.λz.x")); + final LambdaProgram arrow = LambdaDriver.parse(SourceCode.ofArrow("x -> y -> z -> x")); + + assertStructurallyEqual(lambda, arrow); + + final AbstractionExpression outer = (AbstractionExpression) lambda.expression(); + assertEquals("x", outer.parameter()); + final AbstractionExpression middle = (AbstractionExpression) outer.body(); + assertEquals("y", middle.parameter()); + final AbstractionExpression inner = (AbstractionExpression) middle.body(); + assertEquals("z", inner.parameter()); + } + + @Test + public void testChainedApplication() { + final LambdaProgram lambda = LambdaDriver.parse(SourceCode.ofLambda("((f x) y) z")); + final LambdaProgram arrow = LambdaDriver.parse(SourceCode.ofArrow("f(x)(y)(z)")); + + assertStructurallyEqual(lambda, arrow); + + final ApplicationExpression app1 = (ApplicationExpression) lambda.expression(); + assertEquals("z", ((IdentifierExpression) app1.argument()).name()); + final ApplicationExpression app2 = (ApplicationExpression) app1.applicable(); + assertEquals("y", ((IdentifierExpression) app2.argument()).name()); + final ApplicationExpression app3 = (ApplicationExpression) app2.applicable(); + assertEquals("f", ((IdentifierExpression) app3.applicable()).name()); + assertEquals("x", ((IdentifierExpression) app3.argument()).name()); + } + + @Test + public void testMacros() { + final LambdaProgram lambda = LambdaDriver.parse(SourceCode.ofLambda(""" + let id = λx.x; + id + """)); + final LambdaProgram arrow = LambdaDriver.parse(SourceCode.ofArrow(""" + let id = x -> x; + id + """)); + + assertStructurallyEqual(lambda, arrow); + assertEquals(1, lambda.macros().size()); + assertEquals("id", lambda.macros().getFirst().name()); + assertInstanceOf(IdentifierExpression.class, lambda.expression()); + } + + @Test + public void testLineComments() { + final LambdaProgram lambda = LambdaDriver.parse(SourceCode.ofLambda(""" + -- The identity function + let id = λx.x; -- returns its argument + id -- use it + """)); + final LambdaProgram arrow = LambdaDriver.parse(SourceCode.ofArrow(""" + -- The identity function + let id = x -> x; -- returns its argument + id -- use it + """)); + + assertStructurallyEqual(lambda, arrow); + assertEquals(1, lambda.macros().size()); + assertEquals("id", lambda.macros().getFirst().name()); + } + + @Test + public void testComplexProgram() { + final LambdaProgram lambda = LambdaDriver.parse(SourceCode.ofLambda(""" + let zero = λf.λx.x; + let one = λf.λx.f x; + let succ = λn.λf.λx.f (n f x); + let add = λm.λn.λf.λx.m f (n f x); + + succ (add one zero) + """)); + + final LambdaProgram arrow = LambdaDriver.parse(SourceCode.ofArrow(""" + let zero = f -> x -> x; + let one = f -> x -> f(x); + let succ = n -> f -> x -> f(n(f)(x)); + let add = m -> n -> f -> x -> m(f)(n(f)(x)); + + succ(add(one)(zero)) + """)); + + assertStructurallyEqual(lambda, arrow); + + assertEquals(4, lambda.macros().size()); + assertEquals("zero", lambda.macros().get(0).name()); + assertEquals("one", lambda.macros().get(1).name()); + assertEquals("succ", lambda.macros().get(2).name()); + assertEquals("add", lambda.macros().get(3).name()); + } + + @Test + public void testOmegaCombinator() { + final LambdaProgram lambda = LambdaDriver.parse(SourceCode.ofLambda("(λx.x x)(λx.x x)")); + final LambdaProgram arrow = LambdaDriver.parse(SourceCode.ofArrow("(x -> x(x))(x -> x(x))")); + + assertStructurallyEqual(lambda, arrow); + } + + private static void assertStructurallyEqual(final LambdaProgram expected, final LambdaProgram actual) { + assertEquals(expected.macros().size(), actual.macros().size(), "Macro count mismatch"); + for (int i = 0; i < expected.macros().size(); i++) { + assertStructurallyEqual(expected.macros().get(i), actual.macros().get(i)); + } + assertStructurallyEqual(expected.expression(), actual.expression()); + } + + private static void assertStructurallyEqual(final Macro expected, final Macro actual) { + assertEquals(expected.name(), actual.name(), "Macro name mismatch"); + assertEquals(expected.comment().map(SourceComment::text), actual.comment().map(SourceComment::text), + "Macro comment mismatch"); + assertStructurallyEqual(expected.expression(), actual.expression()); + } + + private static void assertStructurallyEqual(final Expression expected, final Expression actual) { + assertEquals(expected.getClass(), actual.getClass(), "Expression type mismatch"); + assertEquals(expected.comment().map(SourceComment::text), actual.comment().map(SourceComment::text), + "Expression comment mismatch"); + + switch (expected) { + case IdentifierExpression e -> + assertEquals(e.name(), ((IdentifierExpression) actual).name(), "Identifier name mismatch"); + case AbstractionExpression e -> { + assertEquals(e.parameter(), ((AbstractionExpression) actual).parameter(), "Parameter mismatch"); + assertStructurallyEqual(e.body(), ((AbstractionExpression) actual).body()); + } + case ApplicationExpression e -> { + assertStructurallyEqual(e.applicable(), ((ApplicationExpression) actual).applicable()); + assertStructurallyEqual(e.argument(), ((ApplicationExpression) actual).argument()); + } + } + } +} |
