import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import createModule from "../../build-wasm/linuxcnc-sim-core.js"; import { fnv1a64 } from "./replay.mjs"; import { ProgramRunner } from "./program-runner.mjs"; import { PythonCallbacks } from "./python-callbacks.mjs"; const ROOT = new URL("../../", import.meta.url); const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1"; async function bytes(path) { return readFile(new URL(path, ROOT)); } async function fixture(paths) { const files = []; for (const path of paths) { const value = await bytes(path); files.push({ path, bytes: value.byteLength, sha256: createHash("sha256").update(value).digest("hex") }); } return files; } function hash(value) { return fnv1a64(value); } async function runProgram(core, path, expectedStatus, expectedDetail = null) { const source = (await bytes(path)).toString("utf8"); const runner = new ProgramRunner(core); const loaded = runner.load(source, path); let observed = loaded; if (loaded.status !== "error") { const started = runner.start(); assert.equal(started.status, "verified"); let guard = 0; while (runner.status === "running" && guard < 2000) { observed = runner.step(); guard += 1; } assert.ok(guard < 2000, `${path} exceeded deterministic control budget`); const snapshot = runner.snapshot(); observed = observed ?? snapshot; } const result = { expected_status: expectedStatus, observed_status: runner.status === "error" ? "error" : runner.status, observed_detail: runner.lastError?.code ?? observed?.detail ?? observed?.code ?? null, line: runner.currentLine, canonical: runner.snapshot().canonical, interpreter_state_hash: runner.snapshot().interpreter_state_hash, source_hash: runner.snapshot().source_hash, }; assert.equal(result.observed_status, expectedStatus, path); if (expectedDetail !== null) assert.equal(result.observed_detail, expectedDetail, path); runner.destroy(); return result; } async function main() { const core = await createModule({ wasmBinary: await bytes("build-wasm/linuxcnc-sim-core.wasm") }); const cases = []; const add = (id, adapter, paths, result) => cases.push({ id, adapter, fixture: paths, result, result_hash: hash(result) }); const owordBug = "linuxcnc/tests/interp/oword-bug315/test.ngc"; const owordBugResult = await runProgram(core, owordBug, "completed"); add("source-test:linuxcnc/tests/interp/oword-bug315/test.sh", "WORKER_SIM_ADAPTER", [owordBug, "linuxcnc/tests/interp/oword-bug315/expected", "linuxcnc/tests/interp/oword-bug315/test.sh"], owordBugResult); const owordSub = "linuxcnc/tests/oword/sub.0/test.ngc"; const owordSubResult = await runProgram(core, owordSub, "completed"); add("source-test:linuxcnc/tests/oword/sub.0/test.sh", "WORKER_SIM_ADAPTER", [owordSub, "linuxcnc/tests/oword/sub.0/expected", "linuxcnc/tests/oword/sub.0/test.sh"], owordSubResult); const unwind = "linuxcnc/tests/interp/oword-unwind/test.ngc"; const unwindResult = await runProgram(core, unwind, "error", "program_subroutine_missing"); add("source-test:linuxcnc/tests/interp/oword-unwind/test.sh", "PURE_FUNCTIONAL", [unwind, "linuxcnc/tests/interp/oword-unwind/expected", "linuxcnc/tests/interp/oword-unwind/test.sh"], unwindResult); const doWhile = "linuxcnc/tests/interp/do-while-break/test.ngc"; const doWhileResult = await runProgram(core, doWhile, "completed"); add("source-test:linuxcnc/tests/interp/do-while-break/test.sh", "WORKER_SIM_ADAPTER", [doWhile, "linuxcnc/tests/interp/do-while-break/expected", "linuxcnc/tests/interp/do-while-break/test.sh"], doWhileResult); const g6164 = "linuxcnc/tests/interp/g6164/test.ngc"; const g6164Result = await runProgram(core, g6164, "completed"); add("source-test:linuxcnc/tests/interp/g6164/test.sh", "WORKER_SIM_ADAPTER", [g6164, "linuxcnc/tests/interp/g6164/expected", "linuxcnc/tests/interp/g6164/test.sh"], g6164Result); const callbacks = new PythonCallbacks(); const squarePy = "linuxcnc/tests/remap/oword-pycall/oword.py"; assert.equal(callbacks.registerRemap("square", { prolog: "square" }, { prolog: ({ args }) => args[0] * args[0] }).status, "verified"); assert.equal(callbacks.registerRemap("multiply", { prolog: "multiply" }, { prolog: ({ args }) => args.reduce((left, right) => left * right, 1) }).status, "verified"); const square = callbacks.invoke("remap", "square", "prolog", [25]); const multiply = callbacks.invoke("remap", "multiply", "prolog", [5, 6, 7]); assert.equal(square.value, 625); assert.equal(multiply.value, 210); const remapResult = { square: square.value, multiply: multiply.value, callbacks: callbacks.state.calls.length, state_hash: callbacks.hash() }; add("source-test:linuxcnc/tests/remap/oword-pycall/test.sh", "WORKER_SIM_ADAPTER", [squarePy, "linuxcnc/tests/remap/oword-pycall/subs.py", "linuxcnc/tests/remap/oword-pycall/test.ini", "linuxcnc/tests/remap/oword-pycall/expected"], remapResult); const failPy = "linuxcnc/tests/remap/fail/body-py/remap.py"; const failureCallbacks = new PythonCallbacks(); assert.equal(failureCallbacks.registerRemap("M400", { prolog: "interp_error" }, { prolog: () => { throw new Error("INTERP_ERROR"); } }).status, "verified"); const callbackFailure = failureCallbacks.invoke("remap", "M400", "prolog", []); assert.equal(callbackFailure.code, "callback_exception"); const failureResult = { status: callbackFailure.status, code: callbackFailure.code, state_hash: failureCallbacks.hash() }; add("source-test:linuxcnc/tests/remap/fail/body-py/test.sh", "WORKER_SIM_ADAPTER", [failPy, "linuxcnc/tests/remap/fail/body-py/subs.py", "linuxcnc/tests/remap/fail/body-py/test.ini", "linuxcnc/tests/remap/fail/body-py/expected"], failureResult); const filterPy = "linuxcnc/tests/remap/remap-io/remap.py"; const filterConfig = "linuxcnc/tests/remap/remap-io/test-py.ini"; const filters = new PythonCallbacks(); assert.equal(filters.registerFilter("remap-io", ({ args }) => args[0]).status, "verified"); const filterSource = (await bytes(squarePy)).toString("utf8"); const filterResult = filters.filter("remap-io", filterSource); const blockedFilter = filters.filter("remap-io", "import subprocess"); assert.equal(filterResult.status, "verified"); assert.equal(blockedFilter.code, "real_hardware_excluded"); add("source-test:linuxcnc/tests/remap/remap-io/test-py.ini", "WORKER_SIM_ADAPTER", [filterPy, filterConfig, "linuxcnc/tests/remap/remap-io/expected"], { status: filterResult.status, blocked: blockedFilter.code, state_hash: filters.hash() }); const fixtureRecords = []; for (const item of cases) item.fixture = await fixture(item.fixture); for (const item of cases) fixtureRecords.push(...item.fixture); const uniqueFixtureCount = new Set(fixtureRecords.map((item) => item.path)).size; const counts = { PASSED: cases.filter((item) => item.adapter !== "WORKER_TASK_MOTION_PENDING").length, WORKER_TASK_MOTION_PENDING: cases.filter((item) => item.adapter === "WORKER_TASK_MOTION_PENDING").length }; const output = { schema: "linuxcnc-source-interpreter-adapter.v1", context_id: CONTEXT_ID, cases, counts, fixture_file_count: uniqueFixtureCount, pending: cases.filter((item) => item.adapter === "WORKER_TASK_MOTION_PENDING").map((item) => ({ source_id: item.id, reason: item.result.reason ?? item.result.expected_adapter })), boundaries: [ "G64/G61 modal motion is represented by the Worker motion_control state; upstream canonical logger parity remains SOURCE_BLOCKED", "remap Python handlers are represented by allowlisted PythonCallbacks; host imports remain REAL-EXCLUDED", "expected/checkresult bytes are hashed and never treated as passed without an adapter result", ], }; output.aggregate_hash = hash({ cases: output.cases, counts: output.counts }); console.log(JSON.stringify(output)); } await main();