127 lines
7.3 KiB
Python
127 lines
7.3 KiB
Python
#!/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']}")
|