139 lines
7.8 KiB
JavaScript
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();
|