Files
cnc_wams/wasm-port/tests/opfs/node/verify_file_service.mjs
wangdequan 3f4eb06b47 按规划继续工作
结论:补齐 LinuxCNC tooldata_common.cc 随机换刀器 load/save 验证边界,WASM/SDK/OPFS 只透传 random-toolchanger 配置;native、WASM、OPFS 和浏览器 smoke 验证已通过。
2026-06-08 08:39:26 +08:00

348 lines
10 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 {
createSessionSnapshot,
loadSessionSnapshot,
saveSessionSnapshot,
validateSessionSnapshot,
} from "../../../runtime/opfs/snapshot-store.js";
import {
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";
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,
};
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");
assert.equal(gcodeProgramPath("fixture.ngc"), "linuxcnc/gcode/fixture.ngc");
assert.equal(previewCachePath("fixture"), "linuxcnc/preview-cache/fixture/preview.json");
assert.equal(sessionSnapshotPath("session-1"), "linuxcnc/sessions/session-1/snapshot.json");
assert.deepEqual(defaultMachinePaths("xyzab-tdr"), {
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
});
assert.deepEqual(machineFilePaths("xyzab-tdr"), defaultMachinePaths("xyzab-tdr"));
const snapshotPayload = {
files: {
ini: machineIniPath("xyzab-tdr"),
toolTable: toolTablePath("xyzab-tdr"),
},
};
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);
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" },
});
assert.deepEqual(await loadSessionSnapshot("session-1", { storage }), snapshot);
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",
});
assert.equal(
await saveGcodeProgram("fixture.ngc", "G0 X0 Y0\nM2\n", { storage }),
"linuxcnc/gcode/fixture.ngc",
);
assert.equal(await loadGcodeProgram("fixture.ngc", { storage }), "G0 X0 Y0\nM2\n");
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 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");
const savedParameters = await saveMachineParametersToOpfs(
bridgeInterp,
"xyzab-tdr",
{ 5161: 12.34, 5162: 56.78 },
{ storage, wasmPath: "/work/bridge.var" },
);
assert.equal(savedParameters.opfsPath, "linuxcnc/machines/xyzab-tdr/linuxcnc.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 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");
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");
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 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",
},
});
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 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",
);
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/,
);
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 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");