Advance simulation parity and 3-axis tutorial coverage
This commit is contained in:
109
scripts/generate_axis3_tutorial_resource_graph.py
Normal file
109
scripts/generate_axis3_tutorial_resource_graph.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the exact source/resource graph for the selected 3axis tutorial case."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CASE_ROOT = Path("linuxcnc/configs/sim/axis/vismach/3axis-tutorial")
|
||||
REPORT = Path("coverage/axis3-tutorial-resource-graph.report.json")
|
||||
SOURCE_FILES = (
|
||||
CASE_ROOT / "3axis-tutorial.ini",
|
||||
CASE_ROOT / "3axis-tutorial.hal",
|
||||
CASE_ROOT / "3axis-tutorial",
|
||||
CASE_ROOT / "tool.tbl",
|
||||
Path("linuxcnc/linuxcnc.gif"),
|
||||
Path("linuxcnc/lib/python/vismach.py"),
|
||||
Path("linuxcnc/lib/python/hal.py"),
|
||||
Path("linuxcnc/lib/hallib/basic_sim.tcl"),
|
||||
Path("linuxcnc/src/libnml/inifile/inifile.cc"),
|
||||
Path("linuxcnc/src/emc/rs274ngc/rs274ngc_pre.cc"),
|
||||
)
|
||||
|
||||
|
||||
def sha256(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def artifact(path: Path, role: str) -> dict[str, Any]:
|
||||
full = ROOT / path
|
||||
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": sha256(full.read_bytes())} if full.is_file() else {"path": path.as_posix(), "role": role, "status": "missing"}
|
||||
|
||||
|
||||
def ini_entries(path: Path) -> list[dict[str, Any]]:
|
||||
section = ""
|
||||
rows = []
|
||||
for line_number, raw in enumerate((ROOT / path).read_text(encoding="utf-8", errors="replace").splitlines(), 1):
|
||||
clean = raw.split("#", 1)[0].strip()
|
||||
section_match = re.match(r"^\[([^]]+)\]$", clean)
|
||||
if section_match:
|
||||
section = section_match.group(1).strip()
|
||||
continue
|
||||
assignment = re.match(r"^([^=:#]+?)\s*=\s*(.*)$", clean)
|
||||
if assignment and assignment.group(1).strip().upper() in {"DISPLAY", "INTRO_GRAPHIC", "PARAMETER_FILE", "HALFILE", "COORDINATES", "TOOL_TABLE", "KINEMATICS", "JOINTS", "VERSION"}:
|
||||
rows.append({"source": path.as_posix(), "line": line_number, "section": section, "key": assignment.group(1).strip().upper(), "value": assignment.group(2).strip()})
|
||||
return rows
|
||||
|
||||
|
||||
def hal_entries(path: Path) -> list[dict[str, Any]]:
|
||||
rows = []
|
||||
for line_number, raw in enumerate((ROOT / path).read_text(encoding="utf-8", errors="replace").splitlines(), 1):
|
||||
clean = raw.split("#", 1)[0].strip()
|
||||
if clean and re.match(r"^(loadrt|loadusr|addf|net)\b", clean, re.I):
|
||||
rows.append({"source": path.as_posix(), "line": line_number, "command": clean.split()[0].lower(), "raw": clean})
|
||||
return rows
|
||||
|
||||
|
||||
def python_contract(path: Path) -> dict[str, Any]:
|
||||
text = (ROOT / path).read_text(encoding="utf-8", errors="replace")
|
||||
return {
|
||||
"imports": re.findall(r"^(?:from\s+([^\s]+)|import\s+([^\s]+))", text, re.M),
|
||||
"component": re.findall(r'hal\.component\("([^"]+)"\)', text),
|
||||
"pins": [{"name": name, "type": typ, "direction": direction} for name, typ, direction in re.findall(r'newpin\("([^"]+)",\s*hal\.([A-Z_]+),\s*hal\.([A-Z_]+)\)', text)],
|
||||
"model_symbols": [name for name in ("HalTranslate", "HalToolCylinder", "CylinderZ", "Capture", "Collection", "main") if re.search(rf"\b{re.escape(name)}\b", text)],
|
||||
"sha256": sha256((ROOT / path).read_bytes()),
|
||||
}
|
||||
|
||||
|
||||
def generate(output: Path = REPORT) -> dict[str, Any]:
|
||||
records = [artifact(path, "case_source" if path in SOURCE_FILES[:4] else "source_dependency") for path in SOURCE_FILES]
|
||||
report: dict[str, Any] = {
|
||||
"schema": "axis3-tutorial-resource-graph.v1",
|
||||
"task_id": "TUTORIAL-01",
|
||||
"case": "linuxcnc/configs/sim/axis/vismach/3axis-tutorial",
|
||||
"entrypoint": "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini",
|
||||
"status": "verified" if all(row["status"] == "present" for row in records) else "blocked",
|
||||
"source_files": records,
|
||||
"ini_entries": ini_entries(CASE_ROOT / "3axis-tutorial.ini"),
|
||||
"hal_entries": hal_entries(CASE_ROOT / "3axis-tutorial.hal"),
|
||||
"vismach_python": python_contract(CASE_ROOT / "3axis-tutorial"),
|
||||
"runtime_generated": [
|
||||
{"path": f"{CASE_ROOT.as_posix()}/3axis-tutorial_mm.var", "kind": "parameter_file", "source": "linuxcnc/src/emc/rs274ngc/rs274ngc_pre.cc::restore_parameters/save_parameters", "policy": "missing file starts with zero defaults and is created/updated at runtime"},
|
||||
{"path": f"{CASE_ROOT.as_posix()}/3axis-tutorial_mm.var.bak", "kind": "parameter_backup", "source": "linuxcnc/src/emc/rs274ngc/rs274ngc_pre.cc::save_parameters", "policy": "native backup created on parameter save"},
|
||||
{"path": f"{CASE_ROOT.as_posix()}/position_mm.txt", "kind": "position_file", "source": "3axis-tutorial.ini:[TRAJ]POSITION_FILE", "policy": "runtime position persistence; not a source fixture"},
|
||||
],
|
||||
"shared_assets": [{"configured_value": "linuxcnc.gif", "resolved_path": "linuxcnc/linuxcnc.gif", "scope": "DISPLAY.INTRO_GRAPHIC only"}],
|
||||
"capabilities": {"trivkins": "SIM-CORE", "axis": "WEB-EQUIV", "vismach_model": "WEB-EQUIV", "hal_manualtoolchange": "SIM-ADAPTER", "./3axis-tutorial": "WEB-EQUIV"},
|
||||
"boundaries": ["only this case is in scope", "exact paths are retained; basename substitution is forbidden", "host Python process is replaced by a source-mapped Web Vismach adapter", "real hardware and realtime scheduling are outside this pure simulation case"],
|
||||
}
|
||||
report["report_sha256"] = sha256(canonical(report).encode("utf-8"))
|
||||
destination = output if output.is_absolute() else ROOT / output
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = generate()
|
||||
print(f"axis3_tutorial_resource_graph={result['status']} report_sha256={result['report_sha256']}")
|
||||
raise SystemExit(0 if result["status"] == "verified" else 1)
|
||||
Reference in New Issue
Block a user