Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import ai.timefold.solver.core.api.domain.common.PlanningId;
import ai.timefold.solver.core.api.domain.solution.cloner.DeepPlanningClone;
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
import ai.timefold.solver.core.api.score.Score;
import ai.timefold.solver.core.impl.domain.common.ReflectionHelper;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
Expand Down Expand Up @@ -83,6 +84,22 @@ public static boolean isFieldDeepCloned(SolutionDescriptor<?> solutionDescriptor
if (isImmutable(fieldType)) {
return false;
} else {
// Problem facts
// assigned to basic variables that enable deep cloning must also enable deep cloning for the fact type.
// Otherwise, the solver might fail to recognize that an assigned value belongs to a value range.
if (isFieldAPlanningBasicVariable(field, owningClass) && isFieldADeepCloneProperty(field, owningClass)
&& !isClassDeepCloned(solutionDescriptor, field.getType())) {
throw new IllegalStateException("""
The field (%s) of class (%s) is configured to be deep-cloned,
but its type (%s) is not deep-cloned.
Maybe remove the @%s annotation from the field?
Maybe annotate the type (%s) with @%s?"""
.formatted(field.getName(), owningClass.getCanonicalName(),
field.getType().getCanonicalName(),
DeepPlanningClone.class.getSimpleName(),
field.getType().getCanonicalName(),
DeepPlanningClone.class.getSimpleName()));
}
return needsDeepClone(solutionDescriptor, field, owningClass);
}

Expand Down Expand Up @@ -207,6 +224,15 @@ private static boolean isFieldAPlanningListVariable(Field field, Class<?> owning
}
}

private static boolean isFieldAPlanningBasicVariable(Field field, Class<?> owningClass) {
if (!field.isAnnotationPresent(PlanningVariable.class)) {
Method getterMethod = ReflectionHelper.getGetterMethod(owningClass, field.getName());
return getterMethod != null && getterMethod.isAnnotationPresent(PlanningVariable.class);
} else {
return true;
}
}

