按规划继续工作

结论:解释器 WASM、SDK 统一入口、浏览器解释器 smoke 与兼容性文档已闭环,native 和 host/WASM/browser 验证全部通过。
This commit is contained in:
2026-06-08 07:24:06 +08:00
parent c28b629ff6
commit 706fd1e775
20 changed files with 629 additions and 122 deletions

View File

@@ -7,7 +7,7 @@
<body>
<pre id="status">running</pre>
<script type="module">
import { createLinuxCncIniSdk } from "../../runtime/sdk/src/linuxcnc-ini.js";
import { createLinuxCncIniSdk } from "../../runtime/sdk/src/index.js";
import { loadTextFile, saveTextFile } from "../../runtime/opfs/file-service.js";
import { machineIniPath } from "../../runtime/opfs/path-model.js";
import {

View File

@@ -0,0 +1,138 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>LinuxCNC Interpreter Browser Smoke</title>
</head>
<body>
<pre id="status">running</pre>
<script type="module">
import { createLinuxCncInterpSdk } from "../../runtime/sdk/src/index.js";
const status = document.getElementById("status");
async function fetchText(path) {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`${path}: HTTP ${response.status}`);
}
return response.text();
}
function verifyExpectedOutput(fixtureName, output, expectedText) {
const expectedLines = expectedText.split("\n").filter(Boolean);
for (const expectedLine of expectedLines) {
if (expectedLine.startsWith("absent=")) {
const forbidden = expectedLine.slice("absent=".length);
if (output.includes(forbidden)) {
throw new Error(`${fixtureName}: unexpected ${forbidden}`);
}
continue;
}
if (!output.includes(expectedLine)) {
throw new Error(`${fixtureName}: missing ${expectedLine}`);
}
}
}
try {
const interp = await createLinuxCncInterpSdk({
locateFile(path) {
if (path === "linuxcnc_interp.wasm") {
return "../../build/wasm/core/linuxcnc_interp.wasm";
}
return path;
},
print() {},
printErr(message) {
console.error(message);
},
});
for (const fixtureName of [
"minimal_linear",
"arc_semantics",
"length_units",
"modal_incremental",
"plane_selection",
"coordinate_offsets",
"g53_machine_coordinates",
"feed_control_modes",
"canned_cycles",
"numbered_params",
"comment_logging",
"tool_semantics",
"tool_table_setup",
"probe_semantics",
"spindle_orient",
"cutter_comp_motion",
"threading_sync",
"nurbs_g5_semantics",
"nurbs_g6_semantics",
"state_tag_motion",
"canon_runtime_edges",
"tool_reload",
"program_end_modal_reset",
]) {
const programText = await fetchText(`../fixtures/gcode/${fixtureName}.ngc`);
const expectedText = await fetchText(`../fixtures/canon/${fixtureName}.events`);
verifyExpectedOutput(fixtureName, interp.runProgram(programText), expectedText.trimEnd());
}
for (const errorFixtureName of [
"g1_zero_feed",
"arc_radius_mismatch",
"arc_zero_radius",
"g53_incremental",
"cutter_comp_plane_change",
"namedparam_readonly",
"numbered_param_readonly",
"tool_length_offset_not_found",
"tool_not_found",
]) {
verifyExpectedOutput(
errorFixtureName,
interp.runProgram(await fetchText(`../fixtures/gcode_errors/${errorFixtureName}.ngc`)),
(await fetchText(`../fixtures/canon_errors/${errorFixtureName}.expected`)).trimEnd(),
);
}
const positionParamsText = await fetchText("../fixtures/gcode/position_params.ngc");
const positionParamsPath = "/work/position_params.ngc";
interp.writeTextFile(positionParamsPath, positionParamsText);
verifyExpectedOutput(
"position_params_file",
interp.runFile(positionParamsPath),
(await fetchText("../fixtures/canon_file/position_params.events")).trimEnd(),
);
for (const fileFixtureName of ["file_open_reset", "percent_file_finish", "oword_subroutine"]) {
const programPath = `/work/${fileFixtureName}.ngc`;
interp.writeTextFile(programPath, await fetchText(`../fixtures/gcode/${fileFixtureName}.ngc`));
verifyExpectedOutput(
`${fileFixtureName}_file`,
interp.runFile(programPath),
(await fetchText(`../fixtures/canon/${fileFixtureName}.events`)).trimEnd(),
);
}
const namedParamIniPath = "/work/namedparams.ini";
interp.writeTextFile(namedParamIniPath, await fetchText("../fixtures/ini/namedparams.ini"));
const namedParamPath = "/work/namedparam_semantics.ngc";
interp.writeTextFile(
namedParamPath,
await fetchText("../fixtures/gcode/namedparam_semantics.ngc"),
);
verifyExpectedOutput(
"namedparam_semantics_file",
interp.runFileWithIni(namedParamPath, namedParamIniPath),
(await fetchText("../fixtures/canon/namedparam_semantics.events")).trimEnd(),
);
status.textContent = "browser_interp_smoke=ok";
} catch (error) {
status.textContent = `browser_interp_smoke=fail ${error.stack || error.message}`;
}
</script>
</body>
</html>

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
CHROMIUM="${CHROMIUM:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || command -v google-chrome-stable || true)}"
if [[ -z "$CHROMIUM" ]]; then
echo "missing Chromium-compatible browser; set CHROMIUM=/path/to/browser" >&2
exit 1
fi
if [[ "${SKIP_INTERP_BUILD:-0}" != "1" ]]; then
"$ROOT_DIR/tools/build_wasm_core.sh"
fi
TMP_DIR="$(mktemp -d)"
PORT_FILE="$TMP_DIR/port"
SERVER_LOG="$TMP_DIR/server.log"
CHROME_PROFILE="$TMP_DIR/chrome-profile"
mkdir -p "$CHROME_PROFILE"
cleanup() {
if [[ -n "${SERVER_PID:-}" ]]; then
kill "$SERVER_PID" 2>/dev/null || true
wait "$SERVER_PID" 2>/dev/null || true
fi
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
python3 - <<'PY' "$ROOT_DIR" "$PORT_FILE" >"$SERVER_LOG" 2>&1 &
import functools
import http.server
import pathlib
import socketserver
import sys
root = pathlib.Path(sys.argv[1])
port_file = pathlib.Path(sys.argv[2])
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(root))
with socketserver.TCPServer(("127.0.0.1", 0), handler) as httpd:
port_file.write_text(str(httpd.server_address[1]), encoding="ascii")
httpd.serve_forever()
PY
SERVER_PID=$!
for _ in $(seq 1 100); do
[[ -s "$PORT_FILE" ]] && break
sleep 0.05
done
if [[ ! -s "$PORT_FILE" ]]; then
echo "browser interpreter smoke HTTP server did not start" >&2
cat "$SERVER_LOG" >&2 || true
exit 1
fi
PORT="$(cat "$PORT_FILE")"
URL="http://127.0.0.1:$PORT/tests/browser/interp_smoke.html"
OUT="$TMP_DIR/chromium.out"
"$CHROMIUM" \
--headless=new \
--disable-gpu \
--no-sandbox \
--user-data-dir="$CHROME_PROFILE" \
--virtual-time-budget=10000 \
--dump-dom \
"$URL" >"$OUT" 2>&1
if ! grep -Fq "browser_interp_smoke=ok" "$OUT"; then
echo "browser interpreter smoke failed" >&2
sed -n '1,220p' "$OUT" >&2
exit 1
fi
echo "browser_interp_smoke=ok"

