From a0d90a2313fef40cac034de34e3d0283ab13a049 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 17 Jul 2026 19:11:50 -0400 Subject: [PATCH 01/18] feat: add plumbing for indictment support --- .../core/impl/bavet/bi/PrecomputeBiNode.java | 4 +- .../impl/bavet/common/AbstractConcatNode.java | 2 + .../bavet/common/AbstractFlattenNode.java | 1 + .../impl/bavet/common/AbstractGroupNode.java | 2 + .../bavet/common/AbstractIfExistsNode.java | 17 +++- .../common/AbstractIndexedIfExistsNode.java | 10 +- .../impl/bavet/common/AbstractJoinNode.java | 2 + .../impl/bavet/common/AbstractMapNode.java | 1 + .../common/AbstractUnindexedIfExistsNode.java | 9 +- .../core/impl/bavet/common/ExistsCounter.java | 15 ++- .../core/impl/bavet/common/tuple/Tuple.java | 21 +++++ .../bavet/common/tuple/UniversalTuple.java | 21 +++++ .../tuple/indictment/IndictmentSource.java | 93 +++++++++++++++++++ .../impl/bavet/quad/PrecomputeQuadNode.java | 4 +- .../impl/bavet/tri/PrecomputeTriNode.java | 4 +- .../bavet/uni/AbstractForEachUniNode.java | 8 +- .../bavet/uni/ForEachFilteredUniNode.java | 4 +- .../bavet/uni/ForEachUnfilteredUniNode.java | 4 +- .../impl/bavet/uni/PrecomputeUniNode.java | 4 +- .../uni/AbstractForEachEnumeratingStream.java | 2 +- .../score/constraint/ConstraintMatch.java | 11 ++- .../constraint/ConstraintMatchPolicy.java | 19 ++-- .../constraint/ConstraintMatchTotal.java | 21 ++++- .../score/director/InnerScoreDirector.java | 2 +- .../bavet/BavetConstraintSessionFactory.java | 10 +- .../bi/BavetPrecomputeBiConstraintStream.java | 3 +- .../bi/BavetScoringBiConstraintStream.java | 5 +- .../bavet/bi/BiBigDecimalImpactHandler.java | 2 +- .../stream/bavet/bi/BiLongImpactHandler.java | 2 +- .../common/BavetPrecomputeBuildHelper.java | 3 +- .../common/ConstraintNodeBuildHelper.java | 6 ++ .../BavetPrecomputeQuadConstraintStream.java | 3 +- .../BavetScoringQuadConstraintStream.java | 5 +- .../quad/QuadBigDecimalImpactHandler.java | 3 +- .../bavet/quad/QuadLongImpactHandler.java | 3 +- .../BavetPrecomputeTriConstraintStream.java | 3 +- .../tri/BavetScoringTriConstraintStream.java | 5 +- .../bavet/tri/TriBigDecimalImpactHandler.java | 3 +- .../bavet/tri/TriLongImpactHandler.java | 3 +- .../uni/BavetForEachUniConstraintStream.java | 5 +- .../BavetPrecomputeUniConstraintStream.java | 3 +- .../uni/BavetScoringUniConstraintStream.java | 5 +- .../bavet/uni/UniBigDecimalImpactHandler.java | 2 +- .../bavet/uni/UniLongImpactHandler.java | 2 +- .../inliner/ConstraintMatchSupplier.java | 52 +++++++---- .../bavet/uni/ForEachUniNodeActivityTest.java | 12 +-- .../score/constraint/ConstraintMatchTest.java | 3 + .../common/ConstraintStreamTestExtension.java | 2 +- 48 files changed, 345 insertions(+), 81 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java index ee4f3575c31..6afb77873a3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java @@ -23,6 +23,8 @@ public PrecomputeBiNode(Supplier>> prec @Override protected BiTuple remapTuple(BiTuple tuple) { - return BiTuple.of(tuple.getA(), tuple.getB(), outputStoreSize); + var out = BiTuple.of(tuple.getA(), tuple.getB(), outputStoreSize); + out.setIndictmentSource(tuple.getIndictmentSource()); + return out; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractConcatNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractConcatNode.java index 31946fe0b50..a2c2028ee43 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractConcatNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractConcatNode.java @@ -66,6 +66,7 @@ protected boolean canProduceTuples() { @Override public final void insertLeft(LeftTuple_ tuple) { var outTuple = getOutTupleFromLeft(tuple); + outTuple.setIndictmentSource(tuple.getIndictmentSource()); tuple.setStore(leftSourceTupleCloneStoreIndex, outTuple); propagationQueue.insert(outTuple); } @@ -109,6 +110,7 @@ public final void retractLeft(LeftTuple_ tuple) { @Override public final void insertRight(RightTuple_ tuple) { var outTuple = getOutTupleFromRight(tuple); + outTuple.setIndictmentSource(tuple.getIndictmentSource()); tuple.setStore(rightSourceTupleCloneStoreIndex, outTuple); propagationQueue.insert(outTuple); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java index b20fe59e1c2..16fdff5bc58 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java @@ -73,6 +73,7 @@ private void addTuple(InTuple_ originalTuple, FlattenedItem_ item, var reuse = bag.reuseOrAdvance(); if (reuse == null) { var created = createTuple(originalTuple, bag.value); + created.setIndictmentSource(originalTuple.getIndictmentSource()); bag.append(created); propagationQueue.insert(created); } else { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java index 87c6ecc0e5e..a6c830fc9f0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; public abstract class AbstractGroupNode extends AbstractSingleInputNode { @@ -110,6 +111,7 @@ private void createTuple(InTuple_ tuple, GroupKey_ userSuppliedKey) { } tuple.setStore(groupStoreIndex, group); var outTuple = group.getTuple(); + outTuple.setIndictmentSource(IndictmentSource.aggregating(tuple, outTuple)); switch (outTuple.getState()) { case CREATING, UPDATING -> { // Already in the correct state. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index cb57dd83d0b..0bf612b8423 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -104,7 +105,7 @@ protected void killCounterLeft(ExistsCounter counter) { } } - protected void incrementCounterRight(ExistsCounter counter) { + protected void incrementCounterRightWithoutIndictment(ExistsCounter counter) { if (counter.countRight == 0) { if (shouldExist) { doInsertCounter(counter); @@ -115,6 +116,18 @@ protected void incrementCounterRight(ExistsCounter counter) { counter.countRight++; } + protected void incrementCounterRightUpdatingIndictment(ExistsCounter counter, UniTuple rightTuple) { + if (counter.countRight == 0) { + if (shouldExist) { + doInsertCounter(counter); + } else { + doRetractCounter(counter); + } + } // Else do not even propagate an update + counter.outTuple.setIndictmentSource(IndictmentSource.sourceWithSupport(counter.outTuple, rightTuple)); + counter.countRight++; + } + protected void decrementCounterRight(ExistsCounter counter) { counter.countRight--; if (counter.countRight == 0) { @@ -250,7 +263,7 @@ protected void updateCounterRight(ExistsCounter counter, UniTuple(counter, rightTuple); linkLeft(tracker); linkRight(tracker); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index 7e27a733fbf..05e3d838a62 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -14,6 +14,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import ai.timefold.solver.core.impl.util.ListEntry; import org.jspecify.annotations.Nullable; @@ -203,7 +204,14 @@ public final void insertRight(UniTuple rightTuple) { private void updateCounterLeft(UniTuple rightTuple, Object compositeKey) { if (!isFiltering) { - forEachLeftCounter(rightTuple, compositeKey, this::incrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + forEachLeftCounter(rightTuple, compositeKey, this::incrementCounterRightWithoutIndictment); + } else { + forEachLeftCounter(rightTuple, compositeKey, + counter -> incrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { // Trackers link themselves into the right tuple's inputStoreIndexRightTrackerList slot. // No list object is needed; the slot starts null and the first tracker becomes the head. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java index 6edbe0b583f..40bdd719a37 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleList; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.Nullable; @@ -110,6 +111,7 @@ private void insertOutTupleIfActiveFiltered(LeftTuple_ leftTuple, UniTuple rightTuple) { var outTuple = createOutTuple(leftTuple, rightTuple); + outTuple.setIndictmentSource(IndictmentSource.joining(leftTuple, rightTuple)); TupleList outTupleListLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleList); outTupleListLeft.add(outTuple); outTuple.setStore(outputStoreIndexLeftOutTupleList, outTupleListLeft); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractMapNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractMapNode.java index 6fbc4ed2a12..394354f5af6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractMapNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractMapNode.java @@ -30,6 +30,7 @@ public final void insert(InTuple_ tuple) { + ") was already added in the tupleStore."); } var outTuple = map(tuple); + outTuple.setIndictmentSource(tuple.getIndictmentSource()); tuple.setStore(inputStoreIndex, outTuple); propagationQueue.insert(outTuple); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java index b0a2224f260..59082e93bd4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import ai.timefold.solver.core.impl.util.ElementAwareLinkedList; /** @@ -103,7 +104,13 @@ public final void insertRight(UniTuple rightTuple) { } rightTuple.setStore(inputStoreIndexRightEntry, rightTupleList.add(rightTuple)); if (!isFiltering) { - counterList.forEach(this::incrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + counterList.forEach(this::incrementCounterRightWithoutIndictment); + } else { + counterList.forEach(counter -> incrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { // Trackers link themselves into the right tuple's inputStoreIndexRightTrackerList slot. // No list object is needed; the slot starts null and the first tracker becomes the head. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java index 0b69b862fb4..6f493d7e34c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java @@ -1,22 +1,33 @@ package ai.timefold.solver.core.impl.bavet.common; +import java.util.ArrayList; + import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; public final class ExistsCounter extends AbstractPropagationMetadataCarrier { final Tuple_ leftTuple; + final Tuple_ outTuple; TupleState state = TupleState.DEAD; // It's the node's job to mark a new instance as CREATING. int countRight = 0; ExistsCounter(Tuple_ leftTuple) { this.leftTuple = leftTuple; + if (leftTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + outTuple = leftTuple; + } else { + outTuple = Tuple.copyOf(leftTuple); + outTuple.setIndictmentSource(new IndictmentSource.IndictmentSourceWithSupport(leftTuple.getIndictmentSource(), + new ArrayList<>())); + } } @Override public Tuple_ getTuple() { - return leftTuple; + return outTuple; } @Override @@ -31,7 +42,7 @@ public void setState(TupleState state) { @Override public String toString() { - return "Counter(" + leftTuple + ")"; + return "Counter(%s)".formatted(leftTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java index c4b03d67f33..d8ad435b280 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java @@ -3,6 +3,7 @@ import java.util.function.Function; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -23,6 +24,26 @@ @NullMarked public sealed interface Tuple permits BiTuple, QuadTuple, TriTuple, UniTuple { + static Tuple_ copyOf(Tuple_ originalTuple) { + var universalTuple = (UniversalTuple) originalTuple; + var copiedTuple = new UniversalTuple<>(universalTuple.storeSize(), universalTuple.cardinality()); + + copiedTuple.setA(universalTuple.getA()); + copiedTuple.setB(universalTuple.getB()); + copiedTuple.setC(universalTuple.getC()); + copiedTuple.setD(universalTuple.getD()); + copiedTuple.setState(universalTuple.getState()); + copiedTuple.setIndictmentSource(universalTuple.getIndictmentSource()); + for (var i = 0; i < universalTuple.storeSize(); i++) { + copiedTuple.setStore(i, universalTuple.getStore(i)); + } + return (Tuple_) copiedTuple; + } + + IndictmentSource getIndictmentSource(); + + void setIndictmentSource(IndictmentSource indictmentSource); + TupleState getState(); void setState(TupleState state); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java index 5efa7381d24..63cadf46844 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.bavet.common.tuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -20,6 +22,7 @@ final class UniversalTuple private @Nullable B b; private @Nullable C c; private @Nullable D d; + private IndictmentSource indictmentSource = IndictmentSource.DISABLED; private TupleState state = TupleState.DEAD; // It's the node's job to mark a new tuple as CREATING. UniversalTuple(int storeSize, int cardinality) { @@ -27,6 +30,14 @@ final class UniversalTuple this.store = storeSize > 0 ? new Object[storeSize] : EMPTY_STORE; } + int cardinality() { + return cardinality; + } + + int storeSize() { + return store.length; + } + @Override public void setA(@Nullable A a) { this.a = a; @@ -95,6 +106,16 @@ public void setStore(int index, @Nullable Object value) { return value; } + @Override + public IndictmentSource getIndictmentSource() { + return indictmentSource; + } + + @Override + public void setIndictmentSource(IndictmentSource indictmentSource) { + this.indictmentSource = indictmentSource; + } + @Override public String toString() { return switch (cardinality) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java new file mode 100644 index 00000000000..65cc64d2adc --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -0,0 +1,93 @@ +package ai.timefold.solver.core.impl.bavet.common.tuple.indictment; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; + +public sealed interface IndictmentSource { + IndictmentSource DISABLED = new DisabledIndictmentSource(); + + void visitSources(Consumer sourceConsumer); + + static IndictmentSource of(Object source) { + return new RootIndictmentSource(source); + } + + static IndictmentSource joining(Tuple left, Tuple right) { + if (left.getIndictmentSource() == DISABLED) { + return DISABLED; + } + return new JoinedIndictmentSource(left.getIndictmentSource(), right.getIndictmentSource()); + } + + static IndictmentSource aggregating(Tuple elementTuple, Tuple groupTuple) { + if (elementTuple.getIndictmentSource() == DISABLED) { + return DISABLED; + } + if (groupTuple.getIndictmentSource() instanceof AggregateIndictmentSource aggregateIndictmentSource) { + aggregateIndictmentSource.sourceList.add(elementTuple.getIndictmentSource()); + return aggregateIndictmentSource; + } else { + var collection = new ArrayList(); + collection.add(elementTuple.getIndictmentSource()); + return new AggregateIndictmentSource(collection); + } + } + + static IndictmentSource sourceWithSupport(Tuple carry, Tuple support) { + if (carry.getIndictmentSource() == DISABLED) { + return DISABLED; + } + if (carry.getIndictmentSource() instanceof IndictmentSourceWithSupport indictmentSourceWithSupport) { + indictmentSourceWithSupport.support.add(support.getIndictmentSource()); + return indictmentSourceWithSupport; + } else { + throw new IllegalStateException("Carry tuple (%s) does not have a %s %s; its source is (%s) instead." + .formatted(carry, IndictmentSourceWithSupport.class.getSimpleName(), IndictmentSource.class.getSimpleName(), + support.getIndictmentSource())); + } + } + + record DisabledIndictmentSource() implements IndictmentSource { + @Override + public void visitSources(Consumer sourceConsumer) { + throw new UnsupportedOperationException("Impossible state: indictments are disabled."); + } + } + + record RootIndictmentSource(Object source) implements IndictmentSource { + @Override + public void visitSources(Consumer sourceConsumer) { + sourceConsumer.accept(source); + } + } + + record JoinedIndictmentSource(IndictmentSource left, IndictmentSource right) implements IndictmentSource { + @Override + public void visitSources(Consumer sourceConsumer) { + left.visitSources(sourceConsumer); + right.visitSources(sourceConsumer); + } + } + + record AggregateIndictmentSource(List sourceList) implements IndictmentSource { + @Override + public void visitSources(Consumer sourceConsumer) { + for (var source : sourceList) { + source.visitSources(sourceConsumer); + } + } + } + + record IndictmentSourceWithSupport(IndictmentSource source, List support) implements IndictmentSource { + @Override + public void visitSources(Consumer sourceConsumer) { + source.visitSources(sourceConsumer); + for (var support : support) { + support.visitSources(sourceConsumer); + } + } + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java index 8c65119bd99..371c8448162 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java @@ -23,6 +23,8 @@ public PrecomputeQuadNode(Supplier remapTuple(QuadTuple tuple) { - return QuadTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), outputStoreSize); + var out = QuadTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), outputStoreSize); + out.setIndictmentSource(tuple.getIndictmentSource()); + return out; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java index 936563f13c5..06d776b782a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java @@ -23,6 +23,8 @@ public PrecomputeTriNode(Supplier>> @Override protected TriTuple remapTuple(TriTuple tuple) { - return TriTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), outputStoreSize); + var out = TriTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), outputStoreSize); + out.setIndictmentSource(tuple.getIndictmentSource()); + return out; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java index a2843c24ea9..cfb61d0607f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java @@ -11,6 +11,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -31,12 +32,14 @@ public abstract sealed class AbstractForEachUniNode private final Class forEachClass; private final int outputStoreSize; private final StaticPropagationQueue> propagationQueue; + private final boolean indictmentsEnabled; protected final Map> tupleMap = HashMap.newHashMap(1000); protected AbstractForEachUniNode(Class forEachClass, TupleLifecycle> nextNodesTupleLifecycle, - int outputStoreSize) { + boolean indictmentsEnabled, int outputStoreSize) { this.forEachClass = forEachClass; this.outputStoreSize = outputStoreSize; + this.indictmentsEnabled = indictmentsEnabled; this.propagationQueue = new StaticPropagationQueue<>(nextNodesTupleLifecycle); } @@ -58,6 +61,9 @@ public Class[] getSourceClasses() { @Override public void insert(@Nullable A a) { var tuple = UniTuple.of(a, outputStoreSize); + if (indictmentsEnabled) { + tuple.setIndictmentSource(IndictmentSource.of(a)); + } var old = tupleMap.put(a, tuple); if (old != null) { throw new IllegalStateException(""" diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNode.java index 015042f2d97..18533d8acfc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNode.java @@ -24,8 +24,8 @@ public final class ForEachFilteredUniNode private int tupleCountWithoutFiltering = 0; public ForEachFilteredUniNode(Class forEachClass, Predicate filter, - TupleLifecycle> nextNodesTupleLifecycle, int outputStoreSize) { - super(forEachClass, nextNodesTupleLifecycle, outputStoreSize); + TupleLifecycle> nextNodesTupleLifecycle, boolean indictmentsEnabled, int outputStoreSize) { + super(forEachClass, nextNodesTupleLifecycle, indictmentsEnabled, outputStoreSize); this.nextNodesTupleLifecycle = Objects.requireNonNull(nextNodesTupleLifecycle); this.filter = Objects.requireNonNull(filter); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUnfilteredUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUnfilteredUniNode.java index fb40cb58be0..46198e99ca8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUnfilteredUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUnfilteredUniNode.java @@ -15,8 +15,8 @@ public final class ForEachUnfilteredUniNode private boolean isActive; public ForEachUnfilteredUniNode(Class forEachClass, TupleLifecycle> nextNodesTupleLifecycle, - int outputStoreSize) { - super(forEachClass, nextNodesTupleLifecycle, outputStoreSize); + boolean indictmentsEnabled, int outputStoreSize) { + super(forEachClass, nextNodesTupleLifecycle, indictmentsEnabled, outputStoreSize); this.nextNodesTupleLifecycle = nextNodesTupleLifecycle; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java index 92dfee56180..70f64d5ad73 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java @@ -23,7 +23,9 @@ public PrecomputeUniNode(Supplier>> preco @Override protected UniTuple remapTuple(UniTuple tuple) { - return UniTuple.of(tuple.getA(), outputStoreSize); + var out = UniTuple.of(tuple.getA(), outputStoreSize); + out.setIndictmentSource(tuple.getIndictmentSource()); + return out; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/AbstractForEachEnumeratingStream.java b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/AbstractForEachEnumeratingStream.java index ebb8cf59945..cf301f81438 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/AbstractForEachEnumeratingStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/AbstractForEachEnumeratingStream.java @@ -41,7 +41,7 @@ public final void collectActiveEnumeratingStreams(Set buildHelper) { TupleLifecycle> tupleLifecycle = buildHelper.getAggregatedTupleLifecycle(childStreamList); var outputStoreSize = buildHelper.extractTupleStoreSize(this); - var node = new ForEachUnfilteredUniNode<>(forEachClass, tupleLifecycle, outputStoreSize); + var node = new ForEachUnfilteredUniNode<>(forEachClass, tupleLifecycle, false, outputStoreSize); if (shouldIncludeNull && node.supports(LifecycleOperation.INSERT)) { node.insert(null); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java index 25a9746b816..dfac8c52ead 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java @@ -2,6 +2,8 @@ import static java.util.Objects.requireNonNull; +import java.util.List; + import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; import ai.timefold.solver.core.api.score.stream.ConstraintRef; @@ -28,6 +30,7 @@ public final class ConstraintMatch> implements Comp private final ConstraintRef constraintRef; private final @Nullable ConstraintJustification justification; + private final List indictedObjects; private final Score_ score; /** @@ -35,9 +38,11 @@ public final class ConstraintMatch> implements Comp * @param justification only null if justifications are disabled * @param score penalty or reward associated with the constraint match */ - public ConstraintMatch(ConstraintRef constraintRef, @Nullable ConstraintJustification justification, Score_ score) { + public ConstraintMatch(ConstraintRef constraintRef, @Nullable ConstraintJustification justification, + List indictedObjects, Score_ score) { this.constraintRef = requireNonNull(constraintRef); this.justification = justification; + this.indictedObjects = indictedObjects; this.score = requireNonNull(score); } @@ -62,6 +67,10 @@ public ConstraintRef getConstraintRef() { return (Justification_) justification; } + public List getIndictedObjects() { + return indictedObjects; + } + public Score_ getScore() { return score; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java index f239ad113da..bd963663f6d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java @@ -13,33 +13,37 @@ @NullMarked public enum ConstraintMatchPolicy { - DISABLED(false, false), - ENABLED_WITHOUT_JUSTIFICATIONS(true, false), - ENABLED(true, true); + DISABLED(false, false, false), + ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS(true, false, false), + ENABLED_WITHOUT_JUSTIFICATIONS(true, false, true), + ENABLED_WITHOUT_INDICTMENTS(true, true, false), + ENABLED(true, true, true); /** * To achieve the most performance out of the underlying solver, * the policy should match whatever policy was used for score analysis. * For example, if the fetch policy specifies that only match counts are necessary and not matches themselves * ({@link ScoreAnalysisFetchPolicy#FETCH_MATCH_COUNT}), - * we can configure the solver to not produce justifications ({@link #ENABLED_WITHOUT_JUSTIFICATIONS}). + * we can configure the solver to not produce justifications ({@link #ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS}). * * @param scoreAnalysisFetchPolicy * @return Match policy best suited for the given fetch policy. */ public static ConstraintMatchPolicy match(ScoreAnalysisFetchPolicy scoreAnalysisFetchPolicy) { return switch (scoreAnalysisFetchPolicy) { - case FETCH_MATCH_COUNT, FETCH_SHALLOW -> ENABLED_WITHOUT_JUSTIFICATIONS; + case FETCH_MATCH_COUNT, FETCH_SHALLOW -> ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS; case FETCH_ALL -> ENABLED; }; } private final boolean enabled; private final boolean justificationEnabled; + private final boolean indictmentsEnabled; - ConstraintMatchPolicy(boolean enabled, boolean justificationEnabled) { + ConstraintMatchPolicy(boolean enabled, boolean justificationEnabled, boolean indictmentsEnabled) { this.enabled = enabled; this.justificationEnabled = justificationEnabled; + this.indictmentsEnabled = indictmentsEnabled; } public boolean isEnabled() { @@ -50,4 +54,7 @@ public boolean isJustificationEnabled() { return justificationEnabled; } + public boolean isIndictmentsEnabled() { + return indictmentsEnabled; + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java index 6902f3014eb..31502a8336c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -68,18 +69,32 @@ public Score_ getScore() { * @return never null */ public ConstraintMatch addConstraintMatch(List justifications, Score_ score) { - return addConstraintMatch(DefaultConstraintJustification.of(score, justifications), score); + return addConstraintMatch(DefaultConstraintJustification.of(score, justifications), justifications, score); } /** * Creates a {@link ConstraintMatch} and adds it to the collection returned by {@link #getConstraintMatchSet()}. - * It will be justified with the provided {@link ConstraintJustification}. + * It will be justified with the provided {@link ConstraintJustification} and will indict the given objects. + * + * @param score never null + * @return never null + */ + public ConstraintMatch addConstraintMatch(ConstraintJustification justification, List indictedObjects, + Score_ score) { + var constraintMatch = new ConstraintMatch<>(constraintRef, justification, indictedObjects, score); + addConstraintMatch(constraintMatch); + return constraintMatch; + } + + /** + * Creates a {@link ConstraintMatch} and adds it to the collection returned by {@link #getConstraintMatchSet()}. + * It will be justified with the provided {@link ConstraintJustification} and will indict no objects. * * @param score never null * @return never null */ public ConstraintMatch addConstraintMatch(ConstraintJustification justification, Score_ score) { - var constraintMatch = new ConstraintMatch(constraintRef, justification, score); + var constraintMatch = new ConstraintMatch<>(constraintRef, justification, Collections.emptyList(), score); addConstraintMatch(constraintMatch); return constraintMatch; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java index 756ffedcc49..26fd18fbd0f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java @@ -82,7 +82,7 @@ public interface InnerScoreDirector> /** * @return {@link ConstraintMatchPolicy#ENABLED} if {@link #getConstraintMatchTotalMap()} can be called. - * {@link ConstraintMatchPolicy#ENABLED_WITHOUT_JUSTIFICATIONS} if only the former can be called. + * {@link ConstraintMatchPolicy#ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS} if only the former can be called. * {@link ConstraintMatchPolicy#DISABLED} if neither can be called. */ ConstraintMatchPolicy getConstraintMatchPolicy(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java index 7629a033f5e..d30aa1805ef 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java @@ -117,13 +117,17 @@ public BavetConstraintSession buildSession(Solution_ workingSolution, } return new BavetConstraintSession<>(scoreInliner, buildNodeNetwork(workingSolution, consistencyTracker, constraintStreamSet, scoreInliner, constraintProfiler, - scoreDirectorDerived)); + constraintMatchPolicy.isIndictmentsEnabled(), scoreDirectorDerived)); } private ConstraintStreamsBavetNodeNetwork buildNodeNetwork(Solution_ workingSolution, ConsistencyTracker consistencyTracker, Set> constraintStreamSet, - AbstractScoreInliner scoreInliner, InnerConstraintProfiler profiler, boolean scoreDirectorDerived) { - var buildHelper = new ConstraintNodeBuildHelper<>(consistencyTracker, constraintStreamSet, scoreInliner, profiler); + AbstractScoreInliner scoreInliner, InnerConstraintProfiler profiler, + boolean indictmentsEnabled, + boolean scoreDirectorDerived) { + var buildHelper = + new ConstraintNodeBuildHelper<>(consistencyTracker, constraintStreamSet, scoreInliner, indictmentsEnabled, + profiler); var declaredClassToNodeMap = new LinkedHashMap, List>>(); var nodeList = buildHelper.buildNodeList(constraintStreamSet, buildHelper, BavetAbstractConstraintStream::buildNode, node -> { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetPrecomputeBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetPrecomputeBiConstraintStream.java index fa5de8e8069..af65f4a3e48 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetPrecomputeBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetPrecomputeBiConstraintStream.java @@ -39,7 +39,8 @@ public void setAftBridge(BavetAftBridgeBiConstraintStream aftSt @Override public > void buildNode(ConstraintNodeBuildHelper buildHelper) { Supplier>> precomputeBuildHelperSupplier = - () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, entityClassSet); + () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, + buildHelper.isIndictmentEnabled(), entityClassSet); var outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); buildHelper.addNode(new PrecomputeBiNode<>(precomputeBuildHelperSupplier, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetScoringBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetScoringBiConstraintStream.java index 3bf4653eb17..4360cfb9428 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetScoringBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetScoringBiConstraintStream.java @@ -53,8 +53,9 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + scoreImpact::impactWithoutJustification; }; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java index a24737b796e..0a86db1d218 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java @@ -31,7 +31,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, BiTuple impactFull(WeightedScoreImpacter impacter, BiTuple { public BavetPrecomputeBuildHelper( BavetAbstractConstraintStream recordingPrecomputeConstraintStream, + boolean indictmentsEnabled, Set> entityClassSet) { if (recordingPrecomputeConstraintStream.getRetrievalSemantics() != RetrievalSemantics.PRECOMPUTE) { throw new IllegalStateException( @@ -63,7 +64,7 @@ public BavetPrecomputeBuildHelper( var buildHelper = new ConstraintNodeBuildHelper<>(new ConsistencyTracker<>(), streamSet, AbstractScoreInliner.buildScoreInliner(new SimpleScoreDefinition(), Collections.emptyMap(), ConstraintMatchPolicy.DISABLED), - null); + indictmentsEnabled, null); var declaredClassToNodeMap = new LinkedHashMap, List>>(); var nodeList = buildHelper.buildNodeList(streamSet, buildHelper, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java index 0179f24fd5d..c5d82ee5736 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java @@ -41,6 +41,7 @@ public final class ConstraintNodeBuildHelper scoreInliner; private final ConsistencyTracker consistencyTracker; private final @Nullable InnerConstraintProfiler constraintProfiler; + private final boolean indictmentsEnabled; private final Map, Map>> entityDescriptorToForEachCriteriaToPredicateMap; private final Map, List>> streamToProfileIdSets; private final Map, Scorer> streamToScorers = new HashMap<>(); @@ -49,11 +50,13 @@ public final class ConstraintNodeBuildHelper consistencyTracker, Set> activeStreamSet, AbstractScoreInliner scoreInliner, + boolean indictmentsEnabled, @Nullable InnerConstraintProfiler profiler) { super(activeStreamSet); this.consistencyTracker = consistencyTracker; this.scoreInliner = scoreInliner; this.constraintProfiler = profiler; + this.indictmentsEnabled = indictmentsEnabled; this.entityDescriptorToForEachCriteriaToPredicateMap = new HashMap<>(); this.streamToProfileIdSets = HashMap.newHashMap(Math.max(16, activeStreamSet.size() / 2)); } @@ -153,4 +156,7 @@ public ConstraintStreamsBavetNodeNetwork buildPrecomputeNodeNetwork(List> void buildNode(ConstraintNodeBuildHelper buildHelper) { Supplier>> precomputeBuildHelperSupplier = - () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, entityClassSet); + () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, + buildHelper.isIndictmentEnabled(), entityClassSet); var outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); buildHelper.addNode(new PrecomputeQuadNode<>(precomputeBuildHelperSupplier, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetScoringQuadConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetScoringQuadConstraintStream.java index ebaecf8b6da..8f81dd5acab 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetScoringQuadConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetScoringQuadConstraintStream.java @@ -54,8 +54,9 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + scoreImpact::impactWithoutJustification; }; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java index acbe999c17c..2e48e38937a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java @@ -35,8 +35,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, QuadTuple var d = tuple.getD(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.apply(a, b, c, d), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, - c, d)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java index c58f81da4c0..b3866e237ff 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java @@ -33,8 +33,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, QuadTuple var d = tuple.getD(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.applyAsLong(a, b, c, d), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, - c, d)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetPrecomputeTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetPrecomputeTriConstraintStream.java index 151f648749f..315f588a46f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetPrecomputeTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetPrecomputeTriConstraintStream.java @@ -38,7 +38,8 @@ public void setAftBridge(BavetAftBridgeTriConstraintStream a @Override public > void buildNode(ConstraintNodeBuildHelper buildHelper) { Supplier>> precomputeBuildHelperSupplier = - () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, entityClassSet); + () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, + buildHelper.isIndictmentEnabled(), entityClassSet); var outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); buildHelper.addNode(new PrecomputeTriNode<>(precomputeBuildHelperSupplier, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetScoringTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetScoringTriConstraintStream.java index 41e55d2a562..6412a07540f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetScoringTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetScoringTriConstraintStream.java @@ -54,8 +54,9 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + scoreImpact::impactWithoutJustification; }; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java index b2f3f62902d..9cb71402639 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java @@ -27,8 +27,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, TriTuple< var c = tuple.getC(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.apply(a, b, c), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, - c)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java index 5f8b21f8ffb..6fd63bf641e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java @@ -31,8 +31,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, TriTuple< var c = tuple.getC(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.applyAsLong(a, b, c), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, - c)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetForEachUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetForEachUniConstraintStream.java index f0458c90a6b..f0505a1eeb6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetForEachUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetForEachUniConstraintStream.java @@ -54,9 +54,10 @@ public > void buildNode(ConstraintNodeBuildHelper> getAggregatedTupleLifecycle(childStreamList); int outputStoreSize = buildHelper.extractTupleStoreSize(this); var filter = filterFunction != null ? filterFunction.apply(buildHelper) : null; - var node = filter == null ? new ForEachUnfilteredUniNode<>(forEachClass, tupleLifecycle, outputStoreSize) + var node = filter == null ? new ForEachUnfilteredUniNode<>(forEachClass, tupleLifecycle, + buildHelper.isIndictmentEnabled(), outputStoreSize) : new ForEachFilteredUniNode<>(forEachClass, filter, tupleLifecycle, - outputStoreSize); + buildHelper.isIndictmentEnabled(), outputStoreSize); buildHelper.addNode(node, this, null); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetPrecomputeUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetPrecomputeUniConstraintStream.java index 0b26746e9f4..3c1720a171b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetPrecomputeUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetPrecomputeUniConstraintStream.java @@ -38,7 +38,8 @@ public void setAftBridge(BavetAftBridgeUniConstraintStream aftStre @Override public > void buildNode(ConstraintNodeBuildHelper buildHelper) { Supplier>> precomputeBuildHelperSupplier = - () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, entityClassSet); + () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, + buildHelper.isIndictmentEnabled(), entityClassSet); var outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); buildHelper.addNode(new PrecomputeUniNode<>(precomputeBuildHelperSupplier, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetScoringUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetScoringUniConstraintStream.java index 2ae32839cb5..091dd1611a4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetScoringUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetScoringUniConstraintStream.java @@ -53,8 +53,9 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + scoreImpact::impactWithoutJustification; }; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java index 0da5ce44888..9bd7b27c833 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java @@ -28,7 +28,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, UniTuple< var a = tuple.getA(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.apply(a), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java index 27d8c2a6312..5484b5e4bda 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java @@ -22,7 +22,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, UniTuple< var a = tuple.getA(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.applyAsLong(a), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java index 09c4e7115d1..e0c1c287ce0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java @@ -1,6 +1,9 @@ package ai.timefold.solver.core.impl.score.stream.common.inliner; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.function.BiFunction; import java.util.stream.Collectors; @@ -11,6 +14,12 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; +import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; import org.jspecify.annotations.NullMarked; @@ -31,6 +40,15 @@ public interface ConstraintMatchSupplier> extends BiFunction> { + static List collectIndictments(Tuple tuple) { + if (tuple.getIndictmentSource() == IndictmentSource.DISABLED) { + return Collections.emptyList(); + } + var out = new ArrayList<>(); + tuple.getIndictmentSource().visitSources(out::add); + return out; + } + /** * * @return the constraint match returned by the supplier will have its justification set to null. @@ -39,18 +57,18 @@ public interface ConstraintMatchSupplier> */ static > ConstraintMatchSupplier empty() { return (constraint, impact) -> new ConstraintMatch<>(constraint.getConstraintRef(), null, - impact); + Collections.emptyList(), impact); } static > ConstraintMatchSupplier of( BiFunction, ConstraintJustification> justificationMapping, - A a) { + UniTuple tuple) { return (constraint, impact) -> { try { - var justification = justificationMapping.apply(a, impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); + var justification = justificationMapping.apply(tuple.getA(), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); } catch (Exception e) { - throw createJustificationException(constraint, e, a); + throw createJustificationException(constraint, e, tuple.getA()); } }; } @@ -68,39 +86,39 @@ private static String factsToString(Object... facts) { static > ConstraintMatchSupplier of( TriFunction justificationMapping, - A a, B b) { + BiTuple tuple) { return (constraint, impact) -> { try { - var justification = justificationMapping.apply(a, b, impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); + var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); } catch (Exception e) { - throw createJustificationException(constraint, e, a, b); + throw createJustificationException(constraint, e, tuple.getA(), tuple.getB()); } }; } static > ConstraintMatchSupplier of( QuadFunction justificationMapping, - A a, B b, C c) { + TriTuple tuple) { return (constraint, impact) -> { try { - var justification = justificationMapping.apply(a, b, c, impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); + var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); } catch (Exception e) { - throw createJustificationException(constraint, e, a, b, c); + throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC()); } }; } static > ConstraintMatchSupplier of( PentaFunction justificationMapping, - A a, B b, C c, D d) { + QuadTuple tuple) { return (constraint, impact) -> { try { - var justification = justificationMapping.apply(a, b, c, d, impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); + var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); } catch (Exception e) { - throw createJustificationException(constraint, e, a, b, c, d); + throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD()); } }; } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUniNodeActivityTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUniNodeActivityTest.java index 5017b89c41b..40b09c9ade7 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUniNodeActivityTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUniNodeActivityTest.java @@ -25,7 +25,7 @@ private static TupleLifecycle> mockDownstream(boolean active) { @Test void unfilteredInactiveWhenNoFacts() { var downstream = mockDownstream(true); - var node = new ForEachUnfilteredUniNode<>(String.class, downstream, 1); + var node = new ForEachUnfilteredUniNode<>(String.class, downstream, false, 1); node.afterAllFactsInserted(true); assertThat(node.isActive()).isFalse(); } @@ -33,7 +33,7 @@ void unfilteredInactiveWhenNoFacts() { @Test void unfilteredActiveWhenFactsExist() { var downstream = mockDownstream(true); - var node = new ForEachUnfilteredUniNode<>(String.class, downstream, 1); + var node = new ForEachUnfilteredUniNode<>(String.class, downstream, false, 1); node.insert("a"); node.afterAllFactsInserted(true); assertThat(node.isActive()).isTrue(); @@ -42,7 +42,7 @@ void unfilteredActiveWhenFactsExist() { @Test void unfilteredInactiveWhenDownstreamInactive() { var downstream = mockDownstream(false); - var node = new ForEachUnfilteredUniNode<>(String.class, downstream, 1); + var node = new ForEachUnfilteredUniNode<>(String.class, downstream, false, 1); node.insert("a"); node.afterAllFactsInserted(true); assertThat(node.isActive()).isFalse(); @@ -51,7 +51,7 @@ void unfilteredInactiveWhenDownstreamInactive() { @Test void filteredInactiveWhenNoFacts() { var downstream = mockDownstream(true); - var node = new ForEachFilteredUniNode<>(String.class, s -> true, downstream, 1); + var node = new ForEachFilteredUniNode<>(String.class, s -> true, downstream, false, 1); node.afterAllFactsInserted(true); assertThat(node.isActive()).isFalse(); } @@ -59,7 +59,7 @@ void filteredInactiveWhenNoFacts() { @Test void filteredActiveWhenFactInsertedEvenIfFilteredOut() { var downstream = mockDownstream(true); - var node = new ForEachFilteredUniNode<>(String.class, s -> false, downstream, 1); + var node = new ForEachFilteredUniNode<>(String.class, s -> false, downstream, false, 1); node.insert("a"); node.afterAllFactsInserted(true); assertThat(node.isActive()).isTrue(); @@ -68,7 +68,7 @@ void filteredActiveWhenFactInsertedEvenIfFilteredOut() { @Test void filteredInactiveWhenDownstreamInactive() { var downstream = mockDownstream(false); - var node = new ForEachFilteredUniNode<>(String.class, s -> true, downstream, 1); + var node = new ForEachFilteredUniNode<>(String.class, s -> true, downstream, false, 1); node.insert("a"); node.afterAllFactsInserted(true); assertThat(node.isActive()).isFalse(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java index 5102f4cd01b..f7e31759389 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java @@ -3,6 +3,8 @@ import static ai.timefold.solver.core.api.score.SimpleScore.ONE; import static ai.timefold.solver.core.api.score.SimpleScore.ZERO; +import java.util.List; + import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.score.stream.ConstraintRef; @@ -26,6 +28,7 @@ private > ConstraintMatch buildConstraintMa Object... facts) { return new ConstraintMatch<>(ConstraintRef.of(constraintName), DefaultConstraintJustification.of(score, facts), + List.of(facts), score); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java index c16f5df7d54..d2edef784a4 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java @@ -33,7 +33,7 @@ public boolean supportsTestTemplate(ExtensionContext context) { @Override public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { return Stream - .of(ConstraintMatchPolicy.ENABLED, ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS, + .of(ConstraintMatchPolicy.ENABLED, ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, ConstraintMatchPolicy.DISABLED) .map(ConstraintStreamTestExtension::invocationContext); } From dac65ef2a88afafd2813abde8f42f520a0d3fc4a Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Mon, 20 Jul 2026 13:41:14 -0400 Subject: [PATCH 02/18] chore: remove creating a new tuple for `ifExists` indictments --- .../impl/bavet/common/AbstractIfExistsNode.java | 3 +-- .../core/impl/bavet/common/ExistsCounter.java | 13 +------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 0bf612b8423..3dab1a550da 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -5,7 +5,6 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -124,7 +123,7 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - counter.outTuple.setIndictmentSource(IndictmentSource.sourceWithSupport(counter.outTuple, rightTuple)); + // TODO: Add right tuple to support counter.countRight++; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java index 6f493d7e34c..db646736653 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java @@ -1,33 +1,22 @@ package ai.timefold.solver.core.impl.bavet.common; -import java.util.ArrayList; - import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; -import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; public final class ExistsCounter extends AbstractPropagationMetadataCarrier { final Tuple_ leftTuple; - final Tuple_ outTuple; TupleState state = TupleState.DEAD; // It's the node's job to mark a new instance as CREATING. int countRight = 0; ExistsCounter(Tuple_ leftTuple) { this.leftTuple = leftTuple; - if (leftTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - outTuple = leftTuple; - } else { - outTuple = Tuple.copyOf(leftTuple); - outTuple.setIndictmentSource(new IndictmentSource.IndictmentSourceWithSupport(leftTuple.getIndictmentSource(), - new ArrayList<>())); - } } @Override public Tuple_ getTuple() { - return outTuple; + return leftTuple; } @Override From bd643dbafdc7fcff31937e46762bea5de225d916 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Mon, 20 Jul 2026 16:51:19 -0400 Subject: [PATCH 03/18] chore: use a Map to store extra indicted objects from ifExists --- .../bavet/common/AbstractIfExistsNode.java | 27 +++++++++++---- .../common/AbstractIndexedIfExistsNode.java | 24 +++++++++++-- .../bavet/common/AbstractNodeBuildHelper.java | 22 ++++++++++++ .../common/AbstractUnindexedIfExistsNode.java | 8 ++++- .../core/impl/bavet/common/tuple/Tuple.java | 19 ++--------- .../bavet/common/tuple/UniversalTuple.java | 14 ++++++++ .../score/constraint/ConstraintMatch.java | 3 +- .../common/ConstraintNodeBuildHelper.java | 8 +++++ .../score/stream/bavet/common/Scorer.java | 5 ++- .../stream/common/AbstractConstraint.java | 9 +++++ .../common/inliner/AbstractScoreInliner.java | 3 +- .../inliner/ConstraintMatchSupplier.java | 21 ++++++++---- .../stream/common/inliner/ScoreContext.java | 4 +++ .../ForEachFilteredUniNodeActivityTest.java | 2 +- .../common/AbstractConstraintStreamTest.java | 34 +++++++++++++++---- .../common/ConstraintStreamTestExtension.java | 15 ++++++-- .../uni/AbstractUniConstraintStreamTest.java | 11 ++++-- 17 files changed, 182 insertions(+), 47 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 3dab1a550da..c3ed5e96192 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -1,10 +1,13 @@ package ai.timefold.solver.core.impl.bavet.common; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.common.tuple.InTupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -123,12 +126,15 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - // TODO: Add right tuple to support + counter.getTuple().getIndictmentSupportForNodeId(getId()) + .add(Objects.requireNonNull(rightTuple.getA())); counter.countRight++; } protected void decrementCounterRight(ExistsCounter counter) { counter.countRight--; + counter.getTuple().getIndictmentSupportForNodeId(getId()) + .remove(Objects.requireNonNull(rightTuple.getA())); if (counter.countRight == 0) { if (shouldExist) { doRetractCounter(counter); @@ -176,11 +182,20 @@ private void removeRight(FilteringTracker tracker) { // Walk safety: removeFromLeft only touches left-side links, so rightNext is stable across the call. protected void clearRightTrackerList(UniTuple rightTuple) { FilteringTracker tracker = rightTuple.removeStore(inputStoreIndexRightTrackerList); - while (tracker != null) { - var next = tracker.rightNext; - decrementCounterRight(tracker.counter); - removeLeft(tracker); - tracker = next; + if (rightTuple.getIndictmentSource() != IndictmentSource.DISABLED) { + while (tracker != null) { + var next = tracker.rightNext; + decrementCounterRightUpdatingIndictment(tracker.counter, rightTuple); + removeFromLeft(tracker); + tracker = next; + } + } else { + while (tracker != null) { + var next = tracker.rightNext; + decrementCounterRightWithoutIndictment(tracker.counter); + removeFromLeft(tracker); + tracker = next; + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index 05e3d838a62..10154b232cd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -250,7 +250,13 @@ public final void updateRight(UniTuple rightTuple) { indexerRight.remove(oldCompositeKey, entry); } if (!isFiltering) { - forEachLeftCounter(rightTuple, oldCompositeKey, this::decrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + forEachLeftCounter(rightTuple, oldCompositeKey, this::decrementCounterRightWithoutIndictment); + } else { + forEachLeftCounter(rightTuple, oldCompositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { clearRightTrackerList(rightTuple); } @@ -273,14 +279,26 @@ public final void retractRight(UniTuple rightTuple) { bucket.removeRight(compositeKey, entry); fusedEqualIndex.removeBucketIfEmpty(compositeKey, bucket); if (!isFiltering) { - bucket.forEachLeft(compositeKey, this::decrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + bucket.forEachLeft(compositeKey, this::decrementCounterRightWithoutIndictment); + } else { + bucket.forEachLeft(compositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { clearRightTrackerList(rightTuple); } } else { indexerRight.remove(compositeKey, entry); if (!isFiltering) { - indexerLeft.forEach(compositeKey, this::decrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + indexerLeft.forEach(compositeKey, this::decrementCounterRightWithoutIndictment); + } else { + indexerLeft.forEach(compositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { clearRightTrackerList(rightTuple); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java index 4543fc0d978..188ebc9a3ff 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java @@ -24,6 +24,7 @@ public abstract class AbstractNodeBuildHelper { private final Set activeStreamSet; private final Map nodeCreatorMap; + private final Map streamToCreatedNodeMap; private final Map> tupleLifecycleMap; private final Map storeIndexMap; @@ -34,6 +35,7 @@ protected AbstractNodeBuildHelper(Set activeStreamSet) { this.activeStreamSet = activeStreamSet; int activeStreamSetSize = activeStreamSet.size(); this.nodeCreatorMap = HashMap.newHashMap(Math.max(16, activeStreamSetSize)); + this.streamToCreatedNodeMap = HashMap.newHashMap(Math.max(16, activeStreamSetSize)); this.tupleLifecycleMap = HashMap.newHashMap(Math.max(16, activeStreamSetSize)); this.storeIndexMap = HashMap.newHashMap(Math.max(16, activeStreamSetSize / 2)); this.reversedNodeList = new ArrayList<>(activeStreamSetSize); @@ -51,6 +53,7 @@ public void addNode(AbstractNode node, Stream_ creator, @Nullable Stream_ parent reversedNodeList.add(node); node.addLocationSet(creator.getLocationSet()); nodeCreatorMap.put(node, creator); + streamToCreatedNodeMap.put(creator, node); if (!(node instanceof AbstractRootNode)) { if (parent == null) { throw new IllegalStateException("Impossible state: The node (%s) has no parent (%s).".formatted(node, parent)); @@ -63,6 +66,7 @@ public void addNode(AbstractNode node, Stream_ creator, Stream_ leftParent, Stre reversedNodeList.add(node); node.addLocationSet(creator.getLocationSet()); nodeCreatorMap.put(node, creator); + streamToCreatedNodeMap.put(creator, node); putInsertUpdateRetract(leftParent, TupleLifecycle.ofLeft((LeftTupleLifecycle) node)); putInsertUpdateRetract(rightParent, TupleLifecycle.ofRight((RightTupleLifecycle) node)); } @@ -143,6 +147,24 @@ public Stream_ getNodeCreatingStream(AbstractNode node) { return nodeCreatorMap.get(node); } + public List getParentNodeList(Stream_ stream) { + var out = new ArrayList(); + while (stream != null) { + var node = streamToCreatedNodeMap.get(stream); + if (node != null) { + out.add(node); + } + if (stream instanceof BavetStreamBinaryOperation binaryOperation) { + out.addAll(getParentNodeList((Stream_) binaryOperation.getLeftParent())); + out.addAll(getParentNodeList((Stream_) binaryOperation.getRightParent())); + return out; + } else { + stream = stream.getParent(); + } + } + return out; + } + public AbstractNode findParentNode(Stream_ childNodeCreator) { if (childNodeCreator == null) { // We've recursed to the bottom without finding a parent node. throw new IllegalStateException( diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java index 59082e93bd4..5367d35851e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java @@ -145,7 +145,13 @@ public final void retractRight(UniTuple rightTuple) { } rightEntry.remove(); if (!isFiltering) { - counterList.forEach(this::decrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + counterList.forEach(this::decrementCounterRightWithoutIndictment); + } else { + counterList.forEach(counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { clearRightTrackerList(rightTuple); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java index d8ad435b280..f077455fdda 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.bavet.common.tuple; +import java.util.Set; import java.util.function.Function; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; @@ -24,26 +25,12 @@ @NullMarked public sealed interface Tuple permits BiTuple, QuadTuple, TriTuple, UniTuple { - static Tuple_ copyOf(Tuple_ originalTuple) { - var universalTuple = (UniversalTuple) originalTuple; - var copiedTuple = new UniversalTuple<>(universalTuple.storeSize(), universalTuple.cardinality()); - - copiedTuple.setA(universalTuple.getA()); - copiedTuple.setB(universalTuple.getB()); - copiedTuple.setC(universalTuple.getC()); - copiedTuple.setD(universalTuple.getD()); - copiedTuple.setState(universalTuple.getState()); - copiedTuple.setIndictmentSource(universalTuple.getIndictmentSource()); - for (var i = 0; i < universalTuple.storeSize(); i++) { - copiedTuple.setStore(i, universalTuple.getStore(i)); - } - return (Tuple_) copiedTuple; - } - IndictmentSource getIndictmentSource(); void setIndictmentSource(IndictmentSource indictmentSource); + Set getIndictmentSupportForNodeId(long nodeId); + TupleState getState(); void setState(TupleState state); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java index 63cadf46844..fbeb4b1e426 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java @@ -1,5 +1,10 @@ package ai.timefold.solver.core.impl.bavet.common.tuple; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; @@ -23,6 +28,7 @@ final class UniversalTuple private @Nullable C c; private @Nullable D d; private IndictmentSource indictmentSource = IndictmentSource.DISABLED; + private @Nullable Map> nodeIdToIndictmentSupport; private TupleState state = TupleState.DEAD; // It's the node's job to mark a new tuple as CREATING. UniversalTuple(int storeSize, int cardinality) { @@ -116,6 +122,14 @@ public void setIndictmentSource(IndictmentSource indictmentSource) { this.indictmentSource = indictmentSource; } + @Override + public Set getIndictmentSupportForNodeId(long nodeId) { + if (nodeIdToIndictmentSupport == null) { + nodeIdToIndictmentSupport = new LinkedHashMap<>(); + } + return nodeIdToIndictmentSupport.computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); + } + @Override public String toString() { return switch (cardinality) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java index dfac8c52ead..2c8f6a2e6b5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java @@ -98,7 +98,8 @@ public int compareTo(ConstraintMatch other) { @Override public String toString() { - return "%s/%s=%s".formatted(getConstraintRef().id(), justification, score); + return "%s/%s=%s (indicting %s)".formatted(getConstraintRef().id(), justification, score, + indictedObjects); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java index c5d82ee5736..9c677bf4f1b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java @@ -135,6 +135,14 @@ public Scorer getScorer(BavetScoringConstraintStream stream) { public ConstraintStreamsBavetNodeNetwork buildNodeNetwork(List nodeList, Map, List>> declaredClassToNodeMap, Map, Scorer> constraintToScorerMap, boolean scoreDirectorDerived) { + for (var scorerEntry : constraintToScorerMap.entrySet()) { + var scorerParentNodes = getParentNodeList( + (BavetAbstractConstraintStream) scorerEntry.getKey().getScoringConstraintStream()); + var scorerNodeIds = scorerParentNodes.stream() + .mapToLong(AbstractNode::getId) + .toArray(); + scorerEntry.getValue().setNodeIds(scorerNodeIds); + } return ConstraintStreamsBavetNodeNetwork.of(nodeList, declaredClassToNodeMap, (Map) constraintToScorerMap, node -> { if (constraintProfiler == null) { return node.getPropagator(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/Scorer.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/Scorer.java index df121f529b0..45a934dd16c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/Scorer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/Scorer.java @@ -78,6 +78,10 @@ public void retract(Tuple_ tuple) { } } + public void setNodeIds(long[] scorerNodeIds) { + weightedScoreImpacter.getContext().setInvolvedNodeIds(scorerNodeIds); + } + public ConstraintRef getConstraintRef() { var context = weightedScoreImpacter.getContext(); return context.getConstraint().getConstraintRef(); @@ -94,5 +98,4 @@ public String toString() { .formatted(getClass().getSimpleName(), context.getConstraint().getConstraintRef(), context.getConstraintWeight()); } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java index 2da51dbbf06..093c962a70f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java @@ -25,6 +25,7 @@ public abstract class AbstractConstraint> Score_ extractConstraintWeight(Solution_ solution) { return adjustConstraintWeight((Score_) determineConstraintWeight(solution)); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java index ed7863f6930..70f5b66b6f7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java @@ -104,7 +104,8 @@ private void validateConstraintWeight(Constraint constraint, Score_ constraintWe public abstract WeightedScoreImpacter buildWeightedScoreImpacter(AbstractConstraint constraint); protected final ScoreImpact addConstraintMatch(Constraint constraint, - ConstraintMatchSupplier constraintMatchSupplier, ScoreImpact scoreImpact) { + ConstraintMatchSupplier constraintMatchSupplier, + ScoreImpact scoreImpact) { var constraintMatchList = getConstraintMatchList(constraint); /* * Creating a constraint match is a heavy operation which may yet be undone. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java index e0c1c287ce0..8ffa4b40ede 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.function.BiFunction; @@ -21,6 +22,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; +import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; import org.jspecify.annotations.NullMarked; @@ -40,13 +42,18 @@ public interface ConstraintMatchSupplier> extends BiFunction> { - static List collectIndictments(Tuple tuple) { + @SuppressWarnings("unchecked") + static List collectIndictments(Constraint constraint, Tuple tuple) { if (tuple.getIndictmentSource() == IndictmentSource.DISABLED) { return Collections.emptyList(); } - var out = new ArrayList<>(); + var out = new LinkedHashSet<>(); tuple.getIndictmentSource().visitSources(out::add); - return out; + var abstractConstraint = (AbstractConstraint) constraint; + for (var involvedNodeId : Objects.requireNonNull(abstractConstraint.getInvolvedNodeIds())) { + out.addAll(tuple.getIndictmentSupportForNodeId(involvedNodeId)); + } + return new ArrayList<>(out); } /** @@ -66,7 +73,7 @@ static > ConstraintMatchSupplier of( return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA()); } @@ -90,7 +97,7 @@ static > ConstraintMatchSupplier of( return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB()); } @@ -103,7 +110,7 @@ static > ConstraintMatchSupplier o return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC()); } @@ -116,7 +123,7 @@ static > ConstraintMatchSupplier { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD()); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ScoreContext.java index ebdd5f0b040..f38bdb980fa 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ScoreContext.java @@ -29,6 +29,10 @@ public final Score_ getConstraintWeight() { return constraintWeight; } + public void setInvolvedNodeIds(long[] involvedNodeIds) { + constraint.setInvolvedNodeIds(involvedNodeIds); + } + protected final ScoreImpact possiblyAddConstraintMatch(ScoreImpact scoreImpact, @Nullable ConstraintMatchSupplier constraintMatchSupplier) { if (!inliner.constraintMatchPolicy.isEnabled()) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNodeActivityTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNodeActivityTest.java index 59b936e7a4d..8826d97609a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNodeActivityTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNodeActivityTest.java @@ -26,7 +26,7 @@ class ForEachFilteredUniNodeActivityTest { private final Set passing = new HashSet<>(); private ForEachFilteredUniNode node(TupleLifecycle> downstream) { - return new ForEachFilteredUniNode<>(String.class, passing::contains, downstream, 1); + return new ForEachFilteredUniNode<>(String.class, passing::contains, downstream, false, 1); } @SuppressWarnings("unchecked") diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java index 530b4205c8d..655be6a2a03 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java @@ -69,6 +69,7 @@ protected void assertScore(InnerScoreDirector assertableMatch.score) .sum(); if (implSupport.constraintMatchPolicy().isJustificationEnabled()) { + var indictmentsEnabled = implSupport.constraintMatchPolicy().isIndictmentsEnabled(); for (var assertableMatch : assertableMatches) { var constraintMatchTotals = scoreDirector.getConstraintMatchTotalMap(); @@ -78,7 +79,8 @@ protected void assertScore(InnerScoreDirector + assertableMatch.isEqualTo(obj, indictmentsEnabled))) { fail("The assertableMatch (" + assertableMatch + ") is lacking," + " it's not in the constraintMatchSet (" + constraintMatchTotal.getConstraintMatchSet() + ")."); @@ -89,7 +91,7 @@ protected void assertScore(InnerScoreDirector assertableMatch.constraintRef.equals(constraintMatch.getConstraintRef())) - .noneMatch(assertableMatch -> assertableMatch.isEqualTo(constraintMatch))) { + .noneMatch(assertableMatch -> assertableMatch.isEqualTo(constraintMatch, indictmentsEnabled))) { fail("The constraintMatch (" + constraintMatch + ") is in excess," + " it's not in the assertableMatches (" + Arrays.toString(assertableMatches) + ")."); } @@ -127,14 +129,21 @@ protected static class AssertableMatch { private final int score; private final ConstraintRef constraintRef; private final List justificationList; + private List indictmentList; public AssertableMatch(int score, ConstraintRef constraintRef, Object... justifications) { this.justificationList = Arrays.asList(justifications); this.constraintRef = constraintRef; this.score = score; + this.indictmentList = justificationList; } - public boolean isEqualTo(ConstraintMatch constraintMatch) { + public AssertableMatch withIndictedObjects(Object... indictedObjects) { + this.indictmentList = Arrays.asList(indictedObjects); + return this; + } + + public boolean isEqualTo(ConstraintMatch constraintMatch, boolean indictmentsEnabled) { if (score != ((SimpleScore) constraintMatch.getScore()).score()) { return false; } @@ -148,19 +157,32 @@ public boolean isEqualTo(ConstraintMatch constraintMatch) { return false; } // Can't simply compare the lists, since the elements may be in different orders. The order is not relevant. - return justificationList.containsAll(actualJustificationList); + if (!justificationList.containsAll(actualJustificationList)) { + return false; + } } else { // Support for custom justification mapping. if (justificationList.size() != 1) { Assertions.fail("Expected number of justifications (" + justificationList.size() + ") does not match actual (1; " + justification + ")."); } - return justification == justificationList.getFirst(); + if (justification != justificationList.getFirst()) { + return false; + } + } + if (!indictmentsEnabled) { + return true; + } + var indictedObjects = constraintMatch.getIndictedObjects(); + if (indictedObjects.size() != indictmentList.size()) { + return false; } + return indictmentList.containsAll(indictedObjects); } @Override public String toString() { - return constraintRef + " " + justificationList + "=" + score; + return "%s %s=%d (indicting %s)".formatted(constraintRef, justificationList, score, + indictmentList); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java index d2edef784a4..531de75f5d4 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java @@ -32,9 +32,20 @@ public boolean supportsTestTemplate(ExtensionContext context) { @Override public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { - return Stream - .of(ConstraintMatchPolicy.ENABLED, ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, + if (ConstraintStreamPrecomputeTest.class.isAssignableFrom(context.getTestClass().get())) { + // Precompute test use + return Stream + .of(ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, ConstraintMatchPolicy.DISABLED) + .map(ConstraintStreamTestExtension::invocationContext); + } + return Stream + .of(ConstraintMatchPolicy.ENABLED, + ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS, + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, + ConstraintMatchPolicy.DISABLED) .map(ConstraintStreamTestExtension::invocationContext); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index 883b8856886..04e971e2eda 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -462,8 +462,15 @@ public void ifExists_0Joiner0Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstValueGroup()), - assertMatch(valueGroup)); + assertMatch(solution.getFirstValueGroup()) + .withIndictedObjects( + solution.getFirstValueGroup(), + solution.getFirstEntityGroup(), + entityGroup), + assertMatch(valueGroup).withIndictedObjects( + valueGroup, + solution.getFirstEntityGroup(), + entityGroup)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); From 84d5815c19fb7c55c554523088cf391777ec6811 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 11:51:36 -0400 Subject: [PATCH 04/18] chore: make UniConstraintStream tests aware of indictments --- .../common/AbstractIndexedIfExistsNode.java | 3 +- .../inliner/ConstraintMatchSupplier.java | 12 +- .../common/AbstractConstraintStreamTest.java | 4 +- .../common/ConstraintStreamTestExtension.java | 12 +- .../uni/AbstractUniConstraintStreamTest.java | 618 +++++++++++------- 5 files changed, 399 insertions(+), 250 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index 10154b232cd..9d3e78d2b50 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -255,7 +255,8 @@ public final void updateRight(UniTuple rightTuple) { if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { forEachLeftCounter(rightTuple, oldCompositeKey, this::decrementCounterRightWithoutIndictment); } else { - forEachLeftCounter(rightTuple, oldCompositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + forEachLeftCounter(rightTuple, oldCompositeKey, + counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); } } else { clearRightTrackerList(rightTuple); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java index 8ffa4b40ede..6780cd24475 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java @@ -73,7 +73,8 @@ static > ConstraintMatchSupplier of( return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, + collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA()); } @@ -97,7 +98,8 @@ static > ConstraintMatchSupplier of( return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, + collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB()); } @@ -110,7 +112,8 @@ static > ConstraintMatchSupplier o return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, + collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC()); } @@ -123,7 +126,8 @@ static > ConstraintMatchSupplier { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, + collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD()); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java index 655be6a2a03..6487bc17450 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java @@ -79,8 +79,8 @@ protected void assertScore(InnerScoreDirector - assertableMatch.isEqualTo(obj, indictmentsEnabled))) { + if (constraintMatchTotal.getConstraintMatchSet().stream() + .noneMatch(obj -> assertableMatch.isEqualTo(obj, indictmentsEnabled))) { fail("The assertableMatch (" + assertableMatch + ") is lacking," + " it's not in the constraintMatchSet (" + constraintMatchTotal.getConstraintMatchSet() + ")."); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java index 531de75f5d4..3953260c7f1 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java @@ -36,16 +36,16 @@ public Stream provideTestTemplateInvocationContex // Precompute test use return Stream .of(ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, - ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, - ConstraintMatchPolicy.DISABLED) + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, + ConstraintMatchPolicy.DISABLED) .map(ConstraintStreamTestExtension::invocationContext); } return Stream .of(ConstraintMatchPolicy.ENABLED, - ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, - ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS, - ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, - ConstraintMatchPolicy.DISABLED) + ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS, + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, + ConstraintMatchPolicy.DISABLED) .map(ConstraintStreamTestExtension::invocationContext); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index 04e971e2eda..2cc786a0f12 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -255,22 +255,22 @@ public void join_1Equal() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity1, entity3), - assertMatch(entity2, entity2), - assertMatch(entity3, entity1), - assertMatch(entity3, entity3)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity3, entity1).withIndictedObjects(entity3, entity1), + assertMatch(entity3, entity3).withIndictedObjects(entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); entity3.setValue(value2); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity2, entity2), - assertMatch(entity2, entity3), - assertMatch(entity3, entity2), - assertMatch(entity3, entity3)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2), + assertMatch(entity3, entity3).withIndictedObjects(entity3)); // Incremental for which the first change matches a join that doesn't survive the second change scoreDirector.beforeVariableChanged(entity1, "value"); @@ -280,11 +280,11 @@ public void join_1Equal() { entity3.setValue(value1); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity2, entity2), - assertMatch(entity1, entity2), - assertMatch(entity2, entity1), - assertMatch(entity3, entity3)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity1).withIndictedObjects(entity2, entity1), + assertMatch(entity3, entity3).withIndictedObjects(entity3)); } /** @@ -365,24 +365,26 @@ public void join_2Equal() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getFirstEntity()), - assertMatch(entity1, entity1), - assertMatch(entity1, entity2), - assertMatch(entity2, entity1), - assertMatch(entity2, entity2), - assertMatch(entity3, entity3)); + assertMatch(solution.getFirstEntity(), solution.getFirstEntity()) + .withIndictedObjects(solution.getFirstEntity()), + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity1).withIndictedObjects(entity2, entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity3, entity3).withIndictedObjects(entity3)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity1); entity1.setIntegerProperty(8); scoreDirector.afterProblemPropertyChanged(entity1); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getFirstEntity()), - assertMatch(entity1, entity1), - assertMatch(entity1, entity3), - assertMatch(entity2, entity2), - assertMatch(entity3, entity1), - assertMatch(entity3, entity3)); + assertMatch(solution.getFirstEntity(), solution.getFirstEntity()) + .withIndictedObjects(solution.getFirstEntity()), + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity3, entity1).withIndictedObjects(entity3, entity1), + assertMatch(entity3, entity3).withIndictedObjects(entity3)); } @Override @@ -412,24 +414,24 @@ public void joinAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, extra1), - assertMatch(1L, extra2)); + assertMatch(1L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(1L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, extra1), - assertMatch(2L, extra2)); + assertMatch(2L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(2L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, extra1), - assertMatch(1L, extra2)); + assertMatch(1L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(1L, extra2).withIndictedObjects(entity1, entity2, extra2)); } @Override @@ -477,8 +479,14 @@ public void ifExists_0Joiner0Filter() { solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(solution.getFirstValueGroup()), - assertMatch(valueGroup)); + assertMatch(solution.getFirstValueGroup()).withIndictedObjects( + solution.getFirstValueGroup(), + solution.getFirstEntityGroup(), + entityGroup), + assertMatch(valueGroup).withIndictedObjects( + valueGroup, + solution.getFirstEntityGroup(), + entityGroup)); } @Override @@ -503,17 +511,19 @@ public void ifExists_0Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup()), + assertMatch(entity1).withIndictedObjects(entity1, entityGroup), + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup()), + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); } @Override @@ -537,17 +547,19 @@ public void ifExists_1Join0Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup()), + assertMatch(entity1).withIndictedObjects(entity1, entityGroup), + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup()), + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); } @Override @@ -574,15 +586,15 @@ public void ifExists_1Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(entity1).withIndictedObjects(entity1, entityGroup), + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(entity2)); + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); } @TestTemplate @@ -605,16 +617,16 @@ public void ifExistsOther_1Join0Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), entity2), + assertMatch(entity2).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); entity2.setEntityGroup(entityGroup); scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(entity1).withIndictedObjects(entity1, entity2), + assertMatch(entity2).withIndictedObjects(entity2)); } @Override @@ -641,16 +653,16 @@ public void ifExistsDoesNotIncludeUnassigned() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), entity2), + assertMatch(entity2).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); entity2.setEntityGroup(entityGroup); scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(entity1).withIndictedObjects(entity1, entity2), + assertMatch(entity2).withIndictedObjects(entity2)); } @Override @@ -877,21 +889,21 @@ public void ifExistsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L)); + assertMatch(1L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L)); + assertMatch(2L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L)); + assertMatch(1L).withIndictedObjects(entity1, entity2)); } @TestTemplate @@ -1043,7 +1055,7 @@ public void forEach_listVarNotAllowsUnassignedValues_noInverseVar() { // v2 is not assigned, so it should not be matched scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(v1)); + assertMatch(v1).withIndictedObjects(v1, e1)); // Incremental scoreDirector.beforeListVariableChanged(e2, "valueList", 0, 1); @@ -1052,8 +1064,8 @@ public void forEach_listVarNotAllowsUnassignedValues_noInverseVar() { scoreDirector.afterListVariableElementAssigned(e2, "valueList", v2); scoreDirector.afterListVariableChanged(e2, "valueList", 0, 1); assertScore(scoreDirector, - assertMatch(v1), - assertMatch(v2)); + assertMatch(v1).withIndictedObjects(v1, e1), + assertMatch(v2).withIndictedObjects(v2, e2)); scoreDirector.beforeListVariableChanged(e1, "valueList", 0, 0); scoreDirector.beforeListVariableElementUnassigned(e1, "valueList", v1); @@ -1061,7 +1073,7 @@ public void forEach_listVarNotAllowsUnassignedValues_noInverseVar() { scoreDirector.afterListVariableElementUnassigned(e1, "valueList", v1); scoreDirector.afterListVariableChanged(e1, "valueList", 0, 0); assertScore(scoreDirector, - assertMatch(v2)); + assertMatch(v2).withIndictedObjects(v2, e2)); } @TestTemplate @@ -1414,7 +1426,7 @@ public void groupBy_1Mapping0Collect_filtered() { .asConstraint(TEST_CONSTRAINT_ID)); scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-1, entityGroup1)); + assertScore(scoreDirector, assertMatchWithScore(-1, entityGroup1).withIndictedObjects(entity1, entity2)); } @TestTemplate @@ -1439,8 +1451,16 @@ public void groupBy_1Mapping1Collect_filtered() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, 2L), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 8L)); + assertMatchWithScore(-1, entityGroup1, 2L).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 8L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity3)); } @TestTemplate @@ -1466,8 +1486,8 @@ public void groupBy_joinedAndFiltered() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, entity1), - assertMatchWithScore(-1, entityGroup1, entity2)); + assertMatchWithScore(-1, entityGroup1, entity1).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, entityGroup1, entity2).withIndictedObjects(entity1, entity2)); } @Override @@ -1493,8 +1513,16 @@ public void groupBy_1Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup()), - assertMatchWithScore(-1, entityGroup1)); + assertMatchWithScore(-1, solution.getFirstEntityGroup()).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity3), + assertMatchWithScore(-1, entityGroup1).withIndictedObjects(entity1, entity2)); // Incremental Stream.of(entity1, entity2).forEach(entity -> { @@ -1502,7 +1530,15 @@ public void groupBy_1Mapping0Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); }); - assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup())); + assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup()).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity3)); } @Override @@ -1518,10 +1554,13 @@ public void groupBy_1Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L), - assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L)); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L) + .withIndictedObjects(solution.getEntityList().get(1))); // Incremental + var originalFirstEntity = solution.getFirstEntity(); Stream.of(solution.getEntityList().get(0), solution.getEntityList().get(1)) .forEach(entity -> { scoreDirector.beforeEntityRemoved(entity); @@ -1529,7 +1568,8 @@ public void groupBy_1Mapping1Collector() { scoreDirector.afterEntityRemoved(entity); }); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L)); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L) + .withIndictedObjects(originalFirstEntity, solution.getEntityList().get(0))); } @Override @@ -1551,16 +1591,20 @@ public void groupBy_1Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L, asSet(entity1, entity3)), - assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, Collections.singleton(entity2))); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L, asSet(entity1, entity3)) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, Collections.singleton(entity2)) + .withIndictedObjects(solution.getEntityList().get(1))); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, Collections.singleton(entity3)), - assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, Collections.singleton(entity2))); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, Collections.singleton(entity3)) + .withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, Collections.singleton(entity2)) + .withIndictedObjects(entity2)); } @Override @@ -1583,16 +1627,20 @@ public void groupBy_1Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L, 2, asSet(entity1, entity3)), - assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, 1, Collections.singleton(entity2))); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L, 2, asSet(entity1, entity3)) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, 1, Collections.singleton(entity2)) + .withIndictedObjects(solution.getEntityList().get(1))); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, 1, Collections.singleton(entity3)), - assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, 1, Collections.singleton(entity2))); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, 1, Collections.singleton(entity3)) + .withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, 1, Collections.singleton(entity2)) + .withIndictedObjects(entity2)); } @Override @@ -1617,13 +1665,29 @@ public void groupBy_0Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-10, 10L)); + assertScore(scoreDirector, assertMatchWithScore(-10, 10L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity3); solution.getEntityList().remove(entity3); scoreDirector.afterEntityRemoved(entity3); - assertScore(scoreDirector, assertMatchWithScore(-9, 9L)); + assertScore(scoreDirector, assertMatchWithScore(-9, 9L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity1, entity2, entity3)); } @Override @@ -1641,13 +1705,15 @@ public void groupBy_0Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 3)); + assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 3).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1), solution.getEntityList().get(2))); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); - assertScore(scoreDirector, assertMatchWithScore(-1, 2L, 2)); + assertScore(scoreDirector, assertMatchWithScore(-1, 2L, 2).withIndictedObjects(entity1, solution.getEntityList().get(0), + solution.getEntityList().get(1))); } @Override @@ -1672,14 +1738,16 @@ public void groupBy_0Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0, 2)); + assertMatchWithScore(-1, 3L, 0, 2).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1), solution.getEntityList().get(2))); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 2L, 1, 2)); + assertMatchWithScore(-1, 2L, 1, 2).withIndictedObjects(entity1, solution.getEntityList().get(0), + solution.getEntityList().get(1))); } @Override @@ -1705,14 +1773,17 @@ public void groupBy_0Mapping4Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0, 2, asSet(entity1, entity2, entity3))); + assertMatchWithScore(-1, 3L, 0, 2, asSet(entity1, entity2, entity3)).withIndictedObjects( + solution.getFirstEntity(), solution.getEntityList().get(1), solution.getEntityList().get(2))); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 2L, 1, 2, asSet(entity2, entity3))); + assertMatchWithScore(-1, 2L, 1, 2, asSet(entity2, entity3)).withIndictedObjects(entity1, + solution.getEntityList().get(0), + solution.getEntityList().get(1))); } @TestTemplate @@ -1740,17 +1811,33 @@ public void groupBy_1Mapping1Collector_groupingOnPrimitives() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-8, 1, 8L), - assertMatchWithScore(-1, 2, 1L), - assertMatchWithScore(-1, 3, 1L)); + assertMatchWithScore(-8, 1, 8L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity1), + assertMatchWithScore(-1, 2, 1L).withIndictedObjects(entity2), + assertMatchWithScore(-1, 3, 1L).withIndictedObjects(entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity3); solution.getEntityList().remove(entity3); scoreDirector.afterEntityRemoved(entity3); assertScore(scoreDirector, - assertMatchWithScore(-8, 1, 8L), - assertMatchWithScore(-1, 2, 1L)); + assertMatchWithScore(-8, 1, 8L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity1), + assertMatchWithScore(-1, 2, 1L).withIndictedObjects(entity2)); } @Override @@ -1776,37 +1863,52 @@ public void groupBy_2Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()), - assertMatchWithScore(-1, entityGroup1, secondValue), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4))); + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, entityGroup1, secondValue).withIndictedObjects(entity3), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)) + .withIndictedObjects(solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)) + .withIndictedObjects(solution.getEntityList().get(3)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4)) + .withIndictedObjects(solution.getEntityList().get(4))); // Incremental scoreDirector.beforeEntityRemoved(entity3); solution.getEntityList().remove(entity3); scoreDirector.afterEntityRemoved(entity3); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4))); + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)) + .withIndictedObjects(solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)) + .withIndictedObjects(solution.getEntityList().get(3)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4)) + .withIndictedObjects(solution.getEntityList().get(4))); // Ensure that the first match is still there when entity2, as it still has entity1 scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4))); + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)) + .withIndictedObjects(solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)) + .withIndictedObjects(solution.getEntityList().get(3)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4)) + .withIndictedObjects(solution.getEntityList().get(4))); } @Override @@ -1833,17 +1935,32 @@ public void groupBy_2Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-7, solution.getFirstEntityGroup(), solution.getFirstValue(), 7L), - assertMatchWithScore(-2, entityGroup1, value1, 2L), - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue(), 1L)); + assertMatchWithScore(-7, solution.getFirstEntityGroup(), solution.getFirstValue(), 7L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6)), + assertMatchWithScore(-2, entityGroup1, value1, 2L).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue(), 1L).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); entity2.setEntityGroup(solution.getFirstEntityGroup()); scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, - assertMatchWithScore(-8, solution.getFirstEntityGroup(), solution.getFirstValue(), 8L), - assertMatchWithScore(-2, entityGroup1, value1, 2L)); + assertMatchWithScore(-8, solution.getFirstEntityGroup(), solution.getFirstValue(), 8L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity2), + assertMatchWithScore(-2, entityGroup1, value1, 2L).withIndictedObjects(entity1, entity3)); } @Override @@ -1870,17 +1987,32 @@ public void groupBy_2Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-7, solution.getFirstEntityGroup(), solution.getFirstValue(), 7L, 7L), - assertMatchWithScore(-2, entityGroup1, value1, 2L, 2L), - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue(), 1L, 1L)); + assertMatchWithScore(-7, solution.getFirstEntityGroup(), solution.getFirstValue(), 7L, 7L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6)), + assertMatchWithScore(-2, entityGroup1, value1, 2L, 2L).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue(), 1L, 1L).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); entity2.setEntityGroup(solution.getFirstEntityGroup()); scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, - assertMatchWithScore(-8, solution.getFirstEntityGroup(), solution.getFirstValue(), 8L, 8L), - assertMatchWithScore(-2, entityGroup1, value1, 2L, 2L)); + assertMatchWithScore(-8, solution.getFirstEntityGroup(), solution.getFirstValue(), 8L, 8L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity2), + assertMatchWithScore(-2, entityGroup1, value1, 2L, 2L).withIndictedObjects(entity1, entity3)); } @Override @@ -1909,21 +2041,21 @@ public void groupBy_3Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, value1, entity1.getCode()), - assertMatchWithScore(-1, group2, value2, entity2.getCode()), - assertMatchWithScore(-1, group1, value3, entity3.getCode()), - assertMatchWithScore(-1, group2, value1, entity4.getCode()), - assertMatchWithScore(-1, group1, value2, entity5.getCode())); + assertMatchWithScore(-1, group1, value1, entity1.getCode()).withIndictedObjects(entity1), + assertMatchWithScore(-1, group2, value2, entity2.getCode()).withIndictedObjects(entity2), + assertMatchWithScore(-1, group1, value3, entity3.getCode()).withIndictedObjects(entity3), + assertMatchWithScore(-1, group2, value1, entity4.getCode()).withIndictedObjects(entity4), + assertMatchWithScore(-1, group1, value2, entity5.getCode()).withIndictedObjects(entity5)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, entity2.getCode()), - assertMatchWithScore(-1, group1, value3, entity3.getCode()), - assertMatchWithScore(-1, group2, value1, entity4.getCode()), - assertMatchWithScore(-1, group1, value2, entity5.getCode())); + assertMatchWithScore(-1, group2, value2, entity2.getCode()).withIndictedObjects(entity2), + assertMatchWithScore(-1, group1, value3, entity3.getCode()).withIndictedObjects(entity3), + assertMatchWithScore(-1, group2, value1, entity4.getCode()).withIndictedObjects(entity4), + assertMatchWithScore(-1, group1, value2, entity5.getCode()).withIndictedObjects(entity5)); } @Override @@ -1952,21 +2084,30 @@ public void groupBy_3Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, value1, entity1.getCode(), Collections.singleton(entity1)), - assertMatchWithScore(-1, group2, value2, entity2.getCode(), Collections.singleton(entity2)), - assertMatchWithScore(-1, group1, value3, entity3.getCode(), Collections.singleton(entity3)), - assertMatchWithScore(-1, group2, value1, entity4.getCode(), Collections.singleton(entity4)), - assertMatchWithScore(-1, group1, value2, entity5.getCode(), Collections.singleton(entity5))); + assertMatchWithScore(-1, group1, value1, entity1.getCode(), Collections.singleton(entity1)) + .withIndictedObjects(entity1), + assertMatchWithScore(-1, group2, value2, entity2.getCode(), Collections.singleton(entity2)) + .withIndictedObjects(entity2), + assertMatchWithScore(-1, group1, value3, entity3.getCode(), Collections.singleton(entity3)) + .withIndictedObjects(entity3), + assertMatchWithScore(-1, group2, value1, entity4.getCode(), Collections.singleton(entity4)) + .withIndictedObjects(entity4), + assertMatchWithScore(-1, group1, value2, entity5.getCode(), Collections.singleton(entity5)) + .withIndictedObjects(entity5)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, entity2.getCode(), Collections.singleton(entity2)), - assertMatchWithScore(-1, group1, value3, entity3.getCode(), Collections.singleton(entity3)), - assertMatchWithScore(-1, group2, value1, entity4.getCode(), Collections.singleton(entity4)), - assertMatchWithScore(-1, group1, value2, entity5.getCode(), Collections.singleton(entity5))); + assertMatchWithScore(-1, group2, value2, entity2.getCode(), Collections.singleton(entity2)) + .withIndictedObjects(entity2), + assertMatchWithScore(-1, group1, value3, entity3.getCode(), Collections.singleton(entity3)) + .withIndictedObjects(entity3), + assertMatchWithScore(-1, group2, value1, entity4.getCode(), Collections.singleton(entity4)) + .withIndictedObjects(entity4), + assertMatchWithScore(-1, group1, value2, entity5.getCode(), Collections.singleton(entity5)) + .withIndictedObjects(entity5)); } @Override @@ -1995,21 +2136,21 @@ public void groupBy_4Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity1, group1, value1, entity1.getCode()), - assertMatchWithScore(-1, entity2, group2, value2, entity2.getCode()), - assertMatchWithScore(-1, entity3, group1, value3, entity3.getCode()), - assertMatchWithScore(-1, entity4, group2, value1, entity4.getCode()), - assertMatchWithScore(-1, entity5, group1, value2, entity5.getCode())); + assertMatchWithScore(-1, entity1, group1, value1, entity1.getCode()).withIndictedObjects(entity1), + assertMatchWithScore(-1, entity2, group2, value2, entity2.getCode()).withIndictedObjects(entity2), + assertMatchWithScore(-1, entity3, group1, value3, entity3.getCode()).withIndictedObjects(entity3), + assertMatchWithScore(-1, entity4, group2, value1, entity4.getCode()).withIndictedObjects(entity4), + assertMatchWithScore(-1, entity5, group1, value2, entity5.getCode()).withIndictedObjects(entity5)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, entity2, group2, value2, entity2.getCode()), - assertMatchWithScore(-1, entity3, group1, value3, entity3.getCode()), - assertMatchWithScore(-1, entity4, group2, value1, entity4.getCode()), - assertMatchWithScore(-1, entity5, group1, value2, entity5.getCode())); + assertMatchWithScore(-1, entity2, group2, value2, entity2.getCode()).withIndictedObjects(entity2), + assertMatchWithScore(-1, entity3, group1, value3, entity3.getCode()).withIndictedObjects(entity3), + assertMatchWithScore(-1, entity4, group2, value1, entity4.getCode()).withIndictedObjects(entity4), + assertMatchWithScore(-1, entity5, group1, value2, entity5.getCode()).withIndictedObjects(entity5)); } @Override @@ -2047,8 +2188,8 @@ public void mapToUniWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group), - assertMatch(group)); + assertMatch(group).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2057,7 +2198,7 @@ public void mapToUniWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group)); + assertMatch(group).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2076,8 +2217,8 @@ public void mapToUniWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2086,7 +2227,7 @@ public void mapToUniWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2)); + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2105,7 +2246,7 @@ public void mapToUniAndDistinctWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group)); + assertMatch(group).withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2114,7 +2255,7 @@ public void mapToUniAndDistinctWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group)); + assertMatch(group).withIndictedObjects(entity, solution.getEntityList().get(0))); } @Override @@ -2134,8 +2275,8 @@ public void mapToUniAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2144,7 +2285,7 @@ public void mapToUniAndDistinctWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2)); + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2166,8 +2307,8 @@ public void mapToBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, value1), - assertMatch(group2, value2)); + assertMatch(group1, value1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2, value2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2176,7 +2317,7 @@ public void mapToBi() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2)); + assertMatch(group2, value2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2201,8 +2342,8 @@ public void mapToTri() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, value1, code1), - assertMatch(group2, value2, code2)); + assertMatch(group1, value1, code1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2, value2, code2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2211,7 +2352,7 @@ public void mapToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2, code2)); + assertMatch(group2, value2, code2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2239,8 +2380,8 @@ public void mapToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, value1, code1, property1), - assertMatch(group2, value2, code2, property2)); + assertMatch(group1, value1, code1, property1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2, value2, code2, property2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2249,7 +2390,7 @@ public void mapToQuad() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2, code2, property2)); + assertMatch(group2, value2, code2, property2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2268,8 +2409,8 @@ public void expandToBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group1), - assertMatch(solution.getEntityList().get(1), group2)); + assertMatch(solution.getFirstEntity(), group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(solution.getEntityList().get(1), group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2278,7 +2419,7 @@ public void expandToBi() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2)); + assertMatch(solution.getFirstEntity(), group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2299,8 +2440,9 @@ public void expandToTri() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group1, value1), - assertMatch(solution.getEntityList().get(1), group2, value2)); + assertMatch(solution.getFirstEntity(), group1, value1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(solution.getEntityList().get(1), group2, value2) + .withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2309,7 +2451,7 @@ public void expandToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2, value2)); + assertMatch(solution.getFirstEntity(), group2, value2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2333,8 +2475,9 @@ public void expandToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group1, value1, code1), - assertMatch(solution.getEntityList().get(1), group2, value2, code2)); + assertMatch(solution.getFirstEntity(), group1, value1, code1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(solution.getEntityList().get(1), group2, value2, code2) + .withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2343,7 +2486,8 @@ public void expandToQuad() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2, value2, code2)); + assertMatch(solution.getFirstEntity(), group2, value2, code2) + .withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2369,11 +2513,11 @@ public void flattenLastWithDuplicates() { // group1 is used instead of group1Dup because it equals to it scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group1), - assertMatch(group2), - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group1).withIndictedObjects(solution.getEntityList().get(1)), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2383,17 +2527,17 @@ public void flattenLastWithDuplicates() { scoreDirector.afterVariableChanged(entity, "value"); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2), - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group1).withIndictedObjects(solution.getEntityList().get(1)), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getEntityList().get(0)), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2412,8 +2556,8 @@ public void flattenLastWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2422,7 +2566,7 @@ public void flattenLastWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2)); + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2444,18 +2588,18 @@ public void flatten() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, group1), - assertMatch(entity1, group2), - assertMatch(entity2, group1), - assertMatch(entity2, group2)); + assertMatch(entity1, group1).withIndictedObjects(entity1), + assertMatch(entity1, group2).withIndictedObjects(entity1), + assertMatch(entity2, group1).withIndictedObjects(entity2), + assertMatch(entity2, group2).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(entity2, group1), - assertMatch(entity2, group2)); + assertMatch(entity2, group1).withIndictedObjects(entity2), + assertMatch(entity2, group2).withIndictedObjects(entity2)); } @Override @@ -2475,8 +2619,8 @@ public void flattenLastAndDistinctWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1)), + assertMatch(group2).withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2485,8 +2629,8 @@ public void flattenLastAndDistinctWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(entity, solution.getEntityList().get(0)), + assertMatch(group2).withIndictedObjects(entity, solution.getEntityList().get(0))); } @Override @@ -2506,8 +2650,8 @@ public void flattenLastAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2516,7 +2660,7 @@ public void flattenLastAndDistinctWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2)); + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2725,8 +2869,8 @@ public void concatBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null), - assertMatch(entity2, entity3)); + assertMatch(entity1, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2737,8 +2881,8 @@ public void concatBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null), - assertMatch(entity3, entity2)); + assertMatch(entity1, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2)); } @Override @@ -2772,8 +2916,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null), - assertMatch(entity2, entity3)); + assertMatch(entity1, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2784,8 +2928,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null), - assertMatch(entity3, entity2)); + assertMatch(entity1, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2)); } @Override @@ -2820,8 +2964,8 @@ public void concatTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null, null), - assertMatch(entity2, entity3, entity1)); + assertMatch(entity1, null, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3, entity1).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2832,8 +2976,8 @@ public void concatTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null, null), - assertMatch(entity3, entity2, entity1)); + assertMatch(entity1, null, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2, entity1).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2869,8 +3013,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null, null), - assertMatch(entity2, entity3, entity1)); + assertMatch(entity1, null, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3, entity1).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2881,8 +3025,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null, null), - assertMatch(entity3, entity2, entity1)); + assertMatch(entity1, null, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2, entity1).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2919,8 +3063,8 @@ public void concatQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null, null, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, null, null, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2931,8 +3075,8 @@ public void concatQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null, null, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, null, null, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2970,8 +3114,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null, null, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, null, null, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2982,8 +3126,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null, null, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, null, null, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -3016,16 +3160,16 @@ public void concatAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, 1L), - assertMatchWithScore(-1, value2, 1L)); + assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity1), + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); entity3.setValue(value2); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, 1L), - assertMatchWithScore(-2, value2, 2L)); + assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity1), + assertMatchWithScore(-2, value2, 2L).withIndictedObjects(entity2, entity3)); // Incremental for which the first change matches a join that doesn't survive the second change scoreDirector.beforeVariableChanged(entity1, "value"); @@ -3035,8 +3179,8 @@ public void concatAfterGroupBy() { entity3.setValue(value1); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, 1L), - assertMatchWithScore(-1, value2, 1L)); + assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity3), + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, entity3)); } @Override From b67e7809cf1953096f6387a6849adc85a07c2433 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 12:10:41 -0400 Subject: [PATCH 05/18] fix: remove old indicted objects from original group in GroupNode --- .../impl/bavet/common/AbstractGroupNode.java | 4 ++ .../tuple/indictment/IndictmentSource.java | 14 ++++ .../uni/AbstractUniConstraintStreamTest.java | 68 +++++++++---------- 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java index a6c830fc9f0..0c588892276 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java @@ -192,6 +192,8 @@ public final void update(InTuple_ tuple) { if (sameKey) { updateGroup(tuple, oldGroup); } else { + var oldOutTuple = oldGroup.getTuple(); + oldOutTuple.setIndictmentSource(IndictmentSource.removeFromAggregate(tuple, oldOutTuple)); if (hasCollector) { groupRetract(tuple); } @@ -276,6 +278,8 @@ public final void retract(InTuple_ tuple) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } + var oldOutTuple = group.getTuple(); + oldOutTuple.setIndictmentSource(IndictmentSource.removeFromAggregate(tuple, oldOutTuple)); if (hasCollector) { groupRetract(tuple); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index 65cc64d2adc..8340f1c23d9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -36,6 +36,20 @@ static IndictmentSource aggregating(Tuple elementTuple, Tuple groupTuple) { } } + static IndictmentSource removeFromAggregate(Tuple elementTuple, Tuple groupTuple) { + if (elementTuple.getIndictmentSource() == DISABLED) { + return DISABLED; + } + if (groupTuple.getIndictmentSource() instanceof AggregateIndictmentSource aggregateIndictmentSource) { + aggregateIndictmentSource.sourceList.remove(elementTuple.getIndictmentSource()); + return aggregateIndictmentSource; + } else { + var collection = new ArrayList(); + collection.add(elementTuple.getIndictmentSource()); + return new AggregateIndictmentSource(collection); + } + } + static IndictmentSource sourceWithSupport(Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { return DISABLED; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index 2cc786a0f12..8ae79d3d862 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -163,7 +163,7 @@ public void filter_entity() { @TestTemplate public void filter_consecutive() { var solution = TestdataLavishSolution.generateSolution(4, 4); - var entity1 = solution.getEntityList().get(0); + var entity1 = solution.getEntityList().getFirst(); var entity2 = solution.getEntityList().get(1); var entity3 = solution.getEntityList().get(2); var entity4 = solution.getEntityList().get(3); @@ -430,8 +430,8 @@ public void joinAfterGroupBy() { solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, extra1).withIndictedObjects(entity1, entity2, extra1), - assertMatch(1L, extra2).withIndictedObjects(entity1, entity2, extra2)); + assertMatch(1L, extra1).withIndictedObjects(entity1, extra1), + assertMatch(1L, extra2).withIndictedObjects(entity1, extra2)); } @Override @@ -903,7 +903,7 @@ public void ifExistsAfterGroupBy() { solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L).withIndictedObjects(entity1, entity2)); + assertMatch(1L).withIndictedObjects(entity1)); } @TestTemplate @@ -1561,7 +1561,7 @@ public void groupBy_1Mapping1Collector() { // Incremental var originalFirstEntity = solution.getFirstEntity(); - Stream.of(solution.getEntityList().get(0), solution.getEntityList().get(1)) + Stream.of(solution.getEntityList().getFirst(), solution.getEntityList().get(1)) .forEach(entity -> { scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); @@ -1569,7 +1569,7 @@ public void groupBy_1Mapping1Collector() { }); assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L) - .withIndictedObjects(originalFirstEntity, solution.getEntityList().get(0))); + .withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -1602,7 +1602,7 @@ public void groupBy_1Mapping2Collector() { scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, Collections.singleton(entity3)) - .withIndictedObjects(entity1, entity3), + .withIndictedObjects(entity3), assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, Collections.singleton(entity2)) .withIndictedObjects(entity2)); } @@ -1638,7 +1638,7 @@ public void groupBy_1Mapping3Collector() { scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, 1, Collections.singleton(entity3)) - .withIndictedObjects(entity1, entity3), + .withIndictedObjects(entity3), assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, 1, Collections.singleton(entity2)) .withIndictedObjects(entity2)); } @@ -1687,7 +1687,7 @@ public void groupBy_0Mapping1Collector() { solution.getEntityList().get(4), solution.getEntityList().get(5), solution.getEntityList().get(6), - entity1, entity2, entity3)); + entity1, entity2)); } @Override @@ -1712,7 +1712,7 @@ public void groupBy_0Mapping2Collector() { scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); - assertScore(scoreDirector, assertMatchWithScore(-1, 2L, 2).withIndictedObjects(entity1, solution.getEntityList().get(0), + assertScore(scoreDirector, assertMatchWithScore(-1, 2L, 2).withIndictedObjects(solution.getEntityList().getFirst(), solution.getEntityList().get(1))); } @@ -1746,7 +1746,7 @@ public void groupBy_0Mapping3Collector() { solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 2L, 1, 2).withIndictedObjects(entity1, solution.getEntityList().get(0), + assertMatchWithScore(-1, 2L, 1, 2).withIndictedObjects(solution.getEntityList().getFirst(), solution.getEntityList().get(1))); } @@ -1781,8 +1781,8 @@ public void groupBy_0Mapping4Collector() { solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 2L, 1, 2, asSet(entity2, entity3)).withIndictedObjects(entity1, - solution.getEntityList().get(0), + assertMatchWithScore(-1, 2L, 1, 2, asSet(entity2, entity3)).withIndictedObjects( + solution.getEntityList().getFirst(), solution.getEntityList().get(1))); } @@ -1865,7 +1865,7 @@ public void groupBy_2Mapping0Collector() { assertScore(scoreDirector, assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), assertMatchWithScore(-1, entityGroup1, secondValue).withIndictedObjects(entity3), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().getFirst()) .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), @@ -1882,7 +1882,7 @@ public void groupBy_2Mapping0Collector() { scoreDirector.afterEntityRemoved(entity3); assertScore(scoreDirector, assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().getFirst()) .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), @@ -1898,8 +1898,8 @@ public void groupBy_2Mapping0Collector() { solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().getFirst()) .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), @@ -2198,7 +2198,7 @@ public void mapToUniWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2227,7 +2227,7 @@ public void mapToUniWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2255,7 +2255,7 @@ public void mapToUniAndDistinctWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group).withIndictedObjects(entity, solution.getEntityList().get(0))); + assertMatch(group).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2285,7 +2285,7 @@ public void mapToUniAndDistinctWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2317,7 +2317,7 @@ public void mapToBi() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2, value2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2352,7 +2352,7 @@ public void mapToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2, code2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2, value2, code2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2390,7 +2390,7 @@ public void mapToQuad() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2, code2, property2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2, value2, code2, property2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2419,7 +2419,7 @@ public void expandToBi() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(solution.getFirstEntity(), group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2451,7 +2451,7 @@ public void expandToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2, value2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(solution.getFirstEntity(), group2, value2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2487,7 +2487,7 @@ public void expandToQuad() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatch(solution.getFirstEntity(), group2, value2, code2) - .withIndictedObjects(solution.getEntityList().get(0))); + .withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2536,8 +2536,8 @@ public void flattenLastWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group1).withIndictedObjects(solution.getEntityList().get(0)), - assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group1).withIndictedObjects(solution.getEntityList().getFirst()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2566,7 +2566,7 @@ public void flattenLastWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2629,8 +2629,8 @@ public void flattenLastAndDistinctWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group1).withIndictedObjects(entity, solution.getEntityList().get(0)), - assertMatch(group2).withIndictedObjects(entity, solution.getEntityList().get(0))); + assertMatch(group1).withIndictedObjects(solution.getEntityList().getFirst()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2660,7 +2660,7 @@ public void flattenLastAndDistinctWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -3180,7 +3180,7 @@ public void concatAfterGroupBy() { scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity3), - assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, entity3)); + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2)); } @Override From 57b24b180db2c8f1d8f0368d887fdee5c241ef7a Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 13:38:52 -0400 Subject: [PATCH 06/18] chore: move indictment code for IfExists to IndictmentSource --- .../bavet/common/AbstractIfExistsNode.java | 8 ++------ .../core/impl/bavet/common/tuple/Tuple.java | 2 +- .../bavet/common/tuple/UniversalTuple.java | 4 ++-- .../tuple/indictment/IndictmentSource.java | 18 +++++++++--------- .../inliner/ConstraintMatchSupplier.java | 5 +++-- .../uni/AbstractUniConstraintStreamTest.java | 6 ++---- 6 files changed, 19 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index c3ed5e96192..649b4af144b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -1,7 +1,5 @@ package ai.timefold.solver.core.impl.bavet.common; -import java.util.Objects; - import ai.timefold.solver.core.impl.bavet.common.tuple.InTupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; @@ -126,15 +124,13 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - counter.getTuple().getIndictmentSupportForNodeId(getId()) - .add(Objects.requireNonNull(rightTuple.getA())); + IndictmentSource.addSupport(getId(), counter.leftTuple, rightTuple); counter.countRight++; } protected void decrementCounterRight(ExistsCounter counter) { counter.countRight--; - counter.getTuple().getIndictmentSupportForNodeId(getId()) - .remove(Objects.requireNonNull(rightTuple.getA())); + IndictmentSource.removeSupport(getId(), counter.leftTuple, rightTuple); if (counter.countRight == 0) { if (shouldExist) { doRetractCounter(counter); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java index f077455fdda..20c18c91d6c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java @@ -29,7 +29,7 @@ public sealed interface Tuple permits BiTuple, QuadTuple, TriTuple, UniTuple { void setIndictmentSource(IndictmentSource indictmentSource); - Set getIndictmentSupportForNodeId(long nodeId); + Set getIndictmentSupportForNodeId(long nodeId); TupleState getState(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java index fbeb4b1e426..b6fdc71f9f6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java @@ -28,7 +28,7 @@ final class UniversalTuple private @Nullable C c; private @Nullable D d; private IndictmentSource indictmentSource = IndictmentSource.DISABLED; - private @Nullable Map> nodeIdToIndictmentSupport; + private @Nullable Map> nodeIdToIndictmentSupport; private TupleState state = TupleState.DEAD; // It's the node's job to mark a new tuple as CREATING. UniversalTuple(int storeSize, int cardinality) { @@ -123,7 +123,7 @@ public void setIndictmentSource(IndictmentSource indictmentSource) { } @Override - public Set getIndictmentSupportForNodeId(long nodeId) { + public Set getIndictmentSupportForNodeId(long nodeId) { if (nodeIdToIndictmentSupport == null) { nodeIdToIndictmentSupport = new LinkedHashMap<>(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index 8340f1c23d9..b44a5146f02 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -50,18 +50,18 @@ static IndictmentSource removeFromAggregate(Tuple elementTuple, Tuple groupTuple } } - static IndictmentSource sourceWithSupport(Tuple carry, Tuple support) { + static void addSupport(long nodeId, Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { - return DISABLED; + return; } - if (carry.getIndictmentSource() instanceof IndictmentSourceWithSupport indictmentSourceWithSupport) { - indictmentSourceWithSupport.support.add(support.getIndictmentSource()); - return indictmentSourceWithSupport; - } else { - throw new IllegalStateException("Carry tuple (%s) does not have a %s %s; its source is (%s) instead." - .formatted(carry, IndictmentSourceWithSupport.class.getSimpleName(), IndictmentSource.class.getSimpleName(), - support.getIndictmentSource())); + carry.getIndictmentSupportForNodeId(nodeId).add(support.getIndictmentSource()); + } + + static void removeSupport(long nodeId, Tuple carry, Tuple support) { + if (carry.getIndictmentSource() == DISABLED) { + return; } + carry.getIndictmentSupportForNodeId(nodeId).remove(support.getIndictmentSource()); } record DisabledIndictmentSource() implements IndictmentSource { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java index 6780cd24475..d5ee7f3518e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java @@ -42,7 +42,6 @@ public interface ConstraintMatchSupplier> extends BiFunction> { - @SuppressWarnings("unchecked") static List collectIndictments(Constraint constraint, Tuple tuple) { if (tuple.getIndictmentSource() == IndictmentSource.DISABLED) { return Collections.emptyList(); @@ -51,7 +50,9 @@ static List collectIndictments(Constraint constraint, Tuple tuple) { tuple.getIndictmentSource().visitSources(out::add); var abstractConstraint = (AbstractConstraint) constraint; for (var involvedNodeId : Objects.requireNonNull(abstractConstraint.getInvolvedNodeIds())) { - out.addAll(tuple.getIndictmentSupportForNodeId(involvedNodeId)); + for (var indictmentSource : tuple.getIndictmentSupportForNodeId(involvedNodeId)) { + indictmentSource.visitSources(out::add); + } } return new ArrayList<>(out); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index 8ae79d3d862..e3745e9746b 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -481,12 +481,10 @@ public void ifExists_0Joiner0Filter() { assertScore(scoreDirector, assertMatch(solution.getFirstValueGroup()).withIndictedObjects( solution.getFirstValueGroup(), - solution.getFirstEntityGroup(), - entityGroup), + solution.getFirstEntityGroup()), assertMatch(valueGroup).withIndictedObjects( valueGroup, - solution.getFirstEntityGroup(), - entityGroup)); + solution.getFirstEntityGroup())); } @Override From 83b2569c678c9b5fd4041b00262619b71dbcfc93 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 14:47:31 -0400 Subject: [PATCH 07/18] chore: track support in indictment, not tuple This allows support infomation to be automatically passed when a new tuple is created (ex: in map/expand). --- .../core/impl/bavet/common/tuple/Tuple.java | 3 - .../bavet/common/tuple/UniversalTuple.java | 14 --- .../tuple/indictment/IndictmentSource.java | 118 ++++++++++++++---- .../inliner/ConstraintMatchSupplier.java | 8 +- .../uni/AbstractUniConstraintStreamTest.java | 3 +- 5 files changed, 98 insertions(+), 48 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java index 20c18c91d6c..31421c67e85 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java @@ -1,6 +1,5 @@ package ai.timefold.solver.core.impl.bavet.common.tuple; -import java.util.Set; import java.util.function.Function; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; @@ -29,8 +28,6 @@ public sealed interface Tuple permits BiTuple, QuadTuple, TriTuple, UniTuple { void setIndictmentSource(IndictmentSource indictmentSource); - Set getIndictmentSupportForNodeId(long nodeId); - TupleState getState(); void setState(TupleState state); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java index b6fdc71f9f6..63cadf46844 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java @@ -1,10 +1,5 @@ package ai.timefold.solver.core.impl.bavet.common.tuple; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; @@ -28,7 +23,6 @@ final class UniversalTuple private @Nullable C c; private @Nullable D d; private IndictmentSource indictmentSource = IndictmentSource.DISABLED; - private @Nullable Map> nodeIdToIndictmentSupport; private TupleState state = TupleState.DEAD; // It's the node's job to mark a new tuple as CREATING. UniversalTuple(int storeSize, int cardinality) { @@ -122,14 +116,6 @@ public void setIndictmentSource(IndictmentSource indictmentSource) { this.indictmentSource = indictmentSource; } - @Override - public Set getIndictmentSupportForNodeId(long nodeId) { - if (nodeIdToIndictmentSupport == null) { - nodeIdToIndictmentSupport = new LinkedHashMap<>(); - } - return nodeIdToIndictmentSupport.computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); - } - @Override public String toString() { return switch (cardinality) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index b44a5146f02..04f57047105 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -1,7 +1,13 @@ package ai.timefold.solver.core.impl.bavet.common.tuple.indictment; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; @@ -9,17 +15,40 @@ public sealed interface IndictmentSource { IndictmentSource DISABLED = new DisabledIndictmentSource(); - void visitSources(Consumer sourceConsumer); + void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer); + + Map> support(); + + default void visitSources(long[] involvedNodeIds, Consumer sourceConsumer) { + visitSources(new HashSet<>(), involvedNodeIds, sourceConsumer); + } + + default Set getSupportForNodeId(long nodeId) { + return support().computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); + } + + static boolean checkIfAlreadyVisitedAndVisitSupport(IndictmentSource self, Set visited, + long[] involvedNodeIds, Consumer sourceConsumer) { + if (!visited.add(self)) { + return true; + } + for (var nodeId : involvedNodeIds) { + for (var indictmentSource : self.support().getOrDefault(nodeId, Collections.emptySet())) { + indictmentSource.visitSources(visited, involvedNodeIds, sourceConsumer); + } + } + return false; + } static IndictmentSource of(Object source) { - return new RootIndictmentSource(source); + return new RootIndictmentSource(source, new LinkedHashMap<>()); } static IndictmentSource joining(Tuple left, Tuple right) { if (left.getIndictmentSource() == DISABLED) { return DISABLED; } - return new JoinedIndictmentSource(left.getIndictmentSource(), right.getIndictmentSource()); + return new JoinedIndictmentSource(left.getIndictmentSource(), right.getIndictmentSource(), new LinkedHashMap<>()); } static IndictmentSource aggregating(Tuple elementTuple, Tuple groupTuple) { @@ -32,7 +61,7 @@ static IndictmentSource aggregating(Tuple elementTuple, Tuple groupTuple) { } else { var collection = new ArrayList(); collection.add(elementTuple.getIndictmentSource()); - return new AggregateIndictmentSource(collection); + return new AggregateIndictmentSource(collection, new LinkedHashMap<>()); } } @@ -46,7 +75,7 @@ static IndictmentSource removeFromAggregate(Tuple elementTuple, Tuple groupTuple } else { var collection = new ArrayList(); collection.add(elementTuple.getIndictmentSource()); - return new AggregateIndictmentSource(collection); + return new AggregateIndictmentSource(collection, new LinkedHashMap<>()); } } @@ -54,54 +83,95 @@ static void addSupport(long nodeId, Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { return; } - carry.getIndictmentSupportForNodeId(nodeId).add(support.getIndictmentSource()); + carry.getIndictmentSource().getSupportForNodeId(nodeId).add(support.getIndictmentSource()); } static void removeSupport(long nodeId, Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { return; } - carry.getIndictmentSupportForNodeId(nodeId).remove(support.getIndictmentSource()); + carry.getIndictmentSource().getSupportForNodeId(nodeId).remove(support.getIndictmentSource()); } record DisabledIndictmentSource() implements IndictmentSource { @Override - public void visitSources(Consumer sourceConsumer) { + public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + throw new UnsupportedOperationException("Impossible state: indictments are disabled."); + } + + @Override + public Map> support() { throw new UnsupportedOperationException("Impossible state: indictments are disabled."); } } - record RootIndictmentSource(Object source) implements IndictmentSource { + record RootIndictmentSource(Object source, Map> support) implements IndictmentSource { @Override - public void visitSources(Consumer sourceConsumer) { + public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { + return; + } sourceConsumer.accept(source); } - } - record JoinedIndictmentSource(IndictmentSource left, IndictmentSource right) implements IndictmentSource { @Override - public void visitSources(Consumer sourceConsumer) { - left.visitSources(sourceConsumer); - right.visitSources(sourceConsumer); + public boolean equals(Object o) { + return this == o; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); } } - record AggregateIndictmentSource(List sourceList) implements IndictmentSource { + record JoinedIndictmentSource(IndictmentSource left, IndictmentSource right, + Map> support) implements IndictmentSource { @Override - public void visitSources(Consumer sourceConsumer) { - for (var source : sourceList) { - source.visitSources(sourceConsumer); + public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { + return; } + left.visitSources(visited, involvedNodeIds, sourceConsumer); + right.visitSources(visited, involvedNodeIds, sourceConsumer); + } + + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); } } - record IndictmentSourceWithSupport(IndictmentSource source, List support) implements IndictmentSource { + record AggregateIndictmentSource(List sourceList, + Map> support) implements IndictmentSource { @Override - public void visitSources(Consumer sourceConsumer) { - source.visitSources(sourceConsumer); - for (var support : support) { - support.visitSources(sourceConsumer); + public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { + return; + } + for (var source : sourceList) { + source.visitSources(involvedNodeIds, sourceConsumer); } } + + @Override + public Set getSupportForNodeId(long nodeId) { + return support.computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); + } + + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java index d5ee7f3518e..93d41787cae 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java @@ -47,13 +47,9 @@ static List collectIndictments(Constraint constraint, Tuple tuple) { return Collections.emptyList(); } var out = new LinkedHashSet<>(); - tuple.getIndictmentSource().visitSources(out::add); var abstractConstraint = (AbstractConstraint) constraint; - for (var involvedNodeId : Objects.requireNonNull(abstractConstraint.getInvolvedNodeIds())) { - for (var indictmentSource : tuple.getIndictmentSupportForNodeId(involvedNodeId)) { - indictmentSource.visitSources(out::add); - } - } + var involvedNodeIds = Objects.requireNonNull(abstractConstraint.getInvolvedNodeIds()); + tuple.getIndictmentSource().visitSources(involvedNodeIds, out::add); return new ArrayList<>(out); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index e3745e9746b..80715bab290 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -2449,7 +2449,8 @@ public void expandToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2, value2).withIndictedObjects(solution.getEntityList().getFirst())); + assertMatch(solution.getFirstEntity(), group2, value2) + .withIndictedObjects(solution.getEntityList().getFirst())); } @Override From 478bcd2cce615df150fe13f98d3143e998961b40 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 15:13:02 -0400 Subject: [PATCH 08/18] chore: make getIndictedObjects() return null when indictments disabled, add notes about stale ifExists indictments --- .../core/impl/bavet/common/AbstractIfExistsNode.java | 4 ++++ .../core/impl/score/constraint/ConstraintMatch.java | 6 +++--- .../impl/score/constraint/ConstraintMatchTotal.java | 3 +-- .../stream/common/inliner/ConstraintMatchSupplier.java | 5 +++-- .../common/uni/AbstractUniConstraintStreamTest.java | 10 ++++++++-- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 649b4af144b..a6109119ba0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -113,6 +113,8 @@ protected void incrementCounterRightWithoutIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update + // NOTE: By not propagating here, the left tuple's indicted objects can be stale + // if an element is removed. counter.countRight++; } @@ -124,6 +126,8 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update + // NOTE: By not propagating here, the left tuple's indicted objects can be stale + // if an element is removed. IndictmentSource.addSupport(getId(), counter.leftTuple, rightTuple); counter.countRight++; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java index 2c8f6a2e6b5..f977608580e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java @@ -30,7 +30,7 @@ public final class ConstraintMatch> implements Comp private final ConstraintRef constraintRef; private final @Nullable ConstraintJustification justification; - private final List indictedObjects; + private final @Nullable List indictedObjects; private final Score_ score; /** @@ -39,7 +39,7 @@ public final class ConstraintMatch> implements Comp * @param score penalty or reward associated with the constraint match */ public ConstraintMatch(ConstraintRef constraintRef, @Nullable ConstraintJustification justification, - List indictedObjects, Score_ score) { + @Nullable List indictedObjects, Score_ score) { this.constraintRef = requireNonNull(constraintRef); this.justification = justification; this.indictedObjects = indictedObjects; @@ -67,7 +67,7 @@ public ConstraintRef getConstraintRef() { return (Justification_) justification; } - public List getIndictedObjects() { + public @Nullable List getIndictedObjects() { return indictedObjects; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java index 31502a8336c..4ed2b3e0014 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java @@ -2,7 +2,6 @@ import static java.util.Objects.requireNonNull; -import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -94,7 +93,7 @@ public ConstraintMatch addConstraintMatch(ConstraintJustification justif * @return never null */ public ConstraintMatch addConstraintMatch(ConstraintJustification justification, Score_ score) { - var constraintMatch = new ConstraintMatch<>(constraintRef, justification, Collections.emptyList(), score); + var constraintMatch = new ConstraintMatch<>(constraintRef, justification, null, score); addConstraintMatch(constraintMatch); return constraintMatch; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java index 93d41787cae..623e37f127d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java @@ -25,6 +25,7 @@ import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Allows creating {@link ConstraintMatch} instances lazily if and only if they are required by the end user. @@ -42,9 +43,9 @@ public interface ConstraintMatchSupplier> extends BiFunction> { - static List collectIndictments(Constraint constraint, Tuple tuple) { + static @Nullable List collectIndictments(Constraint constraint, Tuple tuple) { if (tuple.getIndictmentSource() == IndictmentSource.DISABLED) { - return Collections.emptyList(); + return null; } var out = new LinkedHashSet<>(); var abstractConstraint = (AbstractConstraint) constraint; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index 80715bab290..42e57172f94 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -478,13 +478,19 @@ public void ifExists_0Joiner0Filter() { scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); + + // Note: entityGroup is still indicted, despite being removed, + // since only the right tuple of the ifExists changed, so no + // change was propagated. assertScore(scoreDirector, assertMatch(solution.getFirstValueGroup()).withIndictedObjects( solution.getFirstValueGroup(), - solution.getFirstEntityGroup()), + solution.getFirstEntityGroup(), + entityGroup), assertMatch(valueGroup).withIndictedObjects( valueGroup, - solution.getFirstEntityGroup())); + solution.getFirstEntityGroup(), + entityGroup)); } @Override From 246608f553fcda89da63f66e9700ba086eeefacf Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 16:48:06 -0400 Subject: [PATCH 09/18] test: add tests for BiConstraintStream indictments --- .../bavet/common/AbstractIfExistsNode.java | 8 +- .../bi/AbstractBiConstraintStreamTest.java | 613 ++++++++++-------- 2 files changed, 348 insertions(+), 273 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index a6109119ba0..2aa22efc138 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -113,8 +113,8 @@ protected void incrementCounterRightWithoutIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - // NOTE: By not propagating here, the left tuple's indicted objects can be stale - // if an element is removed. + // NOTE: By not propagating here, the left tuple's indicted objects can be stale + // if an element is removed. counter.countRight++; } @@ -126,8 +126,8 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - // NOTE: By not propagating here, the left tuple's indicted objects can be stale - // if an element is removed. + // NOTE: By not propagating here, the left tuple's indicted objects can be stale + // if an element is removed. IndictmentSource.addSupport(getId(), counter.leftTuple, rightTuple); counter.countRight++; } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java index 0fd2d1f078f..7b5cf3873ed 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java @@ -166,10 +166,10 @@ public void join_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(entity, value1, value1), - assertMatch(entity, value1, value2), - assertMatch(entity, value2, value1), - assertMatch(entity, value2, value2)); + assertMatch(entity, value1, value1).withIndictedObjects(entity, value1), + assertMatch(entity, value1, value2).withIndictedObjects(entity, value1, value2), + assertMatch(entity, value2, value1).withIndictedObjects(entity, value1, value2), + assertMatch(entity, value2, value2).withIndictedObjects(entity, value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -181,7 +181,7 @@ public void join_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementUnassigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value2, value2)); + assertMatch(entity, value2, value2).withIndictedObjects(entity, value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -191,10 +191,10 @@ public void join_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value1, value1), - assertMatch(entity, value1, value2), - assertMatch(entity, value2, value1), - assertMatch(entity, value2, value2)); + assertMatch(entity, value1, value1).withIndictedObjects(entity, value1), + assertMatch(entity, value1, value2).withIndictedObjects(entity, value1, value2), + assertMatch(entity, value2, value1).withIndictedObjects(entity, value1, value2), + assertMatch(entity, value2, value2).withIndictedObjects(entity, value2)); } } @@ -231,7 +231,7 @@ public void join_filterOnAssignedValue_unassignOneReassignOther() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(entity, value1, value1)); + assertMatch(entity, value1, value1).withIndictedObjects(entity, value1)); // Unassign+assign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -245,7 +245,7 @@ public void join_filterOnAssignedValue_unassignOneReassignOther() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value2); assertScore(scoreDirector, - assertMatch(entity, value2, value2)); + assertMatch(entity, value2, value2).withIndictedObjects(entity, value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -257,7 +257,7 @@ public void join_filterOnAssignedValue_unassignOneReassignOther() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value1, value1)); + assertMatch(entity, value1, value1).withIndictedObjects(entity, value1)); } } @@ -710,24 +710,24 @@ public void joinAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, 1L, extra1), - assertMatch(1L, 1L, extra2)); + assertMatch(1L, 1L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(1L, 1L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, 2L, extra1), - assertMatch(2L, 2L, extra2)); + assertMatch(2L, 2L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(2L, 2L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, 1L, extra1), - assertMatch(1L, 1L, extra2)); + assertMatch(1L, 1L, extra1).withIndictedObjects(entity1, extra1), + assertMatch(1L, 1L, extra2).withIndictedObjects(entity1, extra2)); } @Override @@ -776,8 +776,8 @@ public void ifExists_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(entity, value1), - assertMatch(entity, value2)); + assertMatch(entity, value1).withIndictedObjects(entity, value1), + assertMatch(entity, value2).withIndictedObjects(entity, value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -789,7 +789,7 @@ public void ifExists_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementUnassigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value2)); + assertMatch(entity, value2).withIndictedObjects(entity, value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -799,8 +799,8 @@ public void ifExists_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value1), - assertMatch(entity, value2)); + assertMatch(entity, value1).withIndictedObjects(entity, value1), + assertMatch(entity, value2).withIndictedObjects(entity, value1, value2)); } } @@ -1221,21 +1221,21 @@ public void ifExistsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, 1L)); + assertMatch(1L, 1L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, 2L)); + assertMatch(2L, 2L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, 1L)); + assertMatch(1L, 1L).withIndictedObjects(entity1)); } @Override @@ -1258,11 +1258,16 @@ public void groupBy_1Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: groupBy accumulates indicted objects from all tuples in the group scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup()), - assertMatchWithScore(-1, entityGroup1)); + assertMatchWithScore(-1, solution.getFirstEntityGroup()) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), entity3), + assertMatchWithScore(-1, entityGroup1).withIndictedObjects(entity1, entity2)); // Incremental Stream.of(entity1, entity2).forEach(entity -> { @@ -1270,7 +1275,11 @@ public void groupBy_1Mapping0Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); }); - assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup())); + assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup()) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), solution.getEntityList().get(4), solution.getEntityList().get(5), + solution.getEntityList().get(6), entity3)); } @Override @@ -1285,11 +1294,20 @@ public void groupBy_1Mapping1Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: groupBy accumulates indicted objects from all tuples in the group scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntity().toString(), 6L), - assertMatchWithScore(-1, solution.getEntityList().get(1).toString(), 5L)); + assertMatchWithScore(-1, solution.getFirstEntity().toString(), 6L) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6)), + assertMatchWithScore(-1, solution.getEntityList().get(1).toString(), 5L) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(2), + solution.getEntityList().get(3), solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6))); // Incremental; we have a new first entity, and less entities in total. var entity = solution.getFirstEntity(); @@ -1297,7 +1315,10 @@ public void groupBy_1Mapping1Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntity().toString(), 5L)); + assertMatchWithScore(-1, solution.getFirstEntity().toString(), 5L) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getEntityList().get(2), solution.getEntityList().get(3), + solution.getEntityList().get(4), solution.getEntityList().get(5))); } @Override @@ -1316,11 +1337,13 @@ public void groupBy_1Mapping2Collector() { var entity1 = solution.getFirstEntity(); var entity2 = solution.getEntityList().get(1); - // From scratch + // From scratch: groupBy accumulates indicted objects from all tuples in the group scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)), - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)) + .withIndictedObjects(entity1, entity2, solution.getEntityList().get(2)), + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)) + .withIndictedObjects(entity2, solution.getEntityList().get(2))); // Incremental var entity = solution.getFirstEntity(); @@ -1328,7 +1351,8 @@ public void groupBy_1Mapping2Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)) + .withIndictedObjects(entity2, solution.getEntityList().get(1))); } @Override @@ -1350,13 +1374,15 @@ public void groupBy_1Mapping3Collector() { var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(Long.MIN_VALUE); - // From scratch + // From scratch: groupBy accumulates indicted objects from all tuples in the group scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, - singleton(entity1)), + singleton(entity1)) + .withIndictedObjects(entity1, entity2, solution.getEntityList().get(2)), assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)) + .withIndictedObjects(entity2, solution.getEntityList().get(2))); // Incremental var entity = solution.getFirstEntity(); @@ -1365,7 +1391,8 @@ public void groupBy_1Mapping3Collector() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)) + .withIndictedObjects(entity2, solution.getEntityList().get(1))); } @Override @@ -1379,16 +1406,19 @@ public void groupBy_0Mapping1Collector() { .penalize(SimpleScore.ONE, count -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-3, 3L)); + assertScore(scoreDirector, assertMatchWithScore(-3, 3L).withIndictedObjects(entity1, entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, 1L)); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); + assertScore(scoreDirector, assertMatchWithScore(-1, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -1403,16 +1433,18 @@ public void groupBy_0Mapping2Collector() { .asConstraint(TEST_CONSTRAINT_ID)); var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); - assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -1437,14 +1469,14 @@ public void groupBy_0Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L)); + assertMatchWithScore(-1, 3L, 0L, 1L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L)); + assertMatchWithScore(-1, 1L, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -1470,14 +1502,14 @@ public void groupBy_0Mapping4Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2))); + assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2)).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2))); + assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -1493,21 +1525,23 @@ public void groupBy_2Mapping0Collector() { var group1 = solution.getEntityGroupList().get(0); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2), - assertMatchWithScore(-1, group1, group3), - assertMatchWithScore(-1, group2, group3)); + assertMatchWithScore(-1, group1, group2).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, group1, group3).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, group2, group3).withIndictedObjects(entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group3)); + assertMatchWithScore(-1, group2, group3).withIndictedObjects(entity2, entity3)); } @Override @@ -1523,24 +1557,27 @@ public void groupBy_2Mapping1Collector() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entity4 = solution.getEntityList().get(3); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group1, 1L), - assertMatchWithScore(-3, group1, group2, 3L), - assertMatchWithScore(-1, group2, group1, 1L), - assertMatchWithScore(-1, group2, group2, 1L)); + assertMatchWithScore(-1, group1, group1, 1L).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-3, group1, group2, 3L).withIndictedObjects(entity1, entity2, entity4, entity3), + assertMatchWithScore(-1, group2, group1, 1L).withIndictedObjects(entity2, entity3), + assertMatchWithScore(-1, group2, group2, 1L).withIndictedObjects(entity2, entity4)); - // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + // Incremental: groupBy indicted objects may be stale after incremental changes (acceptable per rules) + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, 1L), - assertMatchWithScore(-1, group1, group2, 1L), - assertMatchWithScore(-1, group2, group1, 1L)); + assertMatchWithScore(-1, group2, group1, 1L).withIndictedObjects(entity2, entity3), + assertMatchWithScore(-1, group2, group2, 1L).withIndictedObjects(entity2, entity4), + assertMatchWithScore(-1, group1, group2, 1L).withIndictedObjects(entity4, entity3)); } @Override @@ -1557,24 +1594,27 @@ public void groupBy_2Mapping2Collector() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entity4 = solution.getEntityList().get(3); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, group1, group1, 1L, 1L), - assertMatchWithScore(-6, group1, group2, 3L, 3L), - assertMatchWithScore(-2, group2, group1, 1L, 1L), - assertMatchWithScore(-2, group2, group2, 1L, 1L)); + assertMatchWithScore(-2, group1, group1, 1L, 1L).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-6, group1, group2, 3L, 3L).withIndictedObjects(entity1, entity2, entity4, entity3), + assertMatchWithScore(-2, group2, group1, 1L, 1L).withIndictedObjects(entity2, entity3), + assertMatchWithScore(-2, group2, group2, 1L, 1L).withIndictedObjects(entity2, entity4)); - // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + // Incremental: groupBy indicted objects may be stale after incremental changes (acceptable per rules) + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-2, group2, group2, 1L, 1L), - assertMatchWithScore(-2, group1, group2, 1L, 1L), - assertMatchWithScore(-2, group2, group1, 1L, 1L)); + assertMatchWithScore(-2, group2, group1, 1L, 1L).withIndictedObjects(entity2, entity3), + assertMatchWithScore(-2, group2, group2, 1L, 1L).withIndictedObjects(entity2, entity4), + assertMatchWithScore(-2, group1, group2, 1L, 1L).withIndictedObjects(entity4, entity3)); } @Override @@ -1592,21 +1632,23 @@ public void groupBy_3Mapping0Collector() { var group3 = solution.getEntityGroupList().get(2); var value1 = solution.getValueList().get(0); var value2 = solution.getValueList().get(1); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, value1), - assertMatchWithScore(-1, group1, group3, value1), - assertMatchWithScore(-1, group2, group3, value2)); + assertMatchWithScore(-1, group1, group2, value1).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, group1, group3, value1).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, group2, group3, value2).withIndictedObjects(entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group3, value2)); + assertMatchWithScore(-1, group2, group3, value2).withIndictedObjects(entity2, entity3)); } @Override @@ -1625,21 +1667,23 @@ public void groupBy_3Mapping1Collector() { var group3 = solution.getEntityGroupList().get(2); var value1 = solution.getValueList().get(0); var value2 = solution.getValueList().get(1); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, value1, 1L), - assertMatchWithScore(-1, group1, group3, value1, 1L), - assertMatchWithScore(-1, group2, group3, value2, 1L)); + assertMatchWithScore(-1, group1, group2, value1, 1L).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, group1, group3, value1, 1L).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, group2, group3, value2, 1L).withIndictedObjects(entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group3, value2, 1L)); + assertMatchWithScore(-1, group2, group3, value2, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -1658,21 +1702,23 @@ public void groupBy_4Mapping0Collector() { var group3 = solution.getEntityGroupList().get(2); var value1 = solution.getValueList().get(0); var value2 = solution.getValueList().get(1); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, value1, value2), - assertMatchWithScore(-1, group1, group3, value1, value1), - assertMatchWithScore(-1, group2, group3, value2, value1)); + assertMatchWithScore(-1, group1, group2, value1, value2).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, group1, group3, value1, value1).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, group2, group3, value2, value1).withIndictedObjects(entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group3, value2, value1)); + assertMatchWithScore(-1, group2, group3, value2, value1).withIndictedObjects(entity2, entity3)); } @Override @@ -1709,22 +1755,23 @@ public void mapToUniWithDuplicates() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity1, entity2), + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3), + assertMatch(asSet(group1)).withIndictedObjects(entity1, entity3)); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3)); } @Override @@ -1740,22 +1787,23 @@ public void mapToUniWithoutDuplicates() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group3)), - assertMatch(asSet(group2, group3))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity1, entity2), + assertMatch(asSet(group1, group3)).withIndictedObjects(entity1, entity3), + assertMatch(asSet(group2, group3)).withIndictedObjects(entity2, entity3)); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(asSet(group2, group3))); + assertMatch(asSet(group2, group3)).withIndictedObjects(entity2, entity3)); } @Override @@ -1771,21 +1819,22 @@ public void mapToUniAndDistinctWithDuplicates() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); - // From scratch + // From scratch: distinct accumulates indicted objects from all tuples with same key scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity1, entity2, entity3), + assertMatch(asSet(group1)).withIndictedObjects(entity1, entity3)); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3)); } @Override @@ -1802,22 +1851,23 @@ public void mapToUniAndDistinctWithoutDuplicates() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group3)), - assertMatch(asSet(group2, group3))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity1, entity2), + assertMatch(asSet(group1, group3)).withIndictedObjects(entity1, entity3), + assertMatch(asSet(group2, group3)).withIndictedObjects(entity2, entity3)); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(asSet(group2, group3))); + assertMatch(asSet(group2, group3)).withIndictedObjects(entity2, entity3)); } @Override @@ -1837,18 +1887,21 @@ public void mapToBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2), - assertMatch(group2, group1), - assertMatch(group1, group1)); + assertMatch(group1, group2).withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1)), + assertMatch(group2, group1).withIndictedObjects(solution.getEntityList().get(1), + solution.getEntityList().get(2)), + assertMatch(group1, group1).withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(2))); - var entity = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(group2, group1)); + assertMatch(group2, group1).withIndictedObjects(entity2, entity3)); } @Override @@ -1872,18 +1925,23 @@ public void mapToTri() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2, sum01), - assertMatch(group2, group1, sum12), - assertMatch(group1, group1, sum02)); + assertMatch(group1, group2, sum01).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1)), + assertMatch(group2, group1, sum12).withIndictedObjects(solution.getEntityList().get(1), + solution.getEntityList().get(2)), + assertMatch(group1, group1, sum02).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(2))); - var entity = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(group2, group1, sum12)); + assertMatch(group2, group1, sum12).withIndictedObjects(entity2, entity3)); } @Override @@ -1907,18 +1965,23 @@ public void mapToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2, value1, value2), - assertMatch(group2, group1, value2, value1), - assertMatch(group1, group1, value1, value1)); + assertMatch(group1, group2, value1, value2).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1)), + assertMatch(group2, group1, value2, value1).withIndictedObjects(solution.getEntityList().get(1), + solution.getEntityList().get(2)), + assertMatch(group1, group1, value1, value1).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(2))); - var entity = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(group2, group1, value2, value1)); + assertMatch(group2, group1, value2, value1).withIndictedObjects(entity2, entity3)); } @Override @@ -1938,9 +2001,12 @@ public void expandToTri() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum01), - assertMatch(solution.getEntityList().get(1), solution.getEntityList().get(2), sum12), - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(2), sum02)); + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum01) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1)), + assertMatch(solution.getEntityList().get(1), solution.getEntityList().get(2), sum12) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(2)), + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(2), sum02) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(2))); var entity = solution.getFirstEntity(); @@ -1949,7 +2015,8 @@ public void expandToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum12)); + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum12) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1))); } @Override @@ -1975,9 +2042,12 @@ public void expandToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum01, concat01), - assertMatch(solution.getEntityList().get(1), solution.getEntityList().get(2), sum12, concat12), - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(2), sum02, concat02)); + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum01, concat01) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1)), + assertMatch(solution.getEntityList().get(1), solution.getEntityList().get(2), sum12, concat12) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(2)), + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(2), sum02, concat02) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(2))); var entity = solution.getFirstEntity(); @@ -1986,7 +2056,8 @@ public void expandToQuad() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum12, concat12)); + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum12, concat12) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1))); } @Override @@ -2007,9 +2078,9 @@ public void flatten() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group2)); + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2), + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2), + assertMatch(entity1, entity2, group2).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -2033,27 +2104,28 @@ public void flattenLastWithDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: flattenLast produces multiple tuples per pair, indicted objects are from original pair scoreDirector.setWorkingSolution(solution); + var entity3 = solution.getEntityList().get(2); assertScore(scoreDirector, - assertMatch(entity1, group1), - assertMatch(entity1, group1), - assertMatch(entity1, group2), - assertMatch(entity2, group2), - assertMatch(entity2, group1), - assertMatch(entity2, group2), - assertMatch(entity1, group1), - assertMatch(entity1, group1), - assertMatch(entity1, group2)); + assertMatch(entity1, group1).withIndictedObjects(entity1, entity2), + assertMatch(entity1, group1).withIndictedObjects(entity1, entity2), + assertMatch(entity1, group2).withIndictedObjects(entity1, entity2), + assertMatch(entity1, group1).withIndictedObjects(entity1, entity3), + assertMatch(entity1, group1).withIndictedObjects(entity1, entity3), + assertMatch(entity1, group2).withIndictedObjects(entity1, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group2).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(entity2, group2), - assertMatch(entity2, group1), - assertMatch(entity2, group2)); + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group2).withIndictedObjects(entity2, entity3)); } @Override @@ -2071,19 +2143,20 @@ public void flattenLastWithoutDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: flattenLast produces one tuple per pair, indicted objects are from original pair scoreDirector.setWorkingSolution(solution); + var entity3 = solution.getEntityList().get(2); assertScore(scoreDirector, - assertMatch(entity2, group1), - assertMatch(entity1, group2), - assertMatch(entity1, group1)); + assertMatch(entity1, group2).withIndictedObjects(entity1, entity2), + assertMatch(entity1, group1).withIndictedObjects(entity1, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(entity2, group1)); + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3)); } @Override @@ -2102,21 +2175,22 @@ public void flattenLastAndDistinctWithDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: flattenLast + distinct produces unique tuples, indicted from original pairs scoreDirector.setWorkingSolution(solution); + var entity3 = solution.getEntityList().get(2); assertScore(scoreDirector, - assertMatch(entity1, group1), - assertMatch(entity1, group2), - assertMatch(entity2, group2), - assertMatch(entity2, group1)); + assertMatch(entity1, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group2).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(entity2, group1), - assertMatch(entity2, group2)); + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group2).withIndictedObjects(entity2, entity3)); } @Override @@ -2135,19 +2209,20 @@ public void flattenLastAndDistinctWithoutDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: flattenLast + distinct produces unique tuples, indicted from original pairs scoreDirector.setWorkingSolution(solution); + var entity3 = solution.getEntityList().get(2); assertScore(scoreDirector, - assertMatch(entity2, group1), - assertMatch(entity1, group2), - assertMatch(entity1, group1)); + assertMatch(entity1, group2).withIndictedObjects(entity1, entity2), + assertMatch(entity1, group1).withIndictedObjects(entity1, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(entity2, group1)); + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3)); } @Override @@ -2180,8 +2255,8 @@ public void concatUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2), - assertMatch(entity2, null)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2192,8 +2267,8 @@ public void concatUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3), - assertMatch(entity3, null)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity3, null).withIndictedObjects(entity3)); } @Override @@ -2227,8 +2302,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2), - assertMatch(entity2, null)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2239,8 +2314,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3), - assertMatch(entity3, null)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity3, null).withIndictedObjects(entity3)); } @Override @@ -2275,8 +2350,8 @@ public void concatBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2), - assertMatch(entity2, entity3)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2287,8 +2362,8 @@ public void concatBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3), - assertMatch(entity3, entity2)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2)); } @Override @@ -2323,8 +2398,8 @@ public void concatBiWithValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2), - assertMatch(entity1, entity2)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2335,8 +2410,8 @@ public void concatBiWithValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3), - assertMatch(entity1, entity3)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3)); } @Override @@ -2372,8 +2447,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2), - assertMatch(entity2, entity3)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2384,8 +2459,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3), - assertMatch(entity3, entity2)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2)); } @Override @@ -2421,7 +2496,7 @@ public void concatAndDistinctBiWithValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2432,7 +2507,7 @@ public void concatAndDistinctBiWithValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3)); } @Override @@ -2469,8 +2544,8 @@ public void concatTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, null), - assertMatch(entity2, entity3, entity1)); + assertMatch(entity1, entity2, null).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3, entity1).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2481,8 +2556,8 @@ public void concatTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, null), - assertMatch(entity3, entity2, entity1)); + assertMatch(entity1, entity3, null).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2, entity1).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2520,8 +2595,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, null), - assertMatch(entity2, entity3, entity1)); + assertMatch(entity1, entity2, null).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3, entity1).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2532,8 +2607,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, null), - assertMatch(entity3, entity2, entity1)); + assertMatch(entity1, entity3, null).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2, entity1).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2572,8 +2647,8 @@ public void concatQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, null, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, null, null).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2584,8 +2659,8 @@ public void concatQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, null, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, null, null).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2625,8 +2700,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, null, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, null, null).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2637,8 +2712,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, null, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, null, null).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2677,15 +2752,15 @@ public void concatAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, 1L), - assertMatchWithScore(-1, value2, value3, 1L)); + assertMatchWithScore(-1, value1, value2, 1L).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, value2, value3, 1L).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); entity3.setValue(value2); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-2, value1, value2, 2L)); + assertMatchWithScore(-2, value1, value2, 2L).withIndictedObjects(entity1, entity2, entity3)); // Incremental for which the first change matches a join that doesn't survive the second change scoreDirector.beforeVariableChanged(entity1, "value"); @@ -2695,8 +2770,8 @@ public void concatAfterGroupBy() { entity3.setValue(value1); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, 1L), - assertMatchWithScore(-1, value2, value3, 1L)); + assertMatchWithScore(-1, value1, value2, 1L).withIndictedObjects(entity3, entity2), + assertMatchWithScore(-1, value2, value3, 1L).withIndictedObjects(entity2, entity1)); } @Override @@ -2729,18 +2804,18 @@ public void complement() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, 0), - assertMatch(entity2, Integer.MAX_VALUE), - assertMatch(entity3, Integer.MAX_VALUE)); + assertMatch(entity1, 0).withIndictedObjects(entity1), + assertMatch(entity2, Integer.MAX_VALUE).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE).withIndictedObjects(entity3)); // Incremental; all entities are still present, but the indexes are different. scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value1); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, 0), - assertMatch(entity2, 0), - assertMatch(entity3, Integer.MAX_VALUE)); + assertMatch(entity1, 0).withIndictedObjects(entity1), + assertMatch(entity2, 0).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE).withIndictedObjects(entity3)); } @Override @@ -3222,15 +3297,15 @@ public void joinerEqualsAndSameness() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, // Each entity's BigDecimal property is joined to itself. - assertMatch(entity1, entity1), - assertMatch(entity2, entity2), - assertMatch(entity3, entity3), + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity3, entity3).withIndictedObjects(entity3), // Each entity's BigDecimal property is joined to each other entity's. - assertMatch(entity1, entity2), - assertMatch(entity1, entity3), - assertMatch(entity2, entity1), - assertMatch(entity2, entity3), - assertMatch(entity3, entity1), - assertMatch(entity3, entity2)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity2, entity1).withIndictedObjects(entity2, entity1), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3), + assertMatch(entity3, entity1).withIndictedObjects(entity3, entity1), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2)); } } From c6b5034c6f8212e3f33352396a5ddb0356454b84 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 17:37:25 -0400 Subject: [PATCH 10/18] test: make TriConstraintStream tests indictment aware --- .../tri/AbstractTriConstraintStreamTest.java | 379 ++++++++++-------- 1 file changed, 209 insertions(+), 170 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java index 90724a2057f..d1d79aef37c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java @@ -129,7 +129,7 @@ public void filter_consecutive() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatch(entity4, entity5, entity4)); + assertScore(scoreDirector, assertMatch(entity4, entity5, entity4).withIndictedObjects(entity4, entity5)); // Remove entity scoreDirector.beforeEntityRemoved(entity4); @@ -328,24 +328,24 @@ public void joinAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L, extra1), - assertMatch(1L, 1L, 1L, extra2)); + assertMatch(1L, 1L, 1L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(1L, 1L, 1L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, 2L, 2L, extra1), - assertMatch(2L, 2L, 2L, extra2)); + assertMatch(2L, 2L, 2L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(2L, 2L, 2L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L, extra1), - assertMatch(1L, 1L, 1L, extra2)); + assertMatch(1L, 1L, 1L, extra1).withIndictedObjects(entity1, extra1), + assertMatch(1L, 1L, 1L, extra2).withIndictedObjects(entity1, extra2)); } @Override @@ -753,21 +753,21 @@ public void ifExistsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L)); + assertMatch(1L, 1L, 1L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, 2L, 2L)); + assertMatch(2L, 2L, 2L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L)); + assertMatch(1L, 1L, 1L).withIndictedObjects(entity1)); } @Override @@ -783,18 +783,26 @@ public void groupBy_0Mapping1Collector() { .penalize(SimpleScore.ONE, count -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entityGroup1 = solution.getFirstEntityGroup(); + var entityGroup2 = solution.getEntityGroupList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); + // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-3, 3L)); + assertMatchWithScore(-3, 3L).withIndictedObjects(entity, entity2, entity3, entityGroup1, entityGroup2, value1, + value2)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-2, 2L)); + assertMatchWithScore(-2, 2L).withIndictedObjects(entity2, entity3, entityGroup1, entityGroup2, value1, value2)); } @Override @@ -810,16 +818,18 @@ public void groupBy_0Mapping2Collector() { .asConstraint(TEST_CONSTRAINT_ID)); var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); - assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -847,14 +857,14 @@ public void groupBy_0Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L)); + assertMatchWithScore(-1, 3L, 0L, 1L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L)); + assertMatchWithScore(-1, 1L, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -883,14 +893,14 @@ public void groupBy_0Mapping4Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2))); + assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2)).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2))); + assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -906,13 +916,18 @@ public void groupBy_1Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entityGroup1 = solution.getFirstEntityGroup(); + var entityGroup2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); var value2 = solution.getValueList().get(1); scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value2), - assertMatchWithScore(-1, value1)); + assertMatchWithScore(-1, value2).withIndictedObjects(entity2, entityGroup2, value2), + assertMatchWithScore(-1, value1).withIndictedObjects(entity1, entity3, entityGroup1, value1)); } @Override @@ -928,23 +943,27 @@ public void groupBy_1Mapping1Collector() { .penalize(SimpleScore.ONE, (group, count) -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entityGroup1 = solution.getFirstEntityGroup(); + var entityGroup2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value2, 1L), - assertMatchWithScore(-2, value1, 2L)); + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, entityGroup2, value2), + assertMatchWithScore(-2, value1, 2L).withIndictedObjects(entity, entity3, entityGroup1, value1)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, value2, 1L), - assertMatchWithScore(-1, value1, 1L)); + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, entityGroup2, value2), + assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity3, entityGroup1, value1)); } @Override @@ -963,12 +982,14 @@ public void groupBy_1Mapping2Collector() { var entity1 = solution.getFirstEntity(); var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)), - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)).withIndictedObjects(entity1, entity2, + entity3), + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)).withIndictedObjects(entity2, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -976,7 +997,7 @@ public void groupBy_1Mapping2Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -1000,14 +1021,15 @@ public void groupBy_1Mapping3Collector() { entity1.setLongProperty(Long.MAX_VALUE); var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(Long.MIN_VALUE); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, - singleton(entity1)), + singleton(entity1)).withIndictedObjects(entity1, entity2, entity3), assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)).withIndictedObjects(entity2, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -1016,7 +1038,7 @@ public void groupBy_1Mapping3Collector() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -1031,6 +1053,9 @@ public void groupBy_2Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -1039,17 +1064,16 @@ public void groupBy_2Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2), - assertMatchWithScore(-1, group1, value1)); + assertMatchWithScore(-1, group2, value2).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-1, group1, value1).withIndictedObjects(entity, entity3, group1, value1)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2), - assertMatchWithScore(-1, group1, value1)); + assertMatchWithScore(-1, group2, value2).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-1, group1, value1).withIndictedObjects(entity3, group1, value1)); } @Override @@ -1065,6 +1089,9 @@ public void groupBy_2Mapping1Collector() { .penalize(SimpleScore.ONE, (group, value, count) -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -1073,17 +1100,16 @@ public void groupBy_2Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, 1L), - assertMatchWithScore(-2, group1, value1, 2L)); + assertMatchWithScore(-1, group2, value2, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-2, group1, value1, 2L).withIndictedObjects(entity, entity3, group1, value1)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, 1L), - assertMatchWithScore(-1, group1, value1, 1L)); + assertMatchWithScore(-1, group2, value2, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-1, group1, value1, 1L).withIndictedObjects(entity3, group1, value1)); } @Override @@ -1100,6 +1126,9 @@ public void groupBy_2Mapping2Collector() { (group, value, count, sameCount) -> count + sameCount) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -1108,17 +1137,16 @@ public void groupBy_2Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, group2, value2, 1L, 1L), - assertMatchWithScore(-4, group1, value1, 2L, 2L)); + assertMatchWithScore(-2, group2, value2, 1L, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-4, group1, value1, 2L, 2L).withIndictedObjects(entity, entity3, group1, value1)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-2, group2, value2, 1L, 1L), - assertMatchWithScore(-2, group1, value1, 1L, 1L)); + assertMatchWithScore(-2, group2, value2, 1L, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-2, group1, value1, 1L, 1L).withIndictedObjects(entity3, group1, value1)); } @Override @@ -1133,6 +1161,9 @@ public void groupBy_3Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getEntityGroupList().get(0); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1140,12 +1171,11 @@ public void groupBy_3Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, group3), - assertMatchWithScore(-1, group1, group3, group2), - assertMatchWithScore(-1, group2, group3, group1)); + assertMatchWithScore(-1, group1, group2, group3).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group1, group3, group2).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group2, group3, group1).withIndictedObjects(entity, entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1165,6 +1195,9 @@ public void groupBy_3Mapping1Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getEntityGroupList().get(0); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1172,12 +1205,11 @@ public void groupBy_3Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, group3, 1L), - assertMatchWithScore(-1, group1, group3, group2, 1L), - assertMatchWithScore(-1, group2, group3, group1, 1L)); + assertMatchWithScore(-1, group1, group2, group3, 1L).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group1, group3, group2, 1L).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group2, group3, group1, 1L).withIndictedObjects(entity, entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1197,6 +1229,9 @@ public void groupBy_4Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getEntityGroupList().get(0); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1206,12 +1241,11 @@ public void groupBy_4Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, group3, value1), - assertMatchWithScore(-1, group1, group3, group2, value2), - assertMatchWithScore(-1, group2, group3, group1, value1)); + assertMatchWithScore(-1, group1, group2, group3, value1).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group1, group3, group2, value2).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group2, group3, group1, value1).withIndictedObjects(entity, entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1252,17 +1286,18 @@ public void mapToUniWithDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group2))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1282,6 +1317,9 @@ public void mapToUniWithoutDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1289,11 +1327,9 @@ public void mapToUniWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group3)), - assertMatch(asSet(group2, group3))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group1, group3)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group2, group3)).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1314,15 +1350,16 @@ public void mapToUniAndDistinctWithDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1343,6 +1380,9 @@ public void mapToUniAndDistinctWithoutDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1350,11 +1390,9 @@ public void mapToUniAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group3)), - assertMatch(asSet(group2, group3))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group1, group3)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group2, group3)).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1375,6 +1413,9 @@ public void mapToBi() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1382,11 +1423,9 @@ public void mapToBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2), - assertMatch(group1, group3), - assertMatch(group2, group3)); - - var entity = solution.getFirstEntity(); + assertMatch(group1, group2).withIndictedObjects(entity, entity2, entity3), + assertMatch(group1, group3).withIndictedObjects(entity, entity2, entity3), + assertMatch(group2, group3).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1408,6 +1447,9 @@ public void mapToTri() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1415,11 +1457,9 @@ public void mapToTri() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2, group3), - assertMatch(group1, group3, group2), - assertMatch(group2, group3, group1)); - - var entity = solution.getFirstEntity(); + assertMatch(group1, group2, group3).withIndictedObjects(entity, entity2, entity3), + assertMatch(group1, group3, group2).withIndictedObjects(entity, entity2, entity3), + assertMatch(group2, group3, group1).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1442,6 +1482,9 @@ public void mapToQuad() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1452,11 +1495,9 @@ public void mapToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2, group3, sum), - assertMatch(group1, group3, group2, sum), - assertMatch(group2, group3, group1, sum)); - - var entity = solution.getFirstEntity(); + assertMatch(group1, group2, group3, sum).withIndictedObjects(entity, entity2, entity3), + assertMatch(group1, group3, group2, sum).withIndictedObjects(entity, entity2, entity3), + assertMatch(group2, group3, group1, sum).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1486,16 +1527,14 @@ public void expandToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, sum), - assertMatch(entity1, entity3, entity2, sum), - assertMatch(entity2, entity3, entity1, sum)); - - var entity = solution.getFirstEntity(); + assertMatch(entity1, entity2, entity3, sum).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, sum).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, sum).withIndictedObjects(entity1, entity2, entity3)); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector); } @@ -1519,15 +1558,15 @@ public void flatten() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group2), - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group2), - assertMatch(entity2, entity3, entity1, group2), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group1)); + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1556,15 +1595,15 @@ public void flattenLastWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group2), - assertMatch(entity1, entity3, group1), - assertMatch(entity1, entity3, group2), - assertMatch(entity1, entity3, group2), - assertMatch(entity2, entity3, group1), - assertMatch(entity2, entity3, group1), - assertMatch(entity2, entity3, group2)); + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1593,12 +1632,12 @@ public void flattenLastWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group2), - assertMatch(entity1, entity3, group1), - assertMatch(entity1, entity3, group2), - assertMatch(entity2, entity3, group1), - assertMatch(entity2, entity3, group2)); + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1628,12 +1667,12 @@ public void flattenLastAndDistinctWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group2), - assertMatch(entity1, entity3, group1), - assertMatch(entity1, entity3, group2), - assertMatch(entity2, entity3, group1), - assertMatch(entity2, entity3, group2)); + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1663,12 +1702,12 @@ public void flattenLastAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group2), - assertMatch(entity1, entity3, group1), - assertMatch(entity1, entity3, group2), - assertMatch(entity2, entity3, group1), - assertMatch(entity2, entity3, group2)); + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1709,8 +1748,8 @@ public void concatUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3), - assertMatch(entity2, null, null)); + assertMatch(entity1, entity2, entity3).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, null, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1721,8 +1760,8 @@ public void concatUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2), - assertMatch(entity3, null, null)); + assertMatch(entity1, entity3, entity2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, null, null).withIndictedObjects(entity3)); } @Override @@ -1758,8 +1797,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3), - assertMatch(entity2, null, null)); + assertMatch(entity1, entity2, entity3).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, null, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1770,8 +1809,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2), - assertMatch(entity3, null, null)); + assertMatch(entity1, entity3, entity2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, null, null).withIndictedObjects(entity3)); } @Override @@ -1808,8 +1847,8 @@ public void concatBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3), - assertMatch(entity2, entity3, null)); + assertMatch(entity1, entity2, entity3).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, null).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1820,8 +1859,8 @@ public void concatBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2), - assertMatch(entity3, entity2, null)); + assertMatch(entity1, entity3, entity2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, entity2, null).withIndictedObjects(entity3, entity2)); } @Override @@ -1859,8 +1898,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3), - assertMatch(entity2, entity3, null)); + assertMatch(entity1, entity2, entity3).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, null).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1871,8 +1910,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2), - assertMatch(entity3, entity2, null)); + assertMatch(entity1, entity3, entity2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, entity2, null).withIndictedObjects(entity3, entity2)); } @Override @@ -2121,8 +2160,8 @@ public void concatQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, entity3, null).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2133,8 +2172,8 @@ public void concatQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, entity2, null).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2176,8 +2215,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, entity3, null).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2188,8 +2227,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, entity2, null).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2233,8 +2272,8 @@ public void concatAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, value3, 1L), - assertMatchWithScore(-1, value3, value2, value1, 1L)); + assertMatchWithScore(-1, value1, value2, value3, 1L).withIndictedObjects(entity1, entity2, entity3), + assertMatchWithScore(-1, value3, value2, value1, 1L).withIndictedObjects(entity3, entity2, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2250,8 +2289,8 @@ public void concatAfterGroupBy() { entity3.setValue(value1); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, value3, 1L), - assertMatchWithScore(-1, value3, value2, value1, 1L)); + assertMatchWithScore(-1, value1, value2, value3, 1L).withIndictedObjects(entity1, entity2, entity3), + assertMatchWithScore(-1, value3, value2, value1, 1L).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2285,18 +2324,18 @@ public void complement() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, 0, 1), - assertMatch(entity2, Integer.MAX_VALUE, -1), - assertMatch(entity3, Integer.MAX_VALUE, -1)); + assertMatch(entity1, 0, 1).withIndictedObjects(entity1), + assertMatch(entity2, Integer.MAX_VALUE, -1).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE, -1).withIndictedObjects(entity3)); // Incremental; all entities are still present, but the indexes are different. scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value1); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, 0, 1), - assertMatch(entity2, 0, 1), - assertMatch(entity3, Integer.MAX_VALUE, -1)); + assertMatch(entity1, 0, 1).withIndictedObjects(entity1), + assertMatch(entity2, 0, 1).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE, -1).withIndictedObjects(entity3)); } @Override From e7205156097e46ae7e5783bbe8846998a3314cef Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 18:37:51 -0400 Subject: [PATCH 11/18] test: Make BavetQuadConstraintStreamTest aware of indictments --- .../AbstractQuadConstraintStreamTest.java | 346 +++++++++++------- 1 file changed, 208 insertions(+), 138 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java index 7673d675b98..eb8eb31b9c7 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java @@ -129,7 +129,7 @@ public void filter_consecutive() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatch(entity4, entity5, entity4, entity4)); + assertScore(scoreDirector, assertMatch(entity4, entity5, entity4, entity4).withIndictedObjects(entity4, entity5)); // Remove entity scoreDirector.beforeEntityRemoved(entity4); @@ -175,7 +175,8 @@ public void ifExists_0Joiner0Filter() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue(), - solution.getFirstEntity())); + solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup(), solution.getFirstValue())); // Incremental scoreDirector.beforeProblemFactRemoved(valueGroup); @@ -343,7 +344,8 @@ public void ifNotExists_0Joiner0Filter() { scoreDirector.afterProblemFactRemoved(valueGroup); assertScore(scoreDirector, assertMatch(solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue(), - solution.getFirstEntity())); + solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup(), solution.getFirstValue())); } @Override @@ -489,21 +491,21 @@ public void ifExistsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L, 1L)); + assertMatch(1L, 1L, 1L, 1L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, 2L, 2L, 2L)); + assertMatch(2L, 2L, 2L, 2L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L, 1L)); + assertMatch(1L, 1L, 1L, 1L).withIndictedObjects(entity1)); } @Override @@ -526,10 +528,17 @@ public void groupBy_0Mapping1Collector() { .penalize(SimpleScore.ONE, count -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-5, 5L)); // E1 G1 V1 E1, E1 G1 V1 E3, E2 G2 V2 E2, E3 G1 V1 E1, E3 G1 V1 E3 + assertMatchWithScore(-5, 5L).withIndictedObjects( // E1 G1 V1 E1, E1 G1 V1 E3, E2 G2 V2 E2, E3 G1 V1 E1, E3 G1 V1 E3 + entity1, solution.getFirstEntityGroup(), solution.getFirstValue(), + entity2, solution.getEntityGroupList().get(1), solution.getValueList().get(1), + entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -537,7 +546,9 @@ public void groupBy_0Mapping1Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-2, 2L)); // E2 G2 V2 E2, E3 G1 V1 E3 + assertMatchWithScore(-2, 2L).withIndictedObjects( // E2 G2 V2 E2, E3 G1 V1 E3 + entity2, solution.getEntityGroupList().get(1), solution.getValueList().get(1), + entity3, solution.getFirstEntityGroup(), solution.getFirstValue())); } @Override @@ -554,16 +565,18 @@ public void groupBy_0Mapping2Collector() { .asConstraint(TEST_CONSTRAINT_ID)); var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); - assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -592,14 +605,14 @@ public void groupBy_0Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L)); + assertMatchWithScore(-1, 3L, 0L, 1L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L)); + assertMatchWithScore(-1, 1L, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -629,14 +642,14 @@ public void groupBy_0Mapping4Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2))); + assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2)).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2))); + assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -659,8 +672,10 @@ public void groupBy_1Mapping0Collector() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value2), - assertMatchWithScore(-1, value1)); + assertMatchWithScore(-1, value2).withIndictedObjects(solution.getEntityList().get(1), + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, value1).withIndictedObjects(solution.getFirstEntity(), solution.getFirstEntityGroup(), + solution.getFirstValue(), solution.getEntityList().get(2))); } @Override @@ -678,14 +693,19 @@ public void groupBy_1Mapping1Collector() { .penalize(SimpleScore.ONE, (group, count) -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var value1 = solution.getFirstValue(); var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value2, 1L), - assertMatchWithScore(-4, value1, 4L)); + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-4, value1, 4L).withIndictedObjects(entity1, solution.getFirstEntityGroup(), value1, + entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -693,8 +713,9 @@ public void groupBy_1Mapping1Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, value2, 1L), - assertMatchWithScore(-1, value1, 1L)); + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity3, solution.getFirstEntityGroup(), value1)); } @Override @@ -714,12 +735,14 @@ public void groupBy_1Mapping2Collector() { var entity1 = solution.getFirstEntity(); var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)), - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)).withIndictedObjects(entity1, entity2, + entity3), + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)).withIndictedObjects(entity2, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -727,7 +750,7 @@ public void groupBy_1Mapping2Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -752,14 +775,15 @@ public void groupBy_1Mapping3Collector() { entity1.setLongProperty(Long.MAX_VALUE); var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(Long.MIN_VALUE); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, - singleton(entity1)), + singleton(entity1)).withIndictedObjects(entity1, entity2, entity3), assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)).withIndictedObjects(entity2, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -768,7 +792,7 @@ public void groupBy_1Mapping3Collector() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -785,6 +809,9 @@ public void groupBy_2Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -793,8 +820,9 @@ public void groupBy_2Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2), - assertMatchWithScore(-1, group1, value1)); + assertMatchWithScore(-1, group2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, value1).withIndictedObjects(entity1, group1, value1, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -802,8 +830,9 @@ public void groupBy_2Mapping0Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2), - assertMatchWithScore(-1, group1, value1)); + assertMatchWithScore(-1, group2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, value1).withIndictedObjects(entity3, group1, value1)); } @Override @@ -822,6 +851,9 @@ public void groupBy_2Mapping1Collector() { .penalize(SimpleScore.ONE, (group, value, count) -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -830,8 +862,9 @@ public void groupBy_2Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, 1L), - assertMatchWithScore(-4, group1, value1, 4L)); + assertMatchWithScore(-1, group2, value2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-4, group1, value1, 4L).withIndictedObjects(entity1, group1, value1, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -839,8 +872,9 @@ public void groupBy_2Mapping1Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, 1L), - assertMatchWithScore(-1, group1, value1, 1L)); + assertMatchWithScore(-1, group2, value2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, value1, 1L).withIndictedObjects(entity3, group1, value1)); } @Override @@ -859,6 +893,9 @@ public void groupBy_2Mapping2Collector() { .penalize(SimpleScore.ONE, (group, value, count, sameCount) -> count + sameCount) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -867,8 +904,8 @@ public void groupBy_2Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, group2, value2, 1L, 1L), - assertMatchWithScore(-8, group1, value1, 4L, 4L)); + assertMatchWithScore(-2, group2, value2, 1L, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-8, group1, value1, 4L, 4L).withIndictedObjects(entity1, group1, value1, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -876,8 +913,8 @@ public void groupBy_2Mapping2Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-2, group2, value2, 1L, 1L), - assertMatchWithScore(-2, group1, value1, 1L, 1L)); + assertMatchWithScore(-2, group2, value2, 1L, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-2, group1, value1, 1L, 1L).withIndictedObjects(entity3, group1, value1)); } @Override @@ -897,6 +934,9 @@ public void groupBy_3Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -905,8 +945,9 @@ public void groupBy_3Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, value2), - assertMatchWithScore(-1, group1, group1, value1)); + assertMatchWithScore(-1, group2, group2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, group1, value1).withIndictedObjects(entity1, group1, value1, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -914,8 +955,9 @@ public void groupBy_3Mapping0Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, value2), - assertMatchWithScore(-1, group1, value1, value1)); + assertMatchWithScore(-1, group2, value2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, value1, value1).withIndictedObjects(entity3, group1, value1)); } @Override @@ -935,14 +977,19 @@ public void groupBy_3Mapping1Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, group2, 1L), - assertMatchWithScore(-1, group1, group1, group1, 4L)); + assertMatchWithScore(-1, group2, group2, group2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, group1, group1, 4L).withIndictedObjects(entity1, group1, + solution.getFirstValue(), entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -950,8 +997,10 @@ public void groupBy_3Mapping1Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, group2, 1L), - assertMatchWithScore(-1, group1, group1, group1, 1L)); + assertMatchWithScore(-1, group2, group2, group2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, group1, group1, 1L).withIndictedObjects(entity3, group1, + solution.getFirstValue())); } @Override @@ -972,6 +1021,9 @@ public void groupBy_4Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -980,8 +1032,9 @@ public void groupBy_4Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, group2, value2), - assertMatchWithScore(-1, group1, group1, group1, value1)); + assertMatchWithScore(-1, group2, group2, group2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, group1, group1, value1).withIndictedObjects(entity1, group1, value1, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -989,8 +1042,9 @@ public void groupBy_4Mapping0Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, group2, value2), - assertMatchWithScore(-1, group1, group1, group1, value1)); + assertMatchWithScore(-1, group2, group2, group2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, group1, group1, value1).withIndictedObjects(entity3, group1, value1)); } @Override @@ -1017,7 +1071,7 @@ public void distinct() { // On a distinct stream, this is a no-op. scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(entity1, entity2, group1, group2), - assertMatch(entity1, entity3, group1, group1), + assertMatch(entity1, entity3, group1, group1).withIndictedObjects(entity1, entity3, group1), assertMatch(entity2, entity3, group2, group1)); } @@ -1036,22 +1090,24 @@ public void mapToUniWithDuplicates() { .asConstraint(TEST_CONSTRAINT_ID)); var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, group1, group2), + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3, group2, group1), + assertMatch(asSet(group1)).withIndictedObjects(entity, entity3, group1)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3, group2, group1)); } @Override @@ -1074,7 +1130,8 @@ public void mapToUniWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1), group1, group2)); var entity = solution.getFirstEntity(); @@ -1101,21 +1158,23 @@ public void mapToUniAndDistinctWithDuplicates() { .asConstraint(TEST_CONSTRAINT_ID)); var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, group1, group2, entity3), + assertMatch(asSet(group1)).withIndictedObjects(entity, entity3, group1)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3, group2, group1)); } @Override @@ -1139,7 +1198,8 @@ public void mapToUniAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1), group1, group2)); var entity = solution.getFirstEntity(); @@ -1168,7 +1228,9 @@ public void mapToBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1))); + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1)).withIndictedObjects( + solution.getFirstEntity(), solution.getEntityList().get(1), solution.getFirstEntityGroup(), + solution.getEntityGroupList().get(1))); var entity = solution.getFirstEntity(); @@ -1199,7 +1261,9 @@ public void mapToTri() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), - solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(1).getLongProperty())); + solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(1).getLongProperty()) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1))); var entity = solution.getFirstEntity(); @@ -1232,7 +1296,9 @@ public void mapToQuad() { assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntity().getCode(), solution.getEntityList().get(1).getCode(), solution.getFirstEntityGroup().getCode(), - solution.getEntityGroupList().get(1).getCode())); + solution.getEntityGroupList().get(1).getCode()) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1))); var entity = solution.getFirstEntity(); @@ -1264,15 +1330,15 @@ public void flattenLastWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group2), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group2), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group2)); + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, group2).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity2, entity3, entity1), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity2, entity3, entity1), + assertMatch(entity2, entity3, entity1, group2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1302,12 +1368,12 @@ public void flattenLastWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group2), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group2), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group2)); + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, group2).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity2, entity3, entity1), + assertMatch(entity2, entity3, entity1, group2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1338,12 +1404,12 @@ public void flattenLastAndDistinctWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group2), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group2), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group2)); + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, group2).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity2, entity3, entity1), + assertMatch(entity2, entity3, entity1, group2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1374,12 +1440,12 @@ public void flattenLastAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group2), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group2), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group2)); + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, group2).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity2, entity3, entity1), + assertMatch(entity2, entity3, entity1, group2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1422,8 +1488,8 @@ public void concatUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, null, null, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, null, null, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1434,8 +1500,8 @@ public void concatUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, null, null, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, null, null, null).withIndictedObjects(entity3)); } @Override @@ -1473,8 +1539,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, null, null, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, null, null, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1485,8 +1551,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, null, null, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, null, null, null).withIndictedObjects(entity3)); } @Override @@ -1525,8 +1591,8 @@ public void concatBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, null, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, null, null).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1537,8 +1603,8 @@ public void concatBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, null, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, null, null).withIndictedObjects(entity3, entity2)); } @Override @@ -1578,8 +1644,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, null, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, null, null).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1590,8 +1656,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, null, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, null, null).withIndictedObjects(entity3, entity2)); } @Override @@ -1632,8 +1698,8 @@ public void concatTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, entity1, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, null).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1644,8 +1710,8 @@ public void concatTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, entity1, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, entity1, null).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -1687,8 +1753,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, entity1, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, null).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1699,8 +1765,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, entity1, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, entity1, null).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -1743,8 +1809,8 @@ public void concatQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1755,8 +1821,8 @@ public void concatQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -1799,8 +1865,8 @@ public void concatQuadWithValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity1, entity2, entity3, entity1)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1811,8 +1877,8 @@ public void concatQuadWithValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity1, entity3, entity2, entity1)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2)); } @Override @@ -1856,8 +1922,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1868,8 +1934,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -1913,7 +1979,7 @@ public void concatAndDistinctQuadWithValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1924,7 +1990,7 @@ public void concatAndDistinctQuadWithValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2)); } @Override @@ -1971,8 +2037,10 @@ public void concatAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, value3.getCode() + value1.getCode(), 1L), - assertMatchWithScore(-1, value3, value2, value1.getCode() + value3.getCode(), 1L)); + assertMatchWithScore(-1, value1, value2, value3.getCode() + value1.getCode(), 1L).withIndictedObjects(entity1, + entity2, entity3), + assertMatchWithScore(-1, value3, value2, value1.getCode() + value3.getCode(), 1L).withIndictedObjects(entity3, + entity2, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1988,8 +2056,10 @@ public void concatAfterGroupBy() { entity3.setValue(value1); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, value3.getCode() + value1.getCode(), 1L), - assertMatchWithScore(-1, value3, value2, value1.getCode() + value3.getCode(), 1L)); + assertMatchWithScore(-1, value1, value2, value3.getCode() + value1.getCode(), 1L).withIndictedObjects(entity1, + entity2, entity3), + assertMatchWithScore(-1, value3, value2, value1.getCode() + value3.getCode(), 1L).withIndictedObjects(entity3, + entity2, entity1)); } @Override @@ -2024,18 +2094,18 @@ public void complement() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, 0, 1, 2), - assertMatch(entity2, Integer.MAX_VALUE, -1, 0), - assertMatch(entity3, Integer.MAX_VALUE, -1, 0)); + assertMatch(entity1, 0, 1, 2).withIndictedObjects(entity1), + assertMatch(entity2, Integer.MAX_VALUE, -1, 0).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE, -1, 0).withIndictedObjects(entity3)); // Incremental; all entities are still present, but the indexes are different. scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value1); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, 0, 1, 2), - assertMatch(entity2, 0, 1, 2), - assertMatch(entity3, Integer.MAX_VALUE, -1, 0)); + assertMatch(entity1, 0, 1, 2).withIndictedObjects(entity1), + assertMatch(entity2, 0, 1, 2).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE, -1, 0).withIndictedObjects(entity3)); } @Override From 871460326e41f305a450a88de71fdff1483100c3 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 23:41:36 -0400 Subject: [PATCH 12/18] test: make AdvanceGroupByTest aware of indictments --- ...ctAdvancedGroupByConstraintStreamTest.java | 152 ++++++++++++------ 1 file changed, 107 insertions(+), 45 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java index 834c2a78217..cd30a4646b2 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java @@ -54,7 +54,7 @@ void collectedDowngradedAndFiltered() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatch(new Pair<>("M", 1L))); + assertScore(scoreDirector, assertMatch(new Pair<>("M", 1L)).withIndictedObjects(entity)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -85,7 +85,7 @@ void collectedAndFiltered() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-10, 10L)); + assertScore(scoreDirector, assertMatchWithScore(-10, 10L).withIndictedObjects(solution.getEntityList().toArray())); // Incremental Stream.of(entity1, entity2).forEach(entity -> { @@ -109,14 +109,14 @@ void collectedFilteredRecollected() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-2, 2L)); + assertScore(scoreDirector, assertMatchWithScore(-2, 2L).withIndictedObjects(solution.getEntityList().toArray())); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 1L).withIndictedObjects(solution.getEntityList().toArray())); } @TestTemplate @@ -136,14 +136,15 @@ void uniGroupByRecollected() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, asSet(entity1.getEntityGroup(), entity2.getEntityGroup()))); + assertMatchWithScore(-2, asSet(entity1.getEntityGroup(), entity2.getEntityGroup())).withIndictedObjects(entity1, + entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, asSet(entity2.getEntityGroup()))); + assertMatchWithScore(-1, asSet(entity2.getEntityGroup())).withIndictedObjects(entity2)); } @TestTemplate @@ -163,7 +164,8 @@ void biGroupByRecollectedToList() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, - Arrays.asList(solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1)))); + Arrays.asList(solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1))) + .withIndictedObjects(solution.getEntityList().toArray())); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); @@ -172,7 +174,8 @@ void biGroupByRecollectedToList() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, - Arrays.asList(solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1)))); + Arrays.asList(solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1))) + .withIndictedObjects(solution.getEntityList().toArray())); } @TestTemplate @@ -192,7 +195,8 @@ void biGroupByRecollectedToMap() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getFirstEntityGroup(), 3L, solution.getEntityGroupList().get(1), 1L))); + asMap(solution.getFirstEntityGroup(), 3L, solution.getEntityGroupList().get(1), 1L)) + .withIndictedObjects(solution.getEntityList().toArray())); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); @@ -201,7 +205,8 @@ void biGroupByRecollectedToMap() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getFirstEntityGroup(), 1L, solution.getEntityGroupList().get(1), 1L))); + asMap(solution.getFirstEntityGroup(), 1L, solution.getEntityGroupList().get(1), 1L)) + .withIndictedObjects(solution.getEntityList().toArray())); } @TestTemplate @@ -224,16 +229,21 @@ void triGroupByRecollected() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getFirstEntityGroup(), 3L, solution.getEntityGroupList().get(1), 3L))); + asMap(solution.getFirstEntityGroup(), 3L, solution.getEntityGroupList().get(1), 3L)) + .withIndictedObjects(solution.getEntityList().toArray())); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); + TestdataLavishEntity group1Entity1 = solution.getEntityList().get(1); + TestdataLavishEntity group1Entity3 = solution.getEntityList().get(3); + TestdataLavishEntity group1Entity5 = solution.getEntityList().get(5); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getEntityGroupList().get(1), 3L))); + asMap(solution.getEntityGroupList().get(1), 3L)) + .withIndictedObjects(group1Entity1, group1Entity3, group1Entity5)); } @TestTemplate @@ -260,16 +270,22 @@ void quadGroupByRecollected() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getFirstEntityGroup(), 12L, solution.getEntityGroupList().get(1), 12L))); + asMap(solution.getFirstEntityGroup(), 12L, solution.getEntityGroupList().get(1), 12L)) + .withIndictedObjects(solution.getEntityList().toArray())); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); + TestdataLavishEntity group1Entity1 = solution.getEntityList().get(1); + TestdataLavishEntity group1Entity3 = solution.getEntityList().get(3); + TestdataLavishEntity group1Entity5 = solution.getEntityList().get(5); + TestdataLavishEntity group1Entity7 = solution.getEntityList().get(7); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getEntityGroupList().get(1), 12L))); + asMap(solution.getEntityGroupList().get(1), 12L)) + .withIndictedObjects(group1Entity1, group1Entity3, group1Entity5, group1Entity7)); } @TestTemplate @@ -286,18 +302,21 @@ void biGroupByRegrouped() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity.getEntityGroup(), 1L), - assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertMatchWithScore(-1, entity.getEntityGroup(), 1L).withIndictedObjects(entity, entityInGroup0_2), + assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, entityInGroup1_3)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertScore(scoreDirector, + assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, entityInGroup1_3)); } @TestTemplate @@ -318,18 +337,25 @@ void triGroupByRegrouped() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup0_4 = solution.getEntityList().get(4); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); + TestdataLavishEntity entityInGroup1_5 = solution.getEntityList().get(5); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity.getEntityGroup(), 1L), - assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertMatchWithScore(-1, entity.getEntityGroup(), 1L).withIndictedObjects(entity, entityInGroup0_2, + entityInGroup0_4), + assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, entityInGroup1_3, + entityInGroup1_5)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, + entityInGroup1_3, entityInGroup1_5)); } @TestTemplate @@ -354,18 +380,27 @@ void quadGroupByRegrouped() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup0_4 = solution.getEntityList().get(4); + TestdataLavishEntity entityInGroup0_6 = solution.getEntityList().get(6); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); + TestdataLavishEntity entityInGroup1_5 = solution.getEntityList().get(5); + TestdataLavishEntity entityInGroup1_7 = solution.getEntityList().get(7); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity.getEntityGroup(), 1L), - assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertMatchWithScore(-1, entity.getEntityGroup(), 1L).withIndictedObjects(entity, entityInGroup0_2, + entityInGroup0_4, entityInGroup0_6), + assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, entityInGroup1_3, + entityInGroup1_5, entityInGroup1_7)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, + entityInGroup1_3, entityInGroup1_5, entityInGroup1_7)); } @TestTemplate @@ -383,18 +418,23 @@ void biGroupByRegroupedDouble() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }), - assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }).withIndictedObjects(entity, + entityInGroup0_2), + assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }).withIndictedObjects(entity2, + entityInGroup1_3)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }) + .withIndictedObjects(entity2, entityInGroup1_3)); } @TestTemplate @@ -417,18 +457,25 @@ void triGroupByRegroupedDouble() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup0_4 = solution.getEntityList().get(4); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); + TestdataLavishEntity entityInGroup1_5 = solution.getEntityList().get(5); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }), - assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }).withIndictedObjects(entity, + entityInGroup0_2, entityInGroup0_4), + assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }).withIndictedObjects(entity2, + entityInGroup1_3, entityInGroup1_5)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }) + .withIndictedObjects(entity2, entityInGroup1_3, entityInGroup1_5)); } @TestTemplate @@ -454,18 +501,27 @@ void quadGroupByRegroupedDouble() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup0_4 = solution.getEntityList().get(4); + TestdataLavishEntity entityInGroup0_6 = solution.getEntityList().get(6); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); + TestdataLavishEntity entityInGroup1_5 = solution.getEntityList().get(5); + TestdataLavishEntity entityInGroup1_7 = solution.getEntityList().get(7); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }), - assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }).withIndictedObjects(entity, + entityInGroup0_2, entityInGroup0_4, entityInGroup0_6), + assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }).withIndictedObjects(entity2, + entityInGroup1_3, entityInGroup1_5, entityInGroup1_7)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }) + .withIndictedObjects(entity2, entityInGroup1_3, entityInGroup1_5, entityInGroup1_7)); } @TestTemplate @@ -491,15 +547,17 @@ void existsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L), - assertMatchWithScore(-2, entityGroup1, 2L)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntity(), + entity3), + assertMatchWithScore(-2, entityGroup1, 2L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup1); solution.getEntityGroupList().remove(entityGroup1); scoreDirector.afterProblemFactRemoved(entityGroup1); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntity(), + entity3)); } @TestTemplate @@ -526,15 +584,17 @@ void groupByAfterExists() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L), - assertMatchWithScore(-2, entityGroup1, 2L)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntityGroup(), + solution.getFirstEntity(), entity3), + assertMatchWithScore(-2, entityGroup1, 2L).withIndictedObjects(entityGroup1, entity1, entity2)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup1); solution.getEntityGroupList().remove(entityGroup1); scoreDirector.afterProblemFactRemoved(entityGroup1); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntityGroup(), + solution.getFirstEntity(), entity3)); } @TestTemplate @@ -561,15 +621,17 @@ void groupByAfterExistsBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L), - assertMatchWithScore(-3, entityGroup1, 3L)); + assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects(solution.getFirstEntity(), + entity1, entity2, entity3), + assertMatchWithScore(-3, entityGroup1, 3L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup1); solution.getEntityGroupList().remove(entityGroup1); scoreDirector.afterProblemFactRemoved(entityGroup1); assertScore(scoreDirector, - assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L)); + assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects(solution.getFirstEntity(), + entity1, entity2, entity3)); } @TestTemplate @@ -639,10 +701,10 @@ void reusedStreamsInJoin() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity1, entity1), - assertMatchWithScore(-1, entity1, entity3), - assertMatchWithScore(-1, entity3, entity1), - assertMatchWithScore(-1, entity3, entity3)); + assertMatchWithScore(-1, entity1, entity1).withIndictedObjects(entity1), + assertMatchWithScore(-1, entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, entity3, entity1).withIndictedObjects(entity3, entity1), + assertMatchWithScore(-1, entity3, entity3).withIndictedObjects(entity3)); } } From 15031efeac4b9f22d4552ca269c1fa26d6b16ca2 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 23:49:55 -0400 Subject: [PATCH 13/18] test: make BavetRegressionTest aware of indictments --- .../stream/bavet/BavetRegressionTest.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java index 9ef46b39528..f9fd7ddb44a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java @@ -52,8 +52,8 @@ void joinWithNullKeyFromRight() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity2, entity2)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2)); // Switch entity1 and entity2 values; now entity2 has null and entity1 does not. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -63,8 +63,8 @@ void joinWithNullKeyFromRight() { entity2.setValue(null); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity2, entity2)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2)); // Put both to null. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -74,10 +74,10 @@ void joinWithNullKeyFromRight() { entity2.setValue(null); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity1, entity2), - assertMatch(entity2, entity1), - assertMatch(entity2, entity2)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity1).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity2).withIndictedObjects(entity2)); } @TestTemplate @@ -491,7 +491,7 @@ void filteringJoinNullConflict() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity2, entity2)); // Only entity1 is left, because forEach/join ignore nulls. + assertMatch(entity2, entity2).withIndictedObjects(entity2)); // Only entity1 is left, because forEach/join ignore nulls. // Switch entity1 and entity2 values; now entity2 has null and entity1 does not. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -501,7 +501,7 @@ void filteringJoinNullConflict() { entity2.setValue(null); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1)); + assertMatch(entity1, entity1).withIndictedObjects(entity1)); // Switch entity1 and entity2 values again to test the same from the other side. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -511,7 +511,7 @@ void filteringJoinNullConflict() { entity2.setValue(value); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity2, entity2)); + assertMatch(entity2, entity2).withIndictedObjects(entity2)); } /** @@ -653,7 +653,7 @@ void filteringJoinNullConflictDifferentNodes() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity2, entity2)); + assertMatch(entity2, entity2).withIndictedObjects(entity2)); // Switch entity1 and entity2 values; now entity2 has null and entity1 does not. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -663,7 +663,7 @@ void filteringJoinNullConflictDifferentNodes() { entity2.setValue(null); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1)); + assertMatch(entity1, entity1).withIndictedObjects(entity1)); // Switch entity1 and entity2 values again to test the same from the other side. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -673,7 +673,7 @@ void filteringJoinNullConflictDifferentNodes() { entity2.setValue(value); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity2, entity2)); + assertMatch(entity2, entity2).withIndictedObjects(entity2)); } /** From 4dfea10ba93829b928a520c2d1ba8e6e4b577a8b Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 22 Jul 2026 12:24:29 -0400 Subject: [PATCH 14/18] fix: Propagate updates to indictments in ifExists even if left side doesn't update, make PrecomputeUni tests aware of indictments --- .../bavet/common/AbstractIfExistsNode.java | 19 ++++ .../common/AbstractIndexedIfExistsNode.java | 6 + .../tuple/indictment/IndictmentSource.java | 9 +- ...ctAdvancedGroupByConstraintStreamTest.java | 22 ++-- .../common/ConstraintStreamTestExtension.java | 8 -- ...ractUniConstraintStreamPrecomputeTest.java | 107 ++++++++++++++---- .../uni/AbstractUniConstraintStreamTest.java | 6 +- 7 files changed, 134 insertions(+), 43 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 2aa22efc138..746eb743865 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -125,6 +125,11 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter } else { doRetractCounter(counter); } + } else { + // count != 0, so only propagate if we are in an `ifExists` + if (shouldExist) { + doUpdateCounter(counter); + } } // Else do not even propagate an update // NOTE: By not propagating here, the left tuple's indicted objects can be stale // if an element is removed. @@ -141,6 +146,11 @@ protected void decrementCounterRight(ExistsCounter counter) { } else { doInsertCounter(counter); } + } else { + // count != 0, so only propagate if we are in an `ifExists` + if (shouldExist) { + doUpdateCounter(counter); + } } // Else do not even propagate an update } @@ -305,6 +315,15 @@ private void doRetractCounter(ExistsCounter counter) { } } + private void doUpdateCounter(ExistsCounter counter) { + switch (counter.state) { + case DYING, OK, UPDATING, CREATING -> propagationQueue.update(counter); + case DEAD, ABORTING -> propagationQueue.insert(counter); + default -> throw new IllegalStateException("Impossible state: the counter (%s) has an impossible insert state (%s)." + .formatted(counter, counter.state)); + } + } + @Override protected boolean canProduceTuples() { // The left input must produce tuples no matter what, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index 9d3e78d2b50..7f012e2e2f1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -100,6 +100,12 @@ private void updateCounterRight(LeftTuple_ leftTuple, Object compositeKey, Exist leftTuple.setStore(inputStoreIndexLeftCounterEntry, counterEntry); if (!isFiltering) { counter.countRight = rightSize(leftTuple, compositeKey); + if (leftTuple.getIndictmentSource() != IndictmentSource.DISABLED) { + IndictmentSource.clearSupport(getId(), leftTuple); + forEachRightFromLeft(leftTuple, compositeKey, rightTuple -> { + IndictmentSource.addSupport(getId(), leftTuple, rightTuple); + }); + } } else { // Trackers link themselves into the left tuple's inputStoreIndexLeftTrackerList slot. // No list object is needed; the slot starts null and the first tracker becomes the head. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index 04f57047105..feabf42bfdc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -86,6 +86,13 @@ static void addSupport(long nodeId, Tuple carry, Tuple support) { carry.getIndictmentSource().getSupportForNodeId(nodeId).add(support.getIndictmentSource()); } + static void clearSupport(long nodeId, Tuple carry) { + if (carry.getIndictmentSource() == DISABLED) { + return; + } + carry.getIndictmentSource().getSupportForNodeId(nodeId).clear(); + } + static void removeSupport(long nodeId, Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { return; @@ -155,7 +162,7 @@ public void visitSources(Set visited, long[] involvedNodeIds, return; } for (var source : sourceList) { - source.visitSources(involvedNodeIds, sourceConsumer); + source.visitSources(visited, involvedNodeIds, sourceConsumer); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java index cd30a4646b2..d7efab5062a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java @@ -547,17 +547,20 @@ void existsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntity(), - entity3), - assertMatchWithScore(-2, entityGroup1, 2L).withIndictedObjects(entity1, entity2)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), + entity3), + assertMatchWithScore(-2, entityGroup1, 2L) + .withIndictedObjects(entityGroup1, entity1, entity2)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup1); solution.getEntityGroupList().remove(entityGroup1); scoreDirector.afterProblemFactRemoved(entityGroup1); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntity(), - entity3)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), + entity3)); } @TestTemplate @@ -621,16 +624,19 @@ void groupByAfterExistsBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects(solution.getFirstEntity(), + assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects( + solution.getFirstEntityGroup(), solution.getFirstEntity(), entity1, entity2, entity3), - assertMatchWithScore(-3, entityGroup1, 3L).withIndictedObjects(entity1, entity2, entity3)); + assertMatchWithScore(-3, entityGroup1, 3L).withIndictedObjects( + entityGroup1, entity1, entity2, entity3)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup1); solution.getEntityGroupList().remove(entityGroup1); scoreDirector.afterProblemFactRemoved(entityGroup1); assertScore(scoreDirector, - assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects(solution.getFirstEntity(), + assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects( + solution.getFirstEntityGroup(), solution.getFirstEntity(), entity1, entity2, entity3)); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java index 3953260c7f1..a2eeaca2027 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java @@ -32,14 +32,6 @@ public boolean supportsTestTemplate(ExtensionContext context) { @Override public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { - if (ConstraintStreamPrecomputeTest.class.isAssignableFrom(context.getTestClass().get())) { - // Precompute test use - return Stream - .of(ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, - ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, - ConstraintMatchPolicy.DISABLED) - .map(ConstraintStreamTestExtension::invocationContext); - } return Stream .of(ConstraintMatchPolicy.ENABLED, ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java index 65b3f749e48..cc9fb27620f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.uni; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -13,7 +15,6 @@ import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamPrecomputeTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamTestExtension; -import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntity; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntityGroup; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishSolution; @@ -132,9 +133,27 @@ public void filter_0_changed() { assertMatch(entity3)); } - private void assertPrecompute(TestdataLavishSolution solution, - List expectedValues, - Function> entityStreamSupplier) { + record ExpectedUni(A a, Object... indicted) { + ExpectedUni addIndicted(Object indictedObject) { + for (var object : indicted) { + if (object == indictedObject) { + return this; + } + } + var indictments = Arrays.copyOf(indicted, indicted.length + 1); + indictments[indicted.length] = indictedObject; + return new ExpectedUni<>(a, indictments); + } + } + + ExpectedUni expect(A a, Object... indicted) { + return new ExpectedUni<>(a, indicted); + } + + private void assertPrecompute(TestdataLavishSolution solution, + List> expectedTuples, + Function> entityStreamSupplier) { + expectedTuples = new ArrayList<>(expectedTuples); var scoreDirector = buildScoreDirector(factory -> factory.precompute(entityStreamSupplier) .ifExists(TestdataLavishEntity.class) @@ -149,10 +168,16 @@ private void assertPrecompute(TestdataLavishSolution solution, scoreDirector.beforeVariableChanged(entity, "value"); entity.setValue(solution.getFirstValue()); scoreDirector.afterVariableChanged(entity, "value"); + var listIterator = expectedTuples.listIterator(); + while (listIterator.hasNext()) { + var expectedTuple = listIterator.next(); + listIterator.set(expectedTuple.addIndicted(entity)); + } } - assertScore(scoreDirector, expectedValues.stream() - .map(AbstractConstraintStreamTest::assertMatch) + assertScore(scoreDirector, expectedTuples.stream() + .map(expected -> AbstractConstraintStreamTest.assertMatch(expected.a) + .withIndictedObjects(expected.indicted)) .toArray(AssertableMatch[]::new)); } @@ -161,14 +186,17 @@ private void assertPrecompute(TestdataLavishSolution solution, public void ifExists() { var solution = TestdataLavishSolution.generateEmptySolution(); var entityWithoutGroup = new TestdataLavishEntity(); + entityWithoutGroup.setCode("A"); var entityWithGroup = new TestdataLavishEntity(); + entityWithGroup.setCode("B"); var entityGroup = new TestdataLavishEntityGroup(); + entityGroup.setCode("C"); entityWithGroup.setEntityGroup(entityGroup); solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityWithGroup), + assertPrecompute(solution, List.of(expect(entityWithGroup, entityWithGroup, entityGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .ifExists(TestdataLavishEntityGroup.class, Joiners.equal( TestdataLavishEntity::getEntityGroup, Function.identity()))); @@ -186,7 +214,7 @@ public void ifNotExists() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityWithoutGroup), + assertPrecompute(solution, List.of(expect(entityWithoutGroup, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .ifNotExists(TestdataLavishEntityGroup.class, Joiners.equal( TestdataLavishEntity::getEntityGroup, Function.identity()))); @@ -204,7 +232,7 @@ public void groupBy() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityGroup), + assertPrecompute(solution, List.of(expect(entityGroup, entityWithGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .groupBy(TestdataLavishEntity::getEntityGroup)); @@ -215,21 +243,42 @@ public void groupBy() { public void flatten() { var solution = TestdataLavishSolution.generateEmptySolution(); var entityWithoutGroup = new TestdataLavishEntity(); + entityWithoutGroup.setCode("A"); var entityWithGroup = new TestdataLavishEntity(); + entityWithGroup.setCode("B"); var entityGroup = new TestdataLavishEntityGroup(); + entityGroup.setCode("C"); entityWithGroup.setEntityGroup(entityGroup); solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecomputeBi(solution, List.of(new Pair<>(entityWithoutGroup, entityWithoutGroup), - new Pair<>(entityWithGroup, entityWithGroup)), + assertPrecomputeBi(solution, List.of(expectBi(entityWithoutGroup, entityWithoutGroup, entityWithoutGroup), + expectBi(entityWithGroup, entityWithGroup, entityWithGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(List::of)); } - private void assertPrecomputeBi(TestdataLavishSolution solution, List> expectedValues, + record ExpectedBi(A a, B b, Object... indicted) { + ExpectedBi addIndicted(Object indictedObject) { + for (var object : indicted) { + if (object == indictedObject) { + return this; + } + } + var indictments = Arrays.copyOf(indicted, indicted.length + 1); + indictments[indicted.length] = indictedObject; + return new ExpectedBi<>(a, b, indictments); + } + } + + ExpectedBi expectBi(A a, B b, Object... indicted) { + return new ExpectedBi<>(a, b, indicted); + } + + private void assertPrecomputeBi(TestdataLavishSolution solution, List> expectedValues, Function> entityStreamSupplier) { + expectedValues = new ArrayList<>(expectedValues); var scoreDirector = buildScoreDirector(factory -> factory.precompute(entityStreamSupplier) .ifExists(TestdataLavishEntity.class) @@ -244,11 +293,16 @@ private void assertPrecomputeBi(TestdataLavishSolution solution, List new Object[] { pair.key(), pair.value() }) - .map(AbstractConstraintStreamTest::assertMatch) + .map(expectedBi -> assertMatch(expectedBi.a(), expectedBi.b()) + .withIndictedObjects(expectedBi.indicted)) .toArray(AssertableMatch[]::new)); } @@ -268,8 +322,8 @@ record ValueHolder(int value) { solution.getValueList().add(new TestdataLavishValue()); assertPrecomputeBi(solution, List.of( - new Pair<>(entity1, new ValueHolder(1)), - new Pair<>(entity2, new ValueHolder(2))), + expectBi(entity1, new ValueHolder(1), entity1), + expectBi(entity2, new ValueHolder(2), entity2)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(entity -> List.of(new ValueHolder(entity.getIntegerProperty())))); } @@ -286,7 +340,8 @@ public void flattenLast() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityWithoutGroup, entityWithGroup), + assertPrecompute(solution, + List.of(expect(entityWithoutGroup, entityWithoutGroup), expect(entityWithGroup, entityWithGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList)); @@ -307,7 +362,8 @@ record ValueHolder(int value) { solution.getEntityList().addAll(List.of(entity1, entity2)); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(new ValueHolder(1), new ValueHolder(2)), + assertPrecompute(solution, List.of(expect(new ValueHolder(1), entity1), + expect(new ValueHolder(2), entity2)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList @@ -330,7 +386,8 @@ public void map() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityGroup, entityGroup), + assertPrecompute(solution, List.of(expect(entityGroup, entityWithGroup1), + expect(entityGroup, entityWithGroup2)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .map(TestdataLavishEntity::getEntityGroup)); @@ -341,14 +398,18 @@ public void map() { public void concat() { var solution = TestdataLavishSolution.generateEmptySolution(); var entityWithoutGroup = new TestdataLavishEntity(); + entityWithoutGroup.setCode("EntityWithoutGroup"); var entityWithGroup = new TestdataLavishEntity(); + entityWithGroup.setCode("EntityWithGroup"); var entityGroup = new TestdataLavishEntityGroup(); + entityGroup.setCode("EntityGroup"); entityWithGroup.setEntityGroup(entityGroup); solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityWithoutGroup, entityWithGroup), + assertPrecompute(solution, List.of(expect(entityWithoutGroup, entityWithoutGroup), + expect(entityWithGroup, entityWithGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() == null) .concat(pf.forEachUnfiltered(TestdataLavishEntity.class) @@ -369,7 +430,7 @@ public void distinct() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityGroup), + assertPrecompute(solution, List.of(expect(entityGroup, entityWithGroup1, entityWithGroup2)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .map(TestdataLavishEntity::getEntityGroup) @@ -390,7 +451,9 @@ public void complement() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityWithGroup1, entityWithGroup2, entityWithoutGroup), + assertPrecompute(solution, List.of(expect(entityWithGroup1, entityWithGroup1), + expect(entityWithGroup2, entityWithGroup2), + expect(entityWithoutGroup, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .complement(TestdataLavishEntity.class)); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index 42e57172f94..64645c474bf 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -485,12 +485,10 @@ public void ifExists_0Joiner0Filter() { assertScore(scoreDirector, assertMatch(solution.getFirstValueGroup()).withIndictedObjects( solution.getFirstValueGroup(), - solution.getFirstEntityGroup(), - entityGroup), + solution.getFirstEntityGroup()), assertMatch(valueGroup).withIndictedObjects( valueGroup, - solution.getFirstEntityGroup(), - entityGroup)); + solution.getFirstEntityGroup())); } @Override From 70a6eff92da6e673747d962a59d208e843bc053e Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 22 Jul 2026 15:28:04 -0400 Subject: [PATCH 15/18] fix: update left indicted set even if its key not changed for indexed if exists, aggregate support for precompute, make BiPrecomputeTest Indictment aware --- .../core/impl/bavet/bi/PrecomputeBiNode.java | 4 +- .../common/AbstractIndexedIfExistsNode.java | 6 ++ .../core/impl/bavet/common/TupleRecorder.java | 17 +++- .../tuple/indictment/IndictmentSource.java | 39 +++++++--- .../impl/bavet/quad/PrecomputeQuadNode.java | 4 +- .../impl/bavet/tri/PrecomputeTriNode.java | 4 +- .../impl/bavet/uni/PrecomputeUniNode.java | 4 +- ...tractBiConstraintStreamPrecomputeTest.java | 78 +++++++++++++------ .../bi/AbstractBiConstraintStreamTest.java | 15 ++-- .../tri/AbstractTriConstraintStreamTest.java | 15 ++-- 10 files changed, 132 insertions(+), 54 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java index 6afb77873a3..ee4f3575c31 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java @@ -23,8 +23,6 @@ public PrecomputeBiNode(Supplier>> prec @Override protected BiTuple remapTuple(BiTuple tuple) { - var out = BiTuple.of(tuple.getA(), tuple.getB(), outputStoreSize); - out.setIndictmentSource(tuple.getIndictmentSource()); - return out; + return BiTuple.of(tuple.getA(), tuple.getB(), outputStoreSize); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index 7f012e2e2f1..91cceb6d511 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -140,6 +140,12 @@ public final void updateLeft(LeftTuple_ leftTuple) { // The indexers contain counters in the DEAD state, to track the rightCount. if (!isFiltering) { updateUnchangedCounterLeft(counter); + if (leftTuple.getIndictmentSource() != IndictmentSource.DISABLED) { + IndictmentSource.clearSupport(getId(), leftTuple); + forEachRightFromLeft(leftTuple, newCompositeKey, rightTuple -> { + IndictmentSource.addSupport(getId(), leftTuple, rightTuple); + }); + } } else { // Call filtering for the leftTuple and rightTuple combinations again clearLeftTrackerList(leftTuple); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java index c2fab90d528..2ae88ccdb76 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java @@ -1,10 +1,13 @@ package ai.timefold.solver.core.impl.bavet.common; +import java.util.ArrayList; import java.util.IdentityHashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.function.UnaryOperator; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; @@ -13,6 +16,18 @@ public record TupleRecorder(List recordedTupleList UnaryOperator mapper, IdentityHashMap inputTupleToOutputTuple) { public void recordTuple(Tuple_ tuple) { - recordedTupleList.add(inputTupleToOutputTuple.computeIfAbsent(tuple, mapper)); + var outTuple = inputTupleToOutputTuple.computeIfAbsent(tuple, mapper); + if (tuple.getIndictmentSource() != IndictmentSource.DISABLED) { + if (outTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + outTuple.setIndictmentSource( + new IndictmentSource.AggregateIndictmentSource(new ArrayList<>(), new LinkedHashMap<>())); + } + // Precompute uses an independent node network, so we need to aggregate its supports to not interfere with the + // outer node network support + var aggregateIndictmentSource = (IndictmentSource.AggregateIndictmentSource) outTuple.getIndictmentSource(); + tuple.getIndictmentSource() + .visitAllSources(source -> aggregateIndictmentSource.sourceList().add(IndictmentSource.of(source))); + } + recordedTupleList.add(outTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index feabf42bfdc..69f565f2421 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -12,13 +12,21 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked public sealed interface IndictmentSource { IndictmentSource DISABLED = new DisabledIndictmentSource(); - void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer); + void visitSources(Set visited, long @Nullable [] involvedNodeIds, Consumer sourceConsumer); Map> support(); + default void visitAllSources(Consumer sourceConsumer) { + visitSources(new HashSet<>(), null, sourceConsumer); + } + default void visitSources(long[] involvedNodeIds, Consumer sourceConsumer) { visitSources(new HashSet<>(), involvedNodeIds, sourceConsumer); } @@ -28,13 +36,22 @@ default Set getSupportForNodeId(long nodeId) { } static boolean checkIfAlreadyVisitedAndVisitSupport(IndictmentSource self, Set visited, - long[] involvedNodeIds, Consumer sourceConsumer) { + long @Nullable [] involvedNodeIds, Consumer sourceConsumer) { if (!visited.add(self)) { return true; } - for (var nodeId : involvedNodeIds) { - for (var indictmentSource : self.support().getOrDefault(nodeId, Collections.emptySet())) { - indictmentSource.visitSources(visited, involvedNodeIds, sourceConsumer); + + if (involvedNodeIds == null) { + for (var indictmentSourceSet : self.support().values()) { + for (var indictmentSource : indictmentSourceSet) { + indictmentSource.visitSources(visited, null, sourceConsumer); + } + } + } else { + for (var nodeId : involvedNodeIds) { + for (var indictmentSource : self.support().getOrDefault(nodeId, Collections.emptySet())) { + indictmentSource.visitSources(visited, involvedNodeIds, sourceConsumer); + } } } return false; @@ -102,7 +119,8 @@ static void removeSupport(long nodeId, Tuple carry, Tuple support) { record DisabledIndictmentSource() implements IndictmentSource { @Override - public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + public void visitSources(Set visited, long @Nullable [] involvedNodeIds, + Consumer sourceConsumer) { throw new UnsupportedOperationException("Impossible state: indictments are disabled."); } @@ -114,7 +132,8 @@ public Map> support() { record RootIndictmentSource(Object source, Map> support) implements IndictmentSource { @Override - public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + public void visitSources(Set visited, long @Nullable [] involvedNodeIds, + Consumer sourceConsumer) { if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { return; } @@ -135,7 +154,8 @@ public int hashCode() { record JoinedIndictmentSource(IndictmentSource left, IndictmentSource right, Map> support) implements IndictmentSource { @Override - public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + public void visitSources(Set visited, long @Nullable [] involvedNodeIds, + Consumer sourceConsumer) { if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { return; } @@ -157,7 +177,8 @@ public int hashCode() { record AggregateIndictmentSource(List sourceList, Map> support) implements IndictmentSource { @Override - public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + public void visitSources(Set visited, long @Nullable [] involvedNodeIds, + Consumer sourceConsumer) { if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { return; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java index 371c8448162..8c65119bd99 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java @@ -23,8 +23,6 @@ public PrecomputeQuadNode(Supplier remapTuple(QuadTuple tuple) { - var out = QuadTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), outputStoreSize); - out.setIndictmentSource(tuple.getIndictmentSource()); - return out; + return QuadTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), outputStoreSize); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java index 06d776b782a..936563f13c5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java @@ -23,8 +23,6 @@ public PrecomputeTriNode(Supplier>> @Override protected TriTuple remapTuple(TriTuple tuple) { - var out = TriTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), outputStoreSize); - out.setIndictmentSource(tuple.getIndictmentSource()); - return out; + return TriTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), outputStoreSize); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java index 70f64d5ad73..92dfee56180 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java @@ -23,9 +23,7 @@ public PrecomputeUniNode(Supplier>> preco @Override protected UniTuple remapTuple(UniTuple tuple) { - var out = UniTuple.of(tuple.getA(), outputStoreSize); - out.setIndictmentSource(tuple.getIndictmentSource()); - return out; + return UniTuple.of(tuple.getA(), outputStoreSize); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java index 032a4493f05..48d9ae75ea3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.bi; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -11,7 +13,6 @@ import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamPrecomputeTest; -import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntity; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntityGroup; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishSolution; @@ -354,9 +355,27 @@ public void filter_1_changed_forEachUnfilteredUniquePair() { assertMatch(entity2, entity3)); } + record Expected(A a, B b, Object... indicted) { + Expected addIndicted(Object indictedObject) { + for (var object : indicted) { + if (object == indictedObject) { + return this; + } + } + var newIndictments = Arrays.copyOf(indicted, indicted.length + 1); + newIndictments[indicted.length] = indictedObject; + return new Expected<>(a, b, newIndictments); + } + } + + Expected expect(A a, B b, Object... indicted) { + return new Expected<>(a, b, indicted); + } + private void assertPrecompute(TestdataLavishSolution solution, - List> expectedValues, + List> expectedValues, Function> entityStreamSupplier) { + expectedValues = new ArrayList<>(expectedValues); var scoreDirector = buildScoreDirector(factory -> factory.precompute(entityStreamSupplier) .ifExists(TestdataLavishEntity.class) @@ -371,11 +390,16 @@ private void assertPrecompute(TestdataLavishSolution solution, scoreDirector.beforeVariableChanged(entity, "value"); entity.setValue(solution.getFirstValue()); scoreDirector.afterVariableChanged(entity, "value"); + var listIterator = expectedValues.listIterator(); + while (listIterator.hasNext()) { + var expected = listIterator.next(); + listIterator.set(expected.addIndicted(entity)); + } } assertScore(scoreDirector, expectedValues.stream() - .map(pair -> new Object[] { pair.key(), pair.value() }) - .map(AbstractConstraintStreamTest::assertMatch) + .map(expected -> assertMatch(expected.a, expected.b) + .withIndictedObjects(expected.indicted)) .toArray(AssertableMatch[]::new)); } @@ -384,15 +408,19 @@ private void assertPrecompute(TestdataLavishSolution solution, public void ifExists() { var solution = TestdataLavishSolution.generateEmptySolution(); var entityWithoutGroup = new TestdataLavishEntity(); + entityWithoutGroup.setCode("A"); var entityWithGroup = new TestdataLavishEntity(); + entityWithGroup.setCode("B"); var entityGroup = new TestdataLavishEntityGroup(); + entityGroup.setCode("C"); entityWithGroup.setEntityGroup(entityGroup); solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); solution.getEntityGroupList().add(entityGroup); var value = new TestdataLavishValue(); + value.setCode("D"); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityWithGroup, value)), + assertPrecompute(solution, List.of(expect(entityWithGroup, value, entityWithGroup, value, entityGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .ifExists(TestdataLavishEntityGroup.class, Joiners.equal( @@ -413,7 +441,7 @@ public void ifNotExists() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, value)), + assertPrecompute(solution, List.of(expect(entityWithoutGroup, value, entityWithoutGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .ifNotExists(TestdataLavishEntityGroup.class, Joiners.equal( @@ -434,7 +462,7 @@ public void groupBy() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityGroup, 1L)), + assertPrecompute(solution, List.of(expect(entityGroup, 1L)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .groupBy(TestdataLavishEntity::getEntityGroup, ConstraintCollectors.count())); @@ -453,8 +481,8 @@ public void flatten() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, entityWithoutGroup), - new Pair<>(entityWithGroup, entityWithoutGroup)), + assertPrecompute(solution, List.of(expect(entityWithoutGroup, entityWithoutGroup), + expect(entityWithGroup, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(List::of)); } @@ -475,8 +503,8 @@ record ValueHolder(int value) { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entity1, new ValueHolder(entity1.getIntegerProperty())), - new Pair<>(entity2, new ValueHolder(entity2.getIntegerProperty()))), + assertPrecompute(solution, List.of(expect(entity1, new ValueHolder(entity1.getIntegerProperty())), + expect(entity2, new ValueHolder(entity2.getIntegerProperty()))), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(entity -> List.of(new ValueHolder(entity.getIntegerProperty())))); } @@ -494,8 +522,8 @@ public void flattenLast() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, value), - new Pair<>(entityWithGroup, value)), + assertPrecompute(solution, List.of(expect(entityWithoutGroup, value, entityWithoutGroup, value), + expect(entityWithGroup, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList) @@ -518,8 +546,8 @@ record ValueHolder(int value) { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(new ValueHolder(1), value), - new Pair<>(new ValueHolder(2), value)), + assertPrecompute(solution, List.of(expect(new ValueHolder(1), value, entity1, value), + expect(new ValueHolder(2), value, entity2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList @@ -544,8 +572,8 @@ public void map() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityGroup, value), - new Pair<>(entityGroup, value)), + assertPrecompute(solution, List.of(expect(entityGroup, value, entityWithGroup1, value), + expect(entityGroup, value, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .filter((entity, joinedValue) -> entity.getEntityGroup() != null) @@ -558,15 +586,21 @@ public void map() { public void concat() { var solution = TestdataLavishSolution.generateEmptySolution(); var entityWithoutGroup = new TestdataLavishEntity(); + entityWithoutGroup.setCode("A"); var entityWithGroup = new TestdataLavishEntity(); + entityWithGroup.setCode("B"); var entityGroup = new TestdataLavishEntityGroup(); + entityGroup.setCode("C"); entityWithGroup.setEntityGroup(entityGroup); solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); solution.getEntityGroupList().add(entityGroup); var value = new TestdataLavishValue(); + value.setCode("D"); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, value), new Pair<>(entityWithGroup, value)), + assertPrecompute(solution, List.of( + expect(entityWithoutGroup, value, entityWithoutGroup, value), + expect(entityWithGroup, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .filter((entity, joinedValue) -> entity.getEntityGroup() == null) @@ -590,7 +624,7 @@ public void distinct() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityGroup, value)), + assertPrecompute(solution, List.of(expect(entityGroup, value, entityWithGroup1, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .filter((entity, joinedValue) -> entity.getEntityGroup() != null) @@ -615,9 +649,9 @@ public void complement() { solution.getValueList().add(value); assertPrecompute(solution, List.of( - new Pair<>(entityWithGroup1, value), - new Pair<>(entityWithGroup2, value), - new Pair<>(entityWithoutGroup, null)), + expect(entityWithGroup1, value, entityWithGroup1, value), + expect(entityWithGroup2, value, entityWithGroup2, value), + expect(entityWithoutGroup, null, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .filter((entity, joinedValue) -> entity.getEntityGroup() != null) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java index 7b5cf3873ed..87fcbeed658 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java @@ -949,17 +949,22 @@ public void ifExists_1Join0Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), entity1), - assertMatch(solution.getFirstEntity(), entity2), - assertMatch(entity1, entity2)); + assertMatch(solution.getFirstEntity(), entity1) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), entity1), + assertMatch(solution.getFirstEntity(), entity2) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), entity2), + assertMatch(entity1, entity2) + .withIndictedObjects(entityGroup, entity1, entity2)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), entity1), - assertMatch(solution.getFirstEntity(), entity2)); + assertMatch(solution.getFirstEntity(), entity1) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), entity1), + assertMatch(solution.getFirstEntity(), entity2) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), entity2)); } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java index d1d79aef37c..d027d54c48d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java @@ -449,17 +449,22 @@ public void ifExists_1Join0Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity2, entity1, solution.getFirstEntity()), - assertMatch(entity1, entity2, solution.getFirstEntity()), - assertMatch(solution.getFirstEntity(), entity1, entity2)); + assertMatch(entity2, entity1, solution.getFirstEntity()) + .withIndictedObjects(solution.getFirstEntityGroup(), entity2, entity1, solution.getFirstEntity()), + assertMatch(entity1, entity2, solution.getFirstEntity()) + .withIndictedObjects(entityGroup, entity1, entity2, solution.getFirstEntity()), + assertMatch(solution.getFirstEntity(), entity1, entity2) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), entity1, entity2)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(entity2, entity1, solution.getFirstEntity()), - assertMatch(entity1, entity2, solution.getFirstEntity())); + assertMatch(entity2, entity1, solution.getFirstEntity()) + .withIndictedObjects(solution.getFirstEntityGroup(), entity2, entity1, solution.getFirstEntity()), + assertMatch(entity1, entity2, solution.getFirstEntity()) + .withIndictedObjects(solution.getFirstEntityGroup(), entity1, entity2, solution.getFirstEntity())); } @Override From 04622f2b71aca15c0e0aa305a66902004743701e Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 22 Jul 2026 16:18:34 -0400 Subject: [PATCH 16/18] test: make TriPrecompute test indictment aware --- ...ractTriConstraintStreamPrecomputeTest.java | 74 +++++++++++++------ 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java index d80cfab093a..56b83bddd81 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.tri; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -162,9 +164,27 @@ public void filter_2_changed() { (entity, value, entityGroup) -> new Triple<>(value, entityGroup, entity)); } + record ExpectedTri(A a, B b, C c, Object... indicted) { + ExpectedTri withIndictedObject(Object indictedObject) { + for (var obj : indicted) { + if (obj == indictedObject) { + return this; + } + } + var newIndicted = Arrays.copyOf(indicted, indicted.length + 1); + newIndicted[indicted.length] = indictedObject; + return new ExpectedTri<>(a, b, c, newIndicted); + } + } + + ExpectedTri expected(A a, B b, C c, Object... indicted) { + return new ExpectedTri<>(a, b, c, indicted); + } + private void assertPrecompute(TestdataLavishSolution solution, - List> expectedValues, + List> expectedValues, Function> entityStreamSupplier) { + expectedValues = new ArrayList<>(expectedValues); var scoreDirector = buildScoreDirector(factory -> factory.precompute(entityStreamSupplier) .ifExists(TestdataLavishEntity.class) @@ -179,11 +199,16 @@ private void assertPrecompute(TestdataLavishSolution solution, scoreDirector.beforeVariableChanged(entity, "value"); entity.setValue(solution.getFirstValue()); scoreDirector.afterVariableChanged(entity, "value"); + var listIterator = expectedValues.listIterator(); + while (listIterator.hasNext()) { + var expected = listIterator.next(); + listIterator.set(expected.withIndictedObject(entity)); + } } assertScore(scoreDirector, expectedValues.stream() - .map(triple -> new Object[] { triple.a(), triple.b(), triple.c() }) - .map(AbstractConstraintStreamTest::assertMatch) + .map(expected -> assertMatch(expected.a, expected.b, expected.c) + .withIndictedObjects(expected.indicted)) .toArray(AssertableMatch[]::new)); } @@ -200,7 +225,7 @@ public void ifExists() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityWithGroup, value, value)), + assertPrecompute(solution, List.of(expected(entityWithGroup, value, value, entityWithGroup, value, entityGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -222,7 +247,7 @@ public void ifNotExists() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityWithoutGroup, value, value)), + assertPrecompute(solution, List.of(expected(entityWithoutGroup, value, value, entityWithoutGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -244,7 +269,7 @@ public void groupBy() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityGroup, 1L, 1L)), + assertPrecompute(solution, List.of(expected(entityGroup, 1L, 1L)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .groupBy(TestdataLavishEntity::getEntityGroup, @@ -265,8 +290,9 @@ public void flatten() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityWithoutGroup, entityWithoutGroup, value), - new Triple<>(entityWithGroup, entityWithoutGroup, value)), + assertPrecompute(solution, List.of( + expected(entityWithoutGroup, entityWithoutGroup, value, entityWithoutGroup, value), + expected(entityWithGroup, entityWithoutGroup, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(List::of) .join(TestdataLavishValue.class)); @@ -288,8 +314,9 @@ record ValueHolder(int value) { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entity1, new ValueHolder(entity1.getIntegerProperty()), value), - new Triple<>(entity2, new ValueHolder(entity2.getIntegerProperty()), value)), + assertPrecompute(solution, List.of( + expected(entity1, new ValueHolder(entity1.getIntegerProperty()), value, entity1, value), + expected(entity2, new ValueHolder(entity2.getIntegerProperty()), value, entity2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(entity -> List.of(new ValueHolder(entity.getIntegerProperty()))) .join(TestdataLavishValue.class)); @@ -308,8 +335,9 @@ public void flattenLast() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityWithoutGroup, value, value), - new Triple<>(entityWithGroup, value, value)), + assertPrecompute(solution, List.of( + expected(entityWithoutGroup, value, value, entityWithoutGroup, value), + expected(entityWithGroup, value, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList) @@ -333,8 +361,9 @@ record ValueHolder(int value) { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(new ValueHolder(1), value, value), - new Triple<>(new ValueHolder(2), value, value)), + assertPrecompute(solution, List.of( + expected(new ValueHolder(1), value, value, entity1, value), + expected(new ValueHolder(2), value, value, entity2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList @@ -360,8 +389,9 @@ public void map() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityGroup, value, value), - new Triple<>(entityGroup, value, value)), + assertPrecompute(solution, List.of( + expected(entityGroup, value, value, entityWithGroup1, value), + expected(entityGroup, value, value, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -385,7 +415,8 @@ public void concat() { solution.getValueList().add(value); assertPrecompute(solution, - List.of(new Triple<>(entityWithoutGroup, value, value), new Triple<>(entityWithGroup, value, value)), + List.of(expected(entityWithoutGroup, value, value, entityWithoutGroup, value), + expected(entityWithGroup, value, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -411,7 +442,8 @@ public void distinct() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityGroup, value, value)), + assertPrecompute(solution, List.of( + expected(entityGroup, value, value, entityWithGroup1, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -438,9 +470,9 @@ public void complement() { solution.getValueList().add(value); assertPrecompute(solution, List.of( - new Triple<>(entityWithGroup1, value, value), - new Triple<>(entityWithGroup2, value, value), - new Triple<>(entityWithoutGroup, null, null)), + expected(entityWithGroup1, value, value, entityWithGroup1, value), + expected(entityWithGroup2, value, value, entityWithGroup2, value), + expected(entityWithoutGroup, null, null, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) From a1b0cb5b7718f47e75a7562807196acbde653ed1 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 22 Jul 2026 17:08:14 -0400 Subject: [PATCH 17/18] test: make QuadPrecompute tests indictment aware --- ...actQuadConstraintStreamPrecomputeTest.java | 64 +++++++++++++------ 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamPrecomputeTest.java index 06d6b9df156..d12b05d80fe 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamPrecomputeTest.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.quad; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -186,9 +188,27 @@ public void filter_3_changed() { (entity, value, entityGroup, valueGroup) -> new Quadruple<>(value, entityGroup, valueGroup, entity)); } + record ExpectedQuad(A a, B b, C c, D d, Object... indicted) { + ExpectedQuad withIndictedObject(Object indictedObject) { + for (var obj : indicted) { + if (obj == indictedObject) { + return this; + } + } + var newIndicted = Arrays.copyOf(indicted, indicted.length + 1); + newIndicted[indicted.length] = indictedObject; + return new ExpectedQuad<>(a, b, c, d, newIndicted); + } + } + + ExpectedQuad expected(A a, B b, C c, D d, Object... indicted) { + return new ExpectedQuad<>(a, b, c, d, indicted); + } + private void assertPrecompute(TestdataLavishSolution solution, - List> expectedValues, + List> expectedValues, Function> entityStreamSupplier) { + expectedValues = new ArrayList<>(expectedValues); var scoreDirector = buildScoreDirector(factory -> factory.precompute(entityStreamSupplier) .ifExists(TestdataLavishEntity.class) @@ -203,11 +223,16 @@ private void assertPrecompute(TestdataLavishSolution solution, scoreDirector.beforeVariableChanged(entity, "value"); entity.setValue(solution.getFirstValue()); scoreDirector.afterVariableChanged(entity, "value"); + var listIterator = expectedValues.listIterator(); + while (listIterator.hasNext()) { + var expected = listIterator.next(); + listIterator.set(expected.withIndictedObject(entity)); + } } assertScore(scoreDirector, expectedValues.stream() - .map(quad -> new Object[] { quad.a(), quad.b(), quad.c(), quad.d() }) - .map(AbstractConstraintStreamTest::assertMatch) + .map(expected -> assertMatch(expected.a, expected.b, expected.c, expected.d) + .withIndictedObjects(expected.indicted)) .toArray(AssertableMatch[]::new)); } @@ -224,7 +249,7 @@ public void ifExists() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityWithGroup, value, value, value)), + assertPrecompute(solution, List.of(expected(entityWithGroup, value, value, value, entityWithGroup, value, entityGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -247,7 +272,7 @@ public void ifNotExists() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityWithoutGroup, value, value, value)), + assertPrecompute(solution, List.of(expected(entityWithoutGroup, value, value, value, entityWithoutGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -270,7 +295,7 @@ public void groupBy() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityGroup, 1L, 1L, 1L)), + assertPrecompute(solution, List.of(expected(entityGroup, 1L, 1L, 1L)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .groupBy(TestdataLavishEntity::getEntityGroup, @@ -292,8 +317,9 @@ public void flattenLast() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityWithoutGroup, value, value, value), - new Quadruple<>(entityWithGroup, value, value, value)), + assertPrecompute(solution, List.of( + expected(entityWithoutGroup, value, value, value, entityWithoutGroup, value), + expected(entityWithGroup, value, value, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList) @@ -318,8 +344,9 @@ record ValueHolder(int value) { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(new ValueHolder(1), value, value, value), - new Quadruple<>(new ValueHolder(2), value, value, value)), + assertPrecompute(solution, List.of( + expected(new ValueHolder(1), value, value, value, entity1, value), + expected(new ValueHolder(2), value, value, value, entity2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList @@ -346,8 +373,8 @@ public void map() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityGroup, value, value, value), - new Quadruple<>(entityGroup, value, value, value)), + assertPrecompute(solution, List.of(expected(entityGroup, value, value, value, entityWithGroup1, value), + expected(entityGroup, value, value, value, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -373,8 +400,8 @@ public void concat() { solution.getValueList().add(value); assertPrecompute(solution, - List.of(new Quadruple<>(entityWithoutGroup, value, value, value), - new Quadruple<>(entityWithGroup, value, value, value)), + List.of(expected(entityWithoutGroup, value, value, value, entityWithoutGroup, value), + expected(entityWithGroup, value, value, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -403,7 +430,8 @@ public void distinct() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityGroup, value, value, value)), + assertPrecompute(solution, + List.of(expected(entityGroup, value, value, value, entityWithGroup1, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -432,9 +460,9 @@ public void complement() { solution.getValueList().add(value); assertPrecompute(solution, List.of( - new Quadruple<>(entityWithGroup1, value, value, value), - new Quadruple<>(entityWithGroup2, value, value, value), - new Quadruple<>(entityWithoutGroup, null, null, null)), + expected(entityWithGroup1, value, value, value, entityWithGroup1, value), + expected(entityWithGroup2, value, value, value, entityWithGroup2, value), + expected(entityWithoutGroup, null, null, null, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) From b438f8b4e7cb56429479f409a09d91c6b6802e63 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 28 Jul 2026 16:41:34 -0400 Subject: [PATCH 18/18] chore: merge fixes --- .../bavet/common/AbstractIfExistsNode.java | 81 +++++++++++-------- .../common/AbstractIndexedIfExistsNode.java | 38 ++------- .../common/AbstractUnindexedIfExistsNode.java | 10 +-- .../stream/bavet/BavetRegressionTest.java | 36 ++++----- .../bi/AbstractBiConstraintStreamTest.java | 12 +-- .../AbstractQuadConstraintStreamTest.java | 12 ++- .../tri/AbstractTriConstraintStreamTest.java | 6 +- .../uni/AbstractUniConstraintStreamTest.java | 8 +- 8 files changed, 101 insertions(+), 102 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 746eb743865..9c54c27001d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -105,7 +105,7 @@ protected void killCounterLeft(ExistsCounter counter) { } } - protected void incrementCounterRightWithoutIndictment(ExistsCounter counter) { + protected void incrementCounterRight(ExistsCounter counter) { if (counter.countRight == 0) { if (shouldExist) { doInsertCounter(counter); @@ -113,45 +113,46 @@ protected void incrementCounterRightWithoutIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - // NOTE: By not propagating here, the left tuple's indicted objects can be stale - // if an element is removed. counter.countRight++; } protected void incrementCounterRightUpdatingIndictment(ExistsCounter counter, UniTuple rightTuple) { + IndictmentSource.addSupport(getId(), counter.getTuple(), rightTuple); if (counter.countRight == 0) { if (shouldExist) { doInsertCounter(counter); } else { doRetractCounter(counter); } - } else { - // count != 0, so only propagate if we are in an `ifExists` - if (shouldExist) { - doUpdateCounter(counter); - } - } // Else do not even propagate an update - // NOTE: By not propagating here, the left tuple's indicted objects can be stale - // if an element is removed. - IndictmentSource.addSupport(getId(), counter.leftTuple, rightTuple); + } else if (shouldExist) { + doUpdateCounter(counter); + } counter.countRight++; } protected void decrementCounterRight(ExistsCounter counter) { counter.countRight--; - IndictmentSource.removeSupport(getId(), counter.leftTuple, rightTuple); if (counter.countRight == 0) { if (shouldExist) { doRetractCounter(counter); } else { doInsertCounter(counter); } - } else { - // count != 0, so only propagate if we are in an `ifExists` + } // Else do not even propagate an update + } + + protected void decrementCounterRightUpdatingIndictment(ExistsCounter counter, UniTuple rightTuple) { + IndictmentSource.removeSupport(getId(), counter.getTuple(), rightTuple); + counter.countRight--; + if (counter.countRight == 0) { if (shouldExist) { - doUpdateCounter(counter); + doRetractCounter(counter); + } else { + doInsertCounter(counter); } - } // Else do not even propagate an update + } else if (shouldExist) { + doUpdateCounter(counter); + } } // Clears the left tracker list rooted at leftTuple's inputStoreIndexLeftTrackerList slot, @@ -192,18 +193,18 @@ private void removeRight(FilteringTracker tracker) { // Walk safety: removeFromLeft only touches left-side links, so rightNext is stable across the call. protected void clearRightTrackerList(UniTuple rightTuple) { FilteringTracker tracker = rightTuple.removeStore(inputStoreIndexRightTrackerList); - if (rightTuple.getIndictmentSource() != IndictmentSource.DISABLED) { + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { while (tracker != null) { var next = tracker.rightNext; - decrementCounterRightUpdatingIndictment(tracker.counter, rightTuple); - removeFromLeft(tracker); + decrementCounterRight(tracker.counter); + removeLeft(tracker); tracker = next; } } else { while (tracker != null) { var next = tracker.rightNext; - decrementCounterRightWithoutIndictment(tracker.counter); - removeFromLeft(tracker); + decrementCounterRightUpdatingIndictment(tracker.counter, rightTuple); + removeLeft(tracker); tracker = next; } } @@ -233,13 +234,17 @@ protected void updateCounterLeft(ExistsCounter counter, UniTuple(counter, rightTuple); linkLeft(tracker); linkRight(tracker); + } else { + IndictmentSource.removeSupport(getId(), counter.getTuple(), rightTuple); } } @@ -284,13 +289,17 @@ protected void updateCounterRight(ExistsCounter counter, UniTuple(counter, rightTuple); linkLeft(tracker); linkRight(tracker); + } else { + IndictmentSource.removeSupport(getId(), counter.getTuple(), rightTuple); } } @@ -303,6 +312,21 @@ private void doInsertCounter(ExistsCounter counter) { } } + private void doUpdateCounter(ExistsCounter counter) { + switch (counter.state) { + case CREATING, UPDATING -> { + // Do nothing + } + case OK -> { + propagationQueue.update(counter); + } + case DYING, DEAD, ABORTING -> { + throw new IllegalStateException("Impossible state: The counter (%s) has an impossible retract state (%s)." + .formatted(counter, counter.state)); + } + } + } + private void doRetractCounter(ExistsCounter counter) { switch (counter.state) { case CREATING -> // Kill it before it propagates. @@ -315,15 +339,6 @@ private void doRetractCounter(ExistsCounter counter) { } } - private void doUpdateCounter(ExistsCounter counter) { - switch (counter.state) { - case DYING, OK, UPDATING, CREATING -> propagationQueue.update(counter); - case DEAD, ABORTING -> propagationQueue.insert(counter); - default -> throw new IllegalStateException("Impossible state: the counter (%s) has an impossible insert state (%s)." - .formatted(counter, counter.state)); - } - } - @Override protected boolean canProduceTuples() { // The left input must produce tuples no matter what, @@ -369,4 +384,4 @@ protected static final class FilteringTracker { } -} +} \ No newline at end of file diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index 91cceb6d511..70d21539be6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -101,7 +101,7 @@ private void updateCounterRight(LeftTuple_ leftTuple, Object compositeKey, Exist if (!isFiltering) { counter.countRight = rightSize(leftTuple, compositeKey); if (leftTuple.getIndictmentSource() != IndictmentSource.DISABLED) { - IndictmentSource.clearSupport(getId(), leftTuple); + leftTuple.getIndictmentSource().getSupportForNodeId(getId()).clear(); forEachRightFromLeft(leftTuple, compositeKey, rightTuple -> { IndictmentSource.addSupport(getId(), leftTuple, rightTuple); }); @@ -140,12 +140,6 @@ public final void updateLeft(LeftTuple_ leftTuple) { // The indexers contain counters in the DEAD state, to track the rightCount. if (!isFiltering) { updateUnchangedCounterLeft(counter); - if (leftTuple.getIndictmentSource() != IndictmentSource.DISABLED) { - IndictmentSource.clearSupport(getId(), leftTuple); - forEachRightFromLeft(leftTuple, newCompositeKey, rightTuple -> { - IndictmentSource.addSupport(getId(), leftTuple, rightTuple); - }); - } } else { // Call filtering for the leftTuple and rightTuple combinations again clearLeftTrackerList(leftTuple); @@ -216,10 +210,8 @@ public final void insertRight(UniTuple rightTuple) { private void updateCounterLeft(UniTuple rightTuple, Object compositeKey) { if (!isFiltering) { - // To prevent creating a dynamic lambda on the hot path, - // only call the 2-args version when indictments are enabled if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - forEachLeftCounter(rightTuple, compositeKey, this::incrementCounterRightWithoutIndictment); + forEachLeftCounter(rightTuple, compositeKey, this::incrementCounterRight); } else { forEachLeftCounter(rightTuple, compositeKey, counter -> incrementCounterRightUpdatingIndictment(counter, rightTuple)); @@ -262,14 +254,7 @@ public final void updateRight(UniTuple rightTuple) { indexerRight.remove(oldCompositeKey, entry); } if (!isFiltering) { - // To prevent creating a dynamic lambda on the hot path, - // only call the 2-args version when indictments are enabled - if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - forEachLeftCounter(rightTuple, oldCompositeKey, this::decrementCounterRightWithoutIndictment); - } else { - forEachLeftCounter(rightTuple, oldCompositeKey, - counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); - } + forEachLeftCounter(rightTuple, oldCompositeKey, this::decrementCounterRight); } else { clearRightTrackerList(rightTuple); } @@ -292,12 +277,11 @@ public final void retractRight(UniTuple rightTuple) { bucket.removeRight(compositeKey, entry); fusedEqualIndex.removeBucketIfEmpty(compositeKey, bucket); if (!isFiltering) { - // To prevent creating a dynamic lambda on the hot path, - // only call the 2-args version when indictments are enabled if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - bucket.forEachLeft(compositeKey, this::decrementCounterRightWithoutIndictment); + bucket.forEachLeft(compositeKey, this::decrementCounterRight); } else { - bucket.forEachLeft(compositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + bucket.forEachLeft(compositeKey, + leftTuple -> decrementCounterRightUpdatingIndictment(leftTuple, rightTuple)); } } else { clearRightTrackerList(rightTuple); @@ -305,13 +289,7 @@ public final void retractRight(UniTuple rightTuple) { } else { indexerRight.remove(compositeKey, entry); if (!isFiltering) { - // To prevent creating a dynamic lambda on the hot path, - // only call the 2-args version when indictments are enabled - if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - indexerLeft.forEach(compositeKey, this::decrementCounterRightWithoutIndictment); - } else { - indexerLeft.forEach(compositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); - } + indexerLeft.forEach(compositeKey, this::decrementCounterRight); } else { clearRightTrackerList(rightTuple); } @@ -381,4 +359,4 @@ private void forEachLeftCounter(UniTuple rightTuple, Object compositeKey } } -} +} \ No newline at end of file diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java index 5367d35851e..012931f68c8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java @@ -104,10 +104,8 @@ public final void insertRight(UniTuple rightTuple) { } rightTuple.setStore(inputStoreIndexRightEntry, rightTupleList.add(rightTuple)); if (!isFiltering) { - // To prevent creating a dynamic lambda on the hot path, - // only call the 2-args version when indictments are enabled if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - counterList.forEach(this::incrementCounterRightWithoutIndictment); + counterList.forEach(this::incrementCounterRight); } else { counterList.forEach(counter -> incrementCounterRightUpdatingIndictment(counter, rightTuple)); } @@ -145,10 +143,8 @@ public final void retractRight(UniTuple rightTuple) { } rightEntry.remove(); if (!isFiltering) { - // To prevent creating a dynamic lambda on the hot path, - // only call the 2-args version when indictments are enabled if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - counterList.forEach(this::decrementCounterRightWithoutIndictment); + counterList.forEach(this::decrementCounterRight); } else { counterList.forEach(counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); } @@ -157,4 +153,4 @@ public final void retractRight(UniTuple rightTuple) { } } -} +} \ No newline at end of file diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java index f9fd7ddb44a..623ca650111 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java @@ -112,7 +112,7 @@ public void filteringJoinNullConflictRight() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); // Unassign+assign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -126,7 +126,7 @@ public void filteringJoinNullConflictRight() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value2); assertScore(scoreDirector, - assertMatch(value2, value2)); + assertMatch(value2, value2).withIndictedObjects(value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -138,7 +138,7 @@ public void filteringJoinNullConflictRight() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); } } @@ -173,7 +173,7 @@ public void filteringJoinNullConflictRightUnindexed() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); // Unassign+assign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -187,7 +187,7 @@ public void filteringJoinNullConflictRightUnindexed() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value2); assertScore(scoreDirector, - assertMatch(value2, value2)); + assertMatch(value2, value2).withIndictedObjects(value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -199,7 +199,7 @@ public void filteringJoinNullConflictRightUnindexed() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); } } @@ -236,7 +236,7 @@ public void filteringJoinNullConflictRightViaIfExists() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); // Unassign+assign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -250,7 +250,7 @@ public void filteringJoinNullConflictRightViaIfExists() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value2); assertScore(scoreDirector, - assertMatch(value2, value2)); + assertMatch(value2, value2).withIndictedObjects(value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -262,7 +262,7 @@ public void filteringJoinNullConflictRightViaIfExists() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); } } @@ -301,10 +301,10 @@ public void filteringJoinNullConflictRightUnassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1, value1), - assertMatch(value1, value2), - assertMatch(value2, value1), - assertMatch(value2, value2)); + assertMatch(value1, value1).withIndictedObjects(value1), + assertMatch(value1, value2).withIndictedObjects(value1, value2), + assertMatch(value2, value1).withIndictedObjects(value1, value2), + assertMatch(value2, value2).withIndictedObjects(value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -316,7 +316,7 @@ public void filteringJoinNullConflictRightUnassignOne() { scoreDirector.afterListVariableElementUnassigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(value2, value2)); + assertMatch(value2, value2).withIndictedObjects(value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -326,10 +326,10 @@ public void filteringJoinNullConflictRightUnassignOne() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(value1, value1), - assertMatch(value1, value2), - assertMatch(value2, value1), - assertMatch(value2, value2)); + assertMatch(value1, value1).withIndictedObjects(value1), + assertMatch(value1, value2).withIndictedObjects(value1, value2), + assertMatch(value2, value1).withIndictedObjects(value1, value2), + assertMatch(value2, value2).withIndictedObjects(value2)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java index 87fcbeed658..1642ba043f6 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java @@ -776,8 +776,8 @@ public void ifExists_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(entity, value1).withIndictedObjects(entity, value1), - assertMatch(entity, value2).withIndictedObjects(entity, value2)); + assertMatch(entity, value1).withIndictedObjects(entity, value1, value2), + assertMatch(entity, value2).withIndictedObjects(entity, value1, value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -799,7 +799,7 @@ public void ifExists_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value1).withIndictedObjects(entity, value1), + assertMatch(entity, value1).withIndictedObjects(entity, value1, value2), assertMatch(entity, value2).withIndictedObjects(entity, value1, value2)); } @@ -917,7 +917,8 @@ public void ifExists_0Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), entity2)); + assertMatch(solution.getFirstEntity(), entity2) + .withIndictedObjects(solution.getFirstEntity(), entity2, solution.getFirstEntityGroup())); // Incremental var toRemove = solution.getFirstEntityGroup(); @@ -991,7 +992,8 @@ public void ifExists_1Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2)); + assertMatch(entity1, entity2) + .withIndictedObjects(entity1, entity2, entityGroup)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java index eb8eb31b9c7..48b5bee5d68 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java @@ -210,9 +210,15 @@ public void ifExists_0Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity2, entity1, entityGroup, solution.getFirstValue()), - assertMatch(entity2, solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue()), - assertMatch(entity1, solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue())); + assertMatch(entity2, entity1, entityGroup, solution.getFirstValue()) + .withIndictedObjects(entity2, entity1, entityGroup, solution.getFirstValue(), + solution.getFirstValue().getValueGroup()), + assertMatch(entity2, solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue()) + .withIndictedObjects(entity2, solution.getFirstEntity(), solution.getFirstEntityGroup(), + solution.getFirstValue(), solution.getFirstValue().getValueGroup()), + assertMatch(entity1, solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue()) + .withIndictedObjects(entity1, solution.getFirstEntity(), solution.getFirstEntityGroup(), + solution.getFirstValue(), solution.getFirstValue().getValueGroup())); // Incremental var toRemove = solution.getFirstValueGroup(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java index d027d54c48d..6e79563b8c7 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java @@ -414,7 +414,8 @@ public void ifExists_0Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, solution.getFirstEntity())); + assertMatch(entity1, entity2, solution.getFirstEntity()) + .withIndictedObjects(entity1, entity2, solution.getFirstEntity(), solution.getFirstEntityGroup())); // Incremental var toRemove = solution.getFirstEntityGroup(); @@ -494,7 +495,8 @@ public void ifExists_1Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), entity2, entity1)); + assertMatch(solution.getFirstEntity(), entity2, entity1) + .withIndictedObjects(solution.getFirstEntity(), entity2, entity1, entityGroup)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index 64645c474bf..da4c87235a5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -620,7 +620,7 @@ public void ifExistsOther_1Join0Filter() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), entity2), - assertMatch(entity2).withIndictedObjects(entity2)); + assertMatch(entity2).withIndictedObjects(solution.getFirstEntity(), entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); @@ -628,7 +628,7 @@ public void ifExistsOther_1Join0Filter() { scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, assertMatch(entity1).withIndictedObjects(entity1, entity2), - assertMatch(entity2).withIndictedObjects(entity2)); + assertMatch(entity2).withIndictedObjects(entity1, entity2)); } @Override @@ -656,7 +656,7 @@ public void ifExistsDoesNotIncludeUnassigned() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), entity2), - assertMatch(entity2).withIndictedObjects(entity2)); + assertMatch(entity2).withIndictedObjects(solution.getFirstEntity(), entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); @@ -664,7 +664,7 @@ public void ifExistsDoesNotIncludeUnassigned() { scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, assertMatch(entity1).withIndictedObjects(entity1, entity2), - assertMatch(entity2).withIndictedObjects(entity2)); + assertMatch(entity2).withIndictedObjects(entity1, entity2)); } @Override