1069 lines
32 KiB
JavaScript
1069 lines
32 KiB
JavaScript
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
getOpfsRoot,
|
|
loadTextFile,
|
|
saveTextFile,
|
|
} from "../../../runtime/opfs/file-service.js";
|
|
import {
|
|
defaultMachinePaths,
|
|
gcodeProgramPath,
|
|
machineIniPath,
|
|
normalizeOpfsPath,
|
|
parameterFilePath,
|
|
previewCachePath,
|
|
sessionSnapshotPath,
|
|
toolTablePath,
|
|
} from "../../../runtime/opfs/path-model.js";
|
|
import {
|
|
createMachineSessionSnapshotPayload,
|
|
createSessionSnapshot,
|
|
createVirtualHalSessionPayload,
|
|
loadMachineSessionSnapshot,
|
|
loadSessionSnapshot,
|
|
restoreVirtualHalStateFromSessionSnapshot,
|
|
saveMachineSessionSnapshot,
|
|
saveSessionSnapshot,
|
|
validateSessionSnapshot,
|
|
} from "../../../runtime/opfs/snapshot-store.js";
|
|
import {
|
|
createVirtualHalState,
|
|
executeVirtualHalCommand,
|
|
} from "../../../runtime/sdk/src/linuxcnc-hal.js";
|
|
import {
|
|
gcodeFilenameFromProgramPath,
|
|
loadGcodeProgram,
|
|
loadMachineTextFiles,
|
|
machineFilePaths,
|
|
saveGcodeProgram,
|
|
saveMachineTextFiles,
|
|
} from "../../../runtime/opfs/machine-file-store.js";
|
|
import {
|
|
restoreMachineParametersFromOpfs,
|
|
saveMachineParametersToOpfs,
|
|
} from "../../../runtime/opfs/linuxcnc-parameter-bridge.js";
|
|
import {
|
|
loadMachineToolTableFromOpfs,
|
|
saveMachineToolTableToOpfs,
|
|
} from "../../../runtime/opfs/linuxcnc-tool-table-bridge.js";
|
|
import {
|
|
loadMachineSessionFromOpfs,
|
|
} from "../../../runtime/opfs/linuxcnc-machine-session-bridge.js";
|
|
import {
|
|
readMachineSessionReadiness,
|
|
} from "../../../runtime/opfs/machine-session-readiness.js";
|
|
|
|
class MockFileHandle {
|
|
constructor(name) {
|
|
this.name = name;
|
|
this.textValue = "";
|
|
}
|
|
|
|
async createWritable() {
|
|
return {
|
|
write: async (text) => {
|
|
this.textValue = String(text);
|
|
},
|
|
close: async () => {},
|
|
};
|
|
}
|
|
|
|
async getFile() {
|
|
return {
|
|
text: async () => this.textValue,
|
|
};
|
|
}
|
|
}
|
|
|
|
class MockDirectoryHandle {
|
|
constructor(name = "") {
|
|
this.name = name;
|
|
this.dirs = new Map();
|
|
this.files = new Map();
|
|
}
|
|
|
|
async getDirectoryHandle(name, options = {}) {
|
|
if (!this.dirs.has(name)) {
|
|
if (!options.create) {
|
|
throw new Error(`missing directory: ${name}`);
|
|
}
|
|
this.dirs.set(name, new MockDirectoryHandle(name));
|
|
}
|
|
return this.dirs.get(name);
|
|
}
|
|
|
|
async getFileHandle(name, options = {}) {
|
|
if (!this.files.has(name)) {
|
|
if (!options.create) {
|
|
throw new Error(`missing file: ${name}`);
|
|
}
|
|
this.files.set(name, new MockFileHandle(name));
|
|
}
|
|
return this.files.get(name);
|
|
}
|
|
}
|
|
|
|
const root = new MockDirectoryHandle();
|
|
const storage = {
|
|
getDirectory: async () => root,
|
|
};
|
|
|
|
function assertGcodeProgramStoragePath(filename, path) {
|
|
assert.equal(
|
|
path,
|
|
`linuxcnc/gcode/${filename}`,
|
|
`${filename} G-code OPFS path drift`,
|
|
);
|
|
}
|
|
|
|
function assertSessionSnapshotStoragePath(sessionId, path, filename = "snapshot.json") {
|
|
assert.equal(
|
|
path,
|
|
`linuxcnc/sessions/${sessionId}/${filename}`,
|
|
`${sessionId}/${filename} session snapshot OPFS path drift`,
|
|
);
|
|
}
|
|
|
|
function assertPreviewCacheStoragePath(cacheKey, path, filename = "preview.json") {
|
|
assert.equal(
|
|
path,
|
|
`linuxcnc/preview-cache/${cacheKey}/${filename}`,
|
|
`${cacheKey}/${filename} preview cache OPFS path drift`,
|
|
);
|
|
}
|
|
|
|
function assertMachineFileStoragePaths(machineId, paths) {
|
|
assert.deepEqual(
|
|
paths,
|
|
{
|
|
ini: `linuxcnc/machines/${machineId}/machine.ini`,
|
|
toolTable: `linuxcnc/machines/${machineId}/tool.tbl`,
|
|
parameters: `linuxcnc/machines/${machineId}/linuxcnc.var`,
|
|
},
|
|
`${machineId} machine file OPFS path drift`,
|
|
);
|
|
}
|
|
|
|
function assertParameterBridgePaths(
|
|
machineId,
|
|
result,
|
|
wasmPath,
|
|
filename = "linuxcnc.var",
|
|
) {
|
|
assert.equal(
|
|
result.opfsPath,
|
|
`linuxcnc/machines/${machineId}/${filename}`,
|
|
`${machineId}/${filename} parameter bridge OPFS path drift`,
|
|
);
|
|
assert.equal(
|
|
result.wasmPath,
|
|
wasmPath,
|
|
`${machineId}/${filename} parameter bridge WASM path drift`,
|
|
);
|
|
}
|
|
|
|
function assertToolTableBridgePaths(
|
|
machineId,
|
|
result,
|
|
wasmPath,
|
|
filename = "tool.tbl",
|
|
) {
|
|
assert.equal(
|
|
result.opfsPath,
|
|
`linuxcnc/machines/${machineId}/${filename}`,
|
|
`${machineId}/${filename} tool table bridge OPFS path drift`,
|
|
);
|
|
assert.equal(
|
|
result.wasmPath,
|
|
wasmPath,
|
|
`${machineId}/${filename} tool table bridge WASM path drift`,
|
|
);
|
|
}
|
|
|
|
function assertSessionBridgePaths(
|
|
machineId,
|
|
result,
|
|
{
|
|
iniWasmPath = "/work/machine.ini",
|
|
parameterWasmPath = "/work/linuxcnc.var",
|
|
toolTableWasmPath = "/work/tool.tbl",
|
|
} = {},
|
|
) {
|
|
assert.equal(result.machineId, machineId, `${machineId} session machine id drift`);
|
|
assert.equal(
|
|
result.ini.opfsPath,
|
|
`linuxcnc/machines/${machineId}/machine.ini`,
|
|
`${machineId} session INI OPFS path drift`,
|
|
);
|
|
assert.equal(result.ini.wasmPath, iniWasmPath, `${machineId} session INI WASM path drift`);
|
|
assertParameterBridgePaths(machineId, result.parameters, parameterWasmPath);
|
|
assertToolTableBridgePaths(machineId, result.toolTable, toolTableWasmPath);
|
|
}
|
|
|
|
assert.equal(await getOpfsRoot(storage), root);
|
|
|
|
assert.equal(normalizeOpfsPath("linuxcnc/machines/xyzab.ini"), "linuxcnc/machines/xyzab.ini");
|
|
assert.equal(machineIniPath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/machine.ini");
|
|
assert.equal(toolTablePath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/tool.tbl");
|
|
assert.equal(parameterFilePath("xyzab-tdr"), "linuxcnc/machines/xyzab-tdr/linuxcnc.var");
|
|
assertGcodeProgramStoragePath("fixture.ngc", gcodeProgramPath("fixture.ngc"));
|
|
assertGcodeProgramStoragePath(
|
|
"custom-fixture.ngc",
|
|
gcodeProgramPath("custom-fixture.ngc"),
|
|
);
|
|
assert.equal(
|
|
gcodeFilenameFromProgramPath("linuxcnc/gcode/fixture.ngc"),
|
|
"fixture.ngc",
|
|
);
|
|
assert.throws(
|
|
() => gcodeFilenameFromProgramPath("linuxcnc/machines/xyzab-tdr/fixture.ngc"),
|
|
/Invalid G-code program OPFS path/,
|
|
);
|
|
assert.throws(
|
|
() => gcodeFilenameFromProgramPath("linuxcnc/gcode/nested/fixture.ngc"),
|
|
/Invalid G-code program OPFS path/,
|
|
);
|
|
assertPreviewCacheStoragePath("fixture", previewCachePath("fixture"));
|
|
assertPreviewCacheStoragePath(
|
|
"fixture",
|
|
previewCachePath("fixture", "custom-preview.json"),
|
|
"custom-preview.json",
|
|
);
|
|
assertSessionSnapshotStoragePath(
|
|
"session-1",
|
|
sessionSnapshotPath("session-1"),
|
|
);
|
|
assertSessionSnapshotStoragePath(
|
|
"session-1",
|
|
sessionSnapshotPath("session-1", "custom-snapshot.json"),
|
|
"custom-snapshot.json",
|
|
);
|
|
assertMachineFileStoragePaths("xyzab-tdr", defaultMachinePaths("xyzab-tdr"));
|
|
assertMachineFileStoragePaths("xyzab-tdr", machineFilePaths("xyzab-tdr"));
|
|
|
|
const snapshotPayload = createMachineSessionSnapshotPayload("xyzab-tdr", {
|
|
gcodeFilename: "fixture.ngc",
|
|
});
|
|
assert.deepEqual(snapshotPayload, {
|
|
machineId: "xyzab-tdr",
|
|
files: {
|
|
ini: machineIniPath("xyzab-tdr"),
|
|
parameters: parameterFilePath("xyzab-tdr"),
|
|
toolTable: toolTablePath("xyzab-tdr"),
|
|
gcode: gcodeProgramPath("fixture.ngc"),
|
|
},
|
|
});
|
|
let virtualHalState = createVirtualHalState();
|
|
virtualHalState = executeVirtualHalCommand(
|
|
virtualHalState,
|
|
"newsig x-pos HAL_FLOAT\nsetp axis.x.pos-cmd 3.5\nsets x-pos 2.25\nnet x-pos axis.x.pos-cmd joint.0.pos-fb\nloadrt trivkins\nloadusr halui\naddf motion-controller servo-thread\nstart servo-thread",
|
|
).state;
|
|
const virtualHalSessionPayload = createVirtualHalSessionPayload(virtualHalState);
|
|
assert.equal(virtualHalSessionPayload.apiName, "linuxcnc-wasm-virtual-hal-session-payload");
|
|
assert.equal(virtualHalSessionPayload.state.source, "browser-virtual-hal");
|
|
assert.equal(virtualHalSessionPayload.state.hal.signals["x-pos"].value, 2.25);
|
|
assert.equal(virtualHalSessionPayload.state.hal.nets["x-pos"].includes("axis.x.pos-cmd"), true);
|
|
assert.equal(virtualHalSessionPayload.sourceCompliance.complete, true);
|
|
assert.equal(virtualHalSessionPayload.simConfigSourceCoverage.complete, true);
|
|
assert.equal(virtualHalSessionPayload.simConfigPromotionCandidates.complete, true);
|
|
assert.equal(virtualHalSessionPayload.simConfigMacroLoadFixtures.complete, true);
|
|
assert.equal(virtualHalSessionPayload.commandScriptFixtures.complete, true);
|
|
assert.equal(virtualHalSessionPayload.motionControllerMatrix.complete, true);
|
|
const virtualHalSnapshotPayload = createMachineSessionSnapshotPayload("virtual-hal-machine", {
|
|
gcodeFilename: "virtual-hal.ngc",
|
|
virtualHal: virtualHalSessionPayload,
|
|
});
|
|
assert.equal(virtualHalSnapshotPayload.virtualHal.state.hal.signals["x-pos"].value, 2.25);
|
|
assert.equal(virtualHalSnapshotPayload.virtualHal.simConfigMacroLoadFixtures.inventoryBaselineUnchanged, true);
|
|
assert.equal(virtualHalSnapshotPayload.virtualHal.commandScriptFixtures.coveredActions.includes("loadusr"), true);
|
|
assert.equal(virtualHalSnapshotPayload.virtualHal.motionControllerMatrix.sourceFiles.includes("linuxcnc/src/emc/motion/axis.c"), true);
|
|
assert.deepEqual(
|
|
createMachineSessionSnapshotPayload("custom-machine", {
|
|
parameterFilename: "custom.var",
|
|
toolTableFilename: "custom-tool.tbl",
|
|
gcodeOpfsPath: "linuxcnc/gcode/custom-fixture.ngc",
|
|
}),
|
|
{
|
|
machineId: "custom-machine",
|
|
files: {
|
|
ini: "linuxcnc/machines/custom-machine/machine.ini",
|
|
parameters: "linuxcnc/machines/custom-machine/custom.var",
|
|
toolTable: "linuxcnc/machines/custom-machine/custom-tool.tbl",
|
|
gcode: "linuxcnc/gcode/custom-fixture.ngc",
|
|
},
|
|
},
|
|
);
|
|
assert.throws(
|
|
() => createMachineSessionSnapshotPayload("bad-machine", { gcodeFilename: "../escape.ngc" }),
|
|
/Invalid G-code filename/,
|
|
);
|
|
const snapshot = createSessionSnapshot("session-1", snapshotPayload, {
|
|
createdAt: "2026-06-08T00:00:00.000Z",
|
|
metadata: { source: "node-smoke" },
|
|
});
|
|
assert.deepEqual(snapshot, {
|
|
format: "linuxcnc-wasm-session-snapshot",
|
|
version: 1,
|
|
sessionId: "session-1",
|
|
createdAt: "2026-06-08T00:00:00.000Z",
|
|
metadata: { source: "node-smoke" },
|
|
payload: snapshotPayload,
|
|
});
|
|
assert.equal(validateSessionSnapshot(snapshot, "session-1"), snapshot);
|
|
const virtualHalSessionSnapshot = createSessionSnapshot("virtual-hal-session", virtualHalSnapshotPayload, {
|
|
createdAt: "2026-06-08T00:00:00.000Z",
|
|
metadata: { source: "node-virtual-hal-session" },
|
|
});
|
|
const restoredVirtualHalState = restoreVirtualHalStateFromSessionSnapshot(virtualHalSessionSnapshot);
|
|
assert.equal(restoredVirtualHalState.hal.signals["x-pos"].value, 2.25);
|
|
assert.equal(restoredVirtualHalState.hal.loadedComponents.some(({ component }) => component === "halui"), true);
|
|
assert.throws(
|
|
() => validateSessionSnapshot({ ...snapshot, format: "other-format" }, "session-1"),
|
|
/Unsupported session snapshot format/,
|
|
);
|
|
assert.throws(
|
|
() => validateSessionSnapshot({ ...snapshot, version: 99 }, "session-1"),
|
|
/Unsupported session snapshot version/,
|
|
);
|
|
assert.throws(
|
|
() => validateSessionSnapshot({ ...snapshot, sessionId: "other-session" }, "session-1"),
|
|
/Session snapshot id mismatch/,
|
|
);
|
|
assert.throws(
|
|
() => validateSessionSnapshot({ ...snapshot, metadata: null }, "session-1"),
|
|
/snapshot metadata must be a plain object/,
|
|
);
|
|
assert.throws(
|
|
() => validateSessionSnapshot({ ...snapshot, payload: [] }, "session-1"),
|
|
/snapshot payload must be a plain object/,
|
|
);
|
|
|
|
await saveTextFile(
|
|
"linuxcnc/machines/xyzab.ini",
|
|
"[EMC]\nMACHINE = opfs-smoke\n",
|
|
storage,
|
|
);
|
|
assert.equal(
|
|
await loadTextFile("linuxcnc/machines/xyzab.ini", storage),
|
|
"[EMC]\nMACHINE = opfs-smoke\n",
|
|
);
|
|
|
|
await saveSessionSnapshot("session-1", snapshotPayload, {
|
|
storage,
|
|
createdAt: "2026-06-08T00:00:00.000Z",
|
|
metadata: { source: "node-smoke" },
|
|
});
|
|
assertSessionSnapshotStoragePath(
|
|
"session-1",
|
|
sessionSnapshotPath("session-1"),
|
|
);
|
|
assert.deepEqual(await loadSessionSnapshot("session-1", { storage }), snapshot);
|
|
await saveSessionSnapshot("virtual-hal-session", virtualHalSnapshotPayload, {
|
|
storage,
|
|
createdAt: "2026-06-08T00:00:00.000Z",
|
|
metadata: { source: "node-virtual-hal-session" },
|
|
});
|
|
const loadedVirtualHalSessionSnapshot = await loadSessionSnapshot("virtual-hal-session", { storage });
|
|
assert.equal(
|
|
loadedVirtualHalSessionSnapshot.payload.virtualHal.state.hal.signals["x-pos"].value,
|
|
2.25,
|
|
);
|
|
assert.equal(
|
|
restoreVirtualHalStateFromSessionSnapshot(loadedVirtualHalSessionSnapshot).hal.nets["x-pos"].includes("joint.0.pos-fb"),
|
|
true,
|
|
);
|
|
const customSnapshot = await saveSessionSnapshot("session-1", snapshotPayload, {
|
|
storage,
|
|
filename: "custom-snapshot.json",
|
|
createdAt: "2026-06-08T00:00:00.000Z",
|
|
metadata: { source: "node-smoke-custom" },
|
|
});
|
|
assertSessionSnapshotStoragePath(
|
|
"session-1",
|
|
sessionSnapshotPath("session-1", "custom-snapshot.json"),
|
|
"custom-snapshot.json",
|
|
);
|
|
assert.equal(
|
|
customSnapshot.metadata.source,
|
|
"node-smoke-custom",
|
|
);
|
|
assert.deepEqual(
|
|
await loadSessionSnapshot("session-1", {
|
|
storage,
|
|
filename: "custom-snapshot.json",
|
|
}),
|
|
customSnapshot,
|
|
);
|
|
const machineSessionSnapshot = await saveMachineSessionSnapshot(
|
|
"machine-session-1",
|
|
"xyzab-tdr",
|
|
{
|
|
storage,
|
|
filename: "machine-session-snapshot.json",
|
|
gcodeFilename: "fixture.ngc",
|
|
createdAt: "2026-06-08T00:00:00.000Z",
|
|
metadata: { source: "node-machine-session" },
|
|
},
|
|
);
|
|
assertSessionSnapshotStoragePath(
|
|
"machine-session-1",
|
|
sessionSnapshotPath("machine-session-1", "machine-session-snapshot.json"),
|
|
"machine-session-snapshot.json",
|
|
);
|
|
assert.deepEqual(machineSessionSnapshot.payload, snapshotPayload);
|
|
assert.deepEqual(
|
|
await loadSessionSnapshot("machine-session-1", {
|
|
storage,
|
|
filename: "machine-session-snapshot.json",
|
|
}),
|
|
machineSessionSnapshot,
|
|
);
|
|
assert.deepEqual(
|
|
await loadMachineSessionSnapshot("machine-session-1", "xyzab-tdr", {
|
|
storage,
|
|
filename: "machine-session-snapshot.json",
|
|
}),
|
|
machineSessionSnapshot,
|
|
);
|
|
await assert.rejects(
|
|
() => loadMachineSessionSnapshot("machine-session-1", "other-machine", {
|
|
storage,
|
|
filename: "machine-session-snapshot.json",
|
|
}),
|
|
/Machine session snapshot id mismatch/,
|
|
);
|
|
await saveTextFile(
|
|
sessionSnapshotPath("machine-session-bad-files"),
|
|
JSON.stringify({
|
|
...machineSessionSnapshot,
|
|
sessionId: "machine-session-bad-files",
|
|
payload: {
|
|
machineId: "xyzab-tdr",
|
|
files: {
|
|
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
|
|
parameters: "../linuxcnc.var",
|
|
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
|
|
},
|
|
},
|
|
}),
|
|
storage,
|
|
);
|
|
await assert.rejects(
|
|
() => loadMachineSessionSnapshot("machine-session-bad-files", "xyzab-tdr", { storage }),
|
|
/Invalid OPFS path/,
|
|
);
|
|
|
|
await saveMachineTextFiles("xyzab-tdr", {
|
|
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
|
|
toolTable: "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n",
|
|
parameters: "5161 0.0\n5162 0.0\n",
|
|
}, { storage });
|
|
assert.deepEqual(await loadMachineTextFiles("xyzab-tdr", { storage }), {
|
|
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
|
|
toolTable: "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n",
|
|
parameters: "5161 0.0\n5162 0.0\n",
|
|
});
|
|
assertGcodeProgramStoragePath(
|
|
"fixture.ngc",
|
|
await saveGcodeProgram("fixture.ngc", "G0 X0 Y0\nM2\n", { storage }),
|
|
);
|
|
assert.equal(await loadGcodeProgram("fixture.ngc", { storage }), "G0 X0 Y0\nM2\n");
|
|
assertGcodeProgramStoragePath(
|
|
"custom-fixture.ngc",
|
|
await saveGcodeProgram("custom-fixture.ngc", "G1 X1 F10\nM2\n", { storage }),
|
|
);
|
|
assert.equal(await loadGcodeProgram("custom-fixture.ngc", { storage }), "G1 X1 F10\nM2\n");
|
|
|
|
const readyMachineSession = await readMachineSessionReadiness("xyzab-tdr", {
|
|
storage,
|
|
sessionId: "machine-session-1",
|
|
snapshotFilename: "machine-session-snapshot.json",
|
|
gcodeFilename: "fixture.ngc",
|
|
});
|
|
assert.equal(readyMachineSession.ready, true);
|
|
assert.equal(readyMachineSession.phase, "ready");
|
|
assert.deepEqual(readyMachineSession.missing, []);
|
|
assert.deepEqual(readyMachineSession.errors, []);
|
|
assert.deepEqual(
|
|
readyMachineSession.checks.map((check) => [check.name, check.ok, check.path]),
|
|
[
|
|
["ini", true, "linuxcnc/machines/xyzab-tdr/machine.ini"],
|
|
["parameters", true, "linuxcnc/machines/xyzab-tdr/linuxcnc.var"],
|
|
["toolTable", true, "linuxcnc/machines/xyzab-tdr/tool.tbl"],
|
|
["gcode", true, "linuxcnc/gcode/fixture.ngc"],
|
|
],
|
|
);
|
|
assert.deepEqual(readyMachineSession.snapshotCheck, {
|
|
name: "snapshot",
|
|
path: "linuxcnc/sessions/machine-session-1/machine-session-snapshot.json",
|
|
ok: true,
|
|
error: null,
|
|
});
|
|
assert.equal(readyMachineSession.snapshot.sessionId, "machine-session-1");
|
|
|
|
const missingMachineSession = await readMachineSessionReadiness("missing-machine", {
|
|
storage,
|
|
sessionId: "missing-session",
|
|
gcodeRequired: true,
|
|
gcodeFilename: "missing.ngc",
|
|
});
|
|
assert.equal(missingMachineSession.ready, false);
|
|
assert.equal(missingMachineSession.phase, "blocked");
|
|
assert.deepEqual(missingMachineSession.missing, [
|
|
"ini",
|
|
"parameters",
|
|
"toolTable",
|
|
"gcode",
|
|
"snapshot",
|
|
]);
|
|
assert.deepEqual(
|
|
missingMachineSession.errors.map((error) => error.name),
|
|
["ini", "parameters", "toolTable", "gcode", "snapshot"],
|
|
);
|
|
assert.match(missingMachineSession.errors[0].error, /missing directory|missing file/);
|
|
|
|
const bridgeFiles = new Map();
|
|
const bridgeInterp = {
|
|
writeTextFile(path, text) {
|
|
bridgeFiles.set(path, text);
|
|
},
|
|
readTextFile(path) {
|
|
if (!bridgeFiles.has(path)) {
|
|
throw new Error(`missing wasm file: ${path}`);
|
|
}
|
|
return bridgeFiles.get(path);
|
|
},
|
|
restoreParameters(path) {
|
|
return `restore_parameters=0\nrestore_path=${path}\n`;
|
|
},
|
|
saveParameters(path, values) {
|
|
bridgeFiles.set(path, "5161\t12.340000\n5162\t56.780000\n");
|
|
bridgeFiles.set(`${path}.bak`, "5161 0.0\n5162 0.0\n");
|
|
return `save_parameters=0\nsave_path=${path}\nvalues=${Object.keys(values).join(",")}\n`;
|
|
},
|
|
loadToolTable(path, options = {}) {
|
|
return [
|
|
"tooldata_load=0",
|
|
`load_tool_path=${path}`,
|
|
`random_toolchanger=${options.randomToolChanger === true ? 1 : 0}`,
|
|
"",
|
|
].join("\n");
|
|
},
|
|
saveToolTable(path) {
|
|
bridgeFiles.set(path, "T2 P2 Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n");
|
|
return `tooldata_save=0\nsave_tool_path=${path}\n`;
|
|
},
|
|
};
|
|
|
|
const bridgeIniSdk = {
|
|
writeTextFile(path, text) {
|
|
bridgeFiles.set(`ini:${path}`, text);
|
|
},
|
|
getString(path, section, tag) {
|
|
if (
|
|
path === "/work/ini-file-session.ini" &&
|
|
section === "RS274NGC" &&
|
|
tag === "PARAMETER_FILE"
|
|
) {
|
|
return "custom.var";
|
|
}
|
|
if (
|
|
path === "/work/ini-override-session.ini" &&
|
|
section === "RS274NGC" &&
|
|
tag === "PARAMETER_FILE"
|
|
) {
|
|
return "ignored.var";
|
|
}
|
|
if (
|
|
path === "/work/invalid-ini-file-session.ini" &&
|
|
section === "RS274NGC" &&
|
|
tag === "PARAMETER_FILE"
|
|
) {
|
|
return "../escape.var";
|
|
}
|
|
if (
|
|
path === "/work/ini-file-session.ini" &&
|
|
section === "EMCIO" &&
|
|
tag === "TOOL_TABLE"
|
|
) {
|
|
return "custom-tool.tbl";
|
|
}
|
|
if (
|
|
path === "/work/ini-override-session.ini" &&
|
|
section === "EMCIO" &&
|
|
tag === "TOOL_TABLE"
|
|
) {
|
|
return "ignored-tool.tbl";
|
|
}
|
|
if (
|
|
path === "/work/invalid-tool-file-session.ini" &&
|
|
section === "EMCIO" &&
|
|
tag === "TOOL_TABLE"
|
|
) {
|
|
return "nested/tool.tbl";
|
|
}
|
|
return null;
|
|
},
|
|
getBool(path, section, tag) {
|
|
if (
|
|
path === "/work/ini-derived-session.ini" &&
|
|
section === "EMCIO" &&
|
|
tag === "RANDOM_TOOLCHANGER"
|
|
) {
|
|
return true;
|
|
}
|
|
return null;
|
|
},
|
|
};
|
|
|
|
const restoredParameters = await restoreMachineParametersFromOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{ storage, wasmPath: "/work/bridge.var" },
|
|
);
|
|
assert.deepEqual(restoredParameters, {
|
|
opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
|
|
wasmPath: "/work/bridge.var",
|
|
result: "restore_parameters=0\nrestore_path=/work/bridge.var\n",
|
|
});
|
|
assert.equal(bridgeFiles.get("/work/bridge.var"), "5161 0.0\n5162 0.0\n");
|
|
assertParameterBridgePaths("xyzab-tdr", restoredParameters, "/work/bridge.var");
|
|
|
|
const restoredDefaultParameters = await restoreMachineParametersFromOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{ storage },
|
|
);
|
|
assertParameterBridgePaths("xyzab-tdr", restoredDefaultParameters, "/work/linuxcnc.var");
|
|
assert.equal(
|
|
restoredDefaultParameters.result,
|
|
"restore_parameters=0\nrestore_path=/work/linuxcnc.var\n",
|
|
);
|
|
assert.equal(bridgeFiles.get("/work/linuxcnc.var"), "5161 0.0\n5162 0.0\n");
|
|
|
|
const savedParameters = await saveMachineParametersToOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{ 5161: 12.34, 5162: 56.78 },
|
|
{ storage, wasmPath: "/work/bridge.var" },
|
|
);
|
|
assertParameterBridgePaths("xyzab-tdr", savedParameters, "/work/bridge.var");
|
|
assert.equal(savedParameters.backupOpfsPath, "linuxcnc/machines/xyzab-tdr/linuxcnc.var.bak");
|
|
assert.equal(savedParameters.result.includes("save_parameters=0"), true);
|
|
assert.equal(
|
|
await loadTextFile("linuxcnc/machines/xyzab-tdr/linuxcnc.var", storage),
|
|
"5161\t12.340000\n5162\t56.780000\n",
|
|
);
|
|
assert.equal(
|
|
await loadTextFile("linuxcnc/machines/xyzab-tdr/linuxcnc.var.bak", storage),
|
|
"5161 0.0\n5162 0.0\n",
|
|
);
|
|
const savedDefaultParameters = await saveMachineParametersToOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{ 5161: 12.34 },
|
|
{ storage },
|
|
);
|
|
assertParameterBridgePaths("xyzab-tdr", savedDefaultParameters, "/work/linuxcnc.var");
|
|
assert.equal(
|
|
savedDefaultParameters.backupOpfsPath,
|
|
"linuxcnc/machines/xyzab-tdr/linuxcnc.var.bak",
|
|
);
|
|
assert.equal(savedDefaultParameters.result.includes("save_path=/work/linuxcnc.var"), true);
|
|
|
|
const loadedToolTable = await loadMachineToolTableFromOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{ storage, wasmPath: "/work/bridge-tool.tbl" },
|
|
);
|
|
assert.deepEqual(loadedToolTable, {
|
|
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
|
|
wasmPath: "/work/bridge-tool.tbl",
|
|
result: "tooldata_load=0\nload_tool_path=/work/bridge-tool.tbl\nrandom_toolchanger=0\n",
|
|
});
|
|
assert.equal(bridgeFiles.get("/work/bridge-tool.tbl"), "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n");
|
|
assertToolTableBridgePaths("xyzab-tdr", loadedToolTable, "/work/bridge-tool.tbl");
|
|
|
|
const loadedDefaultToolTable = await loadMachineToolTableFromOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{ storage },
|
|
);
|
|
assertToolTableBridgePaths("xyzab-tdr", loadedDefaultToolTable, "/work/tool.tbl");
|
|
assert.equal(
|
|
loadedDefaultToolTable.result,
|
|
"tooldata_load=0\nload_tool_path=/work/tool.tbl\nrandom_toolchanger=0\n",
|
|
);
|
|
assert.equal(bridgeFiles.get("/work/tool.tbl"), "T0 P0 ; no tool\nT2 P2 Z1.25 D0.25\n");
|
|
|
|
const loadedRandomToolTable = await loadMachineToolTableFromOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{ storage, wasmPath: "/work/random-bridge-tool.tbl", randomToolChanger: true },
|
|
);
|
|
assert.deepEqual(loadedRandomToolTable, {
|
|
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
|
|
wasmPath: "/work/random-bridge-tool.tbl",
|
|
result:
|
|
"tooldata_load=0\nload_tool_path=/work/random-bridge-tool.tbl\nrandom_toolchanger=1\n",
|
|
});
|
|
|
|
const savedToolTable = await saveMachineToolTableToOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{ storage, wasmPath: "/work/bridge-tool.tbl" },
|
|
);
|
|
assert.equal(savedToolTable.opfsPath, "linuxcnc/machines/xyzab-tdr/tool.tbl");
|
|
assert.equal(savedToolTable.wasmPath, "/work/bridge-tool.tbl");
|
|
assertToolTableBridgePaths("xyzab-tdr", savedToolTable, "/work/bridge-tool.tbl");
|
|
assert.equal(savedToolTable.result.includes("tooldata_save=0"), true);
|
|
assert.equal(
|
|
await loadTextFile("linuxcnc/machines/xyzab-tdr/tool.tbl", storage),
|
|
"T2 P2 Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n",
|
|
);
|
|
const savedDefaultToolTable = await saveMachineToolTableToOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{ storage },
|
|
);
|
|
assertToolTableBridgePaths("xyzab-tdr", savedDefaultToolTable, "/work/tool.tbl");
|
|
assert.equal(savedDefaultToolTable.result.includes("save_tool_path=/work/tool.tbl"), true);
|
|
|
|
const loadedSession = await loadMachineSessionFromOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{
|
|
storage,
|
|
iniWasmPath: "/work/session.ini",
|
|
parameterWasmPath: "/work/session.var",
|
|
toolTableWasmPath: "/work/session-tool.tbl",
|
|
},
|
|
);
|
|
assert.deepEqual(loadedSession, {
|
|
machineId: "xyzab-tdr",
|
|
ini: {
|
|
opfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
|
|
wasmPath: "/work/session.ini",
|
|
},
|
|
parameters: {
|
|
opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
|
|
wasmPath: "/work/session.var",
|
|
result: "restore_parameters=0\nrestore_path=/work/session.var\n",
|
|
},
|
|
toolTable: {
|
|
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
|
|
wasmPath: "/work/session-tool.tbl",
|
|
result: "tooldata_load=0\nload_tool_path=/work/session-tool.tbl\nrandom_toolchanger=0\n",
|
|
},
|
|
});
|
|
assertSessionBridgePaths("xyzab-tdr", loadedSession, {
|
|
iniWasmPath: "/work/session.ini",
|
|
parameterWasmPath: "/work/session.var",
|
|
toolTableWasmPath: "/work/session-tool.tbl",
|
|
});
|
|
assert.equal(bridgeFiles.get("/work/session.ini"), "[EMC]\nMACHINE = xyzab-tdr\n");
|
|
assert.equal(bridgeFiles.get("/work/session.var"), "5161\t12.340000\n5162\t56.780000\n");
|
|
assert.equal(
|
|
bridgeFiles.get("/work/session-tool.tbl"),
|
|
"T2 P2 Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n",
|
|
);
|
|
|
|
const loadedDefaultSession = await loadMachineSessionFromOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{ storage },
|
|
);
|
|
assertSessionBridgePaths("xyzab-tdr", loadedDefaultSession);
|
|
assert.equal(bridgeFiles.get("/work/machine.ini"), "[EMC]\nMACHINE = xyzab-tdr\n");
|
|
assert.equal(bridgeFiles.get("/work/linuxcnc.var"), "5161\t12.340000\n5162\t56.780000\n");
|
|
assert.equal(
|
|
bridgeFiles.get("/work/tool.tbl"),
|
|
"T2 P2 Z+1.250000 D+0.250000 ;loaded by LinuxCNC\n",
|
|
);
|
|
|
|
const loadedRandomSession = await loadMachineSessionFromOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{
|
|
storage,
|
|
iniWasmPath: "/work/random-session.ini",
|
|
parameterWasmPath: "/work/random-session.var",
|
|
toolTableWasmPath: "/work/random-session-tool.tbl",
|
|
randomToolChanger: true,
|
|
},
|
|
);
|
|
assert.equal(
|
|
loadedRandomSession.toolTable.result,
|
|
"tooldata_load=0\nload_tool_path=/work/random-session-tool.tbl\nrandom_toolchanger=1\n",
|
|
);
|
|
|
|
const loadedIniDerivedRandomSession = await loadMachineSessionFromOpfs(
|
|
bridgeInterp,
|
|
"xyzab-tdr",
|
|
{
|
|
storage,
|
|
iniSdk: bridgeIniSdk,
|
|
iniWasmPath: "/work/ini-derived-session.ini",
|
|
parameterWasmPath: "/work/ini-derived-session.var",
|
|
toolTableWasmPath: "/work/ini-derived-session-tool.tbl",
|
|
},
|
|
);
|
|
assert.equal(
|
|
bridgeFiles.get("ini:/work/ini-derived-session.ini"),
|
|
"[EMC]\nMACHINE = xyzab-tdr\n",
|
|
);
|
|
assert.equal(
|
|
loadedIniDerivedRandomSession.toolTable.result,
|
|
"tooldata_load=0\nload_tool_path=/work/ini-derived-session-tool.tbl\nrandom_toolchanger=1\n",
|
|
);
|
|
assert.equal(
|
|
loadedIniDerivedRandomSession.parameters.opfsPath,
|
|
"linuxcnc/machines/xyzab-tdr/linuxcnc.var",
|
|
);
|
|
assert.equal(
|
|
loadedIniDerivedRandomSession.toolTable.opfsPath,
|
|
"linuxcnc/machines/xyzab-tdr/tool.tbl",
|
|
);
|
|
|
|
await saveMachineTextFiles("ini-file-session", {
|
|
ini: [
|
|
"[EMC]",
|
|
"MACHINE = ini-file-session",
|
|
"",
|
|
"[RS274NGC]",
|
|
"PARAMETER_FILE = custom.var",
|
|
"",
|
|
"[EMCIO]",
|
|
"TOOL_TABLE = custom-tool.tbl",
|
|
"",
|
|
].join("\n"),
|
|
}, { storage });
|
|
await saveTextFile(
|
|
"linuxcnc/machines/ini-file-session/custom.var",
|
|
"5161 77.0\n5162 88.0\n",
|
|
storage,
|
|
);
|
|
await saveTextFile(
|
|
"linuxcnc/machines/ini-file-session/custom-tool.tbl",
|
|
"T8 P8 Z4.5 D0.5\n",
|
|
storage,
|
|
);
|
|
const loadedIniFileSession = await loadMachineSessionFromOpfs(
|
|
bridgeInterp,
|
|
"ini-file-session",
|
|
{
|
|
storage,
|
|
iniSdk: bridgeIniSdk,
|
|
iniWasmPath: "/work/ini-file-session.ini",
|
|
parameterWasmPath: "/work/ini-file-session.var",
|
|
toolTableWasmPath: "/work/ini-file-session-tool.tbl",
|
|
},
|
|
);
|
|
assert.equal(
|
|
loadedIniFileSession.parameters.opfsPath,
|
|
"linuxcnc/machines/ini-file-session/custom.var",
|
|
);
|
|
assert.equal(
|
|
loadedIniFileSession.toolTable.opfsPath,
|
|
"linuxcnc/machines/ini-file-session/custom-tool.tbl",
|
|
);
|
|
assert.equal(bridgeFiles.get("/work/ini-file-session.var"), "5161 77.0\n5162 88.0\n");
|
|
assert.equal(bridgeFiles.get("/work/ini-file-session-tool.tbl"), "T8 P8 Z4.5 D0.5\n");
|
|
assert.equal(
|
|
loadedIniFileSession.toolTable.result,
|
|
"tooldata_load=0\nload_tool_path=/work/ini-file-session-tool.tbl\nrandom_toolchanger=0\n",
|
|
);
|
|
|
|
await saveMachineTextFiles("ini-override-session", {
|
|
ini: [
|
|
"[EMC]",
|
|
"MACHINE = ini-override-session",
|
|
"",
|
|
"[RS274NGC]",
|
|
"PARAMETER_FILE = ignored.var",
|
|
"",
|
|
"[EMCIO]",
|
|
"TOOL_TABLE = ignored-tool.tbl",
|
|
"",
|
|
].join("\n"),
|
|
}, { storage });
|
|
await saveTextFile(
|
|
"linuxcnc/machines/ini-override-session/explicit.var",
|
|
"5161 11.0\n5162 22.0\n",
|
|
storage,
|
|
);
|
|
await saveTextFile(
|
|
"linuxcnc/machines/ini-override-session/explicit-tool.tbl",
|
|
"T3 P3 Z1.75 D0.375\n",
|
|
storage,
|
|
);
|
|
const loadedIniOverrideSession = await loadMachineSessionFromOpfs(
|
|
bridgeInterp,
|
|
"ini-override-session",
|
|
{
|
|
storage,
|
|
iniSdk: bridgeIniSdk,
|
|
iniWasmPath: "/work/ini-override-session.ini",
|
|
parameterFilename: "explicit.var",
|
|
parameterWasmPath: "/work/ini-override-session.var",
|
|
toolTableFilename: "explicit-tool.tbl",
|
|
toolTableWasmPath: "/work/ini-override-session-tool.tbl",
|
|
},
|
|
);
|
|
assert.equal(
|
|
loadedIniOverrideSession.parameters.opfsPath,
|
|
"linuxcnc/machines/ini-override-session/explicit.var",
|
|
);
|
|
assert.equal(
|
|
loadedIniOverrideSession.toolTable.opfsPath,
|
|
"linuxcnc/machines/ini-override-session/explicit-tool.tbl",
|
|
);
|
|
assert.equal(bridgeFiles.get("/work/ini-override-session.var"), "5161 11.0\n5162 22.0\n");
|
|
assert.equal(bridgeFiles.get("/work/ini-override-session-tool.tbl"), "T3 P3 Z1.75 D0.375\n");
|
|
|
|
await saveMachineTextFiles("invalid-ini-file-session", {
|
|
ini: [
|
|
"[EMC]",
|
|
"MACHINE = invalid-ini-file-session",
|
|
"",
|
|
"[RS274NGC]",
|
|
"PARAMETER_FILE = ../escape.var",
|
|
"",
|
|
].join("\n"),
|
|
}, { storage });
|
|
await assert.rejects(
|
|
() => loadMachineSessionFromOpfs(
|
|
bridgeInterp,
|
|
"invalid-ini-file-session",
|
|
{
|
|
storage,
|
|
iniSdk: bridgeIniSdk,
|
|
iniWasmPath: "/work/invalid-ini-file-session.ini",
|
|
},
|
|
),
|
|
/Invalid parameter filename/,
|
|
);
|
|
|
|
await saveMachineTextFiles("invalid-tool-file-session", {
|
|
ini: [
|
|
"[EMC]",
|
|
"MACHINE = invalid-tool-file-session",
|
|
"",
|
|
"[EMCIO]",
|
|
"TOOL_TABLE = nested/tool.tbl",
|
|
"",
|
|
].join("\n"),
|
|
}, { storage });
|
|
await assert.rejects(
|
|
() => loadMachineSessionFromOpfs(
|
|
bridgeInterp,
|
|
"invalid-tool-file-session",
|
|
{
|
|
storage,
|
|
iniSdk: bridgeIniSdk,
|
|
iniWasmPath: "/work/invalid-tool-file-session.ini",
|
|
},
|
|
),
|
|
/Invalid tool table filename/,
|
|
);
|
|
|
|
await assert.rejects(
|
|
() => loadTextFile("linuxcnc/machines/missing.ini", storage),
|
|
/missing file/,
|
|
);
|
|
await assert.rejects(
|
|
() => saveTextFile("../escape.ini", "", storage),
|
|
/Invalid OPFS path/,
|
|
);
|
|
assert.throws(
|
|
() => machineIniPath("../escape"),
|
|
/Invalid machine id/,
|
|
);
|
|
assert.throws(
|
|
() => sessionSnapshotPath("session-1", "nested/snapshot.json"),
|
|
/Invalid session snapshot filename/,
|
|
);
|
|
assert.throws(
|
|
() => previewCachePath("fixture", "nested/preview.json"),
|
|
/Invalid preview cache filename/,
|
|
);
|
|
assert.throws(
|
|
() => gcodeProgramPath("../escape.ngc"),
|
|
/Invalid G-code filename/,
|
|
);
|
|
await assert.rejects(
|
|
() => saveGcodeProgram("nested/fixture.ngc", "", { storage }),
|
|
/Invalid G-code filename/,
|
|
);
|
|
await assert.rejects(
|
|
() => loadGcodeProgram("../escape.ngc", { storage }),
|
|
/Invalid G-code filename/,
|
|
);
|
|
await assert.rejects(
|
|
() => readMachineSessionReadiness("xyzab-tdr", {
|
|
storage,
|
|
gcodeOpfsPath: "../escape.ngc",
|
|
}),
|
|
/Invalid OPFS path/,
|
|
);
|
|
await assert.rejects(
|
|
() => saveSessionSnapshot("session-1", {}, {
|
|
storage,
|
|
filename: "nested/snapshot.json",
|
|
}),
|
|
/Invalid session snapshot filename/,
|
|
);
|
|
await assert.rejects(
|
|
() => loadSessionSnapshot("session-1", {
|
|
storage,
|
|
filename: "../snapshot.json",
|
|
}),
|
|
/Invalid session snapshot filename/,
|
|
);
|
|
await assert.rejects(
|
|
() => saveMachineTextFiles("xyzab-tdr", null, { storage }),
|
|
/machine files must be a plain object/,
|
|
);
|
|
await saveTextFile(sessionSnapshotPath("bad-json"), "{", storage);
|
|
await assert.rejects(
|
|
() => loadSessionSnapshot("bad-json", { storage }),
|
|
/Invalid session snapshot JSON/,
|
|
);
|
|
await saveTextFile(
|
|
sessionSnapshotPath("bad-format"),
|
|
JSON.stringify({ ...snapshot, sessionId: "bad-format", format: "other-format" }),
|
|
storage,
|
|
);
|
|
await assert.rejects(
|
|
() => loadSessionSnapshot("bad-format", { storage }),
|
|
/Unsupported session snapshot format/,
|
|
);
|
|
await saveTextFile(
|
|
sessionSnapshotPath("bad-version"),
|
|
JSON.stringify({ ...snapshot, sessionId: "bad-version", version: 99 }),
|
|
storage,
|
|
);
|
|
await assert.rejects(
|
|
() => loadSessionSnapshot("bad-version", { storage }),
|
|
/Unsupported session snapshot version/,
|
|
);
|
|
await saveTextFile(
|
|
sessionSnapshotPath("bad-session-id"),
|
|
JSON.stringify({ ...snapshot, sessionId: "other-session" }),
|
|
storage,
|
|
);
|
|
await assert.rejects(
|
|
() => loadSessionSnapshot("bad-session-id", { storage }),
|
|
/Session snapshot id mismatch/,
|
|
);
|
|
await assert.rejects(
|
|
() => loadSessionSnapshot("missing-session", { storage }),
|
|
/missing directory|missing file/,
|
|
);
|
|
await assert.rejects(
|
|
() => getOpfsRoot({}),
|
|
/OPFS is not available/,
|
|
);
|
|
|
|
console.log("opfs_file_service_node_smoke=ok");
|