Complete sim config boundary coverage

This commit is contained in:
2026-06-11 06:12:06 +08:00
parent 5bd8f12872
commit 1ab5571ae1
76 changed files with 18975 additions and 146 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
NATIVE_SUMMARY="$ROOT_DIR/build/native/sim-configs/summary.tsv"
NATIVE_CLASS_SUMMARY="$ROOT_DIR/build/native/sim-configs/class-summary.tsv"
NATIVE_PATH_MATRIX="$ROOT_DIR/build/native/sim-configs/path-matrix.tsv"
NATIVE_SOURCE_PROOF_SUMMARY="$ROOT_DIR/build/native/native-source-proof-summary.tsv"
NATIVE_BUILD_DIR="$ROOT_DIR/build/native/sim-configs"
NATIVE_SOURCE_PROOF_INPUTS=(
"$ROOT_DIR/tools/build_native_probes.sh"
"$ROOT_DIR/tools/source-manifest.txt"
"$ROOT_DIR/runtime/core/linuxcnc_wrap/linuxcnc_millturn_user_m_boundary_probe.cpp"
"$ROOT_DIR/runtime/core/linuxcnc_wrap/linuxcnc_tool_db_boundary_probe.cpp"
"$ROOT_DIR/runtime/core/linuxcnc_wrap/linuxcnc_python_remap_boundary_probe.cpp"
)
native_source_proof_refresh_required() {
if [[ ! -f "$NATIVE_SOURCE_PROOF_SUMMARY" ]]; then
return 0
fi
for input in "${NATIVE_SOURCE_PROOF_INPUTS[@]}"; do
if [[ "$input" -nt "$NATIVE_SOURCE_PROOF_SUMMARY" ]]; then
return 0
fi
done
return 1
}
if [[ ! -f "$NATIVE_SUMMARY" || ! -f "$NATIVE_CLASS_SUMMARY" || ! -f "$NATIVE_PATH_MATRIX" ]]; then
"$ROOT_DIR/tests/native/verify_sim_configs.sh"
fi
if native_source_proof_refresh_required; then
"$ROOT_DIR/tools/build_native_probes.sh"
fi
mkdir -p "$NATIVE_BUILD_DIR"
exec 9>"$NATIVE_BUILD_DIR/inventory.lock"
flock 9
if [[ "${SKIP_INTERP_BUILD:-0}" != "1" ]]; then
"$ROOT_DIR/tools/build_wasm_core.sh"
fi
node "$ROOT_DIR/tests/wasm/node/verify_sim_configs_inventory_wasm.mjs"

View File

