File size: 7,976 Bytes
50f82a1 |
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 |
"""
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,
)
# Test fixtures
CREW_A = Crew(id="A", name="Crew A")
CREW_B = Crew(id="B", name="Crew B")
START_DATE = date(2024, 1, 8) # A Monday
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,
)
# ************************************************************************
# Crew conflict tests
# ************************************************************************
def test_crew_conflict_no_overlap():
"""Two jobs with same crew but no time overlap should not penalize."""
# Job 1: days 0-2 (3 days)
job1 = create_job("1", duration=3, crew=CREW_A, start_offset=0)
# Job 2: days 5-7 (3 days) - no overlap
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."""
# Job 1: days 0-4 (5 days)
job1 = create_job("1", duration=5, crew=CREW_A, start_offset=0)
# Job 2: days 3-7 (5 days) - overlaps on days 3-4
job2 = create_job("2", duration=5, crew=CREW_A, start_offset=3)
# Should penalize for the overlap
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)
# ************************************************************************
# Min start date tests
# ************************************************************************
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, # Can start from day 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, # Can't start until day 5
)
# Started 5 days early
constraint_verifier.verify_that(min_start_date).given(job).penalizes()
# ************************************************************************
# Max end date tests
# ************************************************************************
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, # Due day 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, # Due day 5, but job takes 10 days
)
constraint_verifier.verify_that(max_end_date).given(job).penalizes()
# ************************************************************************
# Before ideal end date tests
# ************************************************************************
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, # Ideal by day 10, job ends after
)
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, # Ideal by day 20, but job ends day 3
)
constraint_verifier.verify_that(before_ideal_end_date).given(job).penalizes()
# ************************************************************************
# After ideal end date tests
# ************************************************************************
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, # Ideal by day 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, # Ideal by day 5, but job takes 10 days
)
constraint_verifier.verify_that(after_ideal_end_date).given(job).penalizes()
# ************************************************************************
# Tag conflict tests
# ************************************************************************
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"} # Shares "Downtown" tag
)
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)
|