/
EnvironmentModeTest.java
305 lines (259 loc) · 14.6 KB
/
EnvironmentModeTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
package ai.timefold.solver.core.config.solver;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.stream.IntStream;
import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator;
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.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;
import ai.timefold.solver.core.config.solver.termination.TerminationConfig;
import ai.timefold.solver.core.config.solver.testutil.calculator.TestdataCorruptedDifferentValuesCalculator;
import ai.timefold.solver.core.config.solver.testutil.calculator.TestdataDifferentValuesCalculator;
import ai.timefold.solver.core.config.solver.testutil.corruptedmove.factory.TestdataCorruptedEntityUndoMoveFactory;
import ai.timefold.solver.core.config.solver.testutil.corruptedmove.factory.TestdataCorruptedUndoMoveFactory;
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;
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
import ai.timefold.solver.core.impl.solver.DefaultSolver;
import ai.timefold.solver.core.impl.solver.random.RandomFactory;
import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity;
import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution;
import ai.timefold.solver.core.impl.testdata.domain.TestdataValue;
import ai.timefold.solver.core.impl.testdata.util.PlannerTestUtils;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
class EnvironmentModeTest {
private static final int NUMBER_OF_RANDOM_NUMBERS_GENERATED = 1000;
private static final int NUMBER_OF_TIMES_RUN = 10;
private static final int NUMBER_OF_TERMINATION_STEP_COUNT_LIMIT = 20;
private static TestdataSolution inputProblem;
@BeforeAll
static void setUpInputProblem() {
inputProblem = new TestdataSolution("s1");
inputProblem.setValueList(Arrays.asList(new TestdataValue("v1"), new TestdataValue("v2"),
new TestdataValue("v3")));
inputProblem.setEntityList(Arrays.asList(new TestdataEntity("e1"), new TestdataEntity("e2"),
new TestdataEntity("e3"), new TestdataEntity("e4")));
}
private static SolverConfig buildSolverConfig(EnvironmentMode environmentMode) {
CustomPhaseConfig initializerPhaseConfig = new CustomPhaseConfig()
.withCustomPhaseCommandClassList(Collections.singletonList(TestdataFirstValueInitializer.class));
LocalSearchPhaseConfig localSearchPhaseConfig = new LocalSearchPhaseConfig();
localSearchPhaseConfig
.setTerminationConfig(
new TerminationConfig().withStepCountLimit(NUMBER_OF_TERMINATION_STEP_COUNT_LIMIT));
return new SolverConfig()
.withSolutionClass(TestdataSolution.class)
.withEntityClasses(TestdataEntity.class)
.withEnvironmentMode(environmentMode)
.withPhases(initializerPhaseConfig, localSearchPhaseConfig);
}
@ParameterizedTest(name = "{0}")
@EnumSource(EnvironmentMode.class)
void determinism(EnvironmentMode environmentMode) {
SolverConfig solverConfig = buildSolverConfig(environmentMode);
setSolverConfigCalculatorClass(solverConfig, TestdataDifferentValuesCalculator.class);
Solver<TestdataSolution> solver1 = SolverFactory.<TestdataSolution> create(solverConfig).buildSolver();
Solver<TestdataSolution> solver2 = SolverFactory.<TestdataSolution> create(solverConfig).buildSolver();
switch (environmentMode) {
case NON_REPRODUCIBLE:
assertNonReproducibility(solver1, solver2);
break;
case FULL_ASSERT:
case FAST_ASSERT:
case NON_INTRUSIVE_FULL_ASSERT:
case REPRODUCIBLE:
assertReproducibility(solver1, solver2);
break;
default:
Assertions.fail("Environment mode not covered: " + environmentMode);
}
}
@ParameterizedTest(name = "{0}")
@EnumSource(EnvironmentMode.class)
void corruptedCustomMoves(EnvironmentMode environmentMode) {
SolverConfig solverConfig = buildSolverConfig(environmentMode);
// Intrusive modes should throw exception about corrupted undoMove
setSolverConfigCalculatorClass(solverConfig, TestdataDifferentValuesCalculator.class);
switch (environmentMode) {
case FULL_ASSERT:
case FAST_ASSERT:
setSolverConfigMoveListFactoryClassToCorrupted(
solverConfig,
TestdataCorruptedUndoMoveFactory.class);
assertIllegalStateExceptionWhileSolving(solverConfig, "corrupted undoMove");
break;
case NON_INTRUSIVE_FULL_ASSERT:
setSolverConfigMoveListFactoryClassToCorrupted(
solverConfig,
TestdataCorruptedEntityUndoMoveFactory.class);
assertIllegalStateExceptionWhileSolving(solverConfig, "not the uncorruptedScore");
break;
case REPRODUCIBLE:
case NON_REPRODUCIBLE:
// No exception expected
break;
default:
Assertions.fail("Environment mode not covered: " + environmentMode);
}
}
@ParameterizedTest(name = "{0}")
@EnumSource(EnvironmentMode.class)
void corruptedConstraints(EnvironmentMode environmentMode) {
SolverConfig solverConfig = buildSolverConfig(environmentMode);
// For full assert modes it should throw exception about corrupted score
setSolverConfigCalculatorClass(solverConfig, TestdataCorruptedDifferentValuesCalculator.class);
switch (environmentMode) {
case FULL_ASSERT:
case NON_INTRUSIVE_FULL_ASSERT:
assertIllegalStateExceptionWhileSolving(
solverConfig,
"not the uncorruptedScore");
break;
case FAST_ASSERT:
assertIllegalStateExceptionWhileSolving(
solverConfig,
"Score corruption analysis could not be generated ");
break;
case REPRODUCIBLE:
case NON_REPRODUCIBLE:
// No exception expected
break;
default:
Assertions.fail("Environment mode not covered: " + environmentMode);
}
}
private void assertReproducibility(Solver<TestdataSolution> solver1, Solver<TestdataSolution> solver2) {
assertGeneratingSameNumbers(((DefaultSolver<TestdataSolution>) solver1).getRandomFactory(),
((DefaultSolver<TestdataSolution>) solver2).getRandomFactory());
assertSameScoreSeries(solver1, solver2);
}
private void assertNonReproducibility(Solver<TestdataSolution> solver1, Solver<TestdataSolution> solver2) {
assertGeneratingDifferentNumbers(((DefaultSolver<TestdataSolution>) solver1).getRandomFactory(),
((DefaultSolver<TestdataSolution>) solver2).getRandomFactory());
assertDifferentScoreSeries(solver1, solver2);
}
private void assertIllegalStateExceptionWhileSolving(SolverConfig solverConfig, String exceptionMessage) {
assertThatExceptionOfType(IllegalStateException.class)
.isThrownBy(() -> PlannerTestUtils.solve(solverConfig, inputProblem))
.withMessageContaining(exceptionMessage);
}
private void assertSameScoreSeries(Solver<TestdataSolution> solver1, Solver<TestdataSolution> solver2) {
TestdataStepScoreListener listener = new TestdataStepScoreListener();
TestdataStepScoreListener listener2 = new TestdataStepScoreListener();
((DefaultSolver<TestdataSolution>) solver1).addPhaseLifecycleListener(listener);
((DefaultSolver<TestdataSolution>) solver2).addPhaseLifecycleListener(listener2);
assertSoftly(softly -> IntStream.range(0, NUMBER_OF_TIMES_RUN)
.forEach(i -> {
solver1.solve(inputProblem);
solver2.solve(inputProblem);
softly.assertThat(listener.getScores())
.as("Score steps should be the same "
+ "in a reproducible environment mode.")
.isEqualTo(listener2.getScores());
}));
}
private void assertDifferentScoreSeries(Solver<TestdataSolution> solver1, Solver<TestdataSolution> solver2) {
TestdataStepScoreListener listener = new TestdataStepScoreListener();
TestdataStepScoreListener listener2 = new TestdataStepScoreListener();
((DefaultSolver<TestdataSolution>) solver1).addPhaseLifecycleListener(listener);
((DefaultSolver<TestdataSolution>) solver2).addPhaseLifecycleListener(listener2);
assertSoftly(softly -> IntStream.range(0, NUMBER_OF_TIMES_RUN)
.forEach(i -> {
solver1.solve(inputProblem);
solver2.solve(inputProblem);
softly.assertThat(listener.getScores())
.as("Score steps should not be the same in a non-reproducible environment mode. "
+ "This might be possible because searchSpace is not infinite and "
+ "two different random scenarios can have the same results. "
+ "Run test again.")
.isNotEqualTo(listener2.getScores());
}));
}
private void assertGeneratingSameNumbers(RandomFactory factory1, RandomFactory factory2) {
Random random = factory1.createRandom();
Random random2 = factory2.createRandom();
assertSoftly(softly -> IntStream.range(0, NUMBER_OF_RANDOM_NUMBERS_GENERATED)
.forEach(i -> softly.assertThat(random.nextInt())
.as("Random factories should generate the same results "
+ "in a reproducible environment mode.")
.isEqualTo(random2.nextInt())));
}
private void assertGeneratingDifferentNumbers(RandomFactory factory1, RandomFactory factory2) {
Random random = factory1.createRandom();
Random random2 = factory2.createRandom();
assertSoftly(softly -> IntStream.range(0, NUMBER_OF_RANDOM_NUMBERS_GENERATED)
.forEach(i -> softly.assertThat(random.nextInt())
.as("Random factories should not generate exactly the same results "
+ "in the non-reproducible environment mode. "
+ "It can happen but the probability is very low. Run test again")
.isNotEqualTo(random2.nextInt())));
}
private void setSolverConfigCalculatorClass(SolverConfig solverConfig,
Class<? extends EasyScoreCalculator> easyScoreCalculatorClass) {
solverConfig.setScoreDirectorFactoryConfig(new ScoreDirectorFactoryConfig()
.withEasyScoreCalculatorClass(easyScoreCalculatorClass));
}
private void setSolverConfigMoveListFactoryClassToCorrupted(SolverConfig solverConfig,
Class<? extends MoveListFactory<TestdataSolution>> move) {
MoveListFactoryConfig moveListFactoryConfig = new MoveListFactoryConfig();
moveListFactoryConfig.setMoveListFactoryClass(move);
CustomPhaseConfig initializerPhaseConfig = new CustomPhaseConfig()
.withCustomPhaseCommandClassList(Collections.singletonList(TestdataFirstValueInitializer.class));
LocalSearchPhaseConfig localSearchPhaseConfig = new LocalSearchPhaseConfig();
localSearchPhaseConfig.setMoveSelectorConfig(moveListFactoryConfig);
localSearchPhaseConfig
.setTerminationConfig(
new TerminationConfig().withStepCountLimit(NUMBER_OF_TERMINATION_STEP_COUNT_LIMIT));
solverConfig.withPhases(initializerPhaseConfig, localSearchPhaseConfig);
}
public static class TestdataFirstValueInitializer implements CustomPhaseCommand<TestdataSolution> {
@Override
public void changeWorkingSolution(ScoreDirector<TestdataSolution> scoreDirector) {
TestdataSolution solution = scoreDirector.getWorkingSolution();
TestdataValue firstValue = solution.getValueList().get(0);
for (TestdataEntity entity : solution.getEntityList()) {
scoreDirector.beforeVariableChanged(entity, "value");
entity.setValue(firstValue);
scoreDirector.afterVariableChanged(entity, "value");
}
scoreDirector.triggerVariableListeners();
InnerScoreDirector<TestdataSolution, ?> innerScoreDirector =
(InnerScoreDirector<TestdataSolution, ?>) scoreDirector;
Score<?> score = innerScoreDirector.calculateScore();
if (!score.isSolutionInitialized()) {
throw new IllegalStateException("The solution (" + TestdataEntity.class.getSimpleName()
+ ") was not fully initialized by CustomSolverPhase: ("
+ this.getClass().getCanonicalName() + ")");
}
}
}
public static class TestdataStepScoreListener extends PhaseLifecycleListenerAdapter<TestdataSolution> {
private List<SimpleScore> scores = new ArrayList<>();
@Override
public void stepEnded(AbstractStepScope<TestdataSolution> stepScope) {
TestdataSolution solution = stepScope.getWorkingSolution();
if (solution.getScore() != null) {
scores.add(solution.getScore());
}
}
public List<SimpleScore> getScores() {
return scores;
}
}
}