| """ |
| Unit tests for the maintenance scheduling constraints using ConstraintVerifier. |
| """ |
|
|
| from datetime import date, timedelta |
|
|
| from solverforge_legacy.solver.test import ConstraintVerifier |
|
|
| from maintenance_scheduling.domain import ( |
| Crew, |
| Job, |
| MaintenanceSchedule, |
| WorkCalendar, |
| calculate_end_date, |
| ) |
| from maintenance_scheduling.constraints import ( |
| define_constraints, |
| crew_conflict, |
| min_start_date, |
| max_end_date, |
| before_ideal_end_date, |
| after_ideal_end_date, |
| tag_conflict, |
| ) |
|
|
|
|
| |
| CREW_A = Crew(id="A", name="Crew A") |
| CREW_B = Crew(id="B", name="Crew B") |
| START_DATE = date(2024, 1, 8) |
| WORK_CALENDAR = WorkCalendar( |
| id="cal", |
| from_date=START_DATE, |
| to_date=START_DATE + timedelta(days=60) |
| ) |
|
|
|
|
| constraint_verifier = ConstraintVerifier.build( |
| define_constraints, MaintenanceSchedule, Job |
| ) |
|
|
|
|
| def create_job( |
| job_id: str, |
| duration: int = 3, |
| crew: Crew = None, |
| start_offset: int = 0, |
| tags: set = None, |
| min_start_offset: int = 0, |
| max_end_offset: int = 30, |
| ideal_end_offset: int = 20, |
| ) -> Job: |
| """Helper function to create a Job with computed dates.""" |
| start = calculate_end_date(START_DATE, start_offset) if crew else None |
| min_start = calculate_end_date(START_DATE, min_start_offset) |
| max_end = calculate_end_date(START_DATE, max_end_offset) |
| ideal_end = calculate_end_date(START_DATE, ideal_end_offset) |
|
|
| return Job( |
| id=job_id, |
| name=f"Job {job_id}", |
| duration_in_days=duration, |
| min_start_date=min_start, |
| max_end_date=max_end, |
| ideal_end_date=ideal_end, |
| tags=tags or set(), |
| crew=crew, |
| start_date=start, |
| ) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def test_crew_conflict_no_overlap(): |
| """Two jobs with same crew but no time overlap should not penalize.""" |
| |
| job1 = create_job("1", duration=3, crew=CREW_A, start_offset=0) |
| |
| job2 = create_job("2", duration=3, crew=CREW_A, start_offset=5) |
|
|
| constraint_verifier.verify_that(crew_conflict).given(job1, job2).penalizes_by(0) |
|
|
|
|
| def test_crew_conflict_with_overlap(): |
| """Two jobs with same crew and overlapping dates should penalize.""" |
| |
| job1 = create_job("1", duration=5, crew=CREW_A, start_offset=0) |
| |
| job2 = create_job("2", duration=5, crew=CREW_A, start_offset=3) |
|
|
| |
| constraint_verifier.verify_that(crew_conflict).given(job1, job2).penalizes() |
|
|
|
|
| def test_different_crews_no_conflict(): |
| """Two overlapping jobs with different crews should not penalize.""" |
| job1 = create_job("1", duration=5, crew=CREW_A, start_offset=0) |
| job2 = create_job("2", duration=5, crew=CREW_B, start_offset=2) |
|
|
| constraint_verifier.verify_that(crew_conflict).given(job1, job2).penalizes_by(0) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def test_min_start_date_valid(): |
| """Job starting on or after min start date should not penalize.""" |
| job = create_job( |
| "1", |
| duration=3, |
| crew=CREW_A, |
| start_offset=5, |
| min_start_offset=0, |
| ) |
|
|
| constraint_verifier.verify_that(min_start_date).given(job).penalizes_by(0) |
|
|
|
|
| def test_min_start_date_violation(): |
| """Job starting before min start date should penalize.""" |
| job = create_job( |
| "1", |
| duration=3, |
| crew=CREW_A, |
| start_offset=0, |
| min_start_offset=5, |
| ) |
|
|
| |
| constraint_verifier.verify_that(min_start_date).given(job).penalizes() |
|
|
|
|
| |
| |
| |
|
|
|
|
| def test_max_end_date_valid(): |
| """Job ending on or before max end date should not penalize.""" |
| job = create_job( |
| "1", |
| duration=3, |
| crew=CREW_A, |
| start_offset=0, |
| max_end_offset=30, |
| ) |
|
|
| constraint_verifier.verify_that(max_end_date).given(job).penalizes_by(0) |
|
|
|
|
| def test_max_end_date_violation(): |
| """Job ending after max end date should penalize.""" |
| job = create_job( |
| "1", |
| duration=10, |
| crew=CREW_A, |
| start_offset=0, |
| max_end_offset=5, |
| ) |
|
|
| constraint_verifier.verify_that(max_end_date).given(job).penalizes() |
|
|
|
|
| |
| |
| |
|
|
|
|
| def test_before_ideal_end_date_valid(): |
| """Job ending at or after ideal end date should not penalize.""" |
| job = create_job( |
| "1", |
| duration=15, |
| crew=CREW_A, |
| start_offset=0, |
| ideal_end_offset=10, |
| ) |
|
|
| constraint_verifier.verify_that(before_ideal_end_date).given(job).penalizes_by(0) |
|
|
|
|
| def test_before_ideal_end_date_violation(): |
| """Job ending before ideal end date should penalize.""" |
| job = create_job( |
| "1", |
| duration=3, |
| crew=CREW_A, |
| start_offset=0, |
| ideal_end_offset=20, |
| ) |
|
|
| constraint_verifier.verify_that(before_ideal_end_date).given(job).penalizes() |
|
|
|
|
| |
| |
| |
|
|
|
|
| def test_after_ideal_end_date_valid(): |
| """Job ending at or before ideal end date should not penalize.""" |
| job = create_job( |
| "1", |
| duration=3, |
| crew=CREW_A, |
| start_offset=0, |
| ideal_end_offset=20, |
| ) |
|
|
| constraint_verifier.verify_that(after_ideal_end_date).given(job).penalizes_by(0) |
|
|
|
|
| def test_after_ideal_end_date_violation(): |
| """Job ending after ideal end date should penalize heavily.""" |
| job = create_job( |
| "1", |
| duration=10, |
| crew=CREW_A, |
| start_offset=0, |
| ideal_end_offset=5, |
| ) |
|
|
| constraint_verifier.verify_that(after_ideal_end_date).given(job).penalizes() |
|
|
|
|
| |
| |
| |
|
|
|
|
| def test_tag_conflict_no_common_tags(): |
| """Overlapping jobs with no common tags should not penalize.""" |
| job1 = create_job("1", duration=5, crew=CREW_A, start_offset=0, tags={"Downtown"}) |
| job2 = create_job("2", duration=5, crew=CREW_B, start_offset=2, tags={"Airport"}) |
|
|
| constraint_verifier.verify_that(tag_conflict).given(job1, job2).penalizes_by(0) |
|
|
|
|
| def test_tag_conflict_with_common_tags(): |
| """Overlapping jobs with common tags should penalize.""" |
| job1 = create_job( |
| "1", duration=5, crew=CREW_A, start_offset=0, |
| tags={"Downtown", "Subway"} |
| ) |
| job2 = create_job( |
| "2", duration=5, crew=CREW_B, start_offset=2, |
| tags={"Downtown"} |
| ) |
|
|
| constraint_verifier.verify_that(tag_conflict).given(job1, job2).penalizes() |
|
|
|
|
| def test_tag_conflict_no_overlap(): |
| """Non-overlapping jobs with common tags should not penalize.""" |
| job1 = create_job("1", duration=3, crew=CREW_A, start_offset=0, tags={"Downtown"}) |
| job2 = create_job("2", duration=3, crew=CREW_B, start_offset=10, tags={"Downtown"}) |
|
|
| constraint_verifier.verify_that(tag_conflict).given(job1, job2).penalizes_by(0) |
|
|