@@ -4,6 +4,7 @@ import { dirname, resolve } from "node:path";
import assert from "node:assert/strict";
import {
analyzeIniRuntimeBoundaries,
createLinuxCncInterpSdk,
planIniFileContextStaging,
planSimConfigStaging,
@@ -46,6 +47,10 @@ const interp = await createLinuxCncInterpSdk({
},
});
function readSimIni(machineRel, iniFile) {
return readFileSync(resolve(vendorRoot, `configs/sim/${machineRel}/${iniFile}`), "utf8");
}
const syntheticPlan = planSimConfigStaging({
manifestText: [
"configs/sim/example/test.ini",
@@ -80,6 +85,80 @@ assert.deepEqual(
],
"synthetic sim-config staging plan",
);
const syntheticBoundary = analyzeIniRuntimeBoundaries({
manifestText: [
"configs/sim/example/test.ini",
"configs/sim/example/test.ngc",
"configs/sim/example/tool.tbl",
"configs/sim/example/M123",
].join("\n"),
sourceRootRel: "configs/sim/example",
sourceSearchRootRel: "configs/sim",
iniFile: "test.ini",
iniText: [
"[DISPLAY]",
"DISPLAY = axis",
"OPEN_FILE = test.ngc",
"[RS274NGC]",
"USER_M_PATH = .",
"[EMCIO]",
"TOOL_TABLE = tool.tbl",
].join("\n"),
});
assert.deepEqual(
syntheticBoundary,
{
dependencies: [],
recommendedBlockedKind: "-",
toolDatabaseProgram: "",
halRuntime: { values: [], requiresProcess: false },
uiRuntime: { values: ["axis"], requiresProcess: false },
haluiRuntime: { mdiCommands: [], requiresProcess: false },
userMRuntime: {
paths: ["."],
vendoredExecutableCount: 1,
executionCodes: [],
unstagedExecutionCodes: [],
requiresExternalProcess: false,
},
pythonRuntime: { references: [], remapReferences: [], uiReferences: [], requiresProcess: false },
},
"synthetic runtime-boundary analysis",
);
const mixedUserMBoundary = analyzeIniRuntimeBoundaries({
manifestText: [
"configs/sim/example/test.ini",
"configs/sim/example/test.ngc",
"configs/sim/example/M123",
].join("\n"),
sourceRootRel: "configs/sim/example",
sourceSearchRootRel: "configs/sim",
iniFile: "test.ini",
iniText: [
"[DISPLAY]",
"DISPLAY = axis",
"[RS274NGC]",
"USER_M_PATH = .",
].join("\n"),
executionTexts: ["M123\nM124\n"],
});
assert.equal(
mixedUserMBoundary.recommendedBlockedKind,
"L4-USER-M-PROCESS",
"mixed vendored and unstaged user-M calls must block",
);
assert.deepEqual(
mixedUserMBoundary.userMRuntime.executionCodes,
["M123", "M124"],
"mixed user-M execution codes",
);
assert.deepEqual(
mixedUserMBoundary.userMRuntime.unstagedExecutionCodes,
["M124"],
"only unstaged user-M execution codes require the external process boundary",
);
assert.deepEqual(
syntheticPlan.files.map((file) => [file.sourceRel, file.wasmPath]).sort(),
[
@@ -134,6 +213,111 @@ assert.deepEqual(
"generic INI file-context staging plan",
);
function simBoundary(machineRel, iniFile, executionRelFiles = []) {
return analyzeIniRuntimeBoundaries({
manifestText,
sourceRootRel: `configs/sim/${machineRel}`,
sourceSearchRootRel: "configs/sim",
iniFile,
iniText: readSimIni(machineRel, iniFile),
executionTexts: executionRelFiles.map((relFile) =>
readFileSync(resolve(vendorRoot, `configs/sim/${machineRel}/${relFile}`), "utf8"),
),
});
}
const dbDemoBoundary = simBoundary("axis/db_demo", "db_nonran.ini");
assert.deepEqual(
dbDemoBoundary.dependencies,
["python_runtime", "tool_database_process"],
"db_demo tool database boundary dependencies",
);
assert.equal(dbDemoBoundary.recommendedBlockedKind, "L4-TOOL-DB", "db_demo blocked kind");
assert.equal(dbDemoBoundary.toolDatabaseProgram, "./db_nonran.py", "db_demo DB_PROGRAM");
assert.deepEqual(
dbDemoBoundary.pythonRuntime.uiReferences,
["./db_nonran.py"],
"db_demo Python dependency is the DB program process",
);
assert.deepEqual(
dbDemoBoundary.pythonRuntime.remapReferences,
[],
"db_demo does not declare Python remap runtime",
);
const millturnBoundary = simBoundary("axis/vismach/millturn", "millturn.ini", [
"example.ngc",
"remap_subs/428remap.ngc",
"remap_subs/429remap.ngc",
]);
assert.equal(
millturnBoundary.recommendedBlockedKind,
"L4-USER-M-PROCESS",
"millturn blocked kind",
);
assert.deepEqual(
millturnBoundary.dependencies,
["external_user_m_process", "hal_process", "halui_mdi_process", "ui_process"],
"millturn full-process boundary dependencies",
);
assert.equal(
millturnBoundary.userMRuntime.vendoredExecutableCount,
0,
"millturn external user-M files are not staged",
);
assert.deepEqual(
millturnBoundary.userMRuntime.executionCodes,
["M128", "M129"],
"millturn execution chain calls external user-M processes",
);
assert.deepEqual(
millturnBoundary.userMRuntime.unstagedExecutionCodes,
["M128", "M129"],
"millturn execution chain calls unstaged user-M processes",
);
const pumaBoundary = simBoundary("axis/vismach/puma", "puma_cube.ini");
assert.equal(pumaBoundary.recommendedBlockedKind, "-", "puma_cube representative is not blocked");
assert.equal(pumaBoundary.userMRuntime.vendoredExecutableCount, 0, "puma_cube has no vendored user-M processes");
assert.equal(pumaBoundary.halRuntime.requiresProcess, true, "puma_cube declares HAL process files");
assert.equal(pumaBoundary.haluiRuntime.requiresProcess, true, "puma_cube declares HALUI MDI commands");
const melfaBoundary = simBoundary("axis/vismach/melfa-sim", "melfa.ini");
assert.equal(melfaBoundary.recommendedBlockedKind, "-", "melfa representative is not blocked");
assert.deepEqual(
melfaBoundary.dependencies,
["hal_process", "halui_mdi_process", "ui_process"],
"melfa declares HAL/UI/HALUI process dependencies",
);
assert.equal(melfaBoundary.pythonRuntime.requiresProcess, false, "melfa representative has no Python runtime");
const woodpeckerBoundary = simBoundary("woodpecker", "woodpecker.ini");
assert.equal(woodpeckerBoundary.recommendedBlockedKind, "-", "woodpecker representative is not blocked");
assert.deepEqual(
woodpeckerBoundary.dependencies,
["hal_process", "ui_process"],
"woodpecker declares HAL/UI process dependencies",
);
assert.equal(woodpeckerBoundary.haluiRuntime.requiresProcess, false, "woodpecker representative has no HALUI MDI");
const gladevcpBoundary = simBoundary("axis/gladevcp", "gladevcp_panel.ini");
assert.equal(gladevcpBoundary.recommendedBlockedKind, "-", "gladevcp probe representative is not blocked");
assert.ok(
gladevcpBoundary.dependencies.includes("ui_process"),
"gladevcp declares UI process dependency",
);
assert.equal(gladevcpBoundary.pythonRuntime.requiresProcess, true, "gladevcp declares Python UI handler");
assert.deepEqual(
gladevcpBoundary.pythonRuntime.uiReferences,
["-u hitcounter.py manual-example.ui"],
"gladevcp Python dependency is a UI handler",
);
assert.deepEqual(
gladevcpBoundary.pythonRuntime.remapReferences,
[],
"gladevcp representative must not be treated as Python remap runtime",
);
function simMachine(machineRel, iniFile, programFile = null) {
const iniText = readFileSync(
resolve(vendorRoot, `configs/sim/${machineRel}/${iniFile}`),
@@ -158,6 +342,23 @@ function simMachine(machineRel, iniFile, programFile = null) {
};
}
function wrapSimProgram(machine, wrappedName) {
const sourceFile = machine.files.find((file) => file.path === machine.programPath);
assert.ok(sourceFile, `${machine.programPath}: wrapped source file`);
return {
...machine,
programPath: `${machine.wasmDir}/${wrappedName}`,
files: [
...machine.files,
{
path: `${machine.wasmDir}/${wrappedName}`,
text: `${sourceFile.text}\nM2\n`,
executable: false,
},
],
};
}
const foam = simMachine("axis/foam", "axis_foam.ini", "foam.ngc");
verifyExpectedOutput(
"sim_axis_foam_uv_wasm",
@@ -225,6 +426,49 @@ verifyExpectedOutput(
].join("\n"),
);
const gladevcpProbe = simMachine("axis/gladevcp", "gladevcp_panel.ini", "probe.ngc");
verifyExpectedOutput(
"sim_axis_gladevcp_probe_wasm",
interp.runSimConfigProgram({
files: gladevcpProbe.files,
programPath: gladevcpProbe.programPath,
iniPath: gladevcpProbe.iniPath,
}),
[
"file_open=0",
"file_saw_error=0",
"canon_event=PROGRAM_END",
"absent=file_error_text=",
].join("\n"),
);
const externalOffsetsMacro = wrapSimProgram(
simMachine("axis/external_offsets", "dynamic_offsets.ini", "circles.ngc"),
"__wrapped_circles.ngc",
);
verifyExpectedOutput(
"sim_axis_external_offsets_circles_macro_wasm",
interp.runSimConfigProgram({
files: externalOffsetsMacro.files,
programPath: externalOffsetsMacro.programPath,
iniPath: externalOffsetsMacro.iniPath,
}),
[
"file_open=0",
"file_execute_45=1",
"statement_uri=M2",
"file_read_count=45",
"file_execute_count=45",
"file_saw_error=0",
"canon_event=COMMENT: info: Multiple circles at radius R, use 0 for current val",
"canon_event=PROGRAM_END",
"post_execute.origin_index=1",
"post_execute.mist=0",
"post_execute.flood=0",
"absent=file_error_text=",
].join("\n"),
);
for (const externalOffsetCase of [
{
name: "dyn_demo",
@@ -287,4 +531,62 @@ for (const externalOffsetCase of [
);
}
const melfaSim = simMachine("axis/vismach/melfa-sim", "melfa.ini", "example.ngc");
verifyExpectedOutput(
"sim_axis_vismach_melfa_wasm",
interp.runSimConfigProgram({
files: melfaSim.files,
programPath: melfaSim.programPath,
iniPath: melfaSim.iniPath,
executionMode: "fiveAxisRemap",
}),
[
"fiveaxis_ini_open=1",
"fiveaxis_tool_table_load=0",
"fiveaxis_file_open=0",
"fiveaxis_file_reached_exit=1",
"fiveaxis_linuxcnc_remap_file_execute=1",
].join("\n"),
);
const pumaCube = simMachine("axis/vismach/puma", "puma_cube.ini", "puma_cube.ngc");
verifyExpectedOutput(
"sim_axis_vismach_puma_cube_wasm",
interp.runSimConfigProgram({
files: pumaCube.files,
programPath: pumaCube.programPath,
iniPath: pumaCube.iniPath,
}),
[
"file_open=0",
"file_saw_error=0",
"canon_event=PROGRAM_END",
"absent=file_error_text=",
].join("\n"),
);
const woodpeckerOnAbort = simMachine("woodpecker", "woodpecker.ini", "on_abort.ngc");
verifyExpectedOutput(
"sim_woodpecker_on_abort_wasm",
interp.runSimConfigProgram({
files: woodpeckerOnAbort.files,
programPath: woodpeckerOnAbort.programPath,
iniPath: woodpeckerOnAbort.iniPath,
}),
[
"file_open=0",
"run_step phase=execute step=17 rc=1 line=17",
"statement_uri=m2",
"file_read_count=17",
"file_execute_count=17",
"file_saw_error=0",
"canon_event=ON_RESET",
"canon_event=PROGRAM_END",
"post_execute.origin_index=1",
"post_execute.mist=0",
"post_execute.flood=0",
"absent=file_error_text=",
].join("\n"),
);
console.log("sim_configs_wasm_node_smoke=ok");