-
Notifications
You must be signed in to change notification settings - Fork 175
feat: add ability to run a move during testing #2028
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| * This class is an internal detail and must not be exposed to the user. | ||
| */ | ||
| @NullMarked | ||
| static final class DummyConstraintProvider implements ConstraintProvider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This forces some changes to Quarkus/Spring.
I made these manually, as I didn't trust the agent to figure it out without all the speccing and planning prerequisites.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need a DummyConstraintProvider? MoveAsserter works without one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request adds a new MoveRunner API for testing move implementations during development. The feature was implemented using AI assistance (Claude Sonnet 4.5) based on specifications created through human-AI collaboration.
Changes:
- Added new testing API (
MoveRunnerandMoveRunContext) for executing moves with automatic undo support - Updated Spring Boot and Quarkus auto-configuration to filter out internal test classes
- Enhanced configuration utilities to support non-public inner static classes
- Added comprehensive test coverage using the new API with builtin moves
- Updated documentation with usage examples and best practices
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| MoveRunner.java | New API entry point providing fluent interface for move execution with resource management |
| MoveRunContext.java | New execution context for permanent and temporary move execution |
| InnerScoreDirector.java | Added executeTemporarily variant accepting consumer callback |
| AbstractScoreDirector.java | Implemented new executeTemporarily method delegating to move director |
| ConfigUtils.java | Enhanced to support non-public inner static class instantiation |
| TimefoldSolverAutoConfiguration.java | Added UNIQUENESS_PREDICATE to filter internal classes and abstract types |
| IncludeAbstractClassesEntityScanner.java | Updated to use shared UNIQUENESS_PREDICATE |
| TimefoldProcessor.java | Added UNIQUENESS_PREDICATE and renamed logger constant |
| Moves.java | Updated swap methods with proper type parameterization |
| neighborhoods.adoc | Added comprehensive MoveRunner API documentation |
| *MoveTest.java (6 files) | New test classes demonstrating MoveRunner usage with builtin moves |
| MoveRunnerTest.java | Unit tests for MoveRunner API |
| Specification files | Complete feature specification, design, and implementation plan |
...uarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java
Show resolved
Hide resolved
|
Christopher-Chianelli
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be duplicating MoveAsserter (which also has the benefit of not needing a dummy ConstraintProvider to function). The tests relies on the initial values of entities in generated solutions; I would rather the entities values be set explictly when reading tests.
| try (var runner = MoveRunner.build(Solution.class, Entity.class)) { | ||
| // Execute multiple moves in sequence | ||
| var context1 = runner.using(solution1); | ||
| context1.execute(move1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where are move1/move2 defined? (I know this is for a docs and doesn't need to compile per se, but the other examples define their moves)
| var solution = createTestSolution(); | ||
| try { | ||
| try (var runner = MoveRunner.build(...)) { | ||
| runner.using(solution).executeTemporarily(move, assertions); | ||
| } | ||
| } catch (Exception e) { | ||
| // Solution is now in undefined state - discard it | ||
| solution = createTestSolution(); // Create fresh instance | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a test looked like this, I would call it a bad unit test.
| try { | ||
| return clazz.getDeclaredConstructor().newInstance(); | ||
| var ctor = clazz.getDeclaredConstructor(); | ||
| ctor.setAccessible(true); // For inner static non-public classes; public noarg ctors are effectively not. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will fail when Java enforces encapsulation; when is setAccessible needed?
| @SuppressWarnings("unchecked") | ||
| public static <Solution_, Entity_, Value_> Move<Solution_> swap( | ||
| PlanningVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel, Entity_ leftEntity, Entity_ rightEntity) { | ||
| return swap(List.of((PlanningVariableMetaModel<Solution_, Entity_, Object>) variableMetaModel), leftEntity, | ||
| rightEntity); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous implementation was better
| Objects.requireNonNull(exceptionHandler, "exceptionHandler"); | ||
|
|
||
| try { | ||
| scoreDirector.executeMove(Objects.requireNonNull(move, "move")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we throw an AssertionError when an exception is not thrown? I would only used this method if I am expecting an exception in a test.
| var solution = TestdataSolution.generateSolution(2, 2); | ||
| assertThatThrownBy(() -> runner.using(solution)) | ||
| .isInstanceOf(IllegalStateException.class) | ||
| .hasMessageContaining("closed"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably should be "The MoveRunner cannot be used after it is closed."
|
|
||
| [source,java,options="nowrap"] | ||
| ---- | ||
| try (var runner = MoveRunner.build(SolutionClass.class, EntityClass.class)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| try (var runner = MoveRunner.build(SolutionClass.class, EntityClass.class)) { | |
| try (var runner = MoveRunner.build(Solution.class, Entity.class)) { |
| @Test | ||
| void testChangeMove() { | ||
| // Arrange | ||
| var solution = TestdataSolution.generateSolution(2, 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not use Testdata in the docs and would use an example such as Employee Scheduling
| ---- | ||
| @Test | ||
| void testMoveThenUndo() { | ||
| var solution = TestdataSolution.generateSolution(2, 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
| ---- | ||
| @Test | ||
| void testMoveWithExceptionHandler() { | ||
| var solution = createInvalidSolution(); // May cause exceptions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That comment is pointless/not useful; I would create a move that uses an out of bound index
| } | ||
|
|
||
| @Override | ||
| public void executeTemporarily(Move<Solution_> move, Consumer<SolutionView<Solution_>> consumer) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First, let's rename the method to executeTemporaryMove for consistency. Additionally, I suggest merging the logic of the new method with the existing one. This way, we can reuse the logic and pass a dummy consumer when it is not necessary.
| */ | ||
| InnerScore<Score_> executeTemporaryMove(Move<Solution_> move, boolean assertMoveScoreFromScratch); | ||
|
|
||
| /** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my previous related comment. We can provide a default implementation for the method without a consumer.
| private final InnerScoreDirector<Solution_, ?> scoreDirector; | ||
|
|
||
| MoveRunContext(InnerScoreDirector<Solution_, ?> scoreDirector) { | ||
| this.scoreDirector = Objects.requireNonNull(scoreDirector, "scoreDirector"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder how useful it is to check for nullity when using @NullMarked.
| * @throws NullPointerException if move or assertions is null | ||
| * @throws IllegalStateException if the parent MoveRunner has been closed | ||
| */ | ||
| public void executeTemporarily(Move<Solution_> move, Consumer<SolutionView<Solution_>> assertions) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's rename it to executeTemporaryMove.
| Objects.requireNonNull(solutionClass, "solutionClass"), entityClasses); | ||
|
|
||
| // Create a Constraint Streams configuration with a dummy constraint provider | ||
| var scoreDirectorFactoryConfig = new ScoreDirectorFactoryConfig() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the idea of using a dummy score provider by default. I would prefer to include the score provider as a parameter and move DummyConstraintProvider to a test package. This way, we can reuse the provider across all move test classes without losing cohesion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also provide a facility method that uses the dummy constraint provider by default outside of the MoveRunner class and maintains the current logic of the test move classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After reviewing all classes, I see how the dummy score provider simplifies testing. Maybe we can rename the provider class to something like InternalMoveRunnerConstraintProvider and keep the current implementation.
| @Test | ||
| void changeMoveTemporaryMultipleEntities() { | ||
| // Arrange | ||
| var solution = TestdataSolution.generateSolution(3, 3); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
zepfred
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test coverage looks good. While we have small, self-contained tests, I believe we could move some boilerplate code from these test classes to a parent class to reduce duplication.
The AbstractScoreDirector class can be enhanced, and we should ensure that the names of methods executing temporary moves are consistent with the existing ones.



This was written entirely using SpecKit. Human+AI collaboration went into the specification; the code produced was 99 % done by Claude Sonnet 4.5.
Important pieces:
spec.mdcontains the specification of the featureplan.md(and accompanying data model + API) contains the AI's implementation planconstitution.md- I drafted some amendments as I was watching AI produce code that didn't look like "our" code.I'm asking both @Christopher-Chianelli and @zepfred for a very careful code review. I have two reasons for that:
I do not recommend reviewing commit-by-commit. Review the final product, if you wish to keep your sanity.