fix test bundling bug

This commit is contained in:
2026-07-17 15:17:08 -04:00
parent b5dd611f82
commit a49772aece
5 changed files with 159 additions and 66 deletions
+24 -16
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")
+5
View File
@@ -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]