#!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")" # LinuxCNC source basis: this guard ties Node/browser WASM smoke coverage to # LinuxCNC RS274 parameter/INI handling, M-code canon side effects, generated # switchkins/remap config cases, kinematics sources, and OPFS-only browser # persistence. It validates smoke-test source coverage only; it does not add # CNC behavior, expand smoke parsing, or interpret G-code. linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc} linuxcnc_root=$(cd "$linuxcnc_root" && pwd) grep -F 'int Interp::ini_load(const char *filename)' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'int Interp::restore_parameters(const char *filename)' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'int Interp::save_parameters(const char *filename,' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'std::string tempfile = std::string(filename) + ".new";' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'unlink(bakfile.c_str());' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'if(link(filename, bakfile.c_str()) < 0)' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'if(rename(tempfile.c_str(), filename) < 0)' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'read_text(command, _setup.file_pointer, _setup.linetext,' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_pre.cc" >/dev/null grep -F 'int read_text(const char *command, FILE * inport, char *raw_line,' \ "$linuxcnc_root/src/emc/rs274ngc/rs274ngc_interp.hh" >/dev/null grep -F 'int Interp::convert_m(block_pointer block' \ "$linuxcnc_root/src/emc/rs274ngc/interp_convert.cc" >/dev/null grep -F 'int ret = WAIT(round_to_int(block->p_number), DIGITAL_INPUT, type, timeout);' \ "$linuxcnc_root/src/emc/rs274ngc/interp_convert.cc" >/dev/null grep -F 'SET_AUX_OUTPUT_VALUE(round_to_int(block->e_number), block->q_number);' \ "$linuxcnc_root/src/emc/rs274ngc/interp_convert.cc" >/dev/null grep -F 'extern int WAIT(int index' "$linuxcnc_root/src/emc/nml_intf/canon.hh" >/dev/null grep -F 'extern void SET_AUX_OUTPUT_VALUE(int index, double value);' \ "$linuxcnc_root/src/emc/nml_intf/canon.hh" >/dev/null grep -F 'kp->kinsname = "5axiskins";' "$linuxcnc_root/src/emc/kinematics/5axiskins.c" >/dev/null grep -F 'int xyzbcKinematicsInverse(const EmcPose * pos' \ "$linuxcnc_root/src/emc/kinematics/trtfuncs.c" >/dev/null grep -F 'int xyzacKinematicsInverse(const EmcPose * pos' \ "$linuxcnc_root/src/emc/kinematics/trtfuncs.c" >/dev/null grep -F '*kinv1 = xyzacKinematicsInverse;' \ "$linuxcnc_root/src/emc/kinematics/xyzac-trt-kins.c" >/dev/null grep -F '*kinv1 = xyzbcKinematicsInverse;' \ "$linuxcnc_root/src/emc/kinematics/xyzbc-trt-kins.c" >/dev/null grep -F 'int userkKinematicsInverse(const EmcPose * pos' \ "$linuxcnc_root/src/emc/kinematics/userkfuncs.c" >/dev/null grep -F 'LinuxCNC WASM smoke source map' docs/linuxcnc-wasm-smoke-source-map.md >/dev/null grep -F 'must not expand the temporary smoke parser' docs/linuxcnc-wasm-smoke-source-map.md >/dev/null grep -F 'OPFS-only' docs/linuxcnc-wasm-smoke-source-map.md >/dev/null grep -F '`src/emc/rs274ngc/interp_convert.cc` provides `convert_m()` coverage' \ docs/linuxcnc-wasm-smoke-source-map.md >/dev/null grep -F 'unlinking `filename + ".bak"`' docs/linuxcnc-wasm-smoke-source-map.md >/dev/null grep -F '`src/emc/kinematics/5axiskins.c`, `src/emc/kinematics/trtfuncs.c`' \ docs/linuxcnc-wasm-smoke-source-map.md >/dev/null grep -F 'Generated switchkins config smoke cases must continue to come from' \ docs/linuxcnc-wasm-smoke-source-map.md >/dev/null grep -F 'LinuxCNC source basis: Node smoke only verifies the wasm/API bridge around' \ test-web-wasm-node-smoke.sh >/dev/null grep -F 'minimum_steps=${WASM_NODE_SMOKE_MIN_STEPS:-50}' test-web-wasm-node-smoke.sh >/dev/null grep -F 'WASM Node smoke covered $passed_steps steps, expected at least $minimum_steps' \ test-web-wasm-node-smoke.sh >/dev/null grep -F 'expected Node wasm-core wrapper to leave OPFS disabled' test-web-wasm-node-smoke.cjs >/dev/null grep -F 'expected Node wasm-core parseFile to require OPFS' test-web-wasm-node-smoke.cjs >/dev/null grep -F 'linuxcnc_switchkins_remap_config_cases.json' test-web-wasm-node-smoke.cjs >/dev/null grep -F 'expected LinuxCNC WASM M68 E3 Q${kinstype} remap side effect' \ test-web-wasm-node-smoke.cjs >/dev/null grep -F 'expected LinuxCNC WASM M66 E0 L0 remap side effect' \ test-web-wasm-node-smoke.cjs >/dev/null grep -F 'expected LinuxCNC WASM 5axiskins RTCP pivot from LinuxCNC 5axiskins inverse' \ test-web-wasm-node-smoke.cjs >/dev/null grep -F 'expected LinuxCNC WASM configured xyzbc-trt RTCP pivot from LinuxCNC trtfuncs inverse' \ test-web-wasm-node-smoke.cjs >/dev/null grep -F 'expected LinuxCNC WASM configured xyzac-trt RTCP pivot from LinuxCNC xyzab_tdr inverse' \ test-web-wasm-node-smoke.cjs >/dev/null grep -F 'LinuxCNC source basis: browser smoke verifies OPFS-backed wasm/API/app flows' \ test-web-wasm-browser-smoke.sh >/dev/null grep -F 'minimum_sections=${BROWSER_SMOKE_MIN_SECTIONS:-50}' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'BROWSER_SMOKE_MIN_SECTIONS must be a positive integer' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'browser WASM smoke covered $passed_sections sections, expected at least $minimum_sections' \ test-web-wasm-browser-smoke.sh >/dev/null grep -F 'required_browser_sections = {' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'runSection("linuxcnc generated switchkins cases load"' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'runSection("opfs parameter restore in new instance"' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'runSection("opfs directory persist and load"' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'runSection("browser opfs policy"' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'runSection("browser app opfs restore"' test-web-wasm-browser-smoke.sh >/dev/null grep -F 'runLinuxCncBrowserSections(context, simulator)' web/test-browser-wasm-smoke-sections.js >/dev/null grep -F 'runBrowserOpfsWorkspaceSections(context, simulator)' web/test-browser-wasm-smoke-sections.js >/dev/null grep -F 'runBrowserOpfsPolicySections(context)' web/test-browser-wasm-smoke-sections.js >/dev/null grep -F 'runBrowserAppOpfsSections(context)' web/test-browser-wasm-smoke-sections.js >/dev/null grep -F 'runBrowserOpfsBasicSections(context, simulator)' \ web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null grep -F 'runBrowserOpfsParameterSections(context, simulator)' \ web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null grep -F 'runBrowserOpfsMirrorSections(context, simulator)' \ web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null grep -F 'runBrowserOpfsDirectorySections(context, simulator)' \ web/test-browser-wasm-smoke-opfs-workspace-sections.js >/dev/null grep -F 'linuxcnc_switchkins_remap_config_cases.json' \ web/test-browser-wasm-smoke-linuxcnc-sections.js >/dev/null grep -F 'missing LinuxCNC switchkins config cases after INI_FILE_NAME remap-load filtering' \ web/test-browser-wasm-smoke-linuxcnc-sections.js >/dev/null grep -F 'LinuxCNC source basis: rs274ngc_pre.cc restore_parameters()' \ web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null grep -F 'LinuxCNC source basis: rs274ngc_pre.cc save_parameters()' \ web/test-browser-wasm-smoke-opfs-parameter-sections.js >/dev/null grep -F 'LinuxCNC source basis: rs274ngc_pre.cc read_text() reads' \ web/test-browser-wasm-smoke-opfs-basic-sections.js >/dev/null grep -F 'runSection("opfs directory persist and load"' \ web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null grep -F 'runSection("opfs metadata queries avoid wasm mirror pollution"' \ web/test-browser-wasm-smoke-opfs-directory-sections.js >/dev/null grep -F 'runSection("browser opfs policy"' \ web/test-browser-wasm-smoke-opfs-policy-sections.js >/dev/null grep -F 'app frame did not persist current program into OPFS' \ web/test-browser-wasm-smoke-app-sections.js >/dev/null grep -F 'app frame did not persist LinuxCNC parameter backup into OPFS' \ web/test-browser-wasm-smoke-app-sections.js >/dev/null grep -F 'app frame did not clean up LinuxCNC temporary parameter file in OPFS' \ web/test-browser-wasm-smoke-app-sections.js >/dev/null if grep -R -n -E 'localStorage|sessionStorage|indexedDB|showOpenFilePicker|showSaveFilePicker|showDirectoryPicker|webkitRequestFileSystem|FileReader' web/src; then echo "browser app/wasm code must not add filesystem persistence outside OPFS" >&2 exit 1 fi if grep -n -E 'localStorage|sessionStorage|indexedDB|showOpenFilePicker|showSaveFilePicker|showDirectoryPicker|webkitRequestFileSystem|FileReader' \ web/test-browser-wasm-smoke-*.js \ web/test-browser-wasm-smoke.html \ test-web-wasm-browser-smoke.sh; then echo "browser WASM smoke harness must not add filesystem persistence outside OPFS" >&2 exit 1 fi python3 - <<'PY' from pathlib import Path import json import re source_map = Path("docs/linuxcnc-wasm-smoke-source-map.md").read_text(encoding="utf-8") json_cases = json.loads(Path("web/public/linuxcnc_switchkins_remap_config_cases.json").read_text(encoding="utf-8")) if not isinstance(json_cases, list) or len(json_cases) == 0: raise SystemExit("generated LinuxCNC switchkins browser config cases are missing") node_wrapper = Path("test-web-wasm-node-smoke.sh").read_text(encoding="utf-8") browser_wrapper = Path("test-web-wasm-browser-smoke.sh").read_text(encoding="utf-8") node_min = re.search(r"minimum_steps=\$\{WASM_NODE_SMOKE_MIN_STEPS:-(\d+)\}", node_wrapper) browser_min = re.search(r"minimum_sections=\$\{BROWSER_SMOKE_MIN_SECTIONS:-(\d+)\}", browser_wrapper) if not node_min or int(node_min.group(1)) < 50: raise SystemExit("Node WASM smoke minimum step threshold is missing or below 50") if not browser_min or int(browser_min.group(1)) < 50: raise SystemExit("browser WASM smoke minimum section threshold is missing or below 50") for path in [ "test-web-wasm-node-smoke.sh", "test-web-wasm-node-smoke.cjs", "test-web-wasm-browser-smoke.sh", "web/test-browser-wasm-smoke-linuxcnc-sections.js", "web/test-browser-wasm-smoke-opfs-workspace-sections.js", "web/test-browser-wasm-smoke-opfs-basic-sections.js", "web/test-browser-wasm-smoke-opfs-parameter-sections.js", "web/test-browser-wasm-smoke-opfs-mirror-sections.js", "web/test-browser-wasm-smoke-opfs-directory-sections.js", "web/test-browser-wasm-smoke-opfs-policy-sections.js", "web/test-browser-wasm-smoke-app-sections.js", ]: if f"| `{path}` |" not in source_map: raise SystemExit(f"docs/linuxcnc-wasm-smoke-source-map.md missing smoke entry: {path}") required_phrases = [ "must not add CNC behavior", "must not expand the temporary smoke parser", "persistence OPFS-only", "Node smoke must leave OPFS disabled", "Browser smoke harness files must not use Local Storage", "Generated switchkins config smoke cases", "browser smoke shell guard also pins required section names", "OPFS workspace/policy/directory handling", 'unlinking `filename + ".bak"`', "./check-linuxcnc-wasm-smoke-source-map.sh", "does not interpret G-code", ] for phrase in required_phrases: if phrase not in source_map: raise SystemExit(f"docs/linuxcnc-wasm-smoke-source-map.md missing boundary phrase: {phrase}") for entry in [ "test-native.sh", "test-linuxcnc-source-link.sh", "build-wasm.sh", ]: text = Path(entry).read_text(encoding="utf-8") if "./check-linuxcnc-wasm-smoke-source-map.sh" not in text: raise SystemExit(f"{entry} must run ./check-linuxcnc-wasm-smoke-source-map.sh") PY