fix test bundling bug
This commit is contained in:
+24
-16
@@ -16,6 +16,7 @@ os.environ.setdefault("DUT", db.DEVICE_DUT)
|
|||||||
os.environ.setdefault("REF", db.DEVICE_REF)
|
os.environ.setdefault("REF", db.DEVICE_REF)
|
||||||
|
|
||||||
from scheduler import Scheduler, Test as SchedulerTest
|
from scheduler import Scheduler, Test as SchedulerTest
|
||||||
|
from scanner import resolve_runtime_path
|
||||||
from test_config import build_bundle_test_configs, build_config_rows
|
from test_config import build_bundle_test_configs, build_config_rows
|
||||||
from test_window import get_shift_capacity_for_date, get_shift_sequence, is_off_day
|
from test_window import get_shift_capacity_for_date, get_shift_sequence, is_off_day
|
||||||
from watcher import configure_result_watcher, stop_result_watcher
|
from watcher import configure_result_watcher, stop_result_watcher
|
||||||
@@ -79,6 +80,28 @@ def _runtime_overrides_from_settings(settings: dict[str, Any]) -> dict[str, dict
|
|||||||
return overrides
|
return overrides
|
||||||
|
|
||||||
|
|
||||||
|
def _is_explicit_path(path_value: str) -> bool:
|
||||||
|
return (
|
||||||
|
path_value.startswith(("/", "\\\\", "//"))
|
||||||
|
or "\\" in path_value
|
||||||
|
or ":" in path_value
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_requested_csv_path(requested_path: str) -> str | Path:
|
||||||
|
received_path = str(requested_path).strip()
|
||||||
|
resolved_path = str(resolve_runtime_path(received_path)).strip()
|
||||||
|
|
||||||
|
if _is_explicit_path(received_path) or _is_explicit_path(resolved_path):
|
||||||
|
return resolved_path
|
||||||
|
|
||||||
|
data_dir_path = APP_ROOT / "data" / received_path
|
||||||
|
if data_dir_path.exists():
|
||||||
|
return data_dir_path
|
||||||
|
|
||||||
|
return APP_ROOT / received_path
|
||||||
|
|
||||||
|
|
||||||
class CompileScheduleRequest(BaseModel):
|
class CompileScheduleRequest(BaseModel):
|
||||||
start_date: str | None = Field(default=None, description="YYYY-MM-DD")
|
start_date: str | None = Field(default=None, description="YYYY-MM-DD")
|
||||||
rule: str = ""
|
rule: str = ""
|
||||||
@@ -314,22 +337,7 @@ def get_settings() -> dict[str, Any]:
|
|||||||
|
|
||||||
@app.post("/api/tests/load")
|
@app.post("/api/tests/load")
|
||||||
def load_tests(request: LoadTestsRequest) -> dict[str, Any]:
|
def load_tests(request: LoadTestsRequest) -> dict[str, Any]:
|
||||||
received_path = request.csv_path
|
csv_path = _resolve_requested_csv_path(request.csv_path)
|
||||||
|
|
||||||
# Handle absolute paths (Windows or Unix) by extracting just the filename
|
|
||||||
if "\\" in received_path or ":" in received_path or received_path.startswith("/"):
|
|
||||||
# Absolute path (Windows with backslash or drive letter, or Unix with /)
|
|
||||||
# Extract just the filename from the path
|
|
||||||
filename = received_path.replace("\\", "/").split("/")[-1]
|
|
||||||
|
|
||||||
data_dir_path = APP_ROOT / "data" / filename
|
|
||||||
if data_dir_path.exists():
|
|
||||||
csv_path = data_dir_path
|
|
||||||
else:
|
|
||||||
csv_path = APP_ROOT / filename
|
|
||||||
else:
|
|
||||||
# Relative paths - prepend APP_ROOT
|
|
||||||
csv_path = APP_ROOT / received_path
|
|
||||||
|
|
||||||
settings = db.read_settings(DB_PATH)
|
settings = db.read_settings(DB_PATH)
|
||||||
smb_credentials = _smb_credentials_from_settings(settings)
|
smb_credentials = _smb_credentials_from_settings(settings)
|
||||||
|
|||||||
+31
-10
@@ -27,6 +27,7 @@ class TestRecord:
|
|||||||
test_type: str
|
test_type: str
|
||||||
rotation: str | None
|
rotation: str | None
|
||||||
rx_tx: str | None
|
rx_tx: str | None
|
||||||
|
power_mode: str | None
|
||||||
has_coe_pair: bool
|
has_coe_pair: bool
|
||||||
coe_pairing: list[str]
|
coe_pairing: list[str]
|
||||||
priority: int
|
priority: int
|
||||||
@@ -80,6 +81,7 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
|
|||||||
test_type TEXT NOT NULL CHECK (test_type IN ('P2P', 'COE', 'P3P')),
|
test_type TEXT NOT NULL CHECK (test_type IN ('P2P', 'COE', 'P3P')),
|
||||||
rotation TEXT,
|
rotation TEXT,
|
||||||
rx_tx TEXT CHECK (rx_tx IN ('RX', 'TX') OR rx_tx IS NULL),
|
rx_tx TEXT CHECK (rx_tx IN ('RX', 'TX') OR rx_tx IS NULL),
|
||||||
|
power_mode TEXT,
|
||||||
has_coe_pair INTEGER NOT NULL DEFAULT 0,
|
has_coe_pair INTEGER NOT NULL DEFAULT 0,
|
||||||
coe_pairing_json TEXT,
|
coe_pairing_json TEXT,
|
||||||
priority INTEGER NOT NULL CHECK (priority BETWEEN 1 AND 5),
|
priority INTEGER NOT NULL CHECK (priority BETWEEN 1 AND 5),
|
||||||
@@ -132,6 +134,7 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
|
|||||||
_ensure_column(conn, "tests", "has_coe_pair", "INTEGER NOT NULL DEFAULT 0")
|
_ensure_column(conn, "tests", "has_coe_pair", "INTEGER NOT NULL DEFAULT 0")
|
||||||
_ensure_column(conn, "tests", "coe_pairing_json", "TEXT")
|
_ensure_column(conn, "tests", "coe_pairing_json", "TEXT")
|
||||||
_ensure_column(conn, "tests", "config_json", "TEXT")
|
_ensure_column(conn, "tests", "config_json", "TEXT")
|
||||||
|
_ensure_column(conn, "tests", "power_mode", "TEXT")
|
||||||
_ensure_column(conn, "tests", "victim_band", "TEXT")
|
_ensure_column(conn, "tests", "victim_band", "TEXT")
|
||||||
_ensure_column(conn, "tests", "excluded", "INTEGER NOT NULL DEFAULT 0")
|
_ensure_column(conn, "tests", "excluded", "INTEGER NOT NULL DEFAULT 0")
|
||||||
_ensure_column(conn, "tests", "throttled", "INTEGER NOT NULL DEFAULT 0")
|
_ensure_column(conn, "tests", "throttled", "INTEGER NOT NULL DEFAULT 0")
|
||||||
@@ -169,6 +172,7 @@ def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> in
|
|||||||
r.test_type,
|
r.test_type,
|
||||||
r.rotation,
|
r.rotation,
|
||||||
r.rx_tx,
|
r.rx_tx,
|
||||||
|
r.power_mode,
|
||||||
int(r.has_coe_pair),
|
int(r.has_coe_pair),
|
||||||
json.dumps(r.coe_pairing or []),
|
json.dumps(r.coe_pairing or []),
|
||||||
json.dumps(r.config),
|
json.dumps(r.config),
|
||||||
@@ -187,15 +191,16 @@ def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> in
|
|||||||
conn.executemany(
|
conn.executemany(
|
||||||
"""
|
"""
|
||||||
INSERT INTO tests(
|
INSERT INTO tests(
|
||||||
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
|
test_id, device, test_type, rotation, rx_tx, power_mode, has_coe_pair,
|
||||||
coe_pairing_json, config_json, priority, victim_band, throttled, estimated_minutes, status, excluded, raw_payload, station_testpoint_map
|
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
|
ON CONFLICT(test_id, device) DO UPDATE SET
|
||||||
test_type = excluded.test_type,
|
test_type = excluded.test_type,
|
||||||
device = excluded.device,
|
device = excluded.device,
|
||||||
rotation = excluded.rotation,
|
rotation = excluded.rotation,
|
||||||
rx_tx = excluded.rx_tx,
|
rx_tx = excluded.rx_tx,
|
||||||
|
power_mode = excluded.power_mode,
|
||||||
has_coe_pair = excluded.has_coe_pair,
|
has_coe_pair = excluded.has_coe_pair,
|
||||||
coe_pairing_json = excluded.coe_pairing_json,
|
coe_pairing_json = excluded.coe_pairing_json,
|
||||||
config_json = excluded.config_json,
|
config_json = excluded.config_json,
|
||||||
@@ -277,12 +282,7 @@ def _entry_value(entry: dict[str, str | None], key: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _record_power_mode(record: TestRecord) -> str:
|
def _record_power_mode(record: TestRecord) -> str:
|
||||||
power_mode = _entry_value(_band_entry(record.config, "6G"), "power_mode")
|
return _normalize_text(record.power_mode)
|
||||||
if power_mode:
|
|
||||||
return power_mode
|
|
||||||
|
|
||||||
raw_payload = record.raw_payload or {}
|
|
||||||
return _normalize_text(raw_payload.get("6GHz Power Mode"))
|
|
||||||
|
|
||||||
|
|
||||||
def _match_any_band_value(record: TestRecord, key: str, token_suffix: str) -> bool:
|
def _match_any_band_value(record: TestRecord, key: str, token_suffix: str) -> bool:
|
||||||
@@ -369,7 +369,7 @@ def list_schedulable_tests(db_path: str | Path = DB_PATH, rule: str = "") -> lis
|
|||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
|
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
|
||||||
config_json, victim_band,
|
power_mode, config_json, victim_band,
|
||||||
coe_pairing_json, priority, throttled, estimated_minutes,
|
coe_pairing_json, priority, throttled, estimated_minutes,
|
||||||
excluded, status, raw_payload
|
excluded, status, raw_payload
|
||||||
FROM tests
|
FROM tests
|
||||||
@@ -387,6 +387,7 @@ def list_schedulable_tests(db_path: str | Path = DB_PATH, rule: str = "") -> lis
|
|||||||
test_type=row["test_type"],
|
test_type=row["test_type"],
|
||||||
rotation=row["rotation"],
|
rotation=row["rotation"],
|
||||||
rx_tx=row["rx_tx"],
|
rx_tx=row["rx_tx"],
|
||||||
|
power_mode=row["power_mode"],
|
||||||
has_coe_pair=bool(row["has_coe_pair"]),
|
has_coe_pair=bool(row["has_coe_pair"]),
|
||||||
coe_pairing=json.loads(row["coe_pairing_json"] or "[]"),
|
coe_pairing=json.loads(row["coe_pairing_json"] or "[]"),
|
||||||
priority=int(row["priority"]),
|
priority=int(row["priority"]),
|
||||||
@@ -411,7 +412,7 @@ def list_tests_for_device(device: str, db_path: str | Path = DB_PATH) -> list[Te
|
|||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
|
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
|
||||||
config_json, victim_band,
|
power_mode, config_json, victim_band,
|
||||||
coe_pairing_json, priority, throttled, estimated_minutes,
|
coe_pairing_json, priority, throttled, estimated_minutes,
|
||||||
excluded, status, raw_payload, station_testpoint_map
|
excluded, status, raw_payload, station_testpoint_map
|
||||||
FROM tests
|
FROM tests
|
||||||
@@ -429,6 +430,7 @@ def list_tests_for_device(device: str, db_path: str | Path = DB_PATH) -> list[Te
|
|||||||
test_type=row["test_type"],
|
test_type=row["test_type"],
|
||||||
rotation=row["rotation"],
|
rotation=row["rotation"],
|
||||||
rx_tx=row["rx_tx"],
|
rx_tx=row["rx_tx"],
|
||||||
|
power_mode=row["power_mode"],
|
||||||
has_coe_pair=bool(row["has_coe_pair"]),
|
has_coe_pair=bool(row["has_coe_pair"]),
|
||||||
coe_pairing=json.loads(row["coe_pairing_json"] or "[]"),
|
coe_pairing=json.loads(row["coe_pairing_json"] or "[]"),
|
||||||
priority=int(row["priority"]),
|
priority=int(row["priority"]),
|
||||||
@@ -670,6 +672,19 @@ def get_schedule_rows(
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def reset_completed_to_pending(db_path: str | Path = DB_PATH) -> int:
|
||||||
|
"""Reset all 'completed' tests back to 'pending'. Used before a full directory rescan."""
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
cursor = conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE tests
|
||||||
|
SET status = 'pending', updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE status = 'completed'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return cursor.rowcount
|
||||||
|
|
||||||
|
|
||||||
def mark_tests_completed(test_ids_with_device: list[tuple[str, str]], db_path: str | Path = DB_PATH) -> int:
|
def mark_tests_completed(test_ids_with_device: list[tuple[str, str]], db_path: str | Path = DB_PATH) -> int:
|
||||||
if not test_ids_with_device:
|
if not test_ids_with_device:
|
||||||
return 0
|
return 0
|
||||||
@@ -811,3 +826,9 @@ def upsert_holidays(dates: list[str], db_path: str | Path = DB_PATH) -> None:
|
|||||||
[(d,) for d in dates],
|
[(d,) for d in dates],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def reset_tests(db_path: str | Path = DB_PATH) -> int:
|
||||||
|
"""Clear all tests records keep empty tests table."""
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
cursor = conn.execute("DELETE FROM tests")
|
||||||
|
return cursor.rowcount
|
||||||
|
|
||||||
|
|||||||
+50
-15
@@ -11,6 +11,7 @@ except ModuleNotFoundError:
|
|||||||
smbclient = None
|
smbclient = None
|
||||||
|
|
||||||
from db import DEVICE_DUT, DEVICE_REF, TestRecord
|
from db import DEVICE_DUT, DEVICE_REF, TestRecord
|
||||||
|
from scanner import resolve_runtime_path
|
||||||
|
|
||||||
P2P_COE_REQUIRED_COLUMNS = [
|
P2P_COE_REQUIRED_COLUMNS = [
|
||||||
"Priority",
|
"Priority",
|
||||||
@@ -96,6 +97,8 @@ def _normalize_input_path(path_value: str | Path) -> str:
|
|||||||
if not path:
|
if not path:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
path = str(resolve_runtime_path(path)).strip()
|
||||||
|
|
||||||
# Accept //server/share style and normalize to UNC for smbclient.
|
# Accept //server/share style and normalize to UNC for smbclient.
|
||||||
if path.startswith("//"):
|
if path.startswith("//"):
|
||||||
return "\\\\" + path.lstrip("/").replace("/", "\\")
|
return "\\\\" + path.lstrip("/").replace("/", "\\")
|
||||||
@@ -104,6 +107,12 @@ def _normalize_input_path(path_value: str | Path) -> str:
|
|||||||
if re.match(r"^/\d{1,3}(?:\.\d{1,3}){3}/[^/]+", path):
|
if re.match(r"^/\d{1,3}(?:\.\d{1,3}){3}/[^/]+", path):
|
||||||
return "\\\\" + path.lstrip("/").replace("/", "\\")
|
return "\\\\" + path.lstrip("/").replace("/", "\\")
|
||||||
|
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
if path.startswith("/"):
|
||||||
|
return path
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
@@ -372,9 +381,10 @@ def _parse_single_csv(
|
|||||||
test_type = _infer_test_type(test_id)
|
test_type = _infer_test_type(test_id)
|
||||||
rx_tx = _infer_rx_tx(test_id)
|
rx_tx = _infer_rx_tx(test_id)
|
||||||
rotation = _empty_to_none(_row_get(row, "Rotation"))
|
rotation = _empty_to_none(_row_get(row, "Rotation"))
|
||||||
|
power_mode = _empty_to_none(_row_get(row, "6GHz Power Mode"))
|
||||||
has_coe_pair = _normalize_yes_no(_row_get(row, "COE Pair")) if csv_format == "p2p_coe" else False
|
has_coe_pair = _normalize_yes_no(_row_get(row, "COE Pair")) if csv_format == "p2p_coe" else False
|
||||||
config = _build_config(row, csv_format)
|
config = _build_config(row, csv_format)
|
||||||
signature = _victim_band_signature(row) if csv_format == "p2p_coe" else None
|
signature = _first_populated_p2p_signature(row) if csv_format == "p2p_coe" and test_type == "P2P" else None
|
||||||
victim_band_source = "Victim Band" if csv_format == "p2p_coe" else "Band"
|
victim_band_source = "Victim Band" if csv_format == "p2p_coe" else "Band"
|
||||||
victim_band = _normalize_victim_band(_row_get(row, victim_band_source))
|
victim_band = _normalize_victim_band(_row_get(row, victim_band_source))
|
||||||
|
|
||||||
@@ -392,6 +402,7 @@ def _parse_single_csv(
|
|||||||
test_type=test_type,
|
test_type=test_type,
|
||||||
rotation=rotation,
|
rotation=rotation,
|
||||||
rx_tx=rx_tx,
|
rx_tx=rx_tx,
|
||||||
|
power_mode=power_mode,
|
||||||
has_coe_pair=has_coe_pair,
|
has_coe_pair=has_coe_pair,
|
||||||
coe_pairing=[],
|
coe_pairing=[],
|
||||||
priority=priority,
|
priority=priority,
|
||||||
@@ -405,14 +416,18 @@ def _parse_single_csv(
|
|||||||
)
|
)
|
||||||
records_with_signature.append((record, signature))
|
records_with_signature.append((record, signature))
|
||||||
|
|
||||||
# Build COE pairing based on victim band signature and RX/TX+band suffix.
|
# Build COE pairing based on full signature and RX/TX+band suffix.
|
||||||
# Example key suffixes: RXAX, TXAX, RXBE, TXBE.
|
# Example key suffixes: RXAX, TXAX, RXBE, TXBE.
|
||||||
coe_by_signature_and_suffix: dict[tuple, list[str]] = {}
|
coe_by_signature_and_suffix: dict[tuple[tuple[str, ...], str], list[str]] = {}
|
||||||
for record, signature in records_with_signature:
|
for record, signature in records_with_signature:
|
||||||
if record.test_type == "COE" and signature is not None:
|
if record.test_type == "COE":
|
||||||
suffix = _extract_pairing_suffix(record.test_id)
|
suffix = _extract_pairing_suffix(record.test_id)
|
||||||
if suffix:
|
if not suffix:
|
||||||
key = (signature, suffix)
|
continue
|
||||||
|
|
||||||
|
# Index all populated COE band signatures so any one can match the selected P2P signature.
|
||||||
|
for coe_signature in _all_band_signatures(record.raw_payload or {}):
|
||||||
|
key = (coe_signature, suffix)
|
||||||
coe_by_signature_and_suffix.setdefault(key, []).append(record.test_id)
|
coe_by_signature_and_suffix.setdefault(key, []).append(record.test_id)
|
||||||
|
|
||||||
for record, signature in records_with_signature:
|
for record, signature in records_with_signature:
|
||||||
@@ -420,7 +435,7 @@ def _parse_single_csv(
|
|||||||
if record.test_type == "P2P" and signature is not None:
|
if record.test_type == "P2P" and signature is not None:
|
||||||
suffix = _extract_pairing_suffix(record.test_id)
|
suffix = _extract_pairing_suffix(record.test_id)
|
||||||
key = (signature, suffix) if suffix else None
|
key = (signature, suffix) if suffix else None
|
||||||
pairs = sorted(coe_by_signature_and_suffix.get(key, [])) if key else []
|
pairs = sorted(set(coe_by_signature_and_suffix.get(key, []))) if key else []
|
||||||
|
|
||||||
for device in (DEVICE_DUT, DEVICE_REF):
|
for device in (DEVICE_DUT, DEVICE_REF):
|
||||||
device_runtime_defaults = runtime_defaults_by_device.get(device, RUNTIME_DEFAULTS)
|
device_runtime_defaults = runtime_defaults_by_device.get(device, RUNTIME_DEFAULTS)
|
||||||
@@ -431,6 +446,7 @@ def _parse_single_csv(
|
|||||||
test_type=record.test_type,
|
test_type=record.test_type,
|
||||||
rotation=record.rotation,
|
rotation=record.rotation,
|
||||||
rx_tx=record.rx_tx,
|
rx_tx=record.rx_tx,
|
||||||
|
power_mode=record.power_mode,
|
||||||
has_coe_pair=bool(pairs),
|
has_coe_pair=bool(pairs),
|
||||||
coe_pairing=pairs,
|
coe_pairing=pairs,
|
||||||
priority=record.priority,
|
priority=record.priority,
|
||||||
@@ -493,13 +509,21 @@ def _empty_to_none(value: str | None) -> str | None:
|
|||||||
return cleaned if cleaned else None
|
return cleaned if cleaned else None
|
||||||
|
|
||||||
|
|
||||||
def _victim_band_signature(row: dict[str, str]) -> tuple[str, ...] | None:
|
def _first_populated_p2p_signature(row: dict[str, str]) -> tuple[str, ...] | None:
|
||||||
band = _normalize_victim_band(_row_get(row, "Victim Band"))
|
for band in ("5G", "6G", "2G"):
|
||||||
if band is None:
|
signature = _band_signature(row, band)
|
||||||
|
if signature is not None:
|
||||||
|
return signature
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _band_signature(row: dict[str, str], band: str) -> tuple[str, ...] | None:
|
||||||
|
normalized_band = _normalize_victim_band(band)
|
||||||
|
if normalized_band is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Source CSVs may use either "5G_Test Point" or "5G Test Point" style headers.
|
# Source CSVs may use either "5G_Test Point" or "5G Test Point" style headers.
|
||||||
prefixes = (f"{band}_", f"{band} ")
|
prefixes = (f"{normalized_band}_", f"{normalized_band} ")
|
||||||
|
|
||||||
def _pick_value(suffix: str) -> str:
|
def _pick_value(suffix: str) -> str:
|
||||||
for prefix in prefixes:
|
for prefix in prefixes:
|
||||||
@@ -512,12 +536,24 @@ def _victim_band_signature(row: dict[str, str]) -> tuple[str, ...] | None:
|
|||||||
channel = _pick_value("Channel")
|
channel = _pick_value("Channel")
|
||||||
rssi = _pick_value("RSSI")
|
rssi = _pick_value("RSSI")
|
||||||
bandwidth = _pick_value("Bandwidth")
|
bandwidth = _pick_value("Bandwidth")
|
||||||
|
direction = _pick_value("Direction")
|
||||||
|
sta = _pick_value("STA")
|
||||||
|
|
||||||
# Direction is intentionally ignored for COE pairing matching.
|
if not all([test_point, channel, rssi, bandwidth, direction, sta]):
|
||||||
if not all([test_point, channel, rssi, bandwidth]):
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return (band, test_point, channel, rssi, bandwidth)
|
return (test_point, channel, bandwidth, rssi, direction, sta)
|
||||||
|
|
||||||
|
|
||||||
|
def _all_band_signatures(row: dict[str, str]) -> list[tuple[str, ...]]:
|
||||||
|
seen: set[tuple[str, ...]] = set()
|
||||||
|
signatures: list[tuple[str, ...]] = []
|
||||||
|
for band in ("5G", "6G", "2G"):
|
||||||
|
signature = _band_signature(row, band)
|
||||||
|
if signature is not None and signature not in seen:
|
||||||
|
seen.add(signature)
|
||||||
|
signatures.append(signature)
|
||||||
|
return signatures
|
||||||
|
|
||||||
|
|
||||||
def _normalize_victim_band(value: str | None) -> str | None:
|
def _normalize_victim_band(value: str | None) -> str | None:
|
||||||
@@ -561,7 +597,6 @@ def _build_legacy_config(row: dict[str, str]) -> dict[str, dict[str, str | None]
|
|||||||
"sta": _empty_to_none(_row_get(row, "5G_STA")),
|
"sta": _empty_to_none(_row_get(row, "5G_STA")),
|
||||||
},
|
},
|
||||||
"6G": {
|
"6G": {
|
||||||
"power_mode": _empty_to_none(_row_get(row, "6GHz Power Mode")),
|
|
||||||
"test_point": _empty_to_none(_row_get(row, "6G_Test Point")),
|
"test_point": _empty_to_none(_row_get(row, "6G_Test Point")),
|
||||||
"channel": _empty_to_none(_row_get(row, "6G_Channel")),
|
"channel": _empty_to_none(_row_get(row, "6G_Channel")),
|
||||||
"bandwidth": _empty_to_none(_row_get(row, "6G_Bandwidth")),
|
"bandwidth": _empty_to_none(_row_get(row, "6G_Bandwidth")),
|
||||||
|
|||||||
+42
-18
@@ -7,7 +7,7 @@ try:
|
|||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
smbclient = None
|
smbclient = None
|
||||||
|
|
||||||
from db import DEVICE_DUT, DEVICE_REF, mark_tests_completed, mark_overdue_as_rerun
|
from db import DEVICE_DUT, DEVICE_REF, mark_tests_completed, mark_overdue_as_rerun, reset_completed_to_pending
|
||||||
|
|
||||||
_SMB_SESSIONS = set()
|
_SMB_SESSIONS = set()
|
||||||
_SCAN_STATE_LOCK = threading.Lock()
|
_SCAN_STATE_LOCK = threading.Lock()
|
||||||
@@ -148,13 +148,12 @@ def resolve_runtime_path(path_value):
|
|||||||
if raw_path.startswith("\\\\") or raw_path.startswith("//"):
|
if raw_path.startswith("\\\\") or raw_path.startswith("//"):
|
||||||
return raw_path
|
return raw_path
|
||||||
|
|
||||||
# Map host paths (Windows or Linux) to the container mount point when running in a container.
|
mount_root = (os.getenv("HOST_MOUNT_ROOT", "/host") or "/host").strip() or "/host"
|
||||||
if os.name != "nt":
|
host_root = (os.getenv("HOST_BROWSE_ROOT", "") or "").strip()
|
||||||
mount_root = os.getenv("HOST_MOUNT_ROOT", "/host").strip() or "/host"
|
|
||||||
host_root = os.getenv("HOST_BROWSE_ROOT", "").strip()
|
|
||||||
|
|
||||||
raw_norm = raw_path.replace("\\", "/")
|
raw_norm = raw_path.replace("\\", "/")
|
||||||
if host_root:
|
|
||||||
|
# In container/Linux runtime, translate host-browse paths into mounted container paths.
|
||||||
|
if os.name != "nt" and host_root:
|
||||||
host_norm = host_root.replace("\\", "/").rstrip("/")
|
host_norm = host_root.replace("\\", "/").rstrip("/")
|
||||||
if raw_norm.lower() == host_norm.lower() or raw_norm.lower().startswith(host_norm.lower() + "/"):
|
if raw_norm.lower() == host_norm.lower() or raw_norm.lower().startswith(host_norm.lower() + "/"):
|
||||||
relative = raw_norm[len(host_norm):].lstrip("/")
|
relative = raw_norm[len(host_norm):].lstrip("/")
|
||||||
@@ -162,6 +161,16 @@ def resolve_runtime_path(path_value):
|
|||||||
return os.path.join(mount_root, *relative.split("/"))
|
return os.path.join(mount_root, *relative.split("/"))
|
||||||
return mount_root
|
return mount_root
|
||||||
|
|
||||||
|
# In native Windows runtime, accept /host/... paths coming from container-oriented settings
|
||||||
|
# and map them back to HOST_BROWSE_ROOT (for example C:/Users/... ).
|
||||||
|
if os.name == "nt" and host_root:
|
||||||
|
mount_norm = mount_root.replace("\\", "/").rstrip("/")
|
||||||
|
if mount_norm and (raw_norm.lower() == mount_norm.lower() or raw_norm.lower().startswith(mount_norm.lower() + "/")):
|
||||||
|
relative = raw_norm[len(mount_norm):].lstrip("/")
|
||||||
|
if relative:
|
||||||
|
return os.path.join(host_root, *relative.split("/"))
|
||||||
|
return host_root
|
||||||
|
|
||||||
return raw_path
|
return raw_path
|
||||||
|
|
||||||
|
|
||||||
@@ -172,25 +181,36 @@ def _normalize_input_path(path_value):
|
|||||||
path = resolve_runtime_path(path_value)
|
path = resolve_runtime_path(path_value)
|
||||||
path = str(path).strip()
|
path = str(path).strip()
|
||||||
|
|
||||||
# Normalize forward slashes to backslashes first
|
# Accept //server/share style and normalize to UNC for smbclient.
|
||||||
path = path.replace("/", "\\")
|
if path.startswith("//"):
|
||||||
|
path = path.lstrip("/").replace("/", "\\")
|
||||||
|
return "\\\\" + path
|
||||||
|
|
||||||
# Accept \\server\share style UNC paths and ensure proper escaping
|
# Accept \\server\share style UNC paths and ensure proper escaping.
|
||||||
if path.startswith("\\\\"):
|
if path.startswith("\\\\"):
|
||||||
# Clean up any doubled backslashes from replacement
|
# Clean up any doubled backslashes from replacement
|
||||||
while "\\\\\\" in path:
|
while "\\\\\\" in path:
|
||||||
path = path.replace("\\\\\\", "\\\\")
|
path = path.replace("\\\\\\", "\\\\")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
# Accept //server/share style and normalize to UNC for smbclient.
|
|
||||||
if path.startswith("//"):
|
|
||||||
path = path.lstrip("/").replace("/", "\\")
|
|
||||||
return "\\\\" + path
|
|
||||||
|
|
||||||
# Accept /<ipv4>/<share>/... and normalize to UNC for Linux-hosted inputs.
|
# Accept /<ipv4>/<share>/... and normalize to UNC for Linux-hosted inputs.
|
||||||
if re.match(r"^\\?\d{1,3}(?:\\\.\d{1,3}){3}\\[^\\]+", path):
|
if re.match(r"^/\d{1,3}(?:\.\d{1,3}){3}/[^/]+", path):
|
||||||
if path.startswith("\\"):
|
return "\\\\" + path.lstrip("/").replace("/", "\\")
|
||||||
path = path.lstrip("\\")
|
|
||||||
|
# Preserve existing slash-based local paths such as /host/... that are valid
|
||||||
|
# in the current runtime, so scanner matches watcher behavior.
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
# Preserve slash-based local mount paths even if they are temporarily missing.
|
||||||
|
if path.startswith("/"):
|
||||||
|
return path
|
||||||
|
|
||||||
|
# Normalize local paths after UNC checks.
|
||||||
|
path = path.replace("/", "\\")
|
||||||
|
|
||||||
|
# Accept \<ipv4>\<share>\... and normalize to UNC.
|
||||||
|
if re.match(r"^\d{1,3}(?:\.\d{1,3}){3}\\[^\\]+", path):
|
||||||
return "\\\\" + path
|
return "\\\\" + path
|
||||||
|
|
||||||
return path
|
return path
|
||||||
@@ -316,6 +336,10 @@ def scan_results(results_dir_dut, results_dir_ref, smb_credentials=None):
|
|||||||
for test_id, device in completed_batch:
|
for test_id, device in completed_batch:
|
||||||
print(f" - {test_id} on {device}")
|
print(f" - {test_id} on {device}")
|
||||||
|
|
||||||
|
reset_count = reset_completed_to_pending()
|
||||||
|
if reset_count:
|
||||||
|
print(f"[scanner] reset {reset_count} previously-completed test(s) to pending before resync")
|
||||||
|
|
||||||
updated_count = mark_tests_completed(completed_batch)
|
updated_count = mark_tests_completed(completed_batch)
|
||||||
print(f"[scanner] {updated_count} test(s) actually updated in database")
|
print(f"[scanner] {updated_count} test(s) actually updated in database")
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ class Scheduler:
|
|||||||
if not bundles:
|
if not bundles:
|
||||||
return f"Error: No test bundles could be created from the provided tests."
|
return f"Error: No test bundles could be created from the provided tests."
|
||||||
|
|
||||||
|
# Print all test bundles for debugging
|
||||||
|
print("Compiled Test Bundles:")
|
||||||
|
for bundle in bundles:
|
||||||
|
print(f"Bundle Index: {bundle.index}, Device: {bundle.device}, Priority: {bundle.priority}, Config: {bundle.config}, Total Minutes: {bundle.total_minutes}, Tests: {bundle.tests}")
|
||||||
|
|
||||||
active_dut_bundles = [b for b in bundles if b.device == DUT]
|
active_dut_bundles = [b for b in bundles if b.device == DUT]
|
||||||
active_ref_bundles = [b for b in bundles if b.device == REF]
|
active_ref_bundles = [b for b in bundles if b.device == REF]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user