View File

@@ -0,0 +1,15 @@
file_open=0
file_read_count=3
file_execute_count=2
canon_event=STRAIGHT_TRAVERSE line=1 x=1 y=2 z=3 a=0 b=0 c=0 u=0 v=0 w=0
canon_event=SET_FEED_RATE rate=80
canon_event=COMMENT: interpreter: distance mode changed to incremental
canon_event=STRAIGHT_FEED line=2 x=1.5 y=1 z=5 a=0 b=0 c=0 u=0 v=0 w=0
setup.current_x=1.5
setup.current_y=1
setup.current_z=5
setup.parameter_5420=1.5
setup.parameter_5421=1
setup.parameter_5422=5
post_execute.distance_mode=1
post_execute.motion_mode=10

View File

@@ -9,5 +9,6 @@ SKIP_INI_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_ini_wasm.sh"
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/wasm/node/verify_interp_wasm.sh"
"$ROOT_DIR/tests/opfs/node/verify_file_service.sh"
SKIP_INI_BUILD=1 "$ROOT_DIR/tests/browser/verify_ini_panel_browser.sh"
SKIP_INTERP_BUILD=1 "$ROOT_DIR/tests/browser/verify_interp_browser.sh"
echo "host_wasm_opfs_browser_smokes=ok"

View File

@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import assert from "node:assert/strict";
import { createLinuxCncIniSdk } from "../../../runtime/sdk/src/linuxcnc-ini.js";
import { createLinuxCncIniSdk } from "../../../runtime/sdk/src/index.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

View File

