From 5c3bcdf421ca2e176a99c81e46f9d77b0be90302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Tue, 6 Feb 2024 09:15:26 +0100 Subject: [PATCH] fix: nearby selection pinning support (#614) --- .../entity/descriptor/EntityDescriptor.java | 9 ++-- .../iterator/UpcomingSelectionIterator.java | 25 +++++++++++ .../list/OriginalListChangeIterator.java | 7 ++- .../list/RandomListChangeIterator.java | 7 ++- .../list/RandomSubListChangeMoveIterator.java | 7 ++- .../list/kopt/KOptListMoveIterator.java | 23 +++++++--- .../score/director/AbstractScoreDirector.java | 22 ++++++++++ .../generic/list/kopt/KOptListMoveTest.java | 9 ++-- .../generic/list/kopt/TwoOptListMoveTest.java | 9 ++-- .../TestdataPinnedWithIndexDistanceMeter.java | 26 +++++++++++ .../TimefoldAutoConfigurationTest.java | 29 ++++++++----- ...ldMultipleSolverAutoConfigurationTest.java | 43 ++++++++++++------- ...coreConstraintSpringTestConfiguration.java | 4 +- ...coreConstraintSpringTestConfiguration.java | 4 +- .../easy/DummyChainedSpringEasyScore.java} | 4 +- .../DummyChainedSpringIncrementalScore.java} | 4 +- .../easy/DummySpringEasyScore.java} | 4 +- .../DummySpringIncrementalScore.java} | 4 +- .../autoconfigure/easyScoreSolverConfig.xml | 2 +- .../incrementalScoreSolverConfig.xml | 2 +- 20 files changed, 181 insertions(+), 63 deletions(-) create mode 100644 core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/list/pinned/index/TestdataPinnedWithIndexDistanceMeter.java rename spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/{easyScoreConstraints/DummyTestdataChainedSpringEasyScore.java => constraints/easy/DummyChainedSpringEasyScore.java} (74%) rename spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/{incrementalScoreConstraints/DummyTestdataChainedSpringIncrementalScore.java => constraints/incremental/DummyChainedSpringIncrementalScore.java} (86%) rename spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/{easyScoreConstraints/DummyTestdataSpringEasyScore.java => constraints/easy/DummySpringEasyScore.java} (75%) rename spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/{incrementalScoreConstraints/DummyTestdataSpringIncrementalScore.java => constraints/incremental/DummySpringIncrementalScore.java} (87%) diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java index d331c1f73a..a30a6fd3fe 100644 --- a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java +++ b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java @@ -646,8 +646,6 @@ public List extractEntities(Solution_ solution) { /** * Returns the {@link PinningStatus} of the entity. - * If {@link PlanningPin} is enabled on the entity, the entity is fully pinned. - * Otherwise if {@link PlanningPinToIndex} is specified, returns the value of it. * * @param scoreDirector * @param entity @@ -684,12 +682,15 @@ public int extractFirstUnpinnedIndex(Object entity) { public record PinningStatus(boolean hasPin, boolean entireEntityPinned, int firstUnpinnedIndex) { + private static final PinningStatus FULLY_PINNED = new PinningStatus(true, true, -1); + private static final PinningStatus UNPINNED = new PinningStatus(false, false, -1); + public static PinningStatus ofUnpinned() { - return new PinningStatus(false, false, -1); + return UNPINNED; } public static PinningStatus ofFullyPinned() { - return new PinningStatus(true, true, -1); + return FULLY_PINNED; } public static PinningStatus ofPinIndex(int firstUnpinnedIndex) { diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/UpcomingSelectionIterator.java b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/UpcomingSelectionIterator.java index 12297f053b..53ee59de54 100644 --- a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/UpcomingSelectionIterator.java +++ b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/UpcomingSelectionIterator.java @@ -3,9 +3,11 @@ import java.util.Iterator; import java.util.NoSuchElementException; +import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.heuristic.selector.Selector; import ai.timefold.solver.core.impl.heuristic.selector.entity.mimic.MimicReplayingEntitySelector; +import ai.timefold.solver.core.impl.heuristic.selector.list.ElementRef; /** * IMPORTANT: The constructor of any subclass of this abstract class, should never call any of its child @@ -60,4 +62,27 @@ public String toString() { } } + /** + * Some destination iterators, such as nearby destination iterators, may return even elements which are pinned. + * This is because the nearby matrix always picks from all nearby elements, and is unaware of any pinning. + * This means that later we need to filter out the pinned elements, so that moves aren't generated for them. + * + * @param destinationIterator never null + * @param listVariableDescriptor never null + * @return null if no unpinned destination was found, at which point the iterator is exhausted. + */ + public static ElementRef findUnpinnedDestination(Iterator destinationIterator, + ListVariableDescriptor listVariableDescriptor) { + while (destinationIterator.hasNext()) { + var destination = destinationIterator.next(); + var pinningStatus = listVariableDescriptor.getEntityDescriptor().extractPinningStatus(null, destination.entity()); + var isPinned = pinningStatus.hasPin() + && (pinningStatus.entireEntityPinned() || pinningStatus.firstUnpinnedIndex() > destination.index()); + if (!isPinned) { + return destination; + } + } + return null; + } + } diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/OriginalListChangeIterator.java b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/OriginalListChangeIterator.java index 44b7bb8f44..513c030e14 100644 --- a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/OriginalListChangeIterator.java +++ b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/OriginalListChangeIterator.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic.list; +import static ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.RandomListChangeIterator.findUnpinnedDestination; + import java.util.Collections; import java.util.Iterator; @@ -56,7 +58,10 @@ protected Move createUpcomingSelection() { destinationIterator = destinationSelector.iterator(); } - ElementRef destination = destinationIterator.next(); + ElementRef destination = findUnpinnedDestination(destinationIterator, listVariableDescriptor); + if (destination == null) { + return noUpcomingSelection(); + } if (upcomingSourceEntity == null && upcomingSourceIndex == null) { return new ListAssignMove<>( diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomListChangeIterator.java b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomListChangeIterator.java index 04bd946ea5..ebbaae6082 100644 --- a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomListChangeIterator.java +++ b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomListChangeIterator.java @@ -42,8 +42,10 @@ protected Move createUpcomingSelection() { } Object upcomingValue = valueIterator.next(); - ElementRef destination = destinationIterator.next(); - + ElementRef destination = findUnpinnedDestination(destinationIterator, listVariableDescriptor); + if (destination == null) { + return noUpcomingSelection(); + } return new ListChangeMove<>( listVariableDescriptor, inverseVariableSupply.getInverseSingleton(upcomingValue), @@ -51,4 +53,5 @@ protected Move createUpcomingSelection() { destination.entity(), destination.index()); } + } diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveIterator.java b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveIterator.java index 56e4730e00..af055ddd02 100644 --- a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveIterator.java +++ b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/RandomSubListChangeMoveIterator.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic.list; +import static ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.RandomListChangeIterator.findUnpinnedDestination; + import java.util.Iterator; import java.util.Random; @@ -38,7 +40,10 @@ protected Move createUpcomingSelection() { } SubList subList = subListIterator.next(); - ElementRef destination = destinationIterator.next(); + ElementRef destination = findUnpinnedDestination(destinationIterator, listVariableDescriptor); + if (destination == null) { + return noUpcomingSelection(); + } boolean reversing = selectReversingMoveToo && workingRandom.nextBoolean(); return new SubListChangeMove<>( diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveIterator.java b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveIterator.java index e60041ab83..c760738f89 100644 --- a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveIterator.java +++ b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveIterator.java @@ -105,16 +105,27 @@ private Iterator getValuesOnSelectedEntitiesIterator(Node_[] pickedValues @SuppressWarnings("unchecked") private KOptDescriptor pickKOptMove(int k) { // The code in the paper used 1-index arrays - Node_[] pickedValues = (Node_[]) new Object[2 * k + 1]; - Iterator originIterator = (Iterator) originSelector.iterator(); + var pickedValues = (Node_[]) new Object[2 * k + 1]; + var originIterator = (Iterator) originSelector.iterator(); pickedValues[1] = originIterator.next(); - int remainingAttempts = 20; + if (pickedValues[1] == null) { + return null; + } + var remainingAttempts = 20; while (remainingAttempts > 0 && getEffectiveListSize(listVariableDescriptor, inverseVariableSupply.getInverseSingleton(pickedValues[1])) < 2) { - pickedValues[1] = originIterator.next(); - remainingAttempts--; + do { + if (!originIterator.hasNext()) { + // Filtered selection due to pinning/unassigned may cause this. + // Filtered selectors only know the upper bound of their size, not their actual size. + // Therefore the iterator may be exhausted before the actual size is reached. + return null; + } + pickedValues[1] = originIterator.next(); + remainingAttempts--; + } while ((pickedValues[1] == null)); } if (remainingAttempts == 0) { @@ -122,7 +133,7 @@ && getEffectiveListSize(listVariableDescriptor, return null; } - EntityOrderInfo entityOrderInfo = EntityOrderInfo.of(pickedValues, inverseVariableSupply, listVariableDescriptor); + var entityOrderInfo = EntityOrderInfo.of(pickedValues, inverseVariableSupply, listVariableDescriptor); pickedValues[2] = workingRandom.nextBoolean() ? getNodeSuccessor(entityOrderInfo, pickedValues[1]) : getNodePredecessor(entityOrderInfo, pickedValues[1]); diff --git a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 2c80d86fb5..579c362b38 100644 --- a/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/core-impl/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -475,6 +475,28 @@ public void afterListVariableElementUnassigned(ListVariableDescriptor @Override public void beforeListVariableChanged(ListVariableDescriptor variableDescriptor, Object entity, int fromIndex, int toIndex) { + // Pinning is implemented in generic moves, but custom moves need to take it into account as well. + // This fail-fast exists to detect situations where pinned things are being moved, in case of user error. + var entityDescriptor = variableDescriptor.getEntityDescriptor(); + var pinningStatus = entityDescriptor.extractPinningStatus(this, entity); + if (pinningStatus.hasPin()) { + if (pinningStatus.entireEntityPinned()) { + throw new IllegalStateException(""" + Attempting to change list variable (%s) on an entity (%s) which is fully pinned. + This is most likely a bug in a move. + Maybe you are using an improperly implemented custom move?""" + .formatted(variableDescriptor, entity)); + } + int firstUnpinnedIndex = pinningStatus.firstUnpinnedIndex(); + if (fromIndex < firstUnpinnedIndex || toIndex < firstUnpinnedIndex) { + throw new IllegalStateException( + """ + Attempting to change list variable (%s) on an entity (%s) in range [%d, %d), but the variable's first unpinned index is (%d). + This is most likely a bug in a move. + Maybe you are using an improperly implemented custom move?""" + .formatted(variableDescriptor, entity, fromIndex, toIndex, firstUnpinnedIndex)); + } + } variableListenerSupport.beforeListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); } diff --git a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveTest.java b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveTest.java index 7bebcc6a1a..03e0d4693e 100644 --- a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveTest.java +++ b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveTest.java @@ -12,7 +12,6 @@ import java.util.List; import java.util.function.Function; -import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.index.IndexVariableDemand; import ai.timefold.solver.core.impl.domain.variable.index.IndexVariableSupply; @@ -102,8 +101,10 @@ void test3OptPinned() { TestdataListEntity e1 = TestdataListEntity.createWithValues("e1", v1, v2, v3, v4, v5, v6, v7); var variableDescriptorSpy = Mockito.spy(variableDescriptor); - var entityDescriptor = Mockito.mock(EntityDescriptor.class); + var entityDescriptor = Mockito.spy(TestdataListSolution.buildSolutionDescriptor() + .findEntityDescriptorOrFail(TestdataListEntity.class)); Mockito.when(variableDescriptorSpy.getEntityDescriptor()).thenReturn(entityDescriptor); + Mockito.when(entityDescriptor.supportsPinning()).thenReturn(true); Mockito.when(entityDescriptor.extractFirstUnpinnedIndex(e1)).thenReturn(1); Mockito.when(entityDescriptor.isMovable(null, e1)).thenReturn(true); @@ -314,8 +315,10 @@ void testMultiEntity3OptPinned() { TestdataListEntity e2 = TestdataListEntity.createWithValues("e2", v4, v5); var variableDescriptorSpy = Mockito.spy(variableDescriptor); - var entityDescriptor = Mockito.mock(EntityDescriptor.class); + var entityDescriptor = Mockito.spy(TestdataListSolution.buildSolutionDescriptor() + .findEntityDescriptorOrFail(TestdataListEntity.class)); Mockito.when(variableDescriptorSpy.getEntityDescriptor()).thenReturn(entityDescriptor); + Mockito.when(entityDescriptor.supportsPinning()).thenReturn(true); Mockito.when(entityDescriptor.extractFirstUnpinnedIndex(e1)).thenReturn(1); Mockito.when(entityDescriptor.isMovable(null, e1)).thenReturn(true); Mockito.when(entityDescriptor.isMovable(null, e2)).thenReturn(true); diff --git a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/TwoOptListMoveTest.java b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/TwoOptListMoveTest.java index 5e8c30637c..51de7e9da3 100644 --- a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/TwoOptListMoveTest.java +++ b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/TwoOptListMoveTest.java @@ -6,7 +6,6 @@ import static org.mockito.Mockito.verify; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; -import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.move.AbstractMove; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; @@ -239,8 +238,10 @@ void doMoveSecondEndsBeforeFirstPinned() { TestdataListEntity e1 = TestdataListEntity.createWithValues("e1", v8, v7, v3, v4, v5, v6, v2, v1); var variableDescriptorSpy = Mockito.spy(variableDescriptor); - var entityDescriptor = Mockito.mock(EntityDescriptor.class); + var entityDescriptor = Mockito.spy(TestdataListSolution.buildSolutionDescriptor() + .findEntityDescriptorOrFail(TestdataListEntity.class)); Mockito.when(variableDescriptorSpy.getEntityDescriptor()).thenReturn(entityDescriptor); + Mockito.when(entityDescriptor.supportsPinning()).thenReturn(true); Mockito.when(entityDescriptor.extractFirstUnpinnedIndex(e1)).thenReturn(1); // 2-Opt((v6, v2), (v7, v3)) @@ -281,9 +282,7 @@ void rebase() { .rebase(destinationScoreDirector)); } - static void assertSameProperties( - Object destinationEntity, int destinationV1, int destinationV2, - TwoOptListMove move) { + static void assertSameProperties(Object destinationEntity, int destinationV1, int destinationV2, TwoOptListMove move) { assertThat(move.getFirstEntity()).isSameAs(destinationEntity); assertThat(move.getFirstEdgeEndpoint()).isEqualTo(destinationV1); assertThat(move.getSecondEdgeEndpoint()).isEqualTo(destinationV2); diff --git a/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/list/pinned/index/TestdataPinnedWithIndexDistanceMeter.java b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/list/pinned/index/TestdataPinnedWithIndexDistanceMeter.java new file mode 100644 index 0000000000..0d2bcbbd1b --- /dev/null +++ b/core/core-impl/src/test/java/ai/timefold/solver/core/impl/testdata/domain/list/pinned/index/TestdataPinnedWithIndexDistanceMeter.java @@ -0,0 +1,26 @@ +package ai.timefold.solver.core.impl.testdata.domain.list.pinned.index; + +import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; +import ai.timefold.solver.core.impl.testdata.domain.TestdataObject; + +/** + * For the sake of test readability, planning values (list variable elements) are placed in a 1-dimensional space. + * An element's coordinate is represented by its ({@link TestdataObject#getCode() code}. If the code is not a number, + * it is interpreted as zero. + */ +public class TestdataPinnedWithIndexDistanceMeter + implements NearbyDistanceMeter { + + @Override + public double getNearbyDistance(TestdataPinnedWithIndexListValue origin, TestdataObject destination) { + return Math.abs(coordinate(destination) - coordinate(origin)); + } + + static int coordinate(TestdataObject o) { + try { + return Integer.parseInt(o.getCode()); + } catch (NumberFormatException e) { + return 0; + } + } +} diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAutoConfigurationTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAutoConfigurationTest.java index a9860cdc12..80f1bcee02 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAutoConfigurationTest.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldAutoConfigurationTest.java @@ -39,6 +39,10 @@ import ai.timefold.solver.spring.boot.autoconfigure.dummy.MultipleSolutionsSpringTestConfiguration; import ai.timefold.solver.spring.boot.autoconfigure.dummy.NoEntitySpringTestConfiguration; import ai.timefold.solver.spring.boot.autoconfigure.dummy.NoSolutionSpringTestConfiguration; +import ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.constraints.easy.DummyChainedSpringEasyScore; +import ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.constraints.incremental.DummyChainedSpringIncrementalScore; +import ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.constraints.easy.DummySpringEasyScore; +import ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.constraints.incremental.DummySpringIncrementalScore; import ai.timefold.solver.spring.boot.autoconfigure.gizmo.GizmoSpringTestConfiguration; import ai.timefold.solver.spring.boot.autoconfigure.invalid.entity.InvalidEntitySpringTestConfiguration; import ai.timefold.solver.spring.boot.autoconfigure.invalid.solution.InvalidSolutionSpringTestConfiguration; @@ -602,7 +606,8 @@ void multipleEasyScoreConstraints() { .withPropertyValues("timefold.solver.termination.best-score-limit=0") .run(context -> context.getBean("solver1"))) .cause().message().contains( - "Multiple score classes classes", "DummyTestdataChainedSpringEasyScore", "DummyTestdataSpringEasyScore", + "Multiple score classes classes", DummyChainedSpringEasyScore.class.getSimpleName(), + DummySpringEasyScore.class.getSimpleName(), "that implements EasyScoreCalculator were found in the classpath."); } @@ -613,8 +618,9 @@ void multipleConstraintProviderConstraints() { .withPropertyValues("timefold.solver.termination.best-score-limit=0") .run(context -> context.getBean("solver1"))) .cause().message().contains( - "Multiple score classes classes", "TestdataChainedSpringConstraintProvider", - "TestdataSpringConstraintProvider", "that implements ConstraintProvider were found in the classpath."); + "Multiple score classes classes", TestdataChainedSpringConstraintProvider.class.getSimpleName(), + TestdataSpringConstraintProvider.class.getSimpleName(), + "that implements ConstraintProvider were found in the classpath."); } @Test @@ -624,8 +630,8 @@ void multipleIncrementalScoreConstraints() { .withPropertyValues("timefold.solver.termination.best-score-limit=0") .run(context -> context.getBean("solver1"))) .cause().message().contains( - "Multiple score classes classes", "DummyTestdataChainedSpringIncrementalScore", - "DummyTestdataSpringIncrementalScore", + "Multiple score classes classes", DummyChainedSpringIncrementalScore.class.getSimpleName(), + DummySpringIncrementalScore.class.getSimpleName(), "that implements IncrementalScoreCalculator were found in the classpath."); } @@ -638,8 +644,8 @@ void multipleEasyScoreConstraintsXml_property() { .run(context -> context.getBean("solver1"))) .cause().message().contains( "Multiple score classes classes", - "DummyTestdataChainedSpringEasyScore", - "DummyTestdataSpringEasyScore", + DummyChainedSpringEasyScore.class.getSimpleName(), + DummySpringEasyScore.class.getSimpleName(), "that implements EasyScoreCalculator were found in the classpath"); } @@ -651,8 +657,9 @@ void multipleConstraintProviderConstraintsXml_property() { "timefold.solver.solver-config-xml=ai/timefold/solver/spring/boot/autoconfigure/normalSolverConfig.xml") .run(context -> context.getBean("solver1"))) .cause().message().contains( - "Multiple score classes classes", "TestdataChainedSpringConstraintProvider", - "TestdataSpringConstraintProvider", "that implements ConstraintProvider were found in the classpath."); + "Multiple score classes classes", TestdataChainedSpringConstraintProvider.class.getSimpleName(), + TestdataSpringConstraintProvider.class.getSimpleName(), + "that implements ConstraintProvider were found in the classpath."); } @Test @@ -663,8 +670,8 @@ void multipleIncrementalScoreConstraintsXml_property() { "timefold.solver.solver-config-xml=ai/timefold/solver/spring/boot/autoconfigure/normalSolverConfig.xml") .run(context -> context.getBean("solver1"))) .cause().message().contains( - "Multiple score classes classes", "DummyTestdataChainedSpringIncrementalScore", - "DummyTestdataSpringIncrementalScore", + "Multiple score classes classes", DummyChainedSpringIncrementalScore.class.getSimpleName(), + DummySpringIncrementalScore.class.getSimpleName(), "that implements IncrementalScoreCalculator were found in the classpath."); } diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldMultipleSolverAutoConfigurationTest.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldMultipleSolverAutoConfigurationTest.java index 85c00c85e1..1998139116 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldMultipleSolverAutoConfigurationTest.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/TimefoldMultipleSolverAutoConfigurationTest.java @@ -21,6 +21,8 @@ import ai.timefold.solver.core.impl.solver.DefaultSolverJob; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.spring.boot.autoconfigure.chained.ChainedSpringTestConfiguration; +import ai.timefold.solver.spring.boot.autoconfigure.chained.constraints.TestdataChainedSpringConstraintProvider; +import ai.timefold.solver.spring.boot.autoconfigure.chained.domain.TestdataChainedSpringSolution; import ai.timefold.solver.spring.boot.autoconfigure.config.TimefoldProperties; import ai.timefold.solver.spring.boot.autoconfigure.dummy.MultipleConstraintProviderSpringTestConfiguration; import ai.timefold.solver.spring.boot.autoconfigure.dummy.MultipleEasyScoreConstraintSpringTestConfiguration; @@ -28,6 +30,10 @@ import ai.timefold.solver.spring.boot.autoconfigure.dummy.MultipleSolutionsSpringTestConfiguration; import ai.timefold.solver.spring.boot.autoconfigure.dummy.NoEntitySpringTestConfiguration; import ai.timefold.solver.spring.boot.autoconfigure.dummy.NoSolutionSpringTestConfiguration; +import ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.constraints.easy.DummyChainedSpringEasyScore; +import ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.constraints.incremental.DummyChainedSpringIncrementalScore; +import ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.constraints.easy.DummySpringEasyScore; +import ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.constraints.incremental.DummySpringIncrementalScore; import ai.timefold.solver.spring.boot.autoconfigure.gizmo.GizmoSpringTestConfiguration; import ai.timefold.solver.spring.boot.autoconfigure.invalid.entity.InvalidEntitySpringTestConfiguration; import ai.timefold.solver.spring.boot.autoconfigure.invalid.solution.InvalidSolutionSpringTestConfiguration; @@ -36,6 +42,7 @@ import ai.timefold.solver.spring.boot.autoconfigure.normal.EmptySpringTestConfiguration; import ai.timefold.solver.spring.boot.autoconfigure.normal.NoConstraintsSpringTestConfiguration; import ai.timefold.solver.spring.boot.autoconfigure.normal.NormalSpringTestConfiguration; +import ai.timefold.solver.spring.boot.autoconfigure.normal.constraints.TestdataSpringConstraintProvider; import ai.timefold.solver.spring.boot.autoconfigure.normal.domain.TestdataSpringEntity; import ai.timefold.solver.spring.boot.autoconfigure.normal.domain.TestdataSpringSolution; import ai.timefold.solver.test.api.score.stream.ConstraintVerifier; @@ -447,7 +454,8 @@ void multipleSolutionClasses() { .cause().message().contains( "Some solver configs", "solver2", "solver1", "don't specify a PlanningSolution class, yet there are multiple available", - "TestdataSpringSolution", "TestdataChainedSpringSolution", + TestdataChainedSpringSolution.class.getSimpleName(), + TestdataSpringSolution.class.getSimpleName(), "on the classpath."); } @@ -495,8 +503,8 @@ void multipleEasyScoreConstraints() { .cause().message().contains( "Some solver configs", "solver2", "solver1", "don't specify a EasyScoreCalculator score class, yet there are multiple available", - "DummyTestdataChainedSpringEasyScore", - "DummyTestdataSpringEasyScore", + DummyChainedSpringEasyScore.class.getSimpleName(), + DummySpringEasyScore.class.getSimpleName(), "on the classpath."); } @@ -510,8 +518,8 @@ void multipleConstraintProviderConstraints() { .cause().message().contains( "Some solver configs", "solver2", "solver1", "don't specify a ConstraintProvider score class, yet there are multiple available", - "TestdataSpringConstraintProvider", - "TestdataChainedSpringConstraintProvider", + TestdataChainedSpringConstraintProvider.class.getSimpleName(), + TestdataSpringConstraintProvider.class.getSimpleName(), "on the classpath."); } @@ -525,8 +533,8 @@ void multipleIncrementalScoreConstraints() { .cause().message().contains( "Some solver configs", "solver2", "solver1", "don't specify a IncrementalScoreCalculator score class, yet there are multiple available", - "DummyTestdataSpringIncrementalScore", - "DummyTestdataChainedSpringIncrementalScore", + DummyChainedSpringIncrementalScore.class.getSimpleName(), + DummySpringIncrementalScore.class.getSimpleName(), "on the classpath."); } @@ -542,8 +550,8 @@ void multipleEasyScoreConstraintsXml_property() { .cause().message().contains( "Some solver configs", "solver2", "solver1", "don't specify a EasyScoreCalculator score class, yet there are multiple available", - "DummyTestdataChainedSpringEasyScore", - "DummyTestdataSpringEasyScore", + DummyChainedSpringEasyScore.class.getSimpleName(), + DummySpringEasyScore.class.getSimpleName(), "on the classpath."); } @@ -559,8 +567,8 @@ void multipleConstraintProviderConstraintsXml_property() { .cause().message().contains( "Some solver configs", "solver2", "solver1", "don't specify a ConstraintProvider score class, yet there are multiple available", - "TestdataSpringConstraintProvider", - "TestdataChainedSpringConstraintProvider", + TestdataChainedSpringConstraintProvider.class.getSimpleName(), + TestdataSpringConstraintProvider.class.getSimpleName(), "on the classpath."); } @@ -576,8 +584,8 @@ void multipleIncrementalScoreConstraintsXml_property() { .cause().message().contains( "Some solver configs", "solver2", "solver1", "don't specify a IncrementalScoreCalculator score class, yet there are multiple available", - "DummyTestdataSpringIncrementalScore", - "DummyTestdataChainedSpringIncrementalScore", + DummyChainedSpringIncrementalScore.class.getSimpleName(), + DummySpringIncrementalScore.class.getSimpleName(), "on the classpath."); } @@ -591,7 +599,8 @@ void unusedEasyScoreConstraints() { "timefold.solver.solver2.solver-config-xml=ai/timefold/solver/spring/boot/autoconfigure/easyScoreSolverConfig.xml") .run(context -> context.getBean("solver1"))) .cause().message().contains( - "Unused classes ([ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.easyScoreConstraints.DummyTestdataSpringEasyScore]) that implements EasyScoreCalculator were found."); + "Unused classes ([" + DummySpringEasyScore.class.getCanonicalName() + + "]) that implements EasyScoreCalculator were found."); } @Test @@ -604,7 +613,8 @@ void unusedConstraintProviderConstraints() { "timefold.solver.solver2.solver-config-xml=ai/timefold/solver/spring/boot/autoconfigure/normalSolverConfig.xml") .run(context -> context.getBean("solver1"))) .cause().message().contains( - "Unused classes ([ai.timefold.solver.spring.boot.autoconfigure.chained.constraints.TestdataChainedSpringConstraintProvider]) that implements ConstraintProvider were found."); + "Unused classes ([" + TestdataChainedSpringConstraintProvider.class.getCanonicalName() + + "]) that implements ConstraintProvider were found."); } @Test @@ -617,7 +627,8 @@ void unusedIncrementalScoreConstraints() { "timefold.solver.solver2.solver-config-xml=ai/timefold/solver/spring/boot/autoconfigure/incrementalScoreSolverConfig.xml") .run(context -> context.getBean("solver1"))) .cause().message().contains( - "Unused classes ([ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.incrementalScoreConstraints.DummyTestdataSpringIncrementalScore]) that implements IncrementalScoreCalculator were found."); + "Unused classes ([" + DummySpringIncrementalScore.class.getCanonicalName() + + "]) that implements IncrementalScoreCalculator were found."); } @Test diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/MultipleEasyScoreConstraintSpringTestConfiguration.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/MultipleEasyScoreConstraintSpringTestConfiguration.java index b594a8f400..09a4c8fc44 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/MultipleEasyScoreConstraintSpringTestConfiguration.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/MultipleEasyScoreConstraintSpringTestConfiguration.java @@ -5,7 +5,7 @@ @Configuration @AutoConfigurationPackage(basePackages = { "ai.timefold.solver.spring.boot.autoconfigure.normal.domain", - "ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.easyScoreConstraints", - "ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.easyScoreConstraints" }) + "ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.constraints.easy", + "ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.constraints.easy" }) public class MultipleEasyScoreConstraintSpringTestConfiguration { } diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/MultipleIncrementalScoreConstraintSpringTestConfiguration.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/MultipleIncrementalScoreConstraintSpringTestConfiguration.java index e8e0742fb2..d10efbc4dd 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/MultipleIncrementalScoreConstraintSpringTestConfiguration.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/MultipleIncrementalScoreConstraintSpringTestConfiguration.java @@ -5,7 +5,7 @@ @Configuration @AutoConfigurationPackage(basePackages = { "ai.timefold.solver.spring.boot.autoconfigure.normal.domain", - "ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.incrementalScoreConstraints", - "ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.incrementalScoreConstraints" }) + "ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.constraints.incremental", + "ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.constraints.incremental" }) public class MultipleIncrementalScoreConstraintSpringTestConfiguration { } diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/easyScoreConstraints/DummyTestdataChainedSpringEasyScore.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/constraints/easy/DummyChainedSpringEasyScore.java similarity index 74% rename from spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/easyScoreConstraints/DummyTestdataChainedSpringEasyScore.java rename to spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/constraints/easy/DummyChainedSpringEasyScore.java index 0522297d1f..1df05ea0d8 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/easyScoreConstraints/DummyTestdataChainedSpringEasyScore.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/constraints/easy/DummyChainedSpringEasyScore.java @@ -1,10 +1,10 @@ -package ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.easyScoreConstraints; +package ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.constraints.easy; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.spring.boot.autoconfigure.chained.domain.TestdataChainedSpringSolution; -public class DummyTestdataChainedSpringEasyScore implements EasyScoreCalculator { +public class DummyChainedSpringEasyScore implements EasyScoreCalculator { @Override public SimpleScore calculateScore(TestdataChainedSpringSolution testdataSpringSolution) { return null; diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/incrementalScoreConstraints/DummyTestdataChainedSpringIncrementalScore.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/constraints/incremental/DummyChainedSpringIncrementalScore.java similarity index 86% rename from spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/incrementalScoreConstraints/DummyTestdataChainedSpringIncrementalScore.java rename to spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/constraints/incremental/DummyChainedSpringIncrementalScore.java index 65cfc45715..92342e8879 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/incrementalScoreConstraints/DummyTestdataChainedSpringIncrementalScore.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/chained/constraints/incremental/DummyChainedSpringIncrementalScore.java @@ -1,9 +1,9 @@ -package ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.incrementalScoreConstraints; +package ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.constraints.incremental; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; -public class DummyTestdataChainedSpringIncrementalScore implements IncrementalScoreCalculator { +public class DummyChainedSpringIncrementalScore implements IncrementalScoreCalculator { @Override public void resetWorkingSolution(Object workingSolution) { diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/easyScoreConstraints/DummyTestdataSpringEasyScore.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/constraints/easy/DummySpringEasyScore.java similarity index 75% rename from spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/easyScoreConstraints/DummyTestdataSpringEasyScore.java rename to spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/constraints/easy/DummySpringEasyScore.java index daa1b61fd2..c17ed5de29 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/easyScoreConstraints/DummyTestdataSpringEasyScore.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/constraints/easy/DummySpringEasyScore.java @@ -1,10 +1,10 @@ -package ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.easyScoreConstraints; +package ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.constraints.easy; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.spring.boot.autoconfigure.normal.domain.TestdataSpringSolution; -public class DummyTestdataSpringEasyScore implements EasyScoreCalculator { +public class DummySpringEasyScore implements EasyScoreCalculator { @Override public SimpleScore calculateScore(TestdataSpringSolution testdataSpringSolution) { return null; diff --git a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/incrementalScoreConstraints/DummyTestdataSpringIncrementalScore.java b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/constraints/incremental/DummySpringIncrementalScore.java similarity index 87% rename from spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/incrementalScoreConstraints/DummyTestdataSpringIncrementalScore.java rename to spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/constraints/incremental/DummySpringIncrementalScore.java index 87130257d2..c2a52c9bf2 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/incrementalScoreConstraints/DummyTestdataSpringIncrementalScore.java +++ b/spring-integration/spring-boot-autoconfigure/src/test/java/ai/timefold/solver/spring/boot/autoconfigure/dummy/normal/constraints/incremental/DummySpringIncrementalScore.java @@ -1,9 +1,9 @@ -package ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.incrementalScoreConstraints; +package ai.timefold.solver.spring.boot.autoconfigure.dummy.normal.constraints.incremental; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; -public class DummyTestdataSpringIncrementalScore implements IncrementalScoreCalculator { +public class DummySpringIncrementalScore implements IncrementalScoreCalculator { @Override public void resetWorkingSolution(Object workingSolution) { diff --git a/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/easyScoreSolverConfig.xml b/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/easyScoreSolverConfig.xml index fe80043ef5..61c99ff727 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/easyScoreSolverConfig.xml +++ b/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/easyScoreSolverConfig.xml @@ -5,7 +5,7 @@ ai.timefold.solver.spring.boot.autoconfigure.normal.domain.TestdataSpringEntity - ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.easyScoreConstraints.DummyTestdataChainedSpringEasyScore + ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.constraints.easy.DummyChainedSpringEasyScore diff --git a/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/incrementalScoreSolverConfig.xml b/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/incrementalScoreSolverConfig.xml index c1ef8fef44..14fc6cd4cb 100644 --- a/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/incrementalScoreSolverConfig.xml +++ b/spring-integration/spring-boot-autoconfigure/src/test/resources/ai/timefold/solver/spring/boot/autoconfigure/incrementalScoreSolverConfig.xml @@ -5,7 +5,7 @@ ai.timefold.solver.spring.boot.autoconfigure.normal.domain.TestdataSpringEntity - ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.incrementalScoreConstraints.DummyTestdataChainedSpringIncrementalScore + ai.timefold.solver.spring.boot.autoconfigure.dummy.chained.constraints.incremental.DummyChainedSpringIncrementalScore