From 1643b2316cf8677a231db67d3e572fb075dae4b2 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Mon, 8 Jul 2024 09:35:06 -0400 Subject: [PATCH] fix: Add tests for new ConstraintVerifier methods --- tests/test_constraint_verifier.py | 110 +++++++++++++++++- .../src/main/python/test/__init__.py | 52 +++++++-- 2 files changed, 146 insertions(+), 16 deletions(-) diff --git a/tests/test_constraint_verifier.py b/tests/test_constraint_verifier.py index bddaff5..e0c300e 100644 --- a/tests/test_constraint_verifier.py +++ b/tests/test_constraint_verifier.py @@ -17,6 +17,7 @@ MultiConstraintVerification as JavaMultiConstraintVerification) def verifier_suite(verifier: ConstraintVerifier, same_value, is_value_one, + EntityValueIndictment, EntityValueJustification, EntityValuePairJustification, solution, e1, e2, e3, v1, v2, v3): verifier.verify_that(same_value) \ .given(e1, e2) \ @@ -37,6 +38,11 @@ def verifier_suite(verifier: ConstraintVerifier, same_value, is_value_one, .given(e1, e2) \ .penalizes(1) + with pytest.raises(AssertionError): + verifier.verify_that(same_value) \ + .given(e1, e2) \ + .indicts_with_exactly(EntityValueIndictment(e1, v1)) + e1.value = v1 e2.value = v1 e3.value = v1 @@ -47,7 +53,47 @@ def verifier_suite(verifier: ConstraintVerifier, same_value, is_value_one, verifier.verify_that(same_value) \ .given(e1, e2) \ - .penalizes() + .penalizes(1) + + verifier.verify_that(same_value) \ + .given(e1, e2) \ + .indicts_with(EntityValueIndictment(e1, e1.value), EntityValueIndictment(e2, v1)) \ + .penalizes_by(1) + + verifier.verify_that(same_value) \ + .given(e1, e2) \ + .indicts_with_exactly(EntityValueIndictment(e1, e1.value), EntityValueIndictment(e2, v1)) \ + .penalizes_by(1) + + verifier.verify_that(same_value) \ + .given(e1, e2) \ + .indicts_with(EntityValueIndictment(e1, v1)) + + verifier.verify_that(same_value) \ + .given(e1, e2) \ + .justifies_with(EntityValuePairJustification((e1, e2), v1, SimpleScore(-1))) \ + .penalizes_by(1) + + verifier.verify_that(same_value) \ + .given(e1, e2) \ + .justifies_with_exactly(EntityValuePairJustification((e1, e2), v1, SimpleScore(-1))) \ + .penalizes_by(1) + + with pytest.raises(AssertionError): + verifier.verify_that(same_value) \ + .given(e1, e2) \ + .indicts_with_exactly(EntityValueIndictment(e1, v1)) + + + with pytest.raises(AssertionError): + verifier.verify_that(same_value) \ + .given(e1, e2) \ + .justifies_with(EntityValuePairJustification((e1, e2), v1, SimpleScore(1))) + + with pytest.raises(AssertionError): + verifier.verify_that(same_value) \ + .given(e1, e2, e3) \ + .justifies_with_exactly(EntityValuePairJustification((e1, e2), v1, SimpleScore(1))) with pytest.raises(AssertionError): verifier.verify_that(same_value) \ @@ -68,10 +114,28 @@ def verifier_suite(verifier: ConstraintVerifier, same_value, is_value_one, .given(e1, e2, e3) \ .penalizes(3) + verifier.verify_that(same_value) \ + .given(e1, e2, e3) \ + .penalizes_more_than(2) + + verifier.verify_that(same_value) \ + .given(e1, e2, e3) \ + .penalizes_less_than(4) + verifier.verify_that(same_value) \ .given(e1, e2, e3) \ .penalizes() + with pytest.raises(AssertionError): + verifier.verify_that(same_value) \ + .given(e1, e2, e3) \ + .penalizes_more_than(3) + + with pytest.raises(AssertionError): + verifier.verify_that(same_value) \ + .given(e1, e2, e3) \ + .penalizes_less_than(3) + with pytest.raises(AssertionError): verifier.verify_that(same_value) \ .given(e1, e2, e3) \ @@ -199,21 +263,55 @@ def verifier_suite(verifier: ConstraintVerifier, same_value, is_value_one, def test_constraint_verifier_create(): - @dataclass + @dataclass(unsafe_hash=True) class Value: code: str + def __str__(self): + return f'Value({self.code})' + @planning_entity - @dataclass + @dataclass(unsafe_hash=True) class Entity: code: str - value: Annotated[Value, PlanningVariable] = field(default=None) + value: Annotated[Value | None, PlanningVariable] = field(default=None) + + def __str__(self): + return f'Entity({self.code}, {self.value})' + + @dataclass(unsafe_hash=True) + class EntityValueIndictment: + entity: Entity + value: Value + + def __str__(self): + return f'EntityValueIndictment({self.entity}, {self.value})' + + @dataclass(unsafe_hash=True) + class EntityValueJustification(ConstraintJustification): + entity: Entity + value: Value + score: SimpleScore + + def __str__(self): + return f'EntityValueJustification({self.entity}, {self.value}, {self.score})' + + @dataclass(unsafe_hash=True) + class EntityValuePairJustification(ConstraintJustification): + entities: tuple[Entity] + value: Value + score: SimpleScore + + def __str__(self): + return f'EntityValuePairJustification({self.entities}, {self.value}, {self.score})' def same_value(constraint_factory: ConstraintFactory): return (constraint_factory.for_each(Entity) .join(Entity, Joiners.less_than(lambda e: e.code), Joiners.equal(lambda e: e.value)) .penalize(SimpleScore.ONE) + .indict_with(lambda e1, e2: [EntityValueIndictment(e1, e1.value), EntityValueIndictment(e2, e2.value)]) + .justify_with(lambda e1, e2, score: EntityValuePairJustification((e1, e2), e1.value, score)) .as_constraint('Same Value') ) @@ -221,6 +319,8 @@ def is_value_one(constraint_factory: ConstraintFactory): return (constraint_factory.for_each(Entity) .filter(lambda e: e.value.code == 'v1') .reward(SimpleScore.ONE) + .indict_with(lambda e: [EntityValueIndictment(e, e.value)]) + .justify_with(lambda e, score: EntityValueJustification(e, e.value, score)) .as_constraint('Value 1') ) @@ -259,6 +359,7 @@ class Solution: solution = Solution([e1, e2, e3], [v1, v2, v3]) verifier_suite(verifier, same_value, is_value_one, + EntityValueIndictment, EntityValueJustification, EntityValuePairJustification, solution, e1, e2, e3, v1, v2, v3) verifier = ConstraintVerifier.build(my_constraints, Solution, Entity) @@ -274,6 +375,7 @@ class Solution: solution = Solution([e1, e2, e3], [v1, v2, v3]) verifier_suite(verifier, same_value, is_value_one, + EntityValueIndictment, EntityValueJustification, EntityValuePairJustification, solution, e1, e2, e3, v1, v2, v3) diff --git a/timefold-solver-python-core/src/main/python/test/__init__.py b/timefold-solver-python-core/src/main/python/test/__init__.py index e5056bb..e3fd433 100644 --- a/timefold-solver-python-core/src/main/python/test/__init__.py +++ b/timefold-solver-python-core/src/main/python/test/__init__.py @@ -173,7 +173,7 @@ class SingleConstraintAssertion: def __init__(self, delegate): self.delegate = delegate - def justifies_with(self, message: str = None, *justifications: 'ConstraintJustification') \ + def justifies_with(self, *justifications: 'ConstraintJustification', message: str = None) \ -> 'SingleConstraintAssertion': """ Asserts that the constraint being tested, given a set of facts, results in given justifications. @@ -192,15 +192,22 @@ def justifies_with(self, message: str = None, *justifications: 'ConstraintJustif when the expected justifications are not observed """ from java.lang import AssertionError as JavaAssertionError # noqa + from _jpyinterpreter import convert_to_java_python_like_object + from java.util import HashMap + reference_map = HashMap() + wrapped_justifications = [] + for justification in justifications: + wrapped_justification = convert_to_java_python_like_object(justification, reference_map) + wrapped_justifications.append(wrapped_justification) try: if message is None: - return self.delegate.justifiesWith(justifications) + return SingleConstraintAssertion(self.delegate.justifiesWith(*wrapped_justifications)) else: - return self.delegate.justifiesWith(message, justifications) + return SingleConstraintAssertion(self.delegate.justifiesWith(message, *wrapped_justifications)) except JavaAssertionError as e: raise AssertionError(e.getMessage()) - def justifies_with_exactly(self, message: str = None, *justifications: 'ConstraintJustification') \ + def justifies_with_exactly(self, *justifications: 'ConstraintJustification', message: str = None) \ -> 'SingleConstraintAssertion': """ Asserts that the constraint being tested, given a set of facts, results in given justifications an no others. @@ -219,15 +226,22 @@ def justifies_with_exactly(self, message: str = None, *justifications: 'Constrai when the expected justifications are not observed """ from java.lang import AssertionError as JavaAssertionError # noqa + from _jpyinterpreter import convert_to_java_python_like_object + from java.util import HashMap + reference_map = HashMap() + wrapped_justifications = [] + for justification in justifications: + wrapped_justification = convert_to_java_python_like_object(justification, reference_map) + wrapped_justifications.append(wrapped_justification) try: if message is None: - return self.delegate.justifiesWithExactly(justifications) + return SingleConstraintAssertion(self.delegate.justifiesWithExactly(*wrapped_justifications)) else: - return self.delegate.justifiesWithExactly(message, justifications) + return SingleConstraintAssertion(self.delegate.justifiesWithExactly(message, *wrapped_justifications)) except JavaAssertionError as e: raise AssertionError(e.getMessage()) - def indicts_with(self, message: str = None, *indictments) -> 'SingleConstraintAssertion': + def indicts_with(self, *indictments, message: str = None) -> 'SingleConstraintAssertion': """ Asserts that the constraint being tested, given a set of facts, results in given indictments. @@ -245,15 +259,22 @@ def indicts_with(self, message: str = None, *indictments) -> 'SingleConstraintAs when the expected indictments are not observed """ from java.lang import AssertionError as JavaAssertionError # noqa + from _jpyinterpreter import convert_to_java_python_like_object + from java.util import HashMap + reference_map = HashMap() + wrapped_indictments = [] + for indictment in indictments: + wrapped_indictment = convert_to_java_python_like_object(indictment, reference_map) + wrapped_indictments.append(wrapped_indictment) try: if message is None: - return self.delegate.indictsWith(indictments) + return SingleConstraintAssertion(self.delegate.indictsWith(*wrapped_indictments)) else: - return self.delegate.indictsWith(message, indictments) + return SingleConstraintAssertion(self.delegate.indictsWith(message, *wrapped_indictments)) except JavaAssertionError as e: raise AssertionError(e.getMessage()) - def indicts_with_exactly(self, message: str = None, *indictments) -> 'SingleConstraintAssertion': + def indicts_with_exactly(self, *indictments, message: str = None) -> 'SingleConstraintAssertion': """ Asserts that the constraint being tested, given a set of facts, results in given indictments an no others. @@ -271,11 +292,18 @@ def indicts_with_exactly(self, message: str = None, *indictments) -> 'SingleCons when the expected indictments are not observed """ from java.lang import AssertionError as JavaAssertionError # noqa + from _jpyinterpreter import convert_to_java_python_like_object + from java.util import HashMap + reference_map = HashMap() + wrapped_indictments = [] + for indictment in indictments: + wrapped_indictment = convert_to_java_python_like_object(indictment, reference_map) + wrapped_indictments.append(wrapped_indictment) try: if message is None: - return self.delegate.indictsWithExactly(indictments) + return SingleConstraintAssertion(self.delegate.indictsWithExactly(*wrapped_indictments)) else: - return self.delegate.indictsWithExactly(message, indictments) + return SingleConstraintAssertion(self.delegate.indictsWithExactly(message, *wrapped_indictments)) except JavaAssertionError as e: raise AssertionError(e.getMessage())