|
|
""" |
|
|
Integration tests for the employee scheduling solver. |
|
|
|
|
|
Tests that the solver can find feasible solutions for demo data |
|
|
and that the REST API works correctly. |
|
|
""" |
|
|
from employee_scheduling.rest_api import app |
|
|
from employee_scheduling.domain import EmployeeScheduleModel |
|
|
from employee_scheduling.converters import model_to_schedule |
|
|
|
|
|
from fastapi.testclient import TestClient |
|
|
from time import sleep |
|
|
from pytest import fail |
|
|
import pytest |
|
|
|
|
|
client = TestClient(app) |
|
|
|
|
|
|
|
|
@pytest.mark.timeout(120) |
|
|
def test_feasible(): |
|
|
"""Test that the solver can find a feasible solution for SMALL demo data.""" |
|
|
demo_data_response = client.get("/demo-data/SMALL") |
|
|
assert demo_data_response.status_code == 200 |
|
|
|
|
|
job_id_response = client.post("/schedules", json=demo_data_response.json()) |
|
|
assert job_id_response.status_code == 200 |
|
|
job_id = job_id_response.text[1:-1] |
|
|
|
|
|
ATTEMPTS = 1_000 |
|
|
best_score = None |
|
|
for _ in range(ATTEMPTS): |
|
|
sleep(0.1) |
|
|
schedule_response = client.get(f"/schedules/{job_id}") |
|
|
schedule_json = schedule_response.json() |
|
|
schedule_model = EmployeeScheduleModel.model_validate(schedule_json) |
|
|
schedule = model_to_schedule(schedule_model) |
|
|
if schedule.score is not None: |
|
|
best_score = schedule.score |
|
|
if schedule.score.is_feasible: |
|
|
stop_solving_response = client.delete(f"/schedules/{job_id}") |
|
|
assert stop_solving_response.status_code == 200 |
|
|
return |
|
|
|
|
|
client.delete(f"/schedules/{job_id}") |
|
|
fail(f"Solution is not feasible after 100 seconds. Best score: {best_score}") |
|
|
|
|
|
|
|
|
def test_demo_data_list(): |
|
|
"""Test that demo data list endpoint returns available datasets.""" |
|
|
response = client.get("/demo-data") |
|
|
assert response.status_code == 200 |
|
|
data = response.json() |
|
|
assert isinstance(data, list) |
|
|
assert len(data) > 0 |
|
|
assert "SMALL" in data |
|
|
|
|
|
|
|
|
def test_demo_data_small_structure(): |
|
|
"""Test that SMALL demo data has expected structure.""" |
|
|
response = client.get("/demo-data/SMALL") |
|
|
assert response.status_code == 200 |
|
|
data = response.json() |
|
|
|
|
|
|
|
|
assert "employees" in data |
|
|
assert "shifts" in data |
|
|
|
|
|
|
|
|
assert len(data["employees"]) > 0 |
|
|
for employee in data["employees"]: |
|
|
assert "name" in employee |
|
|
|
|
|
|
|
|
assert len(data["shifts"]) > 0 |
|
|
for shift in data["shifts"]: |
|
|
assert "id" in shift |
|
|
assert "start" in shift |
|
|
assert "end" in shift |
|
|
assert "location" in shift |
|
|
assert "requiredSkill" in shift |
|
|
|
|
|
|
|
|
def test_solver_start_and_stop(): |
|
|
"""Test that solver can be started and stopped.""" |
|
|
demo_data_response = client.get("/demo-data/SMALL") |
|
|
assert demo_data_response.status_code == 200 |
|
|
|
|
|
|
|
|
start_response = client.post("/schedules", json=demo_data_response.json()) |
|
|
assert start_response.status_code == 200 |
|
|
job_id = start_response.text[1:-1] |
|
|
|
|
|
|
|
|
sleep(0.5) |
|
|
|
|
|
|
|
|
status_response = client.get(f"/schedules/{job_id}") |
|
|
assert status_response.status_code == 200 |
|
|
schedule = status_response.json() |
|
|
assert "solverStatus" in schedule |
|
|
|
|
|
|
|
|
stop_response = client.delete(f"/schedules/{job_id}") |
|
|
assert stop_response.status_code == 200 |
|
|
|
|
|
|
|
|
def test_solver_assigns_employees(): |
|
|
"""Test that solver actually assigns employees to shifts.""" |
|
|
demo_data_response = client.get("/demo-data/SMALL") |
|
|
assert demo_data_response.status_code == 200 |
|
|
|
|
|
job_id_response = client.post("/schedules", json=demo_data_response.json()) |
|
|
assert job_id_response.status_code == 200 |
|
|
job_id = job_id_response.text[1:-1] |
|
|
|
|
|
|
|
|
sleep(2) |
|
|
|
|
|
schedule_response = client.get(f"/schedules/{job_id}") |
|
|
schedule_json = schedule_response.json() |
|
|
|
|
|
|
|
|
assigned_shifts = [s for s in schedule_json["shifts"] if s.get("employee") is not None] |
|
|
assert len(assigned_shifts) > 0, "Solver should assign some employees to shifts" |
|
|
|
|
|
client.delete(f"/schedules/{job_id}") |
|
|
|
|
|
|
|
|
def test_score_analysis(): |
|
|
"""Test that score analysis endpoint returns constraint analysis.""" |
|
|
demo_data_response = client.get("/demo-data/SMALL") |
|
|
assert demo_data_response.status_code == 200 |
|
|
|
|
|
|
|
|
job_id_response = client.post("/schedules", json=demo_data_response.json()) |
|
|
assert job_id_response.status_code == 200 |
|
|
job_id = job_id_response.text[1:-1] |
|
|
|
|
|
|
|
|
sleep(2) |
|
|
|
|
|
schedule_response = client.get(f"/schedules/{job_id}") |
|
|
schedule_json = schedule_response.json() |
|
|
|
|
|
|
|
|
client.delete(f"/schedules/{job_id}") |
|
|
|
|
|
|
|
|
analyze_response = client.put("/schedules/analyze", json=schedule_json) |
|
|
assert analyze_response.status_code == 200 |
|
|
analysis = analyze_response.json() |
|
|
|
|
|
|
|
|
assert "constraints" in analysis |
|
|
assert isinstance(analysis["constraints"], list) |
|
|
assert len(analysis["constraints"]) > 0 |
|
|
|
|
|
|
|
|
for constraint in analysis["constraints"]: |
|
|
assert "name" in constraint |
|
|
assert "weight" in constraint |
|
|
assert "score" in constraint |
|
|
assert "matches" in constraint |
|
|
assert isinstance(constraint["matches"], list) |
|
|
|