From 14c9f79130c80b3bb06c15af5438eeb55c2a0164 Mon Sep 17 00:00:00 2001 From: Reed von Redwitz Date: Mon, 11 May 2026 10:50:47 +0200 Subject: [PATCH] chore(base-query, base-mereology): close remaining test gaps --- .../build/base/mereology/CompositeTests.java | 17 ++ .../base/query/IndexCompatibilityTests.java | 172 +++++++++++++++++- 2 files changed, 185 insertions(+), 4 deletions(-) diff --git a/base-mereology/src/test/java/build/base/mereology/CompositeTests.java b/base-mereology/src/test/java/build/base/mereology/CompositeTests.java index fc4c687..279a60f 100644 --- a/base-mereology/src/test/java/build/base/mereology/CompositeTests.java +++ b/base-mereology/src/test/java/build/base/mereology/CompositeTests.java @@ -842,6 +842,23 @@ void shouldDetermineAtomStructurally() { assertThat(Entity.boundary(Composite.empty()).isAtom()).isTrue(); } + /** + * Ensure a {@link Composite} reachable via two paths in a diamond DAG is visited only once. + */ + @Test + void shouldVisitSharedCompositeOnceInDiamondDag() { + final var shared = Composites.of("deep"); + final var left = Composites.of(shared); + final var right = Composites.of(shared); + final var root = Composites.of(left, right); + + assertThat(root.composition(Composite.class)) + .containsExactlyInAnyOrder(left, right, shared, shared); + + assertThat(root.composition(String.class)) + .containsExactly("deep"); + } + /** * Ensure {@link Composites#verify} returns an empty list when every reachable {@link Composite} has parts. */ diff --git a/base-query/src/test/java/build/base/query/IndexCompatibilityTests.java b/base-query/src/test/java/build/base/query/IndexCompatibilityTests.java index c818010..9daf849 100644 --- a/base-query/src/test/java/build/base/query/IndexCompatibilityTests.java +++ b/base-query/src/test/java/build/base/query/IndexCompatibilityTests.java @@ -3,7 +3,10 @@ import org.junit.jupiter.api.Test; import java.lang.reflect.Field; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; import java.util.function.Function; +import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -160,7 +163,7 @@ default void shouldIndexAndUnindexSpecificObjects() { .findFirst()) .isEmpty(); } - + /** * Ensure an object can be unindexed even when its state has changed since being indexed. */ @@ -372,11 +375,172 @@ default void shouldFindSubclassInstancesWithIndexedScopeWhenQueryingBySupertype( /** * A simple marker interface for supertype-query tests. */ - interface Shape {} + interface Shape { + } + + record Circle(String color) implements Shape { + } + + record Square(String color) implements Shape { + } + + /** + * Ensure {@link Terminal#findAll()} returns all objects that share the same indexed value. + */ + @Test + default void shouldReturnAllObjectsWithSameIndexedValue() { + final var index = createIndex(); + final var red1 = new MutableColorful(Color.RED); + final var red2 = new MutableColorful(Color.RED); + + index.index(red1); + index.index(red2); + + assertThat(index.match(MutableColorful.class) + .where(MutableColorful.COLOR) + .isEqualTo(Color.RED) + .findAll() + .toList() + ).containsExactlyInAnyOrder(red1, red2); + } + + /** + * Ensure {@link Condition#matches(java.util.function.Predicate)} works on the indexed path when the extracted + * value is {@code null} — indexed path. + */ + @Test + default void shouldMatchObjectWithNullValueUsingPredicateOnIndexedPath() { + final var index = createIndex(); + final var colorful = new NullColorful(); + index.index(colorful); + + assertThat(index.match(NullColorful.class) + .where(NullColorful.COLOR) + .matches(c -> c == null) + .findFirst() + ).contains(colorful); + assertThat(index.match(NullColorful.class) + .where(NullColorful.COLOR) + .matches(c -> c != null) + .findFirst() + ).isEmpty(); + } + + /** + * Ensure {@link Condition#matches(java.util.function.Predicate)} works on the fallback path when the extracted + * value is {@code null} — fallback path. + */ + @Test + default void shouldMatchObjectWithNullValueUsingPredicateOnFallbackPath() { + final var index = createIndex(); + final var colorful = new NullFallbackColorful(); + index.add(NullFallbackColorful.class, colorful); + + assertThat(index.match(NullFallbackColorful.class) + .where(NullFallbackColorful.COLOR) + .matches(c -> c == null) + .findFirst() + ).contains(colorful); + assertThat(index.match(NullFallbackColorful.class) + .where(NullFallbackColorful.COLOR) + .matches(c -> c != null) + .findFirst() + ).isEmpty(); + } + + /** + * Ensure {@link Condition#isNotEqualTo} returns all objects not matching the specified value. + */ + @Test + default void shouldReturnObjectsNotEqualToValue() { + final var index = createIndex(); + index.index(Color.RED); + index.index(Color.GREEN); + index.index(Color.BLUE); - record Circle(String color) implements Shape {} + assertThat(index.match(Color.class) + .where(Color.NAME) + .isNotEqualTo("RED") + .findAll() + .toList() + ).containsExactlyInAnyOrder(Color.GREEN, Color.BLUE); + } + + /** + * Ensure {@link Condition#isNotEqualTo(Object) isNotEqualTo(null)} excludes objects whose indexed value is + * {@code null} and includes those with a non-{@code null} value (T4, null case). + */ + @Test + default void shouldReturnObjectsNotEqualToNull() { + final var index = createIndex(); + final var withNull = new NullColorful(); + index.index(withNull); + + assertThat(index.match(NullColorful.class) + .where(NullColorful.COLOR) + .isNotEqualTo(null) + .findFirst() + ).isEmpty(); + } - record Square(String color) implements Shape {} + /** + * Ensure concurrent {@link Index#index} and {@link Index#unindex} calls do not throw or corrupt state. + */ + @Test + default void shouldHandleConcurrentIndexAndUnindex() throws InterruptedException { + final var index = createIndex(); + final var latch = new CountDownLatch(1); + final var errors = new CopyOnWriteArrayList(); + + final Runnable task = () -> { + try { + latch.await(); + for (int i = 0; i < 50; i++) { + final var colorful = new MutableColorful(Color.RED); + index.index(colorful); + index.unindex(colorful); + } + } catch (final Throwable t) { + errors.add(t); + } + }; + + final var threads = IntStream.range(0, 8) + .mapToObj(_ -> new Thread(task)) + .toList(); + + threads.forEach(Thread::start); + latch.countDown(); + for (final var thread : threads) { + thread.join(); + } + + assertThat(errors).isEmpty(); + } + + /** + * Ensure the index does not retain stale entries after many index/unindex cycles. + */ + @Test + default void shouldCleanUpEntriesAfterManyIndexUnindexCycles() { + final var index = createIndex(); + + for (int i = 0; i < 100; i++) { + final var colorful = new MutableColorful(Color.RED); + index.index(colorful); + index.unindex(colorful); + } + + assertThat(index.match(MutableColorful.class) + .findAll() + .toList() + ).isEmpty(); + assertThat(index.match(MutableColorful.class) + .where(MutableColorful.COLOR) + .isEqualTo(Color.RED) + .findFirst() + ).isEmpty(); + } /** * Ensure a non-{@link Indexable} {@link Object}, which throws an {@link Exception} when indexed, can't be indexed.