export working on network files
This commit is contained in:
+36
-6
@@ -12,7 +12,7 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
import db as db
|
import db as db
|
||||||
from parser import CsvValidationError
|
from parser import CsvValidationError
|
||||||
from file_manager import resolve_requested_csv_path, read_file_bytes
|
from file_manager import resolve_requested_csv_path, read_file_bytes, path_name
|
||||||
|
|
||||||
# Ensure new_scheduler resolves the same DUT/REF labels as db records.
|
# Ensure new_scheduler resolves the same DUT/REF labels as db records.
|
||||||
os.environ.setdefault("DUT", db.DUT)
|
os.environ.setdefault("DUT", db.DUT)
|
||||||
@@ -22,8 +22,8 @@ from scheduler import Scheduler, Test as SchedulerTest
|
|||||||
from file_manager import resolve_runtime_path
|
from file_manager 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, trigger_scan
|
||||||
from scanner import process_targets
|
from scanner import process_targets, scan_results
|
||||||
|
|
||||||
APP_ROOT = Path(__file__).resolve().parent
|
APP_ROOT = Path(__file__).resolve().parent
|
||||||
DB_PATH = Path(os.getenv("DB_PATH", str(APP_ROOT.parent / "data" / "scheduler.db")))
|
DB_PATH = Path(os.getenv("DB_PATH", str(APP_ROOT.parent / "data" / "scheduler.db")))
|
||||||
@@ -349,6 +349,13 @@ def load_tests(request: LoadTestsRequest) -> dict[str, Any]:
|
|||||||
|
|
||||||
count = process_targets(target_dir, resolved_csv_paths, smb_credentials=smb_credentials, runtime_overrides=runtime_overrides)
|
count = process_targets(target_dir, resolved_csv_paths, smb_credentials=smb_credentials, runtime_overrides=runtime_overrides)
|
||||||
|
|
||||||
|
# Synchronously scan result directories before returning so completed tests are
|
||||||
|
# written to the DB before the caller can compile a new schedule.
|
||||||
|
dut_result_dir = resolve_runtime_path(settings.get("dutResultDir", "") or "")
|
||||||
|
ref_result_dir = resolve_runtime_path(settings.get("refResultDir", "") or "")
|
||||||
|
if dut_result_dir and ref_result_dir:
|
||||||
|
scan_results(dut_result_dir, ref_result_dir, smb_credentials=smb_credentials)
|
||||||
|
|
||||||
print(f"Loaded {count} tests from {resolved_csv_paths}.")
|
print(f"Loaded {count} tests from {resolved_csv_paths}.")
|
||||||
return {
|
return {
|
||||||
"loaded_tests": count,
|
"loaded_tests": count,
|
||||||
@@ -560,18 +567,41 @@ def export_window(window_id: str, version: int | None = None) -> Response:
|
|||||||
}
|
}
|
||||||
file_paths = [path_lookup[k] for k in sorted(wanted) if k in path_lookup]
|
file_paths = [path_lookup[k] for k in sorted(wanted) if k in path_lookup]
|
||||||
|
|
||||||
|
# Include one GLOBAL.ini from the first parent directory that has test files.
|
||||||
|
# GLOBAL.ini is shared config — only one copy belongs in the export.
|
||||||
|
global_paths: list[str] = []
|
||||||
|
for fp in file_paths:
|
||||||
|
normalized = str(fp).replace("/", "\\")
|
||||||
|
if "\\" not in normalized:
|
||||||
|
continue
|
||||||
|
parent_dir = normalized.rsplit("\\", 1)[0]
|
||||||
|
global_paths.append(f"{parent_dir}\\GLOBAL.ini")
|
||||||
|
break
|
||||||
|
|
||||||
|
all_export_paths = file_paths + global_paths
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
|
added_count = 0
|
||||||
|
read_errors: list[str] = []
|
||||||
with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
|
with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
for fp in file_paths:
|
for fp in all_export_paths:
|
||||||
try:
|
try:
|
||||||
data = read_file_bytes(fp, smb_credentials)
|
data = read_file_bytes(fp, smb_credentials)
|
||||||
except OSError as exc:
|
except Exception as exc:
|
||||||
print(f"[export] Could not read {fp}: {exc}")
|
print(f"[export] Could not read {fp}: {exc}")
|
||||||
|
read_errors.append(f"{fp}: {exc}")
|
||||||
continue
|
continue
|
||||||
zf.writestr(os.path.basename(fp), data)
|
zf.writestr(path_name(fp), data)
|
||||||
|
added_count += 1
|
||||||
|
|
||||||
|
if all_export_paths and added_count == 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail=f"Failed to read scheduled network files. First error: {read_errors[0] if read_errors else 'unknown error'}",
|
||||||
|
)
|
||||||
|
|
||||||
start_date_str = datetime.strptime(target_window["start_date"], "%Y-%m-%d").strftime("%m-%d-%Y")
|
start_date_str = datetime.strptime(target_window["start_date"], "%Y-%m-%d").strftime("%m-%d-%Y")
|
||||||
devices = "_".join(sorted({t["device"] for t in target_window["tests"]}))
|
devices = "_".join(sorted({t["device"] for t in target_window["tests"]}))
|
||||||
|
|||||||
@@ -207,14 +207,15 @@ def open_csv_handle(path: str, smb_credentials: dict[str, Any] | None):
|
|||||||
|
|
||||||
|
|
||||||
def read_file_bytes(path: str, smb_credentials: dict[str, Any] | None = None) -> bytes:
|
def read_file_bytes(path: str, smb_credentials: dict[str, Any] | None = None) -> bytes:
|
||||||
if is_unc_path(path):
|
path = normalize_input_path(path)
|
||||||
if smbclient is None:
|
if is_unc_path(path):
|
||||||
raise ModuleNotFoundError("smbclient is required to read UNC paths")
|
if smbclient is None:
|
||||||
_register_smb_session_if_needed(path, smb_credentials=smb_credentials)
|
raise ModuleNotFoundError("smbclient is required to read UNC paths")
|
||||||
with smbclient.open_file(path, mode="rb") as fh:
|
_register_smb_session_if_needed(path, smb_credentials=smb_credentials)
|
||||||
return fh.read()
|
with smbclient.open_file(path, mode="rb") as fh:
|
||||||
with open(path, "rb") as fh:
|
return fh.read()
|
||||||
return fh.read()
|
with open(path, "rb") as fh:
|
||||||
|
return fh.read()
|
||||||
|
|
||||||
def join_path(path, name):
|
def join_path(path, name):
|
||||||
if is_unc_path(path):
|
if is_unc_path(path):
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class Scheduler:
|
|||||||
|
|
||||||
|
|
||||||
def compile_schedule(self) -> str | None:
|
def compile_schedule(self) -> str | None:
|
||||||
bundles = build_test_bundles(self.active_dut, self.active_ref, self.top_priority_tests)
|
bundles = build_test_bundles(self.active_dut, self.active_ref, self.top_priority_tests, log=True)
|
||||||
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."
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ class Scheduler:
|
|||||||
tc_order = self.get_tc_order(self.all_tests)
|
tc_order = self.get_tc_order(self.all_tests)
|
||||||
|
|
||||||
for tc in tc_order:
|
for tc in tc_order:
|
||||||
tc_bundles = all_bundles_by_tc[tc]
|
tc_bundles = all_bundles_by_tc.get(tc)
|
||||||
if tc_bundles is None or len(tc_bundles) == 0:
|
if tc_bundles is None or len(tc_bundles) == 0:
|
||||||
print(f"[scheduler] No bundles found for TC: {tc}. Skipping to next TC.")
|
print(f"[scheduler] No bundles found for TC: {tc}. Skipping to next TC.")
|
||||||
continue
|
continue
|
||||||
@@ -366,7 +366,7 @@ class Scheduler:
|
|||||||
# Implement the logic for getting the test case order
|
# Implement the logic for getting the test case order
|
||||||
all_dut = {test.test_id: test for test in all_tests if test.device == DUT}
|
all_dut = {test.test_id: test for test in all_tests if test.device == DUT}
|
||||||
all_ref = {test.test_id: test for test in all_tests if test.device == REF}
|
all_ref = {test.test_id: test for test in all_tests if test.device == REF}
|
||||||
bundles = build_test_bundles(all_dut, all_ref, [])
|
bundles = build_test_bundles(all_dut, all_ref, [], log=False)
|
||||||
|
|
||||||
all_tcs = set()
|
all_tcs = set()
|
||||||
for bundle in bundles:
|
for bundle in bundles:
|
||||||
|
|||||||
@@ -41,11 +41,12 @@ class TestBundle:
|
|||||||
priority: int
|
priority: int
|
||||||
completed: int
|
completed: int
|
||||||
|
|
||||||
def build_test_bundles(active_dut: dict[str, Test], active_ref: dict[str, Test], top_priority_tests: set[tuple[str, str]]) -> list[TestBundle]:
|
def build_test_bundles(active_dut: dict[str, Test], active_ref: dict[str, Test], top_priority_tests: set[tuple[str, str]], log: bool = False) -> list[TestBundle]:
|
||||||
"""Build deterministic bundles for DP scheduling."""
|
"""Build deterministic bundles for DP scheduling."""
|
||||||
|
|
||||||
print(f"[test_bundle] Top priority tests: {top_priority_tests}")
|
if log:
|
||||||
print(f"[debug] Tests with coe_pairing: {[(tid, t.coe_pairing) for tid, t in active_dut.items() if t.coe_pairing]}")
|
print(f"[test_bundle] Top priority tests: {top_priority_tests}")
|
||||||
|
print(f"[debug] Tests with coe_pairing: {[(tid, t.coe_pairing) for tid, t in active_dut.items() if t.coe_pairing]}")
|
||||||
processed_dut: set[str] = set()
|
processed_dut: set[str] = set()
|
||||||
processed_ref: set[str] = set()
|
processed_ref: set[str] = set()
|
||||||
test_bundles: list[TestBundle] = []
|
test_bundles: list[TestBundle] = []
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import json
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
TEST_CONFIG_DEFINITION = {
|
TEST_CONFIG_DEFINITION = {
|
||||||
"TC1": {"T1D": "STA5", "T1F": "STA56", "T1I": "STA58", "T1L": "STA64", "T2A":"STA63", "T2O":"STA65", "T2J": "STA59", "T2E": "STA6", "T3E": "STA4"},
|
"TC1": {"T1P": "STA5", "T1F": "STA56", "T1I": "STA58", "T1L": "STA64", "T2A":"STA63", "T2O":"STA65", "T2J": "STA59", "T2E": "STA6", "T3E": "STA4"},
|
||||||
"TC2": {"T1D": "STA64", "T1F": "STA4", "T1I": "STA5", "T1L": "STA58", "T2A":"STA56", "T2O":"STA59", "T2J": "STA6", "T2E": "STA65", "T3E": "STA63"},
|
"TC2": {"T1P": "STA64", "T1F": "STA4", "T1I": "STA5", "T1L": "STA58", "T2A":"STA56", "T2O":"STA59", "T2J": "STA6", "T2E": "STA65", "T3E": "STA63"},
|
||||||
"TC3": {"T1D": "STA58", "T1F": "STA63", "T1I": "STA64", "T2J": "STA65", "T2E": "STA59", "T3E": "STA56"},
|
"TC3": {"T1P": "STA58", "T1F": "STA63", "T1I": "STA64", "T2J": "STA65", "T2E": "STA59", "T3E": "STA56"},
|
||||||
"TC4": {"T1B": "STA56", "T1C": "STA4", "T1D": "STA63"},
|
"TC4": {"T1B": "STA56", "T1C": "STA4", "T1D": "STA63"},
|
||||||
"TC5": {"T1B": "STA4", "T1C": "STA63", "T1D": "STA56"},
|
"TC5": {"T1B": "STA4", "T1C": "STA63", "T1D": "STA56"},
|
||||||
"TC6": {"T1B": "STA63", "T1C": "STA56", "T1D": "STA4"},
|
"TC6": {"T1B": "STA63", "T1C": "STA56", "T1D": "STA4"},
|
||||||
@@ -26,6 +26,17 @@ def _norm(value: Any) -> str:
|
|||||||
return " ".join(str(value).strip().upper().split())
|
return " ".join(str(value).strip().upper().split())
|
||||||
|
|
||||||
|
|
||||||
|
# Canonical aliases: any key is normalised to its value before lookup.
|
||||||
|
_TESTPOINT_ALIASES: dict[str, str] = {
|
||||||
|
"T2Q": "T2E",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_testpoint(tp: str) -> str:
|
||||||
|
"""Return the canonical testpoint name, resolving known aliases."""
|
||||||
|
return _TESTPOINT_ALIASES.get(tp, tp)
|
||||||
|
|
||||||
|
|
||||||
def _get_test_config(test: Any) -> dict[str, dict[str, str | None]]:
|
def _get_test_config(test: Any) -> dict[str, dict[str, str | None]]:
|
||||||
if isinstance(test, dict):
|
if isinstance(test, dict):
|
||||||
config = test.get("config") or {}
|
config = test.get("config") or {}
|
||||||
@@ -38,7 +49,7 @@ def _testpoint_to_station_sets(config: dict[str, dict[str, str | None]]) -> dict
|
|||||||
result: dict[str, set[str]] = {}
|
result: dict[str, set[str]] = {}
|
||||||
|
|
||||||
def _add_entry(entry: dict[str, str | None]) -> None:
|
def _add_entry(entry: dict[str, str | None]) -> None:
|
||||||
testpoint = _norm(entry.get("test_point") or entry.get("Test Point"))
|
testpoint = _norm_testpoint(_norm(entry.get("test_point") or entry.get("Test Point")))
|
||||||
sta_raw = _norm(entry.get("sta") or entry.get("STA"))
|
sta_raw = _norm(entry.get("sta") or entry.get("STA"))
|
||||||
if not testpoint or not sta_raw:
|
if not testpoint or not sta_raw:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -271,6 +271,11 @@ def configure_result_watcher(settings: dict[str, Any]) -> None:
|
|||||||
_WATCHER.configure_from_settings(settings)
|
_WATCHER.configure_from_settings(settings)
|
||||||
|
|
||||||
|
|
||||||
|
def trigger_scan(reason: str = "manual") -> None:
|
||||||
|
"""Immediately schedule a result scan on the background thread."""
|
||||||
|
_WATCHER.scan_now(reason=reason)
|
||||||
|
|
||||||
|
|
||||||
def stop_result_watcher() -> None:
|
def stop_result_watcher() -> None:
|
||||||
_WATCHER.stop()
|
_WATCHER.stop()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user