private DeepCloningUtils() {
// No external instances.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static ai.timefold.solver.core.testutil.PlannerAssert.assertCode;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.SoftAssertions.assertSoftly;

import java.time.Duration;
Expand Down Expand Up @@ -32,6 +33,7 @@
import ai.timefold.solver.core.testdomain.clone.deepcloning.TestdataVariousTypes;
import ai.timefold.solver.core.testdomain.clone.deepcloning.field.TestdataFieldAnnotatedDeepCloningEntity;
import ai.timefold.solver.core.testdomain.clone.deepcloning.field.TestdataFieldAnnotatedDeepCloningSolution;
import ai.timefold.solver.core.testdomain.clone.deepcloning.field.invalid.TestdataInvalidEntityProvidingSolution;
import ai.timefold.solver.core.testdomain.collection.TestdataArrayBasedEntity;
import ai.timefold.solver.core.testdomain.collection.TestdataArrayBasedSolution;
import ai.timefold.solver.core.testdomain.collection.TestdataEntityCollectionPropertyEntity;
Expand Down Expand Up @@ -1041,6 +1043,21 @@ private void assertDeepCloningEntityClone(TestdataFieldAnnotatedDeepCloningEntit

}

@Test
void failDeepCloneRequiredTypeAnnotation() {
var solutionDescriptor = TestdataInvalidEntityProvidingSolution.buildSolutionDescriptor();
var original = TestdataInvalidEntityProvidingSolution.generateSolution();
assertThatThrownBy(() -> {
var cloner = createSolutionCloner(solutionDescriptor);
cloner.cloneSolution(original);
}).hasMessageContaining(
"The field (value) of class (ai.timefold.solver.core.testdomain.clone.deepcloning.field.invalid.TestdataInvalidEntityProvidingEntity) is configured to be deep-cloned")
.hasMessageContaining("but its type (ai.timefold.solver.core.testdomain.TestdataValue) is not deep-cloned")
.hasMessageContaining("Maybe remove the @DeepPlanningClone annotation from the field?")
.hasMessageContaining(
"Maybe annotate the type (ai.timefold.solver.core.testdomain.TestdataValue) with @DeepPlanningClone?");
}

private static class MaxStackFrameFinder {
int maxStackFrames = 0;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package ai.timefold.solver.core.testdomain.clone.deepcloning.field.invalid;

import java.util.List;

import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.solution.cloner.DeepPlanningClone;
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
import ai.timefold.solver.core.testdomain.TestdataObject;
import ai.timefold.solver.core.testdomain.TestdataValue;

@PlanningEntity
public class TestdataInvalidEntityProvidingEntity extends TestdataObject {

public static EntityDescriptor<TestdataInvalidEntityProvidingSolution> buildEntityDescriptor() {
return TestdataInvalidEntityProvidingSolution.buildSolutionDescriptor()
.findEntityDescriptorOrFail(TestdataInvalidEntityProvidingEntity.class);
}

public static GenuineVariableDescriptor<TestdataInvalidEntityProvidingSolution> buildVariableDescriptorForValue() {
return buildEntityDescriptor().getGenuineVariableDescriptor("value");
}

@ValueRangeProvider(id = "valueRange")
private List<TestdataValue> valueRange;

@DeepPlanningClone
private TestdataValue value; // TestdataValue is not deep-cloned, and the cloning logic should fail-fast

public TestdataInvalidEntityProvidingEntity() {
// Required for cloning
}

public TestdataInvalidEntityProvidingEntity(String code, List<TestdataValue> valueRange) {
this(code, valueRange, null);
}

public TestdataInvalidEntityProvidingEntity(String code, List<TestdataValue> valueRange, TestdataValue value) {
super(code);
this.valueRange = valueRange;
this.value = value;
}

@PlanningVariable(valueRangeProviderRefs = "valueRange")
public TestdataValue getValue() {
return value;
}

public void setValue(TestdataValue value) {
this.value = value;
}

public List<TestdataValue> getValueRange() {
return valueRange;
}

public void setValueRange(List<TestdataValue> valueRange) {
this.valueRange = valueRange;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package ai.timefold.solver.core.testdomain.clone.deepcloning.field.invalid;

import java.util.List;

import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty;
import ai.timefold.solver.core.api.domain.solution.PlanningScore;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.score.SimpleScore;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel;
import ai.timefold.solver.core.testdomain.TestdataObject;
import ai.timefold.solver.core.testdomain.TestdataValue;

@PlanningSolution
public class TestdataInvalidEntityProvidingSolution extends TestdataObject {

public static SolutionDescriptor<TestdataInvalidEntityProvidingSolution> buildSolutionDescriptor() {
return SolutionDescriptor.buildSolutionDescriptor(TestdataInvalidEntityProvidingSolution.class,
TestdataInvalidEntityProvidingEntity.class);
}

public static PlanningSolutionMetaModel<TestdataInvalidEntityProvidingSolution> buildMetaModel() {
return buildSolutionDescriptor().getMetaModel();
}

public static TestdataInvalidEntityProvidingSolution generateSolution() {
var solution = new TestdataInvalidEntityProvidingSolution("s1");
var value1 = new TestdataValue("1");
var value2 = new TestdataValue("2");
var entity1 = new TestdataInvalidEntityProvidingEntity("1", List.of(value1, value2));
entity1.setValue(value1);
var entity2 = new TestdataInvalidEntityProvidingEntity("2", List.of(value1, value2));
entity2.setValue(value2);
solution.setEntityList(List.of(entity1, entity2));
return solution;
}

private List<TestdataInvalidEntityProvidingEntity> entityList;

private SimpleScore score;

public TestdataInvalidEntityProvidingSolution() {
// Required for cloning
}

public TestdataInvalidEntityProvidingSolution(String code) {
super(code);
}

@PlanningEntityCollectionProperty
public List<TestdataInvalidEntityProvidingEntity> getEntityList() {
return entityList;
}

public void setEntityList(List<TestdataInvalidEntityProvidingEntity> entityList) {
this.entityList = entityList;
}

@PlanningScore
public SimpleScore getScore() {
return score;
}

public void setScore(SimpleScore score) {
this.score = score;
}
}
Loading