Files
scheduler/backend/test_bundle.py
T

295 lines
12 KiB
Python
Raw Normal View History

2026-07-12 14:19:58 -04:00
from __future__ import annotations
from dataclasses import dataclass
import os
from test_config import build_bundle_test_configs
2026-07-23 14:02:14 -04:00
@dataclass(frozen=False)
2026-07-12 14:19:58 -04:00
class Test:
test_id: str
device: str
test_type: str
rotation: str | None
rx_tx: str | None
has_coe_pair: bool
coe_pairing: list[str]
config: dict[str, dict[str, str | None]]
throttled: bool
estimated_minutes: int
2026-07-23 14:02:14 -04:00
status: str
2026-07-12 14:19:58 -04:00
# Bundle priority tiers for scheduling order (lower number = higher priority)
2026-07-23 14:02:14 -04:00
BUNDLE_PRIORITY_DUT_COMPLETED_MIRROR = 0 # REF-side tests whose DUT counterpart is already completed
BUNDLE_PRIORITY_USER_TOP = 1 # User-specified top priority tests
BUNDLE_PRIORITY_RERUN = 2 # Rerun tests (DUT or REF)
BUNDLE_PRIORITY_MIRROR = 3
BUNDLE_PRIORITY_P2P_WITH_COE = 4 # P2P tests with COE pairs
BUNDLE_PRIORITY_P2P_ONLY = 5 # P2P tests without COE pairs (RX/TX bundled)
BUNDLE_PRIORITY_COE_ONLY = 6 # COE tests without P2P pairing (should be rare/unschedulable)
BUNDLE_PRIORITY_P3P = 6 # P3P tests
2026-07-12 14:19:58 -04:00
DUT = (os.getenv("DUT") or "DUT").strip()
REF = (os.getenv("REF") or "REF").strip()
2026-07-23 14:02:14 -04:00
@dataclass(frozen=False)
2026-07-12 14:19:58 -04:00
class TestBundle:
index: int
tests: list[str]
total_minutes: int
device: str
config: tuple[str, ...]
priority: int
2026-07-23 14:02:14 -04:00
completed: int
2026-07-12 14:19:58 -04:00
def build_test_bundles(active_dut: dict[str, Test], active_ref: dict[str, Test], top_priority_tests: set[tuple[str, str]]) -> list[TestBundle]:
"""Build deterministic bundles for DP scheduling."""
processed_dut: set[str] = set()
processed_ref: set[str] = set()
test_bundles: list[TestBundle] = []
bundle_index = 0
def dedupe_preserve_order(test_ids: list[str]) -> list[str]:
seen: set[str] = set()
deduped: list[str] = []
for test_id in test_ids:
if test_id in seen:
continue
seen.add(test_id)
deduped.append(test_id)
return deduped
# Phase 1: DUT P2P bundles
for dut_test_id, dut_test in active_dut.items():
if dut_test_id in processed_dut or dut_test.test_type != "P2P":
continue
dut_bundled_tests = [dut_test_id]
processed_dut.add(dut_test_id)
active_coe_pairings = [
test_id
for test_id in dut_test.coe_pairing
if test_id in active_dut and test_id not in processed_dut
]
if active_coe_pairings:
dut_bundled_tests.extend(active_coe_pairings)
processed_dut.update(active_coe_pairings)
priority = BUNDLE_PRIORITY_P2P_WITH_COE
else:
rx_tx_pair_id = get_rx_tx_pair_id(dut_test, DUT, active_dut, active_ref)
if rx_tx_pair_id and rx_tx_pair_id not in processed_dut:
dut_bundled_tests.append(rx_tx_pair_id)
processed_dut.add(rx_tx_pair_id)
priority = BUNDLE_PRIORITY_P2P_ONLY
dut_bundled_tests = dedupe_preserve_order(dut_bundled_tests)
2026-07-23 14:02:14 -04:00
# Check if any of the DUT tests in this bundle are in the top priority list
if any((DUT, dut_test_id) in top_priority_tests for dut_test_id in dut_bundled_tests):
priority = BUNDLE_PRIORITY_USER_TOP
# Check if any of the DUT tests in this bundle are reruns
2026-07-24 15:43:02 -04:00
elif any((dut_test.status == "rerun" for dut_test_id in dut_bundled_tests if (dut_test := active_dut.get(dut_test_id)))):
2026-07-23 14:02:14 -04:00
priority = BUNDLE_PRIORITY_RERUN
2026-07-12 14:19:58 -04:00
test_bundles.append(
create_bundle(dut_bundled_tests, DUT, priority, bundle_index, active_dut, active_ref)
)
ref_bundled_tests = [
test_id
for test_id in dut_bundled_tests
if test_id in active_ref and test_id not in processed_ref
]
if ref_bundled_tests:
ref_bundled_tests = dedupe_preserve_order(ref_bundled_tests)
processed_ref.update(ref_bundled_tests)
2026-07-23 14:02:14 -04:00
# Check if any of the REF tests in this bundle are in the top priority list
if any((REF, ref_test_id) in top_priority_tests for ref_test_id in ref_bundled_tests):
priority = BUNDLE_PRIORITY_USER_TOP
# Check if any of the REF tests in this bundle are reruns
2026-07-24 15:43:02 -04:00
elif any((ref_test.status == "rerun" for ref_test_id in ref_bundled_tests if (ref_test := active_ref.get(ref_test_id)))):
2026-07-23 14:02:14 -04:00
priority = BUNDLE_PRIORITY_RERUN
2026-07-12 14:19:58 -04:00
test_bundles.append(
create_bundle(ref_bundled_tests, REF, priority, bundle_index, active_dut, active_ref)
)
bundle_index += 1
# Phase 2: DUT P3P bundles
for dut_test_id, dut_test in active_dut.items():
if dut_test_id in processed_dut or dut_test.test_type != "P3P":
continue
dut_bundled_tests = [dut_test_id]
processed_dut.add(dut_test_id)
th_ut_pair_id = get_th_ut_pair_id(dut_test, DUT, active_dut, active_ref)
if th_ut_pair_id and th_ut_pair_id not in processed_dut:
dut_bundled_tests.append(th_ut_pair_id)
processed_dut.add(th_ut_pair_id)
dut_bundled_tests = dedupe_preserve_order(dut_bundled_tests)
2026-07-23 14:02:14 -04:00
priority = BUNDLE_PRIORITY_P3P
# Check if any of the DUT tests in this bundle are in the top priority list
if any((DUT, dut_test_id) in top_priority_tests for dut_test_id in dut_bundled_tests):
priority = BUNDLE_PRIORITY_USER_TOP
# Check if any of the DUT tests in this bundle are reruns
2026-07-24 15:43:02 -04:00
elif any((dut_test.status == "rerun" for dut_test_id in dut_bundled_tests if (dut_test := active_dut.get(dut_test_id)))):
2026-07-23 14:02:14 -04:00
priority = BUNDLE_PRIORITY_RERUN
2026-07-12 14:19:58 -04:00
test_bundles.append(
2026-07-23 14:02:14 -04:00
create_bundle(dut_bundled_tests, DUT, priority, bundle_index, active_dut, active_ref)
2026-07-12 14:19:58 -04:00
)
ref_bundled_tests = [
test_id
for test_id in dut_bundled_tests
if test_id in active_ref and test_id not in processed_ref
]
2026-07-23 14:02:14 -04:00
# Check if any of the REF tests in this bundle are in the top priority list
if any((REF, ref_test_id) in top_priority_tests for ref_test_id in ref_bundled_tests):
priority = BUNDLE_PRIORITY_USER_TOP
# Check if any of the REF tests in this bundle are reruns
2026-07-24 15:43:02 -04:00
elif any((ref_test.status == "rerun" for ref_test_id in ref_bundled_tests if (ref_test := active_ref.get(ref_test_id)))):
2026-07-23 14:02:14 -04:00
priority = BUNDLE_PRIORITY_RERUN
2026-07-12 14:19:58 -04:00
if ref_bundled_tests:
ref_bundled_tests = dedupe_preserve_order(ref_bundled_tests)
processed_ref.update(ref_bundled_tests)
test_bundles.append(
2026-07-23 14:02:14 -04:00
create_bundle(ref_bundled_tests, REF, priority, bundle_index, active_dut, active_ref)
2026-07-12 14:19:58 -04:00
)
bundle_index += 1
2026-07-23 14:02:14 -04:00
# Phase 3: DUT leftovers (COE-only)
2026-07-12 14:19:58 -04:00
for dut_test_id in active_dut:
if dut_test_id in processed_dut:
continue
2026-07-23 14:02:14 -04:00
print(f"Orphan COE-only DUT test found: {dut_test_id}")
2026-07-12 14:19:58 -04:00
test_bundles.append(
create_bundle([dut_test_id], DUT, BUNDLE_PRIORITY_COE_ONLY, bundle_index, active_dut, active_ref)
)
bundle_index += 1
processed_dut.add(dut_test_id)
2026-07-23 14:02:14 -04:00
# Phase 4: unmatched REF bundles, meaning DUT is completed
2026-07-12 14:19:58 -04:00
for ref_test_id, ref_test in active_ref.items():
2026-07-23 14:02:14 -04:00
if ref_test_id in processed_ref:
2026-07-12 14:19:58 -04:00
continue
ref_bundled_tests = [ref_test_id]
processed_ref.add(ref_test_id)
2026-07-23 14:02:14 -04:00
if ref_test.test_type == "P2P":
active_coe_pairings = [
test_id
for test_id in ref_test.coe_pairing
if test_id in active_ref and test_id not in processed_ref
]
if active_coe_pairings:
ref_bundled_tests.extend(active_coe_pairings)
processed_ref.update(active_coe_pairings)
priority = BUNDLE_PRIORITY_DUT_COMPLETED_MIRROR
else:
rx_tx_pair_id = get_rx_tx_pair_id(ref_test, REF, active_dut, active_ref)
if rx_tx_pair_id and rx_tx_pair_id not in processed_ref:
ref_bundled_tests.append(rx_tx_pair_id)
processed_ref.add(rx_tx_pair_id)
priority = BUNDLE_PRIORITY_DUT_COMPLETED_MIRROR
if ref_test.test_type == "P3P":
th_ut_pair_id = get_th_ut_pair_id(ref_test, REF, active_dut, active_ref)
if th_ut_pair_id and th_ut_pair_id not in processed_ref:
ref_bundled_tests.append(th_ut_pair_id)
processed_ref.add(th_ut_pair_id)
priority = BUNDLE_PRIORITY_DUT_COMPLETED_MIRROR
2026-07-12 14:19:58 -04:00
ref_bundled_tests = dedupe_preserve_order(ref_bundled_tests)
test_bundles.append(
create_bundle(ref_bundled_tests, REF, priority, bundle_index, active_dut, active_ref)
)
bundle_index += 1
2026-07-23 14:02:14 -04:00
# Phase 5: REF leftovers ( COE-only)
2026-07-12 14:19:58 -04:00
for ref_test_id in active_ref:
if ref_test_id in processed_ref:
continue
2026-07-23 14:02:14 -04:00
print(f"Orphan COE-only REF test found: {ref_test_id}")
2026-07-12 14:19:58 -04:00
test_bundles.append(
2026-07-23 14:02:14 -04:00
create_bundle([ref_test_id], REF, BUNDLE_PRIORITY_DUT_COMPLETED_MIRROR, bundle_index, active_dut, active_ref)
2026-07-12 14:19:58 -04:00
)
bundle_index += 1
processed_ref.add(ref_test_id)
test_bundles.sort(key=lambda b: (b.priority, b.index, b.device, b.config))
return test_bundles
def get_rx_tx_pair_id(test: Test, device: str, active_dut: dict[str, Test], active_ref: dict[str, Test]) -> str | None:
active_tests = active_dut if device == DUT else active_ref
test_id = test.test_id
pair_id = test_id.replace("RX", "TX", 1) if "RX" in test_id else test_id.replace("TX", "RX", 1)
return pair_id if pair_id in active_tests else None
def get_th_ut_pair_id(test: Test, device: str, active_dut: dict[str, Test], active_ref: dict[str, Test]) -> str | None:
active_tests = active_dut if device == DUT else active_ref
test_id = test.test_id
pair_id = test_id.replace("TH", "UT", 1) if "TH" in test_id else test_id.replace("UT", "TH", 1)
return pair_id if pair_id in active_tests else None
def create_bundle(tests: list[str], device: str, priority: int, index: int, active_dut: dict[str, Test], active_ref: dict[str, Test]) -> TestBundle:
if not tests:
raise ValueError("Cannot create bundle with no tests")
if device == DUT:
active_tests = active_dut
else:
active_tests = active_ref
total_minutes = sum(active_tests[test_id].estimated_minutes for test_id in tests)
bundle_tests = [active_tests[test_id] for test_id in tests]
config = tuple(build_bundle_test_configs(bundle_tests))
return TestBundle(
index=index,
tests=tests,
total_minutes=total_minutes,
device=device,
config=config,
priority=priority,
2026-07-23 14:02:14 -04:00
completed=0
2026-07-12 14:19:58 -04:00
)
def bundle_pair_lookup(bundles: list[TestBundle], dut_bundles: list[TestBundle], ref_bundles: list[TestBundle]) -> list[TestBundle]:
device = bundles[0].device if bundles else None
active_bundle_ids = {b.index for b in dut_bundles} if device == REF else {b.index for b in ref_bundles}
bundle_indexes = [b.index for b in bundles if b.index in active_bundle_ids]
if device == DUT:
return [b for b in ref_bundles if b.index in bundle_indexes]
elif device == REF:
return [b for b in dut_bundles if b.index in bundle_indexes]
2026-07-23 14:02:14 -04:00
return []
def update_mirrored_bundle_priorities(knapsack_bundles: list[TestBundle], dut_pending: list[TestBundle], ref_pending: list[TestBundle]) -> tuple[list[TestBundle], list[TestBundle]]:
bundle_pairs = bundle_pair_lookup(knapsack_bundles, dut_pending, ref_pending)
for bundle in dut_pending:
if bundle.index in {b.index for b in bundle_pairs}:
bundle.priority = BUNDLE_PRIORITY_MIRROR
for bundle in ref_pending:
if bundle.index in {b.index for b in bundle_pairs}:
bundle.priority = BUNDLE_PRIORITY_MIRROR
return dut_pending, ref_pending