blackopsrepl commited on
Commit
27dbadf
·
1 Parent(s): 41ad185

feat: add constraint tests

Browse files
src/constraint_solvers/timetable/domain.py CHANGED
@@ -11,6 +11,7 @@ from dataclasses import dataclass, field
11
  class Employee:
12
  name: Annotated[str, PlanningId]
13
  skills: Annotated[set[str], field(default_factory=set)]
 
14
  unavailable_dates: Annotated[set[date], field(default_factory=set)] = field(
15
  default_factory=set
16
  )
@@ -52,11 +53,15 @@ class Task:
52
  start_slot: Annotated[
53
  int, PlanningVariable(value_range_provider_refs=["startSlotRange"])
54
  ] # Slot index when the task starts
 
55
  required_skill: str
 
56
  # Identifier for the project this task belongs to (set by the UI when loading multiple project files)
57
  project_id: str = ""
 
58
  # Sequence number within the project to maintain original task order
59
  sequence_number: int = 0
 
60
  # Whether this task is pinned to its current assignment (for calendar events)
61
  pinned: Annotated[bool, PlanningPin] = False
62
  employee: Annotated[
 
11
  class Employee:
12
  name: Annotated[str, PlanningId]
13
  skills: Annotated[set[str], field(default_factory=set)]
14
+
15
  unavailable_dates: Annotated[set[date], field(default_factory=set)] = field(
16
  default_factory=set
17
  )
 
53
  start_slot: Annotated[
54
  int, PlanningVariable(value_range_provider_refs=["startSlotRange"])
55
  ] # Slot index when the task starts
56
+
57
  required_skill: str
58
+
59
  # Identifier for the project this task belongs to (set by the UI when loading multiple project files)
60
  project_id: str = ""
61
+
62
  # Sequence number within the project to maintain original task order
63
  sequence_number: int = 0
64
+
65
  # Whether this task is pinned to its current assignment (for calendar events)
66
  pinned: Annotated[bool, PlanningPin] = False
67
  employee: Annotated[
tests/test_constraints.py ADDED
@@ -0,0 +1,681 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Test comprehensive constraint validation using ConstraintVerifier
2
+
3
+ import pytest
4
+ from datetime import date, timedelta
5
+ from decimal import Decimal
6
+ from timefold.solver.test import ConstraintVerifier
7
+ from timefold.solver.score import HardSoftDecimalScore
8
+
9
+ from src.constraint_solvers.timetable.constraints import (
10
+ define_constraints,
11
+ required_skill,
12
+ no_overlapping_tasks,
13
+ task_within_schedule,
14
+ task_fits_in_schedule,
15
+ unavailable_employee,
16
+ maintain_project_task_order,
17
+ undesired_day_for_employee,
18
+ desired_day_for_employee,
19
+ balance_employee_task_assignments,
20
+ )
21
+ from src.constraint_solvers.timetable.domain import (
22
+ Employee,
23
+ Task,
24
+ EmployeeSchedule,
25
+ ScheduleInfo,
26
+ )
27
+
28
+
29
+ class TestConstraints:
30
+ """
31
+ Comprehensive test suite for all timetable constraints using ConstraintVerifier.
32
+ Each constraint is tested in isolation to verify correct behavior.
33
+ """
34
+
35
+ def setup_method(self):
36
+ """Set up common test data and ConstraintVerifier instance."""
37
+ self.constraint_verifier = ConstraintVerifier.build(
38
+ define_constraints, EmployeeSchedule, Task
39
+ )
40
+
41
+ # Create common test data using generator functions
42
+ self.dates = create_test_dates()
43
+ self.employees = create_standard_employees(self.dates)
44
+ self.schedule_info = create_schedule_info()
45
+
46
+ # Create shortcuts for commonly used employees
47
+ self.employee_alice = self.employees["alice"]
48
+ self.employee_bob = self.employees["bob"]
49
+ self.employee_charlie = self.employees["charlie"]
50
+
51
+ # ==================== HARD CONSTRAINT TESTS ====================
52
+
53
+ def test_required_skill_constraint_violation(self):
54
+ """Test that tasks requiring skills not possessed by assigned employee are penalized."""
55
+ task = create_task(
56
+ task_id="task1",
57
+ description="Python Development",
58
+ required_skill="Python",
59
+ employee=self.employee_bob, # Bob doesn't have Python skill
60
+ )
61
+
62
+ (
63
+ self.constraint_verifier.verify_that(required_skill)
64
+ .given(task, self.employee_bob, self.schedule_info)
65
+ .penalizes_by(1)
66
+ )
67
+
68
+ def test_required_skill_constraint_satisfied(self):
69
+ """Test that tasks assigned to employees with required skills are not penalized."""
70
+ task = create_task(
71
+ task_id="task1",
72
+ description="Python Development",
73
+ required_skill="Python",
74
+ employee=self.employee_alice, # Alice has Python skill
75
+ )
76
+
77
+ (
78
+ self.constraint_verifier.verify_that(required_skill)
79
+ .given(task, self.employee_alice, self.schedule_info)
80
+ .penalizes_by(0)
81
+ )
82
+
83
+ def test_required_skill_constraint_unassigned_task(self):
84
+ """Test that unassigned tasks don't trigger required skill constraint."""
85
+ task = create_task(
86
+ task_id="task1",
87
+ description="Python Development",
88
+ required_skill="Python",
89
+ employee=None, # Unassigned
90
+ )
91
+
92
+ (
93
+ self.constraint_verifier.verify_that(required_skill)
94
+ .given(task, self.schedule_info)
95
+ .penalizes_by(0)
96
+ )
97
+
98
+ def test_no_overlapping_tasks_constraint_violation(self):
99
+ """Test that overlapping tasks for the same employee are penalized."""
100
+ task1 = create_task(
101
+ task_id="task1",
102
+ description="Task 1",
103
+ duration_slots=4, # slots 0-3
104
+ start_slot=0,
105
+ required_skill="Python",
106
+ employee=self.employee_alice,
107
+ )
108
+
109
+ task2 = create_task(
110
+ task_id="task2",
111
+ description="Task 2",
112
+ duration_slots=3, # slots 2-4 (overlaps with task1 by 2 slots)
113
+ start_slot=2,
114
+ required_skill="Java",
115
+ employee=self.employee_alice, # Same employee
116
+ )
117
+
118
+ # Verify constraint violation (overlap of 2 slots: 2 and 3)
119
+ (
120
+ self.constraint_verifier.verify_that(no_overlapping_tasks)
121
+ .given(task1, task2, self.employee_alice, self.schedule_info)
122
+ .penalizes_by(2)
123
+ )
124
+
125
+ def test_no_overlapping_tasks_constraint_different_employees(self):
126
+ """Test that overlapping tasks for different employees are not penalized."""
127
+ task1 = create_task(
128
+ task_id="task1",
129
+ description="Task 1",
130
+ duration_slots=4,
131
+ start_slot=0,
132
+ required_skill="Python",
133
+ employee=self.employee_alice,
134
+ )
135
+
136
+ task2 = create_task(
137
+ task_id="task2",
138
+ description="Task 2",
139
+ duration_slots=3,
140
+ start_slot=2, # Overlaps in time but different employee
141
+ required_skill="Java",
142
+ employee=self.employee_bob, # Different employee
143
+ )
144
+
145
+ (
146
+ self.constraint_verifier.verify_that(no_overlapping_tasks)
147
+ .given(
148
+ task1, task2, self.employee_alice, self.employee_bob, self.schedule_info
149
+ )
150
+ .penalizes_by(0)
151
+ )
152
+
153
+ def test_no_overlapping_tasks_constraint_adjacent_tasks(self):
154
+ """Test that adjacent (non-overlapping) tasks for the same employee are not penalized."""
155
+ task1 = create_task(
156
+ task_id="task1",
157
+ description="Task 1",
158
+ duration_slots=4, # slots 0-3
159
+ start_slot=0,
160
+ required_skill="Python",
161
+ employee=self.employee_alice,
162
+ )
163
+
164
+ task2 = create_task(
165
+ task_id="task2",
166
+ description="Task 2",
167
+ duration_slots=3, # slots 4-6 (no overlap)
168
+ start_slot=4,
169
+ required_skill="Java",
170
+ employee=self.employee_alice, # Same employee
171
+ )
172
+
173
+ (
174
+ self.constraint_verifier.verify_that(no_overlapping_tasks)
175
+ .given(task1, task2, self.employee_alice, self.schedule_info)
176
+ .penalizes_by(0)
177
+ )
178
+
179
+ def test_task_within_schedule_constraint_violation(self):
180
+ """Test that tasks starting before slot 0 are penalized."""
181
+ task = create_task(
182
+ task_id="task1",
183
+ description="Invalid Task",
184
+ start_slot=-1, # Invalid start slot
185
+ required_skill="Python",
186
+ employee=self.employee_alice,
187
+ )
188
+
189
+ (
190
+ self.constraint_verifier.verify_that(task_within_schedule)
191
+ .given(task, self.employee_alice, self.schedule_info)
192
+ .penalizes_by(1)
193
+ )
194
+
195
+ def test_task_within_schedule_constraint_satisfied(self):
196
+ """Test that tasks starting at valid slots are not penalized."""
197
+ task = create_task(
198
+ task_id="task1",
199
+ description="Valid Task",
200
+ start_slot=0, # Valid start slot
201
+ required_skill="Python",
202
+ employee=self.employee_alice,
203
+ )
204
+
205
+ (
206
+ self.constraint_verifier.verify_that(task_within_schedule)
207
+ .given(task, self.employee_alice, self.schedule_info)
208
+ .penalizes_by(0)
209
+ )
210
+
211
+ def test_task_fits_in_schedule_constraint_violation(self):
212
+ """Test that tasks extending beyond schedule end are penalized."""
213
+ task = create_task(
214
+ task_id="task1",
215
+ description="Overlong Task",
216
+ duration_slots=10, # Task extends to slot 64 (beyond 59)
217
+ start_slot=55,
218
+ required_skill="Python",
219
+ employee=self.employee_alice,
220
+ )
221
+
222
+ (
223
+ self.constraint_verifier.verify_that(task_fits_in_schedule)
224
+ .given(task, self.employee_alice, self.schedule_info)
225
+ .penalizes_by(1)
226
+ )
227
+
228
+ def test_task_fits_in_schedule_constraint_satisfied(self):
229
+ """Test that tasks fitting within schedule are not penalized."""
230
+ task = create_task(
231
+ task_id="task1",
232
+ description="Valid Task",
233
+ start_slot=0,
234
+ required_skill="Python",
235
+ employee=self.employee_alice,
236
+ )
237
+
238
+ (
239
+ self.constraint_verifier.verify_that(task_fits_in_schedule)
240
+ .given(task, self.employee_alice, self.schedule_info)
241
+ .penalizes_by(0)
242
+ )
243
+
244
+ def test_unavailable_employee_constraint_violation(self):
245
+ """Test that tasks assigned to unavailable employees are penalized."""
246
+ # Assuming 20 slots per day, tomorrow starts at slot 20
247
+ task = create_task(
248
+ task_id="task1",
249
+ description="Task on unavailable day",
250
+ start_slot=20, # Tomorrow (when Alice is unavailable)
251
+ required_skill="Python",
252
+ employee=self.employee_alice,
253
+ )
254
+
255
+ (
256
+ self.constraint_verifier.verify_that(unavailable_employee)
257
+ .given(task, self.employee_alice, self.schedule_info)
258
+ .penalizes_by(1)
259
+ )
260
+
261
+ def test_unavailable_employee_constraint_satisfied(self):
262
+ """Test that tasks assigned on available days are not penalized."""
263
+ task = create_task(
264
+ task_id="task1",
265
+ description="Task on available day",
266
+ start_slot=0, # Today (when Alice is available)
267
+ required_skill="Python",
268
+ employee=self.employee_alice,
269
+ )
270
+
271
+ (
272
+ self.constraint_verifier.verify_that(unavailable_employee)
273
+ .given(task, self.employee_alice, self.schedule_info)
274
+ .penalizes_by(0)
275
+ )
276
+
277
+ def test_maintain_project_task_order_constraint_violation(self):
278
+ """Test that tasks violating project sequence order are penalized."""
279
+ task1 = create_task(
280
+ task_id="task1",
281
+ description="Second Task",
282
+ start_slot=0, # Starts first but should come second
283
+ required_skill="Python",
284
+ project_id="project1",
285
+ sequence_number=2,
286
+ employee=self.employee_alice,
287
+ )
288
+
289
+ task2 = create_task(
290
+ task_id="task2",
291
+ description="First Task",
292
+ start_slot=2, # Starts during task1 but should come first
293
+ required_skill="Java",
294
+ project_id="project1",
295
+ sequence_number=1,
296
+ employee=self.employee_bob,
297
+ )
298
+
299
+ (
300
+ self.constraint_verifier.verify_that(maintain_project_task_order)
301
+ .given(
302
+ task1, task2, self.employee_alice, self.employee_bob, self.schedule_info
303
+ )
304
+ .penalizes_by(6)
305
+ )
306
+
307
+ def test_maintain_project_task_order_constraint_satisfied(self):
308
+ """Test that tasks maintaining correct project sequence are not penalized."""
309
+ task1 = create_task(
310
+ task_id="task1",
311
+ description="First Task",
312
+ start_slot=0, # Comes first and finishes before task2
313
+ required_skill="Python",
314
+ project_id="project1",
315
+ sequence_number=1,
316
+ employee=self.employee_alice,
317
+ )
318
+
319
+ task2 = create_task(
320
+ task_id="task2",
321
+ description="Second Task",
322
+ start_slot=5, # Starts after task1 finishes
323
+ required_skill="Java",
324
+ project_id="project1",
325
+ sequence_number=2,
326
+ employee=self.employee_bob,
327
+ )
328
+
329
+ (
330
+ self.constraint_verifier.verify_that(maintain_project_task_order)
331
+ .given(
332
+ task1, task2, self.employee_alice, self.employee_bob, self.schedule_info
333
+ )
334
+ .penalizes_by(0)
335
+ )
336
+
337
+ def test_maintain_project_task_order_different_projects(self):
338
+ """Test that tasks in different projects don't affect each other's sequence."""
339
+ task1 = create_task(
340
+ task_id="task1",
341
+ description="Task in Project A",
342
+ start_slot=0,
343
+ required_skill="Python",
344
+ project_id="projectA",
345
+ sequence_number=2,
346
+ employee=self.employee_alice,
347
+ )
348
+
349
+ task2 = create_task(
350
+ task_id="task2",
351
+ description="Task in Project B",
352
+ start_slot=2, # Overlaps but different project
353
+ required_skill="Java",
354
+ project_id="projectB",
355
+ sequence_number=1,
356
+ employee=self.employee_bob,
357
+ )
358
+
359
+ (
360
+ self.constraint_verifier.verify_that(maintain_project_task_order)
361
+ .given(
362
+ task1, task2, self.employee_alice, self.employee_bob, self.schedule_info
363
+ )
364
+ .penalizes_by(0)
365
+ )
366
+
367
+ # ==================== SOFT CONSTRAINT TESTS ====================
368
+
369
+ def test_undesired_day_for_employee_constraint_violation(self):
370
+ """Test that tasks on undesired days incur soft penalty."""
371
+ # Assuming 20 slots per day, day after tomorrow starts at slot 40
372
+ task = create_task(
373
+ task_id="task1",
374
+ description="Task on undesired day",
375
+ start_slot=40, # Day after tomorrow (Alice's undesired date)
376
+ required_skill="Python",
377
+ employee=self.employee_alice,
378
+ )
379
+
380
+ (
381
+ self.constraint_verifier.verify_that(undesired_day_for_employee)
382
+ .given(task, self.employee_alice, self.schedule_info)
383
+ .penalizes_by(1)
384
+ )
385
+
386
+ def test_undesired_day_for_employee_constraint_satisfied(self):
387
+ """Test that tasks on neutral days don't incur undesired day penalty."""
388
+ task = create_task(
389
+ task_id="task1",
390
+ description="Task on neutral day",
391
+ start_slot=0, # Today (neutral for Alice, though it's also desired)
392
+ required_skill="Python",
393
+ employee=self.employee_alice,
394
+ )
395
+
396
+ (
397
+ self.constraint_verifier.verify_that(undesired_day_for_employee)
398
+ .given(task, self.employee_alice, self.schedule_info)
399
+ .penalizes_by(0)
400
+ )
401
+
402
+ def test_desired_day_for_employee_constraint_reward(self):
403
+ """Test that tasks on desired days provide soft reward."""
404
+ task = create_task(
405
+ task_id="task1",
406
+ description="Task on desired day",
407
+ start_slot=0, # Today (Alice's desired date)
408
+ required_skill="Python",
409
+ employee=self.employee_alice,
410
+ )
411
+
412
+ (
413
+ self.constraint_verifier.verify_that(desired_day_for_employee)
414
+ .given(task, self.employee_alice, self.schedule_info)
415
+ .rewards()
416
+ )
417
+
418
+ def test_desired_day_for_employee_constraint_neutral(self):
419
+ """Test that tasks on neutral days don't provide desired day reward."""
420
+ task = create_task(
421
+ task_id="task1",
422
+ description="Task on neutral day",
423
+ start_slot=20, # Tomorrow (neutral for Alice)
424
+ required_skill="Python",
425
+ employee=self.employee_alice,
426
+ )
427
+
428
+ (
429
+ self.constraint_verifier.verify_that(desired_day_for_employee)
430
+ .given(task, self.employee_alice, self.schedule_info)
431
+ .justifies_with()
432
+ )
433
+
434
+ def test_balance_employee_task_assignments_constraint_balanced(self):
435
+ """Test that balanced task assignments don't incur penalty."""
436
+ # Create balanced task assignments (2 tasks each)
437
+ tasks = [
438
+ create_task(
439
+ "task1",
440
+ "Alice Task 1",
441
+ start_slot=0,
442
+ required_skill="Python",
443
+ employee=self.employee_alice,
444
+ ),
445
+ create_task(
446
+ "task2",
447
+ "Alice Task 2",
448
+ start_slot=5,
449
+ required_skill="Testing",
450
+ employee=self.employee_alice,
451
+ ),
452
+ create_task(
453
+ "task3",
454
+ "Bob Task 1",
455
+ start_slot=10,
456
+ required_skill="Java",
457
+ employee=self.employee_bob,
458
+ ),
459
+ create_task(
460
+ "task4",
461
+ "Bob Task 2",
462
+ start_slot=15,
463
+ required_skill="Documentation",
464
+ employee=self.employee_bob,
465
+ ),
466
+ ]
467
+
468
+ # Verify balanced assignment (both employees have 2 tasks)
469
+ (
470
+ self.constraint_verifier.verify_that(balance_employee_task_assignments)
471
+ .given(
472
+ *tasks,
473
+ self.employee_alice,
474
+ self.employee_bob,
475
+ self.schedule_info,
476
+ )
477
+ .penalizes_by(0)
478
+ )
479
+
480
+ def test_balance_employee_task_assignments_constraint_imbalanced(self):
481
+ """Test that imbalanced task assignments incur penalty."""
482
+ # Create imbalanced task assignments (Alice: 3 tasks, Bob: 0 tasks)
483
+ tasks = [
484
+ create_task(
485
+ "task1",
486
+ "Alice Task 1",
487
+ start_slot=0,
488
+ required_skill="Python",
489
+ employee=self.employee_alice,
490
+ ),
491
+ create_task(
492
+ "task2",
493
+ "Alice Task 2",
494
+ start_slot=5,
495
+ required_skill="Testing",
496
+ employee=self.employee_alice,
497
+ ),
498
+ create_task(
499
+ "task3",
500
+ "Alice Task 3",
501
+ start_slot=10,
502
+ required_skill="Java",
503
+ employee=self.employee_alice,
504
+ ),
505
+ ]
506
+
507
+ (
508
+ self.constraint_verifier.verify_that(balance_employee_task_assignments)
509
+ .given(
510
+ *tasks,
511
+ self.employee_alice,
512
+ self.employee_bob,
513
+ self.schedule_info,
514
+ )
515
+ .penalizes()
516
+ )
517
+
518
+ # ==================== INTEGRATION TESTS ====================
519
+
520
+ def test_all_constraints_together_feasible_solution(self):
521
+ """Test all constraints working together on a feasible solution."""
522
+ # Create a feasible mini schedule
523
+ tasks = [
524
+ create_task(
525
+ "task1",
526
+ "Valid Python Task",
527
+ start_slot=0, # Today (Alice's desired day)
528
+ required_skill="Python",
529
+ project_id="project1",
530
+ sequence_number=1,
531
+ employee=self.employee_alice,
532
+ ),
533
+ create_task(
534
+ "task2",
535
+ "Valid Java Task",
536
+ start_slot=5, # After task1, non-overlapping
537
+ required_skill="Java",
538
+ project_id="project1",
539
+ sequence_number=2,
540
+ employee=self.employee_alice,
541
+ ),
542
+ create_task(
543
+ "task3",
544
+ "Bob's Valid Task",
545
+ start_slot=10,
546
+ required_skill="Java",
547
+ project_id="project2",
548
+ sequence_number=1,
549
+ employee=self.employee_bob,
550
+ ),
551
+ ]
552
+
553
+ (
554
+ self.constraint_verifier.verify_that()
555
+ .given(
556
+ *tasks,
557
+ self.employee_alice,
558
+ self.employee_bob,
559
+ self.schedule_info,
560
+ )
561
+ .scores(HardSoftDecimalScore.of(Decimal("0"), Decimal("1.292893")))
562
+ )
563
+
564
+ def test_all_constraints_together_infeasible_solution(self):
565
+ """Test all constraints working together on an infeasible solution."""
566
+ # Create a mini schedule with multiple constraint violations
567
+ tasks = [
568
+ create_task(
569
+ "task1",
570
+ "Valid Python Task",
571
+ start_slot=0, # Today (Alice's desired day)
572
+ required_skill="Python",
573
+ project_id="project1",
574
+ sequence_number=1,
575
+ employee=self.employee_alice,
576
+ ),
577
+ create_task(
578
+ "task2",
579
+ "Invalid Skill Task",
580
+ start_slot=20, # Tomorrow (Alice unavailable)
581
+ required_skill="NonExistentSkill",
582
+ project_id="project1",
583
+ sequence_number=2,
584
+ employee=self.employee_alice,
585
+ ),
586
+ create_task(
587
+ "task3",
588
+ "Overlapping Task",
589
+ start_slot=2, # Overlaps with task1
590
+ required_skill="Testing",
591
+ project_id="project2",
592
+ sequence_number=1,
593
+ employee=self.employee_alice,
594
+ ),
595
+ ]
596
+
597
+ (
598
+ self.constraint_verifier.verify_that()
599
+ .given(
600
+ *tasks,
601
+ self.employee_alice,
602
+ self.employee_bob,
603
+ self.schedule_info,
604
+ )
605
+ .scores(HardSoftDecimalScore.of(Decimal("-4"), Decimal("-0.12132")))
606
+ )
607
+
608
+
609
+ # ==================== DATA GENERATOR FUNCTIONS ====================
610
+
611
+
612
+ def create_test_dates():
613
+ """Generate common test dates for consistent usage across tests."""
614
+ today = date.today()
615
+ return {
616
+ "today": today,
617
+ "tomorrow": today + timedelta(days=1),
618
+ "day_after": today + timedelta(days=2),
619
+ }
620
+
621
+
622
+ def create_employee(
623
+ name, skills=None, unavailable_dates=None, undesired_dates=None, desired_dates=None
624
+ ):
625
+ """Create an employee with specified attributes."""
626
+ return Employee(
627
+ name=name,
628
+ skills=skills or set(),
629
+ unavailable_dates=unavailable_dates or set(),
630
+ undesired_dates=undesired_dates or set(),
631
+ desired_dates=desired_dates or set(),
632
+ )
633
+
634
+
635
+ def create_task(
636
+ task_id,
637
+ description="Test Task",
638
+ duration_slots=4,
639
+ start_slot=0,
640
+ required_skill="Python",
641
+ project_id=None,
642
+ sequence_number=None,
643
+ employee=None,
644
+ ):
645
+ """Create a task with specified attributes."""
646
+ return Task(
647
+ id=task_id,
648
+ description=description,
649
+ duration_slots=duration_slots,
650
+ start_slot=start_slot,
651
+ required_skill=required_skill,
652
+ project_id=project_id,
653
+ sequence_number=sequence_number,
654
+ employee=employee,
655
+ )
656
+
657
+
658
+ def create_schedule_info(total_slots=60):
659
+ """Create a schedule info object with specified total slots."""
660
+ return ScheduleInfo(total_slots=total_slots)
661
+
662
+
663
+ def create_standard_employees(dates):
664
+ """Create the standard set of test employees used across multiple tests."""
665
+ return {
666
+ "alice": create_employee(
667
+ name="Alice",
668
+ skills={"Python", "Java", "Testing"},
669
+ unavailable_dates={dates["tomorrow"]},
670
+ undesired_dates={dates["day_after"]},
671
+ desired_dates={dates["today"]},
672
+ ),
673
+ "bob": create_employee(
674
+ name="Bob",
675
+ skills={"Java", "Documentation", "Management"},
676
+ ),
677
+ "charlie": create_employee(
678
+ name="Charlie",
679
+ skills={"Python", "Testing", "DevOps"},
680
+ ),
681
+ }