diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/ListVariableTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/ListVariableTracker.java index 2e0e34e270..1fb40c475d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/ListVariableTracker.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/ListVariableTracker.java @@ -1,7 +1,14 @@ package ai.timefold.solver.core.impl.domain.variable.listener.support.violation; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import ai.timefold.solver.core.api.domain.variable.ListVariableListener; import ai.timefold.solver.core.api.score.director.ScoreDirector; @@ -21,13 +28,25 @@ public class ListVariableTracker implements SourcedVariableListener, ListVariableListener, Supply { private final ListVariableDescriptor variableDescriptor; - private final List beforeVariableChangedEntityList; - private final List afterVariableChangedEntityList; + private final Map> beforeVariableChangeEventMap; + private final Map> afterVariableChangeEventMap; + private final Set afterUnassignedEvents; + + private record ChangeRange(int start, int end) implements Comparable { + @Override + public int compareTo(@NonNull ChangeRange other) { + return Comparator.comparingInt(ChangeRange::end) + .thenComparing(ChangeRange::start) + .reversed() + .compare(this, other); + } + } public ListVariableTracker(ListVariableDescriptor variableDescriptor) { this.variableDescriptor = variableDescriptor; - beforeVariableChangedEntityList = new ArrayList<>(); - afterVariableChangedEntityList = new ArrayList<>(); + beforeVariableChangeEventMap = new IdentityHashMap<>(); + afterVariableChangeEventMap = new IdentityHashMap<>(); + afterUnassignedEvents = Collections.newSetFromMap(new IdentityHashMap<>()); } @Override @@ -37,8 +56,9 @@ public VariableDescriptor getSourceVariableDescriptor() { @Override public void resetWorkingSolution(@NonNull ScoreDirector scoreDirector) { - beforeVariableChangedEntityList.clear(); - afterVariableChangedEntityList.clear(); + beforeVariableChangeEventMap.clear(); + afterVariableChangeEventMap.clear(); + afterUnassignedEvents.clear(); } @Override @@ -63,42 +83,69 @@ public void afterEntityRemoved(@NonNull ScoreDirector scoreDirector, @Override public void afterListVariableElementUnassigned(@NonNull ScoreDirector scoreDirector, @NonNull Object element) { - + afterUnassignedEvents.add(element); } @Override public void beforeListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object entity, int fromIndex, int toIndex) { - beforeVariableChangedEntityList.add(entity); + beforeVariableChangeEventMap.computeIfAbsent(entity, k -> new TreeSet<>()) + .add(new ChangeRange(fromIndex, toIndex)); } @Override public void afterListVariableChanged(@NonNull ScoreDirector scoreDirector, @NonNull Object entity, int fromIndex, int toIndex) { - afterVariableChangedEntityList.add(entity); + afterVariableChangeEventMap.computeIfAbsent(entity, k -> new TreeSet<>()) + .add(new ChangeRange(fromIndex, toIndex)); } public List getEntitiesMissingBeforeAfterEvents( - List> changedVariables) { + List> changedVariables, + VariableSnapshotTotal beforeSolution, + VariableSnapshotTotal afterSolution) { List out = new ArrayList<>(); + Set allBeforeValues = Collections.newSetFromMap(new IdentityHashMap<>()); + Set allAfterValues = Collections.newSetFromMap(new IdentityHashMap<>()); for (var changedVariable : changedVariables) { if (!variableDescriptor.equals(changedVariable.variableDescriptor())) { continue; } Object entity = changedVariable.entity(); - if (!beforeVariableChangedEntityList.contains(entity)) { + + if (!beforeVariableChangeEventMap.containsKey(entity)) { out.add("Entity (" + entity + ") is missing a beforeListVariableChanged call for list variable (" + variableDescriptor.getVariableName() + ")."); } - if (!afterVariableChangedEntityList.contains(entity)) { + if (!afterVariableChangeEventMap.containsKey(entity)) { out.add("Entity (" + entity + ") is missing a afterListVariableChanged call for list variable (" + variableDescriptor.getVariableName() + ")."); } + + List beforeList = + new ArrayList<>((List) beforeSolution.getVariableSnapshot(changedVariable).value()); + + List afterList = new ArrayList<>((List) afterSolution.getVariableSnapshot(changedVariable).value()); + + allBeforeValues.addAll(beforeList); + allAfterValues.addAll(afterList); + } + + var unassignedValues = Collections.newSetFromMap(new IdentityHashMap<>()); + unassignedValues.addAll(allBeforeValues); + unassignedValues.removeAll(allAfterValues); + + for (var unassignedValue : unassignedValues) { + if (!afterUnassignedEvents.contains(unassignedValue)) { + out.add("Missing afterListElementUnassigned: " + unassignedValue); + } } - beforeVariableChangedEntityList.clear(); - afterVariableChangedEntityList.clear(); + + beforeVariableChangeEventMap.clear(); + afterVariableChangeEventMap.clear(); + afterUnassignedEvents.clear(); return out; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/SolutionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/SolutionTracker.java index 0e3fccd400..d715f36324 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/SolutionTracker.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/SolutionTracker.java @@ -11,6 +11,8 @@ import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager; +import org.jspecify.annotations.Nullable; + public final class SolutionTracker { private final SolutionDescriptor solutionDescriptor; private final List> normalVariableTrackers; @@ -68,7 +70,7 @@ public void setAfterMoveSolution(Solution_ workingSolution) { if (beforeVariables != null) { missingEventsForward = getEntitiesMissingBeforeAfterEvents(beforeVariables, afterVariables); } else { - missingEventsBackward = Collections.emptyList(); + missingEventsForward = Collections.emptyList(); } } @@ -106,11 +108,23 @@ private List getEntitiesMissingBeforeAfterEvents(VariableSnapshotTotal listVariableTracker : listVariableTrackers) { - out.addAll(listVariableTracker.getEntitiesMissingBeforeAfterEvents(changes)); + out.addAll(listVariableTracker.getEntitiesMissingBeforeAfterEvents(changes, + beforeSolution, afterSolution)); } return out; } + public @Nullable String buildDirectorCorruptionMessage(Object completedAction) { + if (missingEventsForward != null && !missingEventsForward.isEmpty()) { + return """ + Score Director Corruption Detected. + Missing variable listener events for actual move (%s): + %s + """.formatted(completedAction, formatList(missingEventsForward)); + } + return null; + } + public String buildScoreCorruptionMessage() { if (beforeMoveSolution == null) { return ""; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 6d691d449c..1ddc92ff45 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -625,6 +625,12 @@ private void assertScoreFromScratch(Score_ score, Object completedAction, boolea if (assertionScoreDirectorFactory == null) { assertionScoreDirectorFactory = scoreDirectorFactory; } + if (trackingWorkingSolution) { + var directorCorruption = solutionTracker.buildDirectorCorruptionMessage(completedAction); + if (directorCorruption != null) { + throw new IllegalStateException(directorCorruption); + } + } try (var uncorruptedScoreDirector = assertionScoreDirectorFactory.buildDerivedScoreDirector(false, ConstraintMatchPolicy.ENABLED)) { uncorruptedScoreDirector.setWorkingSolution(workingSolution); diff --git a/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java b/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java index 499fe5ee45..c00dd08b64 100644 --- a/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/config/solver/EnvironmentModeTest.java @@ -16,6 +16,8 @@ import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveListFactoryConfig; import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig; import ai.timefold.solver.core.config.phase.custom.CustomPhaseConfig; import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; @@ -26,6 +28,9 @@ import ai.timefold.solver.core.config.solver.testutil.corruptedundoshadow.CorruptedUndoShadowEntity; import ai.timefold.solver.core.config.solver.testutil.corruptedundoshadow.CorruptedUndoShadowSolution; import ai.timefold.solver.core.config.solver.testutil.corruptedundoshadow.CorruptedUndoShadowValue; +import ai.timefold.solver.core.impl.heuristic.move.AbstractMove; +import ai.timefold.solver.core.impl.heuristic.move.Move; +import ai.timefold.solver.core.impl.heuristic.selector.move.factory.MoveListFactory; import ai.timefold.solver.core.impl.phase.custom.CustomPhaseCommand; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; @@ -37,6 +42,7 @@ import ai.timefold.solver.core.impl.testdata.domain.TestdataValue; import ai.timefold.solver.core.impl.testdata.util.PlannerTestUtils; +import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -192,6 +198,35 @@ void corruptedConstraints(EnvironmentMode environmentMode) { } } + @ParameterizedTest(name = "{0}") + @EnumSource(EnvironmentMode.class) + void corruptedScoreDirector(EnvironmentMode environmentMode) { + SolverConfig solverConfig = buildSolverConfig(environmentMode); + // For full assert modes it should throw exception about corrupted score + solverConfig.setPhaseConfigList(List.of( + new ConstructionHeuristicPhaseConfig(), + new LocalSearchPhaseConfig() + .withMoveSelectorConfig( + new MoveListFactoryConfig() + .withMoveListFactoryClass(ChangeMoveWithoutListenersMoveListFactory.class)))); + setSolverConfigCalculatorClass(solverConfig, ConstantEasyScoreCalculator.class); + + switch (environmentMode) { + case TRACKED_FULL_ASSERT -> { + assertIllegalStateExceptionWhileSolving( + solverConfig, + "Score Director Corruption Detected."); + } + case FULL_ASSERT, + NON_INTRUSIVE_FULL_ASSERT, + FAST_ASSERT, + REPRODUCIBLE, + NON_REPRODUCIBLE -> { + // No exception expected + } + } + } + private void assertReproducibility(Solver solver1, Solver solver2) { assertGeneratingSameNumbers(((DefaultSolver) solver1).getRandomFactory(), ((DefaultSolver) solver2).getRandomFactory()); @@ -320,4 +355,44 @@ public List getScores() { return scores; } } + + public static class ConstantEasyScoreCalculator implements EasyScoreCalculator { + @Override + public @NonNull SimpleScore calculateScore(@NonNull TestdataSolution o) { + return SimpleScore.ZERO; + } + } + + public static class ChangeMoveWithoutListeners extends AbstractMove { + private final TestdataEntity entity; + private final TestdataValue value; + + public ChangeMoveWithoutListeners(TestdataEntity entity, TestdataValue value) { + this.entity = entity; + this.value = value; + } + + @Override + protected void doMoveOnGenuineVariables(ScoreDirector scoreDirector) { + entity.setValue(value); + } + + @Override + public boolean isMoveDoable(ScoreDirector scoreDirector) { + return entity.getValue() != value; + } + } + + public static class ChangeMoveWithoutListenersMoveListFactory implements MoveListFactory { + @Override + public List> createMoveList(TestdataSolution testdataSolution) { + var out = new ArrayList>(); + for (var entity : testdataSolution.getEntityList()) { + for (var value : testdataSolution.getValueList()) { + out.add(new ChangeMoveWithoutListeners(entity, value)); + } + } + return out; + } + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/ListVariableTrackerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/ListVariableTrackerTest.java index eda383e7c9..d19b6aa5cd 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/ListVariableTrackerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/variable/listener/support/violation/ListVariableTrackerTest.java @@ -2,27 +2,42 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.Collections; import java.util.List; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; -import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListEntity; import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListSolution; +import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListValue; import ai.timefold.solver.core.impl.testdata.domain.list.shadow_history.TestdataListEntityWithShadowHistory; +import ai.timefold.solver.core.impl.testdata.domain.list.shadow_history.TestdataListSolutionWithShadowHistory; import org.junit.jupiter.api.Test; public class ListVariableTrackerTest { + final static SolutionDescriptor SOLUTION_DESCRIPTOR = TestdataListSolution.buildSolutionDescriptor(); final static ListVariableDescriptor VARIABLE_DESCRIPTOR = - TestdataListEntity.buildVariableDescriptorForValueList(); + SOLUTION_DESCRIPTOR.getListVariableDescriptor(); + + final static SolutionDescriptor SOLUTION_WITH_SHADOW_HISTORY_SOLUTION_DESCRIPTOR = + TestdataListSolutionWithShadowHistory.buildSolutionDescriptor(); + final static ListVariableDescriptor VARIABLE_DESCRIPTOR_FOR_SHADOW_ENTITY = + SOLUTION_WITH_SHADOW_HISTORY_SOLUTION_DESCRIPTOR.getListVariableDescriptor(); @Test void testMissingBeforeEvents() { ListVariableTracker tracker = new ListVariableTracker<>(VARIABLE_DESCRIPTOR); - TestdataEntity a = new TestdataEntity("a"); - TestdataEntity b = new TestdataEntity("b"); + var a = new TestdataListEntity("a"); + var b = new TestdataListEntity("b"); + + var solution = new TestdataListSolution(); + solution.setEntityList(List.of(a, b)); + solution.setValueList(Collections.emptyList()); + var before = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); tracker.beforeListVariableChanged(null, a, 0, 1); tracker.afterListVariableChanged(null, a, 0, 1); @@ -30,9 +45,13 @@ void testMissingBeforeEvents() { // intentionally missing before event for b tracker.afterListVariableChanged(null, b, 0, 1); + var after = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); + assertThat(tracker.getEntitiesMissingBeforeAfterEvents(List.of( new VariableId<>(VARIABLE_DESCRIPTOR, a), - new VariableId<>(VARIABLE_DESCRIPTOR, b)))).containsExactlyInAnyOrder( + new VariableId<>(VARIABLE_DESCRIPTOR, b)), + before, after)).containsExactlyInAnyOrder( "Entity (" + b + ") is missing a beforeListVariableChanged call for list variable (valueList)."); } @@ -40,8 +59,14 @@ void testMissingBeforeEvents() { void testMissingAfterEvents() { ListVariableTracker tracker = new ListVariableTracker<>(VARIABLE_DESCRIPTOR); - TestdataEntity a = new TestdataEntity("a"); - TestdataEntity b = new TestdataEntity("b"); + var a = new TestdataListEntity("a"); + var b = new TestdataListEntity("b"); + + var solution = new TestdataListSolution(); + solution.setEntityList(List.of(a, b)); + solution.setValueList(Collections.emptyList()); + var before = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); tracker.beforeListVariableChanged(null, a, 0, 1); tracker.afterListVariableChanged(null, a, 0, 1); @@ -49,9 +74,13 @@ void testMissingAfterEvents() { // intentionally missing after event for b tracker.beforeListVariableChanged(null, b, 0, 1); + var after = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); + assertThat(tracker.getEntitiesMissingBeforeAfterEvents(List.of( new VariableId<>(VARIABLE_DESCRIPTOR, a), - new VariableId<>(VARIABLE_DESCRIPTOR, b)))).containsExactlyInAnyOrder( + new VariableId<>(VARIABLE_DESCRIPTOR, b)), + before, after)).containsExactlyInAnyOrder( "Entity (" + b + ") is missing a afterListVariableChanged call for list variable (valueList)."); } @@ -59,55 +88,113 @@ void testMissingAfterEvents() { void testMissingBeforeAndAfterEvents() { ListVariableTracker tracker = new ListVariableTracker<>(VARIABLE_DESCRIPTOR); - TestdataEntity a = new TestdataEntity("a"); - TestdataEntity b = new TestdataEntity("b"); + var a = new TestdataListEntity("a"); + var b = new TestdataListEntity("b"); + + var solution = new TestdataListSolution(); + solution.setEntityList(List.of(a, b)); + solution.setValueList(Collections.emptyList()); + var before = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); tracker.beforeListVariableChanged(null, a, 0, 1); tracker.afterListVariableChanged(null, a, 0, 1); + var after = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); + assertThat(tracker.getEntitiesMissingBeforeAfterEvents(List.of( new VariableId<>(VARIABLE_DESCRIPTOR, a), - new VariableId<>(VARIABLE_DESCRIPTOR, b)))).containsExactlyInAnyOrder( + new VariableId<>(VARIABLE_DESCRIPTOR, b)), + before, after)).containsExactlyInAnyOrder( "Entity (" + b + ") is missing a beforeListVariableChanged call for list variable (valueList).", "Entity (" + b + ") is missing a afterListVariableChanged call for list variable (valueList)."); } + @Test + void testMissingUnassignEvents() { + ListVariableTracker tracker = new ListVariableTracker<>(VARIABLE_DESCRIPTOR); + + var value = new TestdataListValue("v1"); + var a = new TestdataListEntity("a", value); + var b = new TestdataListEntity("b"); + + var solution = new TestdataListSolution(); + solution.setEntityList(List.of(a, b)); + solution.setValueList(List.of(value)); + + var before = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); + + tracker.beforeListVariableChanged(null, a, 0, 1); + a.getValueList().clear(); + tracker.afterListVariableChanged(null, a, 0, 0); + + var after = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); + + assertThat(tracker.getEntitiesMissingBeforeAfterEvents(List.of( + new VariableId<>(VARIABLE_DESCRIPTOR, a)), + before, after)).containsExactlyInAnyOrder("Missing afterListElementUnassigned: v1"); + } + @Test void testNoMissingEvents() { ListVariableTracker tracker = new ListVariableTracker<>(VARIABLE_DESCRIPTOR); - TestdataEntity a = new TestdataEntity("a"); - TestdataEntity b = new TestdataEntity("b"); + var a = new TestdataListEntity("a"); + var b = new TestdataListEntity("b"); + + var solution = new TestdataListSolution(); + solution.setEntityList(List.of(a, b)); + solution.setValueList(Collections.emptyList()); + var before = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); tracker.beforeListVariableChanged(null, a, 0, 1); tracker.afterListVariableChanged(null, a, 0, 1); tracker.beforeListVariableChanged(null, b, 0, 1); tracker.afterListVariableChanged(null, b, 0, 1); + var after = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); + assertThat(tracker.getEntitiesMissingBeforeAfterEvents(List.of( new VariableId<>(VARIABLE_DESCRIPTOR, a), - new VariableId<>(VARIABLE_DESCRIPTOR, b)))).isEmpty(); + new VariableId<>(VARIABLE_DESCRIPTOR, b)), + before, after)).isEmpty(); } @Test void testEventsResetAfterCall() { ListVariableTracker tracker = new ListVariableTracker<>(VARIABLE_DESCRIPTOR); - TestdataEntity a = new TestdataEntity("a"); - TestdataEntity b = new TestdataEntity("b"); + var a = new TestdataListEntity("a"); + var b = new TestdataListEntity("b"); + + var solution = new TestdataListSolution(); + solution.setEntityList(List.of(a, b)); + solution.setValueList(Collections.emptyList()); + var before = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); tracker.beforeListVariableChanged(null, a, 0, 1); tracker.afterListVariableChanged(null, a, 0, 1); tracker.beforeListVariableChanged(null, b, 0, 1); tracker.afterListVariableChanged(null, b, 0, 1); + var after = VariableSnapshotTotal.takeSnapshot(SOLUTION_DESCRIPTOR, + solution); + assertThat(tracker.getEntitiesMissingBeforeAfterEvents(List.of( new VariableId<>(VARIABLE_DESCRIPTOR, a), - new VariableId<>(VARIABLE_DESCRIPTOR, b)))).isEmpty(); + new VariableId<>(VARIABLE_DESCRIPTOR, b)), + before, after)).isEmpty(); assertThat(tracker.getEntitiesMissingBeforeAfterEvents(List.of( new VariableId<>(VARIABLE_DESCRIPTOR, a), - new VariableId<>(VARIABLE_DESCRIPTOR, b)))).containsExactlyInAnyOrder( + new VariableId<>(VARIABLE_DESCRIPTOR, b)), + before, after)).containsExactlyInAnyOrder( "Entity (" + a + ") is missing a beforeListVariableChanged call for list variable (valueList).", "Entity (" + a + ") is missing a afterListVariableChanged call for list variable (valueList).", "Entity (" + b + ") is missing a beforeListVariableChanged call for list variable (valueList).", @@ -117,18 +204,29 @@ void testEventsResetAfterCall() { @Test @SuppressWarnings({ "rawtypes", "unchecked" }) void testDoesNotIncludeMissingEventsForOtherVariables() { - ListVariableTracker tracker = new ListVariableTracker<>(VARIABLE_DESCRIPTOR); + ListVariableTracker tracker = + new ListVariableTracker<>(VARIABLE_DESCRIPTOR_FOR_SHADOW_ENTITY); VariableDescriptor otherVariableDescriptor = TestdataListEntityWithShadowHistory.buildVariableDescriptorForValueList(); - TestdataEntity a = new TestdataEntity("a"); - TestdataListEntityWithShadowHistory b = new TestdataListEntityWithShadowHistory("b"); + var a = new TestdataListEntityWithShadowHistory("a"); + var b = new TestdataListEntityWithShadowHistory("b"); + + var solution = new TestdataListSolutionWithShadowHistory(); + solution.setEntityList(List.of(a, b)); + solution.setValueList(Collections.emptyList()); + var before = VariableSnapshotTotal.takeSnapshot(SOLUTION_WITH_SHADOW_HISTORY_SOLUTION_DESCRIPTOR, + solution); tracker.beforeListVariableChanged(null, a, 0, 1); tracker.afterListVariableChanged(null, a, 0, 1); + var after = VariableSnapshotTotal.takeSnapshot(SOLUTION_WITH_SHADOW_HISTORY_SOLUTION_DESCRIPTOR, + solution); + assertThat(tracker.getEntitiesMissingBeforeAfterEvents(List.of( - new VariableId<>(VARIABLE_DESCRIPTOR, a), - new VariableId<>((VariableDescriptor) otherVariableDescriptor, b)))).isEmpty(); + new VariableId<>(VARIABLE_DESCRIPTOR_FOR_SHADOW_ENTITY, a), + new VariableId<>((VariableDescriptor) otherVariableDescriptor, b)), + before, after)).isEmpty(); } }