#!/usr/bin/env python3 """Inventory LinuxCNC source tests using the source runtests contract.""" from __future__ import annotations import hashlib import json import re from collections import Counter from pathlib import Path ROOT = Path(__file__).resolve().parents[1] OUTPUT = ROOT / "coverage/linuxcnc-source-test-s5.report.json" TEST_ROOT = ROOT / "linuxcnc/tests" RUNNER = ROOT / "linuxcnc/scripts/runtests" UNIT_ROOT = ROOT / "linuxcnc/unit_tests" CONFIG_ROOT = ROOT / "linuxcnc/configs/sim" HOST_DIRS = {"linuxcncrsh", "linuxcncrsh-tcp", "mqtt", "hm2-idrom", "rtapi-shmem", "halmodule.0", "halmodule.1", "module-loading", "uspace", "lowlevel", "tclsh-extensions"} SIM_ADAPTER_DIRS = {"abort", "ccomp", "build", "halui", "hard-limits", "io-startup", "lathe", "limit3", "mdi-queue", "mdi-queue-length", "mdi-while-queuebuster-waitflag", "motion", "motion-logger", "oword", "remap", "rs274ngc-startup", "single-step", "startup-state", "statbuffer-g5x-abort", "tlo", "tool-info", "toolchanger", "trajectory-planner"} def sha256(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def canonical(value: object) -> bytes: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8") def file_hashes(directory: Path) -> list[dict[str, object]]: files = [] for path in sorted(p for p in directory.rglob("*") if p.is_file() and ".git" not in p.parts): data = path.read_bytes() files.append({"path": path.relative_to(ROOT).as_posix(), "bytes": len(data), "sha256": sha256(data)}) return files def classify(directory: Path, test_file: Path) -> tuple[str, str]: relative_parts = set(directory.relative_to(TEST_ROOT).parts) names = {path.name for path in directory.iterdir() if path.is_file()} if relative_parts & HOST_DIRS or "control" in names and "sudo" in directory.joinpath("control").read_text(encoding="utf-8", errors="replace"): return "HOST_EXCLUDED", "host_capability_or_privilege" if relative_parts & SIM_ADAPTER_DIRS: return "WORKER_SIM_ADAPTER", "task_motion_or_ui_adapter_required" if "skip" in names: return "SOURCE_BLOCKED", "source_skip_marker" if "expected" in names and test_file.name == "test.hal": return "PURE_REGRESSION", "runtests_expected_stdout" if "expected" in names and test_file.name in {"test", "test.sh"}: return "PURE_REGRESSION", "runtests_expected_stdout" if "checkresult" in names: return "PURE_FUNCTIONAL", "runtests_checkresult" if test_file.name == "test.sh" or test_file.name == "test": return "WORKER_SIM_ADAPTER", "shell_or_executable_adapter_required" return "SOURCE_BLOCKED", "expected_or_checkresult_missing" def runner_command(test_file: Path) -> str: relative = test_file.relative_to(ROOT).as_posix() if test_file.name == "test.hal": return f"halrun -f {relative}" if test_file.name == "test.sh": return f"bash -x {relative}" return f"./{relative}" def discover_tests() -> list[dict[str, object]]: entries = [] for path in sorted(TEST_ROOT.rglob("*")): if not path.is_file() or path.name not in {"test.hal", "test.sh", "test"}: continue directory = path.parent adapter, reason = classify(directory, path) entries.append({ "source_id": f"source-test:{path.relative_to(ROOT).as_posix()}", "path": path.relative_to(ROOT).as_posix(), "test_directory": directory.relative_to(ROOT).as_posix(), "test_file": path.name, "adapter": adapter, "reason": reason, "runner_command": runner_command(path), "has_expected": directory.joinpath("expected").is_file(), "has_checkresult": directory.joinpath("checkresult").is_file(), "has_readme": directory.joinpath("README").is_file(), "xfail": directory.joinpath("xfail").is_file(), "source_hash": sha256(canonical(file_hashes(directory))), "files": file_hashes(directory), }) return entries def unit_entries() -> list[dict[str, object]]: values = [] for suite, target, build in [("interp", "test_interp", "linuxcnc/meson.build"), ("tp", "test_blendmath", "linuxcnc/meson.build")]: directory = UNIT_ROOT / suite values.append({"source_id": f"source-unit:{suite}", "suite": suite, "target": target, "adapter": "UNIT_NATIVE_WASM", "runner_command": f"meson test -C build-linuxcnc {target}", "build_file": build, "source_hash": sha256(canonical(file_hashes(directory))), "files": file_hashes(directory)}) return values def generate(output: Path = OUTPUT) -> dict[str, object]: tests = discover_tests() units = unit_entries() config_paths = sorted(path.relative_to(ROOT).as_posix() for path in CONFIG_ROOT.rglob("*.ini")) config_matrix = json.loads((ROOT / "coverage/full-configuration-startup-matrix.report.json").read_text(encoding="utf-8")) adapter_counts = Counter(entry["adapter"] for entry in tests) report: dict[str, object] = { "schema": "linuxcnc-source-test-matrix.v1", "task_id": "S5-01", "context_id": json.loads((ROOT / "context-lock.json").read_text(encoding="utf-8"))["context_id"], "status": "verified", "runner": {"path": RUNNER.relative_to(ROOT).as_posix(), "sha256": sha256(RUNNER.read_bytes()), "semantics": ["find test.hal/test.sh/test", "halrun or bash/executable", "expected byte compare", "checkresult exit status", "skip/xfail/control handling"]}, "counts": {"test_entries": len(tests), "test_directories": len({entry["test_directory"] for entry in tests}), "unit_suites": len(units), "sim_ini_files": len(config_paths), "matrix_entrypoints": config_matrix["counts"]["entrypoints"], "matrix_registered_entries": config_matrix["counts"]["registered_entries"], "adapter_status": dict(sorted(adapter_counts.items()))}, "adapters": {"PURE_REGRESSION": "expected stdout byte comparison", "PURE_FUNCTIONAL": "checkresult-compatible structured result", "WORKER_SIM_ADAPTER": "real Worker/WASM logical tick adapter", "UNIT_NATIVE_WASM": "native and WASM unit target adapter", "HOST_EXCLUDED": "stable exclusion only; never counted as pass", "SOURCE_BLOCKED": "missing/skip source input retained"}, "source_tests": tests, "unit_tests": units, "sim_entrypoints": config_paths, "next_tasks": ["S5-02 execute pure runtests subset", "S5-03 port interp/tp unit targets", "S5-04 port interp/remap tests", "S5-05 port HAL tests", "S5-06 port IO/startup/device tests", "S5-07 run all sim entrypoints"], "boundaries": ["source tests requiring Linux RT, sudo, TCP, GUI or real devices are HOST_EXCLUDED", "missing expected/checkresult remains SOURCE_BLOCKED", "no derived test is promoted to official oracle"], "report_sha256": None, } report["report_sha256"] = sha256(canonical({key: value for key, value in report.items() if key != "report_sha256"})) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8") return report if __name__ == "__main__": report = generate() print(f"linuxcnc_source_test_matrix={report['status']} tests={report['counts']['test_entries']} units={report['counts']['unit_suites']} sim_entries={report['counts']['matrix_entrypoints']}")