fixed compatibility issue

This commit is contained in:
2026-06-23 12:07:43 -04:00
parent e51a6777fc
commit 21402f7ee3
5 changed files with 80 additions and 64 deletions
+15 -3
View File
@@ -6,6 +6,7 @@ from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterator
from graph import build_station_testpoint_map
APP_ROOT = Path(__file__).resolve().parent
@@ -36,6 +37,7 @@ class TestRecord:
status: str = "pending"
excluded: bool = False
raw_payload: dict[str, Any] | None = None
station_testpoint_map: str | None = None
@dataclass(frozen=True)
@@ -141,6 +143,7 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
_ensure_column(conn, "tests", "victim_band", "TEXT")
_ensure_column(conn, "tests", "excluded", "INTEGER NOT NULL DEFAULT 0")
_ensure_column(conn, "tests", "throttled", "INTEGER NOT NULL DEFAULT 0")
_ensure_column(conn, "tests", "station_testpoint_map", "TEXT")
# Seed runtime defaults for schedule estimation.
conn.execute(
@@ -154,6 +157,12 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
)
def _serialize_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> str:
"""Compute and serialize the station-to-testpoint map from a test config."""
station_map = build_station_testpoint_map(config)
return json.dumps(station_map)
def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> int:
if not records:
return 0
@@ -163,9 +172,9 @@ def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> in
"""
INSERT INTO tests(
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
coe_pairing_json, config_json, priority, victim_band, throttled, estimated_minutes, status, excluded, raw_payload
coe_pairing_json, config_json, priority, victim_band, throttled, estimated_minutes, status, excluded, raw_payload, station_testpoint_map
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(test_id, device) DO UPDATE SET
test_type = excluded.test_type,
device = excluded.device,
@@ -181,6 +190,7 @@ def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> in
status = excluded.status,
excluded = excluded.excluded,
raw_payload = excluded.raw_payload,
station_testpoint_map = excluded.station_testpoint_map,
updated_at = CURRENT_TIMESTAMP
""",
[
@@ -200,6 +210,7 @@ def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> in
r.status,
int(r.excluded),
json.dumps(r.raw_payload or {}),
_serialize_station_testpoint_map(r.config),
)
for r in records
],
@@ -403,7 +414,7 @@ def list_tests_for_device(device: str, db_path: str | Path = DB_PATH) -> list[Te
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
config_json, victim_band,
coe_pairing_json, priority, throttled, estimated_minutes,
excluded, status, raw_payload
excluded, status, raw_payload, station_testpoint_map
FROM tests
WHERE device = ?
""",
@@ -429,6 +440,7 @@ def list_tests_for_device(device: str, db_path: str | Path = DB_PATH) -> list[Te
status=row["status"],
excluded=bool(row["excluded"]),
raw_payload=json.loads(row["raw_payload"] or "{}"),
station_testpoint_map=row["station_testpoint_map"],
)
)
+42 -57
View File
@@ -1,6 +1,8 @@
from __future__ import annotations
import json
from typing import Any
from pathlib import Path
# Immutable-in-practice graph state for the loaded DUT test set.
_TESTS_BY_ID: dict[str, Any] = {}
@@ -13,6 +15,19 @@ def reset_graph_state() -> None:
_GRAPH = {}
def _get_station_map(test: Any) -> dict[str, str]:
"""Get station testpoint map from test record or compute it from config."""
# Try to get from serialized map first (cached from DB)
if hasattr(test, 'station_testpoint_map') and test.station_testpoint_map:
try:
return json.loads(test.station_testpoint_map)
except (json.JSONDecodeError, TypeError):
pass
# Safeguard: return empty map
return {}
def is_graph_built() -> bool:
return bool(_GRAPH)
@@ -45,56 +60,49 @@ def _build_graph(tests: dict[str, Any]) -> dict[str, set[str]]:
if _compatible(a, b):
graph[a_id].add(b_id)
graph[b_id].add(a_id)
# Print out graph for debugging
out = Path('output1.txt')
with out.open('w', encoding='utf-8') as f:
f.write(json.dumps({k: list(v) for k, v in graph.items()}, indent=2))
return graph
def _compatible(a: Any, b: Any) -> bool:
# Hard conflict: same testpoint but different STA assignment means they cannot share a window.
if _has_testpoint_sta_conflict(a.config, b.config):
return False
# Compatible when same rotation, or both non-P3P and overlap has identical testpoints.
if a.rotation == b.rotation:
return True
if a.test_type != "P3P" and b.test_type != "P3P" and _same_test_points_for_overlap(a.config, b.config):
station_map_a = _get_station_map(a)
station_map_b = _get_station_map(b)
if _same_station_to_testpoint(station_map_a, station_map_b, a, b) and _same_testpoint_to_station(station_map_a, station_map_b, a, b):
return True
return False
def _has_testpoint_sta_conflict(
config_a: dict[str, dict[str, str | None]],
config_b: dict[str, dict[str, str | None]],
def _same_station_to_testpoint(
station_map_a: dict[str, str],
station_map_b: dict[str, str], test_a: Any, test_b: Any
) -> bool:
testpoint_to_sta_a = _build_testpoint_sta_map(config_a)
testpoint_to_sta_b = _build_testpoint_sta_map(config_b)
overlap = set(testpoint_to_sta_a.keys()) & set(testpoint_to_sta_b.keys())
for testpoint in overlap:
if testpoint_to_sta_a[testpoint] != testpoint_to_sta_b[testpoint]:
return True
return False
def _same_test_points_for_overlap(
config_a: dict[str, dict[str, str | None]],
config_b: dict[str, dict[str, str | None]],
) -> bool:
station_map_a = _build_station_testpoint_map(config_a)
station_map_b = _build_station_testpoint_map(config_b)
overlap = set(station_map_a.keys()) & set(station_map_b.keys())
for station in overlap:
overlap_stations = set(station_map_a.keys()) & set(station_map_b.keys())
for station in overlap_stations:
if station_map_a[station] != station_map_b[station]:
return False
return True
def _same_testpoint_to_station(
station_map_a: dict[str, str],
station_map_b: dict[str, str], test_a: Any, test_b: Any
) -> bool:
overlap_testpoints = set(station_map_a.values()) & set(station_map_b.values())
for testpoint in overlap_testpoints:
stations_a = {s for s, t in station_map_a.items() if t == testpoint}
stations_b = {s for s, t in station_map_b.items() if t == testpoint}
if stations_a != stations_b:
print(f"Tests {test_a.test_id} and {test_b.test_id} have conflicting stations for testpoint. {station_map_a} vs {station_map_b}")
return False
return True
def _build_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> dict[str, str]:
def build_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> dict[str, str]:
station_to_testpoint: dict[str, str] = {}
def _add_entry(entry: dict[str, str | None]) -> None:
testpoint = _norm(entry.get("test_point") or entry.get("Testpoint"))
sta_raw = _norm(entry.get("sta") or entry.get("STA"))
testpoint = _norm(entry.get("test_point"))
sta_raw = _norm(entry.get("sta"))
if not testpoint or not sta_raw:
return
for sta in sta_raw.split(","):
@@ -113,29 +121,6 @@ def _build_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> di
return station_to_testpoint
def _build_testpoint_sta_map(config: dict[str, dict[str, str | None]]) -> dict[str, set[str]]:
testpoint_to_sta: dict[str, set[str]] = {}
def _add_entry(entry: dict[str, str | None]) -> None:
testpoint = _norm(entry.get("test_point") or entry.get("Testpoint"))
sta_raw = _norm(entry.get("sta") or entry.get("STA"))
if not testpoint or not sta_raw:
return
for sta in sta_raw.split(","):
sta_clean = _norm(sta)
if sta_clean:
testpoint_to_sta.setdefault(testpoint, set()).add(sta_clean)
for band in ("5G", "6G", "2G"):
entry = config.get(band) or {}
_add_entry(entry)
for station_key in ("Station 1", "Station 2", "Station 3"):
entry = config.get(station_key) or {}
_add_entry(entry)
return testpoint_to_sta
def _norm(value: Any) -> str:
if value is None:
+1 -1
View File
@@ -314,7 +314,7 @@ def _fit_bundles_to_shifts(
state = shift_state[date_shift]
state["remaining_minutes"] += prev_remaining # Add back any leftover from previous shift
if state['remaining_minutes'] >= test.estimated_minutes:
print(f"remaining minutes for {date_shift}: {state['remaining_minutes']} - placing {key} ({test.estimated_minutes}m)")
#print(f"remaining minutes for {date_shift}: {state['remaining_minutes']} - placing {key} ({test.estimated_minutes}m)")
entries.append(
ScheduleEntry(
test_id=test.test_id,
+1 -1
View File
@@ -64,7 +64,7 @@ export default function RightPanel({
{/* Top priority */}
<div>
<label className="block text-[11px] font-semibold uppercase tracking-wider text-gray-500 mb-1.5">
Top Priority
Top Priority Tests
</label>
<textarea
rows={3}
+20 -1
View File
@@ -5,6 +5,24 @@ const STATUS_STYLES = {
invalid: 'bg-yellow-800/60 text-yellow-200 border-yellow-600',
}
function formatConfig(config) {
if (!config || typeof config !== 'object') return '—'
const entries = Object.entries(config)
if (entries.length === 0) return '—'
const formattedEntries = entries
.map(([band, data]) => {
if (!data || typeof data !== 'object') return null
const testpoint = data.test_point || data['Test Point']
const sta = data.sta || data.STA
// Only display if both testpoint and sta are present
if (!testpoint || !sta) return null
return `${band}: ${testpoint}${sta}`
})
.filter(Boolean)
return formattedEntries.length > 0 ? formattedEntries.join('; ') : '—'
}
export default function TestCard({ test }) {
const style = STATUS_STYLES[test.status] ?? STATUS_STYLES.pending
@@ -18,10 +36,11 @@ export default function TestCard({ test }) {
</div>
{/* Hover tooltip */}
<div className="absolute z-50 bottom-full left-0 mb-1 hidden group-hover:block min-w-max">
<div className="bg-gray-800 border border-gray-600 rounded-md px-2.5 py-1.5 text-xs text-gray-200 shadow-lg">
<div className="bg-gray-800 border border-gray-600 rounded-md px-2.5 py-1.5 text-xs text-gray-200 shadow-lg max-w-md">
<p><span className="text-gray-400">Type:</span> {test.test_type ?? '—'}</p>
<p><span className="text-gray-400">Rotation:</span> {test.rotation ?? '—'}</p>
<p><span className="text-gray-400">Device:</span> {test.device ?? '—'}</p>
<p className="mt-1 pt-1 border-t border-gray-600"><span className="text-gray-400">Config:</span> {formatConfig(test.config)}</p>
</div>
</div>
</div>