p3p implementation

This commit is contained in:
2026-06-16 16:09:54 -04:00
parent 89e6968dc4
commit 6762586e2d
8 changed files with 238 additions and 2584 deletions
+155 -26
View File
@@ -5,7 +5,7 @@ from pathlib import Path
from db import DEVICE_DUT, DEVICE_REF, TestRecord
REQUIRED_COLUMNS = [
P2P_COE_REQUIRED_COLUMNS = [
"Priority",
"Index",
"Interferer",
@@ -34,6 +34,37 @@ REQUIRED_COLUMNS = [
"2G STA",
]
P3P_REQUIRED_COLUMNS = [
"Priority",
"Index",
"Throttled",
"Rotation",
"TC ID",
"Band",
"6GHz Power Mode",
"Station 1 Test Point",
"Station 1 Channel",
"Station 1 Bandwidth",
"Station 1 RSSI",
"Station 1 Direction",
"Station 1 Rate",
"Station 1 STA",
"Station 2 Test Point",
"Station 2 Channel",
"Station 2 Bandwidth",
"Station 2 RSSI",
"Station 2 Direction",
"Station 2 Rate",
"Station 2 STA",
"Station 3 Test Point",
"Station 3 Channel",
"Station 3 Bandwidth",
"Station 3 RSSI",
"Station 3 Direction",
"Station 3 Rate",
"Station 3 STA",
]
RUNTIME_DEFAULTS = {
"P2P": 80,
"COE": 115,
@@ -50,28 +81,84 @@ class CsvValidationError(ValueError):
pass
def parse_target_csv(csv_path: str | Path) -> ParseResult:
path = Path(csv_path)
if not path.exists():
raise FileNotFoundError(f"CSV file not found: {path}")
def _columns_missing(normalized_fieldnames: set[str], required_columns: list[str]) -> list[str]:
return [
column
for column in required_columns
if column.strip().upper() not in normalized_fieldnames
]
def _detect_csv_format(normalized_fieldnames: set[str]) -> str:
p2p_coe_missing = _columns_missing(normalized_fieldnames, P2P_COE_REQUIRED_COLUMNS)
if not p2p_coe_missing:
return "p2p_coe"
p3p_missing = _columns_missing(normalized_fieldnames, P3P_REQUIRED_COLUMNS)
if not p3p_missing:
return "p3p"
raise CsvValidationError(
"CSV format not recognized. Missing columns for P2P/COE format: "
f"{', '.join(p2p_coe_missing)}; missing columns for P3P format: {', '.join(p3p_missing)}"
)
def parse_target_csv(csv_path: str | Path | list[str | Path] | tuple[str | Path, ...]) -> ParseResult:
paths = _resolve_csv_paths(csv_path)
all_tests: list[TestRecord] = []
all_warnings: list[str] = []
seen_test_keys: set[tuple[str, str]] = set()
for path in paths:
parsed = _parse_single_csv(path)
all_warnings.extend(parsed.warnings)
for record in parsed.tests:
key = (record.test_id, record.device)
if key in seen_test_keys:
all_warnings.append(
f"File {path.name}: duplicate test/device '{record.test_id}/{record.device}', record skipped."
)
continue
seen_test_keys.add(key)
all_tests.append(record)
return ParseResult(tests=all_tests, warnings=all_warnings)
def _resolve_csv_paths(csv_path: str | Path | list[str | Path] | tuple[str | Path, ...]) -> list[Path]:
if isinstance(csv_path, (str, Path)):
paths = [Path(csv_path)]
else:
paths = [Path(item) for item in csv_path]
resolved_paths: list[Path] = []
for path in paths:
if path.is_dir():
resolved_paths.extend(sorted(path.glob("*.csv")))
else:
resolved_paths.append(path)
if not resolved_paths:
raise FileNotFoundError("No CSV files found to parse.")
for path in resolved_paths:
if not path.exists():
raise FileNotFoundError(f"CSV file not found: {path}")
return resolved_paths
def _parse_single_csv(path: Path) -> ParseResult:
with path.open("r", encoding="utf-8-sig", newline="") as handle:
reader = csv.DictReader(handle)
if not reader.fieldnames:
raise CsvValidationError("CSV is missing a header row.")
normalized_fieldnames = {name.strip().upper() for name in reader.fieldnames if name}
# Check for required columns
missing_columns = [
c
for c in REQUIRED_COLUMNS
if c.strip().upper() not in normalized_fieldnames
]
if missing_columns:
raise CsvValidationError(
f"CSV is missing required columns: {', '.join(missing_columns)}"
)
csv_format = _detect_csv_format(normalized_fieldnames)
tests: list[TestRecord] = []
warnings: list[str] = []
@@ -82,31 +169,33 @@ def parse_target_csv(csv_path: str | Path) -> ParseResult:
for row_num, row in enumerate(reader, start=2):
test_id = (row.get("TC ID") or "").strip()
if not test_id:
warnings.append(f"Row {row_num}: missing TC ID, row skipped.")
warnings.append(f"File {path.name}, row {row_num}: missing TC ID, row skipped.")
continue
if test_id in seen_test_ids:
warnings.append(f"Row {row_num}: duplicate TC ID '{test_id}', row skipped.")
warnings.append(f"File {path.name}, row {row_num}: duplicate TC ID '{test_id}', row skipped.")
continue
seen_test_ids.add(test_id)
throttled = _normalize_yes_no(_row_get(row, "Throttled")) if csv_format == "p3p" else False
test_type = _infer_test_type(test_id)
rx_tx = _infer_rx_tx(test_id)
rotation = _empty_to_none(_row_get(row, "Rotation"))
has_coe_pair = _normalize_yes_no(_row_get(row, "COE Pair"))
config = _build_config(row)
signature = _victim_band_signature(row)
has_coe_pair = _normalize_yes_no(_row_get(row, "COE Pair")) if csv_format == "p2p_coe" else False
config = _build_config(row, csv_format)
signature = _victim_band_signature(row) if csv_format == "p2p_coe" else None
estimated_minutes = RUNTIME_DEFAULTS.get(test_type)
victim_band = _normalize_victim_band(_row_get(row, "Victim Band"))
# Default priority based on test type and COE pairing
victim_band_source = "Victim Band" if csv_format == "p2p_coe" else "Band"
victim_band = _normalize_victim_band(_row_get(row, victim_band_source))
# Default priority based on test type and COE pairing.
if test_type == "P2P":
priority = 2 if has_coe_pair else 3
elif test_type == "COE":
priority = 3
else:
priority = 4
record = TestRecord(
test_id=test_id,
device=DEVICE_DUT,
@@ -118,6 +207,7 @@ def parse_target_csv(csv_path: str | Path) -> ParseResult:
priority=priority,
victim_band=victim_band,
config=config,
throttled=throttled,
estimated_minutes=estimated_minutes,
status="pending",
excluded=False,
@@ -155,6 +245,7 @@ def parse_target_csv(csv_path: str | Path) -> ParseResult:
priority=record.priority,
victim_band=record.victim_band,
config=record.config,
throttled=record.throttled,
estimated_minutes=record.estimated_minutes,
status=record.status,
excluded=record.excluded,
@@ -252,7 +343,7 @@ def _normalize_yes_no(value: str | None) -> bool:
return _normalize_value(value) == "YES"
def _build_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]:
def _build_legacy_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]:
return {
"5G": {
"test_point": _empty_to_none(_row_get(row, "5G Test Point")),
@@ -282,6 +373,44 @@ def _build_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]:
}
def _build_p3p_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]:
return {
"Station 1": {
"test_point": _empty_to_none(_row_get(row, "Station 1 Test Point")),
"channel": _empty_to_none(_row_get(row, "Station 1 Channel")),
"bandwidth": _empty_to_none(_row_get(row, "Station 1 Bandwidth")),
"rssi": _empty_to_none(_row_get(row, "Station 1 RSSI")),
"direction": _empty_to_none(_row_get(row, "Station 1 Direction")),
"rate": _empty_to_none(_row_get(row, "Station 1 Rate")),
"sta": _empty_to_none(_row_get(row, "Station 1 STA")),
},
"Station 2": {
"test_point": _empty_to_none(_row_get(row, "Station 2 Test Point")),
"channel": _empty_to_none(_row_get(row, "Station 2 Channel")),
"bandwidth": _empty_to_none(_row_get(row, "Station 2 Bandwidth")),
"rssi": _empty_to_none(_row_get(row, "Station 2 RSSI")),
"direction": _empty_to_none(_row_get(row, "Station 2 Direction")),
"rate": _empty_to_none(_row_get(row, "Station 2 Rate")),
"sta": _empty_to_none(_row_get(row, "Station 2 STA")),
},
"Station 3": {
"test_point": _empty_to_none(_row_get(row, "Station 3 Test Point")),
"channel": _empty_to_none(_row_get(row, "Station 3 Channel")),
"bandwidth": _empty_to_none(_row_get(row, "Station 3 Bandwidth")),
"rssi": _empty_to_none(_row_get(row, "Station 3 RSSI")),
"direction": _empty_to_none(_row_get(row, "Station 3 Direction")),
"rate": _empty_to_none(_row_get(row, "Station 3 Rate")),
"sta": _empty_to_none(_row_get(row, "Station 3 STA")),
},
}
def _build_config(row: dict[str, str], csv_format: str) -> dict[str, dict[str, str | None]]:
if csv_format == "p3p":
return _build_p3p_config(row)
return _build_legacy_config(row)
def _row_get(row: dict[str, str], key: str) -> str | None:
if key in row:
return row.get(key)