import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import createModule from "../../build-wasm/linuxcnc-sim-core.js"; import { ProgramRunner } from "./program-runner.mjs"; import { SimSession, CONTEXT_ID } from "./sim-session.mjs"; import { compareEndpoints, createEndpoint } from "./endpoint-replay.mjs"; const sourcePath = "linuxcnc/tests/interp/do-while-break/test.ngc"; const source = await readFile(new URL(`../../${sourcePath}`, import.meta.url), "utf8"); const wasmBinary = await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)); async function complete(core, pauseAt = null) { const runner = new ProgramRunner(core); assert.equal(runner.load(source, sourcePath).status, "verified"); assert.equal(runner.start().status, "verified"); let steps = 0; while (runner.status === "running" && steps < 1000) { runner.step(); steps += 1; if (pauseAt !== null && steps === pauseAt) { assert.equal(runner.pause().status, "verified"); const paused = runner.snapshot(); assert.equal(runner.resume().status, "verified"); assert.equal(paused.program_status, "paused"); } } assert.equal(runner.status, "completed"); assert.ok(steps < 1000); assert.equal(runner.namedParameters.get("one_pass"), 1); assert.equal(runner.namedParameters.get("two_pass"), 1); assert.equal(runner.namedParameters.get("three_pass"), 1); const snapshot = runner.snapshot(); runner.destroy(); return { snapshot, steps }; } const core = await createModule({ wasmBinary }); const normal = await complete(core); const paused = await complete(core, 5); assert.equal(normal.snapshot.interpreter_state_hash, paused.snapshot.interpreter_state_hash); const malformed = new ProgramRunner(core); assert.equal(malformed.load("O100 ENDDO\nM2\n", "malformed.ngc").code, "program_enddo_without_do"); malformed.destroy(); const tooDeep = new ProgramRunner(core); const nested = `${Array.from({ length: 33 }, (_, index) => `O${index + 1} DO`).join("\n")}\nM2\n${Array.from({ length: 33 }, (_, index) => `O${33 - index} WHILE [1]`).join("\n")}`; assert.equal(tooDeep.load(nested, "depth.ngc").code, "program_control_depth"); tooDeep.destroy(); const sessionCore = await createModule({ wasmBinary }); const session = new SimSession(sessionCore, { sessionId: "s7-do-recovery" }); const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: "s7-do-recovery" }; let seq = 0; const send = (kind, operation_id, payload = {}) => { seq += 1; const response = session.accept({ ...base, kind, seq, operation_id, payload }); assert.notEqual(response.kind, "ERROR", `${kind}:${response.payload?.code ?? "error"}`); return response; }; send("HELLO", "hello"); send("SESSION_ATTACH", "attach"); send("LOAD_WORKSPACE", "workspace", { root_path: "/workspace" }); send("PROGRAM_LOAD", "program", { source, path: sourcePath }); send("PROGRAM_RUN", "run"); send("SIM_STEP", "step-a", { cycles: 5 }); const checkpoint = session.createCheckpoint(); send("SIM_STEP", "step-b", { cycles: 10 }); const continuous = createEndpoint({ session }); assert.equal(session.restoreCheckpoint(checkpoint, session.timeline.export(), (operation) => operation).status, "recovered"); const recovered = createEndpoint({ session }); const parity = compareEndpoints(continuous, recovered); assert.equal(parity.equal, true, JSON.stringify(parity)); console.log(JSON.stringify({ schema: "linuxcnc-source-do-s7-01-sample.v1", status: "verified", context_id: CONTEXT_ID, source: { path: sourcePath, source_hash: normal.snapshot.source_hash }, normal: { status: normal.snapshot.program_status, steps: normal.steps, named_parameters: { one_pass: 1, two_pass: 1, three_pass: 1 }, interpreter_state_hash: normal.snapshot.interpreter_state_hash }, pause_resume: { status: paused.snapshot.program_status, steps: paused.steps, hash_equal: normal.snapshot.interpreter_state_hash === paused.snapshot.interpreter_state_hash }, errors: { malformed_enddo: "program_enddo_without_do", depth: "program_control_depth" }, recovery: { equal: parity.equal, difference: parity.difference, checkpoint_hash: checkpoint.checkpoint_hash }, boundaries: ["LinuxCNC canonical expected output remains SOURCE_BLOCKED", "G64/G61 remains S7 follow-up", "host/Python/real hardware paths unchanged"], }));