aboutsummaryrefslogtreecommitdiff
path: root/core/src/test/java/coffee
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/test/java/coffee')
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystemTest.java188
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/grid/system/GridIndexSystemTest.java70
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/grid/system/GridMovementSystemTest.java48
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystemTest.java46
-rw-r--r--core/src/test/java/coffee/liz/ecs/DAGWorldTest.java164
-rw-r--r--core/src/test/java/coffee/liz/ecs/model/EntityTest.java97
-rw-r--r--core/src/test/java/coffee/liz/lambda/eval/InterpreterTest.java103
-rw-r--r--core/src/test/java/coffee/liz/lambda/eval/ThunkTest.java38
-rw-r--r--core/src/test/java/coffee/liz/lambda/format/FormatterTest.java53
-rw-r--r--core/src/test/java/coffee/liz/lambda/parser/ParserTest.java178
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());
+ }
+ }
+ }
+}