diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java index 3496d7ac328..bc907dfb0dc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java @@ -88,6 +88,10 @@ private void doStep(CustomStepScope stepScope, PhaseCommand phaseTermination.isPhaseTerminated(stepScope.getPhaseScope())); customPhaseCommand.changeWorkingSolution(commandContext); calculateWorkingStepScore(stepScope, customPhaseCommand); + if (stepScope.getScore().isInvalid()) { + throw new IllegalStateException("The custom phase command (%s) resulted in an inconsistent solution." + .formatted(customPhaseCommand)); + } var solver = stepScope.getPhaseScope().getSolverScope().getSolver(); solver.getBestSolutionRecaller().processWorkingSolutionDuringStep(stepScope); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index 79833139d36..83c4c1f9d7e 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -1555,6 +1555,55 @@ void solveWhenIgnoringInconsistentSolutionsThrowsIfInconsistentEntityPinned() { .hasMessageContainingAll("Entity", "b2", "while involved in a dependency loop"); } + @Test + void solveCustomPhaseReturnsInconsistent() { + // Solver config + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataDependencyNoInconsistentFieldSolution.class, TestdataDependencyNoInconsistentFieldEntity.class, + TestdataDependencyNoInconsistentFieldValue.class) + .withEasyScoreCalculatorClass(null) + .withConstraintProviderClass(TestdataDependencyNoInconsistentFieldConstraintProvider.class) + .withPhases(new CustomPhaseConfig() + .withCustomPhaseCommands(command -> { + var solution = (TestdataDependencyNoInconsistentFieldSolution) command.getWorkingSolution(); + var sE1 = solution.getEntities().getFirst(); + var sB1 = solution.getValues().get(2); + var sB2 = solution.getValues().get(3); + var metaModel = command.getSolutionMetaModel() + .genuineEntity(TestdataDependencyNoInconsistentFieldEntity.class) + .listVariable("values", TestdataDependencyNoInconsistentFieldValue.class); + command.execute(Moves.compose( + Moves.assign(metaModel, sB2, sE1, 0), + Moves.assign(metaModel, sB1, sE1, 1))); + })); + + var e1 = new TestdataDependencyNoInconsistentFieldEntity("a"); + var e2 = new TestdataDependencyNoInconsistentFieldEntity("b"); + + var a1 = new TestdataDependencyNoInconsistentFieldValue("a1"); + var a2 = new TestdataDependencyNoInconsistentFieldValue("a2"); + var b1 = new TestdataDependencyNoInconsistentFieldValue("b1"); + var b2 = new TestdataDependencyNoInconsistentFieldValue("b2"); + + a2.setDependencies(List.of(a1)); + b2.setDependencies(List.of(b1)); + + e1.setValues(List.of(b2, b1)); + e2.setValues(List.of(a1, a2)); + + var entities = List.of(e1, e2); + var values = List.of(a1, a2, b1, b2); + + var problem = new TestdataDependencyNoInconsistentFieldSolution(); + + problem.setEntities(entities); + problem.setValues(values); + + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem, false)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContainingAll("The custom phase command", "resulted in an inconsistent solution."); + } + @Test void solveIgnoreInconsistent() { // Solver config