Files
linuxcncWebWASM/web/worker/test_source_interpreter_s5.mjs

139 lines
7.8 KiB
JavaScript

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();