154 lines
5.7 KiB
Python
154 lines
5.7 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import copy
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from server.engines.pool_engine import PoolEngine
|
||
|
|
from server.state.seed import build_demo_world
|
||
|
|
|
||
|
|
BUSINESS_DATE = "2026-08-02"
|
||
|
|
|
||
|
|
|
||
|
|
def _next_id_factory():
|
||
|
|
counters: dict[str, int] = {}
|
||
|
|
|
||
|
|
def next_id(kind: str) -> int:
|
||
|
|
counters[kind] = counters.get(kind, 0) + 1
|
||
|
|
return counters[kind]
|
||
|
|
|
||
|
|
return next_id
|
||
|
|
|
||
|
|
|
||
|
|
def _single_order_world() -> tuple[dict, dict]:
|
||
|
|
world = build_demo_world()
|
||
|
|
order = next(item for item in world["flexOrders"] if item["productCode"] == "HV-HARNESS")
|
||
|
|
world["flexOrders"] = [copy.deepcopy(order)]
|
||
|
|
return world, world["flexOrders"][0]
|
||
|
|
|
||
|
|
|
||
|
|
def _solve(world: dict, *, enforce_teams: bool | None = None) -> dict:
|
||
|
|
return PoolEngine().solve(
|
||
|
|
world,
|
||
|
|
_next_id_factory(),
|
||
|
|
sort_mode="ASC",
|
||
|
|
start_date=BUSINESS_DATE,
|
||
|
|
enforce_teams=enforce_teams,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
("failure", "expected_conflict"),
|
||
|
|
[
|
||
|
|
("routing", "NO_ROUTING"),
|
||
|
|
("capability", "NO_CAPABILITY"),
|
||
|
|
("team", "NO_TEAM"),
|
||
|
|
("mold", "NO_MOLD"),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_failed_order_never_leaves_partial_virtual_line_or_work_orders(
|
||
|
|
failure: str,
|
||
|
|
expected_conflict: str,
|
||
|
|
):
|
||
|
|
world, order = _single_order_world()
|
||
|
|
route = sorted(
|
||
|
|
[row for row in world["flexRoutings"] if row["productCode"] == order["productCode"]],
|
||
|
|
key=lambda row: row["seq"],
|
||
|
|
)
|
||
|
|
enforce_teams = None
|
||
|
|
if failure == "routing":
|
||
|
|
world["flexRoutings"] = [
|
||
|
|
row for row in world["flexRoutings"] if row["productCode"] != order["productCode"]
|
||
|
|
]
|
||
|
|
elif failure == "capability":
|
||
|
|
final_operation = route[-1]["operationCode"]
|
||
|
|
for equipment in world["flexEquipment"]:
|
||
|
|
equipment["capabilities"] = [
|
||
|
|
code for code in equipment.get("capabilities", []) if code != final_operation
|
||
|
|
]
|
||
|
|
elif failure == "team":
|
||
|
|
final_operation = route[-1]["operationCode"]
|
||
|
|
world["flexTeams"] = [{
|
||
|
|
"code": "TEAM-PARTIAL",
|
||
|
|
"name": "Partial team",
|
||
|
|
"memberCount": 20,
|
||
|
|
"supportOps": [row["operationCode"] for row in route if row["operationCode"] != final_operation],
|
||
|
|
}]
|
||
|
|
enforce_teams = True
|
||
|
|
elif failure == "mold":
|
||
|
|
world["flexMolds"] = []
|
||
|
|
|
||
|
|
molds_before = copy.deepcopy(world["flexMolds"])
|
||
|
|
result = _solve(world, enforce_teams=enforce_teams)
|
||
|
|
version_id = result["versionId"]
|
||
|
|
version = next(item for item in world["flexScheduleVersions"] if item["id"] == version_id)
|
||
|
|
work_orders = [item for item in world["flexWorkOrders"] if item["versionId"] == version_id]
|
||
|
|
virtual_lines = [item for item in world["flexVirtualLines"] if item["versionId"] == version_id]
|
||
|
|
conflict_types = {
|
||
|
|
item["conflictType"]
|
||
|
|
for item in world["flexConflicts"]
|
||
|
|
if item["versionId"] == version_id
|
||
|
|
}
|
||
|
|
|
||
|
|
assert result["orderCount"] == 1
|
||
|
|
assert result["vlCount"] == version["vlCount"] == len(virtual_lines) == 0
|
||
|
|
assert result["woCount"] == version["woCount"] == len(work_orders) == 0
|
||
|
|
assert expected_conflict in conflict_types
|
||
|
|
assert world["flexMolds"] == molds_before
|
||
|
|
|
||
|
|
|
||
|
|
def test_successful_order_commits_one_virtual_line_and_exact_routing_work_orders():
|
||
|
|
world, order = _single_order_world()
|
||
|
|
expected_route = sorted(
|
||
|
|
[
|
||
|
|
(row["seq"], row["operationCode"])
|
||
|
|
for row in world["flexRoutings"]
|
||
|
|
if row["productCode"] == order["productCode"]
|
||
|
|
]
|
||
|
|
)
|
||
|
|
|
||
|
|
result = _solve(world)
|
||
|
|
version_id = result["versionId"]
|
||
|
|
version = next(item for item in world["flexScheduleVersions"] if item["id"] == version_id)
|
||
|
|
virtual_lines = [item for item in world["flexVirtualLines"] if item["versionId"] == version_id]
|
||
|
|
work_orders = [item for item in world["flexWorkOrders"] if item["versionId"] == version_id]
|
||
|
|
|
||
|
|
assert result["vlCount"] == version["vlCount"] == len(virtual_lines) == 1
|
||
|
|
assert result["woCount"] == version["woCount"] == len(work_orders) == len(expected_route)
|
||
|
|
assert len({item["id"] for item in work_orders}) == len(work_orders)
|
||
|
|
assert {item["vlId"] for item in work_orders} == {virtual_lines[0]["id"]}
|
||
|
|
assert sorted((item["seq"], item["operationCode"]) for item in work_orders) == expected_route
|
||
|
|
assert sorted(
|
||
|
|
(item["seq"], item["operationCode"])
|
||
|
|
for item in virtual_lines[0]["assignments"]
|
||
|
|
) == expected_route
|
||
|
|
|
||
|
|
|
||
|
|
def test_failed_candidate_releases_resources_before_next_order_is_scheduled():
|
||
|
|
world = build_demo_world()
|
||
|
|
failed = copy.deepcopy(next(item for item in world["flexOrders"] if item["productCode"] == "HV-HARNESS"))
|
||
|
|
successful = copy.deepcopy(next(item for item in world["flexOrders"] if item["productCode"] == "PDU-UNIT"))
|
||
|
|
failed["dueDate"] = "2026-08-03"
|
||
|
|
successful["dueDate"] = "2026-08-04"
|
||
|
|
world["flexOrders"] = [failed, successful]
|
||
|
|
world["flexRoutings"].append({
|
||
|
|
"productCode": failed["productCode"],
|
||
|
|
"seq": 99,
|
||
|
|
"operationCode": "OP-NO-CAPABILITY",
|
||
|
|
"stdTimePerUnit": 1,
|
||
|
|
"requireMold": False,
|
||
|
|
})
|
||
|
|
mold_before = copy.deepcopy(world["flexMolds"])
|
||
|
|
|
||
|
|
result = _solve(world)
|
||
|
|
version_id = result["versionId"]
|
||
|
|
virtual_lines = [item for item in world["flexVirtualLines"] if item["versionId"] == version_id]
|
||
|
|
work_orders = [item for item in world["flexWorkOrders"] if item["versionId"] == version_id]
|
||
|
|
|
||
|
|
assert result["orderCount"] == 2
|
||
|
|
assert result["vlCount"] == 1
|
||
|
|
assert {item["orderNo"] for item in virtual_lines} == {successful["orderNo"]}
|
||
|
|
assert all(item["flexOrderNo"] == successful["orderNo"] for item in work_orders)
|
||
|
|
assert min(item["plannedStartTime"] for item in work_orders) == "2026-08-03 08:00"
|
||
|
|
assert world["flexMolds"] == mold_before
|