@@ -3,69 +3,7 @@ import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import assert from "node:assert/strict";
import createLinuxCncInterpModule from "../../../build/wasm/core/linuxcnc_interp.js";
function allocCString(mod, value) {
const bytes = mod.lengthBytesUTF8(value) + 1;
const ptr = mod._malloc(bytes);
mod.stringToUTF8(value, ptr, bytes);
return ptr;
}
function runProgram(mod, programText) {
const programPtr = allocCString(mod, programText);
let resultPtr = 0;
try {
resultPtr = mod._lcinterp_run_program(programPtr);
assert.notEqual(resultPtr, 0);
return mod.UTF8ToString(resultPtr);
} finally {
if (resultPtr) {
mod._lcinterp_free_string(resultPtr);
}
mod._free(programPtr);
}
}
function runProgramWithIni(mod, programText, iniPath) {
const programPtr = allocCString(mod, programText);
const iniPathPtr = allocCString(mod, iniPath);
let resultPtr = 0;
try {
resultPtr = mod._lcinterp_run_program_with_ini(programPtr, iniPathPtr);
assert.notEqual(resultPtr, 0);
return mod.UTF8ToString(resultPtr);
} finally {
if (resultPtr) {
mod._lcinterp_free_string(resultPtr);
}
mod._free(programPtr);
mod._free(iniPathPtr);
}
}
function ensureDir(mod, path) {
try {
mod.FS.mkdir(path);
} catch {
// Directory already exists.
}
}
function runFile(mod, path) {
const pathPtr = allocCString(mod, path);
let resultPtr = 0;
try {
resultPtr = mod._lcinterp_run_file(pathPtr);
assert.notEqual(resultPtr, 0);
return mod.UTF8ToString(resultPtr);
} finally {
if (resultPtr) {
mod._lcinterp_free_string(resultPtr);
}
mod._free(pathPtr);
}
}
import { createLinuxCncInterpSdk } from "../../../runtime/sdk/src/index.js";
function verifyExpectedOutput(fixtureName, output, expectedText) {
const expectedLines = expectedText.split("\n").filter(Boolean);
@@ -94,7 +32,7 @@ const __dirname = dirname(__filename);
const rootDir = resolve(__dirname, "../../..");
const wasmPath = resolve(rootDir, "build/wasm/core/linuxcnc_interp.wasm");
const interp = await createLinuxCncInterpModule({
const interp = await createLinuxCncInterpSdk({
wasmBinary: readFileSync(wasmPath),
print() {},
printErr(message) {
@@ -139,15 +77,13 @@ for (const fixtureName of fixtureNames) {
"utf8",
).trimEnd();
verifyExpectedOutput(fixtureName, runProgram(interp, programText), expectedEvents);
verifyExpectedOutput(fixtureName, interp.runProgram(programText), expectedEvents);
}
ensureDir(interp, "/work");
const namedParamIniPath = "/work/namedparams.ini";
interp.FS.writeFile(
interp.writeTextFile(
namedParamIniPath,
readFileSync(resolve(rootDir, "tests/fixtures/ini/namedparams.ini"), "utf8"),
{ encoding: "utf8" },
);
const namedParamProgramText = readFileSync(
resolve(rootDir, "tests/fixtures/gcode/namedparam_semantics.ngc"),
@@ -159,7 +95,7 @@ const namedParamExpected = readFileSync(
).trimEnd();
verifyExpectedOutput(
"namedparam_semantics",
runProgramWithIni(interp, namedParamProgramText, namedParamIniPath),
interp.runProgramWithIni(namedParamProgramText, namedParamIniPath),
namedParamExpected,
);
@@ -185,7 +121,7 @@ for (const fixtureName of errorFixtureNames) {
"utf8",
).trimEnd();
verifyExpectedOutput(fixtureName, runProgram(interp, programText), expectedOutput);
verifyExpectedOutput(fixtureName, interp.runProgram(programText), expectedOutput);
}
const fileFixtureNames = [
@@ -228,7 +164,29 @@ for (const fixtureName of fileFixtureNames) {
"utf8",
).trimEnd();
interp.FS.writeFile(programPath, programText, { encoding: "utf8" });
verifyExpectedOutput(fixtureName, runFile(interp, programPath), expectedEvents);
interp.writeTextFile(programPath, programText);
verifyExpectedOutput(fixtureName, interp.runFile(programPath), expectedEvents);
}
const namedParamFilePath = "/work/namedparam_semantics.ngc";
interp.writeTextFile(namedParamFilePath, namedParamProgramText);
verifyExpectedOutput(
"namedparam_semantics_file",
interp.runFileWithIni(namedParamFilePath, namedParamIniPath),
namedParamExpected,
);
const positionParamsFilePath = "/work/position_params.ngc";
interp.writeTextFile(
positionParamsFilePath,
readFileSync(resolve(rootDir, "tests/fixtures/gcode/position_params.ngc"), "utf8"),
);
verifyExpectedOutput(
"position_params_file",
interp.runFile(positionParamsFilePath),
readFileSync(
resolve(rootDir, "tests/fixtures/canon_file/position_params.events"),
"utf8",
).trimEnd(),
);
console.log("interp_wasm_node_smoke=ok");