import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { HalRuntime, parseCompSource } from "./hal-runtime.mjs"; import { fnv1a64 } from "./replay.mjs"; const ROOT = new URL("../../", import.meta.url); const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1"; async function read(path) { return readFile(new URL(path, ROOT)); } async function fixture(paths) { const files = []; for (const path of paths) { const value = await read(path); files.push({ path, bytes: value.byteLength, sha256: createHash("sha256").update(value).digest("hex") }); } return files; } function pendingCommands(source) { return source.split(/\r?\n/).map((line) => line.trim()).filter((line) => /^(?:loadusr|start|loadrt\s+(?:sampler|streamer)\b)/i.test(line)); } function resultHash(value) { return fnv1a64(value); } async function runAbs(path) { const source = (await read(path)).toString("utf8"); const hal = new HalRuntime(); assert.equal(hal.configure({ threads: [{ name: "fast", period_ticks: 1 }], components: [{ instance: "abs.0", type: "abs", thread: "fast" }] }).status, "verified"); assert.equal(hal.setPin("abs.0.in", -0.25).status, "verified"); assert.equal(hal.step(1).status, "verified"); const result = { tick: hal.state.tick, out: hal.getPin("abs.0.out").value, sign: hal.getPin("abs.0.sign").value, positive: hal.getPin("abs.0.is-positive").value, negative: hal.getPin("abs.0.is-negative").value, state_hash: hal.hash(), pending_commands: pendingCommands(source) }; assert.equal(result.out, 0.25); assert.equal(result.sign, 1); assert.equal(result.negative, 1); return result; } async function runEdge(path, startingHigh = false) { const source = (await read(path)).toString("utf8"); const hal = new HalRuntime(); assert.equal(hal.configure({ threads: [{ name: "fast", period_ticks: 1 }], components: [{ instance: "edge.0", type: "edge", thread: "fast" }] }).status, "verified"); assert.equal(hal.setParam("edge.0.in-edge", 0).status, "verified"); assert.equal(hal.setPin("edge.0.in", startingHigh ? 1 : 0).status, "verified"); assert.equal(hal.step(1).status, "verified"); const first = hal.getPin("edge.0.out").value; assert.equal(hal.setPin("edge.0.in", 1).status, "verified"); assert.equal(hal.step(2).status, "verified"); const result = { tick: hal.state.tick, first, rising_output: hal.getPin("edge.0.out").value, last_input: hal.getParam("edge.0.last-in").value, state_hash: hal.hash(), pending_commands: pendingCommands(source) }; assert.equal(result.rising_output, startingHigh ? 0 : 1); return result; } async function parseComponent(path, expected, requireSupported = true) { const source = (await read(path)).toString("utf8"); const parsed = parseCompSource(source); if (requireSupported) assert.equal(parsed.status, "verified"); assert.equal(parsed.component, expected); return { status: parsed.status, component: parsed.component, pins: parsed.pins.length, params: parsed.params.length, functions: parsed.functions.length, options: parsed.options, state_hash: resultHash(parsed) }; } async function main() { const cases = []; const add = async (id, adapter, paths, result, status = "PASSED", reason = null) => cases.push({ id, adapter, status, ...(reason ? { reason } : {}), fixture: await fixture(paths), result, result_hash: resultHash(result) }); const absPath = "linuxcnc/tests/abs.0/test.hal"; await add("source-test:linuxcnc/tests/abs.0/test.hal", "WORKER_SIM_ADAPTER", [absPath, "linuxcnc/tests/abs.0/expected"], await runAbs(absPath)); const edgePath = "linuxcnc/tests/edge/rising/test.hal"; await add("source-test:linuxcnc/tests/edge/rising/test.hal", "WORKER_SIM_ADAPTER", [edgePath, "linuxcnc/tests/edge/rising/expected"], await runEdge(edgePath)); const edgeHighPath = "linuxcnc/tests/edge/rising-starting-high/test.hal"; await add("source-test:linuxcnc/tests/edge/rising-starting-high/test.hal", "WORKER_SIM_ADAPTER", [edgeHighPath, "linuxcnc/tests/edge/rising-starting-high/expected"], await runEdge(edgeHighPath, true)); const namesMatch = "linuxcnc/tests/halcompile/names/names_match.comp"; const namesDontMatch = "linuxcnc/tests/halcompile/names/names_dont_match.comp"; const namesResult = { matched: await parseComponent(namesMatch, "names_match", false), mismatch_parser: (await parseComponent(namesDontMatch, "wrong_name", false)).status, mismatch_expected: "compiler_component_name_mismatch" }; await add("source-test:linuxcnc/tests/halcompile/names/test.sh", "SOURCE_BLOCKED", [namesMatch, namesDontMatch, "linuxcnc/tests/halcompile/names/expected", "linuxcnc/tests/halcompile/names/test.sh"], namesResult, "SOURCE_BLOCKED", "custom .comp declarations require native halcompile and are not static Worker components"); const relative = "linuxcnc/tests/halcompile/relative-header/relative_header.comp"; const relativeParsed = parseCompSource((await read(relative)).toString("utf8")); await add("source-test:linuxcnc/tests/halcompile/relative-header/test.sh", "SOURCE_BLOCKED", [relative, "linuxcnc/tests/halcompile/relative-header/local.h", "linuxcnc/tests/halcompile/relative-header/checkresult", "linuxcnc/tests/halcompile/relative-header/test.sh"], { parser_status: relativeParsed.status, parser_component: relativeParsed.component, reason: "native halcompile and relative header compiler unavailable" }, "SOURCE_BLOCKED", "native halcompile and relative header compiler unavailable"); const userspace = "linuxcnc/tests/halcompile/userspace-count-names/userspace_count_names.comp"; const userspaceParsed = parseCompSource((await read(userspace)).toString("utf8")); await add("source-test:linuxcnc/tests/halcompile/userspace-count-names/test.sh", "HOST_EXCLUDED", [userspace, "linuxcnc/tests/halcompile/userspace-count-names/checkresult", "linuxcnc/tests/halcompile/userspace-count-names/test.sh"], { parser_status: userspaceParsed.status, parser_component: userspaceParsed.component, options: userspaceParsed.options, reason: "userspace component requires host process and signal APIs" }, "HOST_EXCLUDED", "userspace component requires host process and signal APIs"); for (const [directory, component] of [["symbols.0", "test_define"], ["symbols.1", "test_define1"]]) { const path = `linuxcnc/tests/${directory}/test_${directory.slice(-1) === "0" ? "define" : "define1"}.comp`; const parsed = parseCompSource((await read(path)).toString("utf8")); await add(`source-test:linuxcnc/tests/${directory}/test.sh`, "HOST_EXCLUDED", [path, `linuxcnc/tests/${directory}/checkresult`, `linuxcnc/tests/${directory}/test.sh`], { parser_status: parsed.status, parser_component: parsed.component, reason: "source test invokes host halcompile/rtapi component compiler" }, "HOST_EXCLUDED", "source test invokes host halcompile/rtapi component compiler"); } const counts = Object.fromEntries(["PASSED", "SOURCE_BLOCKED", "HOST_EXCLUDED"].map((status) => [status, cases.filter((item) => item.status === status).length])); const output = { schema: "linuxcnc-source-hal-adapter.v1", context_id: CONTEXT_ID, cases, counts, fixture_file_count: new Set(cases.flatMap((item) => item.fixture.map((file) => file.path))).size, boundaries: [ "sampler/streamer/loadusr and realtime compiler paths remain explicit host/resource boundaries", "userspace and dynamic .comp execution is HOST_EXCLUDED; relative-header compiler is SOURCE_BLOCKED", "Worker static HAL behavior is compared with native/WASM SimHAL evidence by the report generator", ], }; output.aggregate_hash = resultHash({ cases: output.cases, counts: output.counts }); console.log(JSON.stringify(output)); } await main();