Advance simulation parity and 3-axis tutorial coverage

This commit is contained in:
2026-08-23 18:00:56 +08:00
parent 4a11d9692b
commit 938654b19d
107 changed files with 13537 additions and 3565 deletions

View File

@@ -6,6 +6,7 @@ from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
@@ -17,6 +18,12 @@ HAL_REPORT = Path("coverage/hal-component-manifest.report.json")
OUTPUT = Path("web/workspace/sim-case-catalog.mjs")
REPORT = Path("coverage/sim-case-catalog.report.json")
SCHEMA = "linuxcnc-sim-case-catalog.v1"
RUNTIME_FIXTURE_FILES = (
"linuxcnc/configs/sim/axis/minimal_xyz_cmds.hal",
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_cmds.hal",
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_cmds.hal",
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_auto_cmds.hal",
)
def canonical(value: Any) -> str:
@@ -32,6 +39,44 @@ def artifact(root: Path, path: str) -> dict[str, Any]:
return {"path": path, "status": "missing"} if not value.is_file() else {"path": path, "bytes": value.stat().st_size, "sha256": sha256(value.read_bytes()), "status": "present"}
def source_files(root: Path, directory: Path) -> list[Path]:
"""Index source files without native runtime outputs.
Native LinuxCNC creates ignored parameter files, backups, bytecode, and
generated HAL fixtures in the source tree. A filesystem glob would make
the browser manifest depend on the last native run, so use the submodule's
tracked set whenever Git metadata is available. The small set of
LinuxCNC runtime HAL fixtures is added explicitly below because those
files are consumed by the loader's existing runtime-fixture contract.
"""
base = root / directory
git_root = root / "linuxcnc"
if (git_root / ".git").exists():
prefix = directory.as_posix().removeprefix("linuxcnc/")
tracked_result = subprocess.run(
["git", "-C", str(git_root), "ls-files", "--cached", "--", prefix],
check=True,
capture_output=True,
text=True,
)
tracked = set(tracked_result.stdout.splitlines())
result = subprocess.run(
["git", "-C", str(git_root), "ls-files", "--cached", "--others", "--exclude-standard", "--", prefix],
check=True,
capture_output=True,
text=True,
)
return [
git_root / line
for line in result.stdout.splitlines()
# Native startup leaves empty tool.tbl files behind; only tracked
# tables are source inputs, while missing tables are materialized
# by the Web loader at their configured path.
if (git_root / line).is_file() and (not line.endswith(".tbl") or line in tracked)
]
return [path for path in sorted(base.rglob("*")) if path.is_file() and "__pycache__" not in path.parts and path.suffix != ".pyc"]
def ini_summary(path: Path) -> dict[str, str | None]:
section = ""
values: dict[str, str] = {}
@@ -61,7 +106,7 @@ def generate(root: Path, output: Path, report_output: Path = REPORT) -> dict[str
if path.startswith("linuxcnc/configs/sim/"):
matrix_by_path.setdefault(path, row)
cases = []
for path in sorted((root / SIM_ROOT).rglob("*.ini")):
for path in sorted(path for path in source_files(root, SIM_ROOT) if path.suffix == ".ini"):
relative = path.relative_to(root).as_posix()
row = matrix_by_path.get(relative, {})
summary = ini_summary(path)
@@ -81,12 +126,13 @@ def generate(root: Path, output: Path, report_output: Path = REPORT) -> dict[str
"unresolved_count": len(row.get("unresolved_resources", [])),
"source_sha256": sha256(path.read_bytes()),
})
file_index = [path.relative_to(root).as_posix() for path in sorted((root / SIM_ROOT).rglob("*")) if path.is_file()]
file_index = [path.relative_to(root).as_posix() for path in sorted(source_files(root, SIM_ROOT))]
for support_root in SUPPORT_ROOTS:
file_index.extend(path.relative_to(root).as_posix() for path in sorted((root / support_root).rglob("*")) if path.is_file())
file_index.extend(path.relative_to(root).as_posix() for path in sorted(source_files(root, support_root)))
shared_asset = root / "linuxcnc/linuxcnc.gif"
if shared_asset.is_file():
file_index.append(shared_asset.relative_to(root).as_posix())
file_index.extend(path for path in RUNTIME_FIXTURE_FILES if (root / path).is_file())
file_index = sorted(set(file_index))
hal_report = json.loads((root / HAL_REPORT).read_text(encoding="utf-8"))
hal_components = sorted({str(row["component"]) for row in hal_report.get("component_manifest", []) if row.get("component")})