结论:新增浏览器 OPFS section,使用同一 workspace 但不同 WASM mount point 的新 simulator 解析已持久化程序,证明程序来自 OPFS 而非旧 WASM FS mirror。LinuxCNC 来源依据为 rs274ngc_pre.cc open()/read_text() 文件读取路径;未扩展 smoke 语义。检查通过:./test-native.sh;./test-linuxcnc-source-link.sh。
148 lines
6.7 KiB
JavaScript
148 lines
6.7 KiB
JavaScript
export async function runBrowserOpfsBasicSections(context, simulator) {
|
|
const {
|
|
expectErrorContaining,
|
|
expectEvent,
|
|
expectMissingWasmPath,
|
|
expectText,
|
|
near,
|
|
runSection,
|
|
withSmokeSimulator,
|
|
} = context;
|
|
|
|
await runSection("opfs workspace clear and healthcheck", async () => {
|
|
if (!simulator.fs.opfs) {
|
|
throw new Error("expected OPFS workspace to be available in browser smoke");
|
|
}
|
|
await simulator.fs.opfs.clear();
|
|
if (simulator.fs.opfs.workspacePath !== "cnc-simulator-smoke") {
|
|
throw new Error("unexpected OPFS workspace path");
|
|
}
|
|
await simulator.fs.opfs.writeFile("healthcheck.ngc", "G21 G90\n");
|
|
if (!(await simulator.fs.opfs.exists("healthcheck.ngc"))) {
|
|
throw new Error("OPFS exists did not find a persisted file");
|
|
}
|
|
if (await simulator.fs.opfs.exists("missing-healthcheck.ngc")) {
|
|
throw new Error("OPFS exists reported a missing file");
|
|
}
|
|
const healthcheckStat = await simulator.fs.opfs.stat("healthcheck.ngc");
|
|
if (healthcheckStat.kind !== "file" || healthcheckStat.size !== 8) {
|
|
throw new Error("OPFS stat did not report the persisted healthcheck file size");
|
|
}
|
|
const clearCheck = new TextDecoder().decode(await simulator.fs.opfs.readFile("healthcheck.ngc"));
|
|
expectText(clearCheck, "G21 G90\n", "OPFS workspace did not survive clear/write round trip");
|
|
});
|
|
|
|
await runSection("opfs default mount check", async () => {
|
|
await withSmokeSimulator((defaultMountSimulator) => {
|
|
if (!defaultMountSimulator.fs.opfs || defaultMountSimulator.fs.opfs.mountPoint !== "/cnc") {
|
|
throw new Error("default OPFS mount point was not preserved");
|
|
}
|
|
});
|
|
});
|
|
|
|
await runSection("opfs program write and read", async () => {
|
|
const encoded = new TextEncoder().encode("G21 G90\nG0 X1\n");
|
|
await simulator.fs.opfs.writeFile("programs/browser-smoke.ngc", encoded);
|
|
const decoded = new TextDecoder().decode(
|
|
await simulator.fs.opfs.readFile("programs/browser-smoke.ngc"),
|
|
);
|
|
expectText(decoded, "G21 G90\nG0 X1\n", "OPFS round trip did not preserve browser smoke program");
|
|
});
|
|
|
|
await runSection("opfs unsafe path rejection", async () => {
|
|
for (const unsafePath of ["../escape.ngc", "programs/../escape.ngc", "/escape.ngc", "programs//escape.ngc"]) {
|
|
await expectErrorContaining(
|
|
() => simulator.fs.opfs.writeFile(unsafePath, "G21\n"),
|
|
"inside the CNC workspace",
|
|
`OPFS accepted unsafe path ${unsafePath}`,
|
|
);
|
|
}
|
|
for (const [operation, action] of [
|
|
["readFile", () => simulator.fs.opfs.readFile("../escape.ngc")],
|
|
["readDirectory", () => simulator.fs.opfs.readDirectory("../escape")],
|
|
["persistFile", () => simulator.fs.opfs.persistFile("../escape.ngc")],
|
|
["persistDirectory", () => simulator.fs.opfs.persistDirectory("../escape")],
|
|
["copyFile", () => simulator.fs.opfs.copyFile("../escape.ngc", "programs/escape.ngc")],
|
|
["copyFileTarget", () => simulator.fs.opfs.copyFile("healthcheck.ngc", "../escape.ngc")],
|
|
["exists", () => simulator.fs.opfs.exists("../escape.ngc")],
|
|
["stat", () => simulator.fs.opfs.stat("../escape.ngc")],
|
|
["moveFile", () => simulator.fs.opfs.moveFile("../escape.ngc", "programs/escape.ngc")],
|
|
["moveFileTarget", () => simulator.fs.opfs.moveFile("healthcheck.ngc", "../escape.ngc")],
|
|
["removeFile", () => simulator.fs.opfs.removeFile("../escape.ngc")],
|
|
["removeDirectory", () => simulator.fs.opfs.removeDirectory("../escape")],
|
|
["parseFile", () => simulator.parseFile("../escape.ngc", "linuxcnc", { backend: "linuxcnc-rs274" })],
|
|
[
|
|
"parseWithParameterFile",
|
|
() =>
|
|
simulator.parseWithParameterFile("G21 G90\nM30\n", "../escape.var", "linuxcnc", {
|
|
backend: "linuxcnc-rs274",
|
|
}),
|
|
],
|
|
[
|
|
"parseFileWithParameterFileProgram",
|
|
() =>
|
|
simulator.parseFileWithParameterFile("../escape.ngc", "parameters/escape.var", "linuxcnc", {
|
|
backend: "linuxcnc-rs274",
|
|
}),
|
|
],
|
|
[
|
|
"parseFileWithParameterFileParameter",
|
|
() =>
|
|
simulator.parseFileWithParameterFile("programs/browser-smoke.ngc", "../escape.var", "linuxcnc", {
|
|
backend: "linuxcnc-rs274",
|
|
}),
|
|
],
|
|
]) {
|
|
await expectErrorContaining(
|
|
action,
|
|
"inside the CNC workspace",
|
|
`OPFS ${operation} accepted an unsafe path`,
|
|
);
|
|
}
|
|
});
|
|
|
|
await runSection("opfs program parse and restore", async () => {
|
|
const opfsProgramEvents = await simulator.parseFile("programs/browser-smoke.ngc", "linuxcnc", {
|
|
backend: "linuxcnc-rs274",
|
|
});
|
|
expectEvent(opfsProgramEvents, (event) => event.type === "rapid" && near(event.end.x, 1),
|
|
"OPFS program file was not parsed through LinuxCNC WASM");
|
|
for (const parameterPath of ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"]) {
|
|
expectMissingWasmPath(
|
|
() => simulator.fs.module.FS.stat(parameterPath),
|
|
`browser parseFile left LinuxCNC parameter state outside OPFS: ${parameterPath}`,
|
|
);
|
|
}
|
|
await withSmokeSimulator(async (restoredProgramSimulator) => {
|
|
// LinuxCNC source basis: rs274ngc_pre.cc read_text() reads
|
|
// blocks from the program stream after the OPFS mirror is loaded.
|
|
const restoredProgramEvents = await restoredProgramSimulator.parseFile(
|
|
"programs/browser-smoke.ngc",
|
|
"linuxcnc",
|
|
{ backend: "linuxcnc-rs274" },
|
|
);
|
|
expectEvent(restoredProgramEvents, (event) => event.type === "rapid" && near(event.end.x, 1),
|
|
"LinuxCNC OPFS program file was not restored across WASM simulator instances");
|
|
});
|
|
});
|
|
|
|
await runSection("opfs program restore with independent mount", async () => {
|
|
await withSmokeSimulator(async (independentMountSimulator) => {
|
|
// LinuxCNC source basis: rs274ngc_pre.cc open()/read_text()
|
|
// reads the mirrored program file. A different mount point proves
|
|
// the program came from OPFS instead of a prior WASM FS mirror.
|
|
const independentMountEvents = await independentMountSimulator.parseFile(
|
|
"programs/browser-smoke.ngc",
|
|
"linuxcnc",
|
|
{ backend: "linuxcnc-rs274" },
|
|
);
|
|
expectEvent(independentMountEvents, (event) => event.type === "rapid" && near(event.end.x, 1),
|
|
"LinuxCNC OPFS program file was not restored through an independent WASM mount");
|
|
const independentMountPath = independentMountSimulator.fs.opfs.resolvePath("programs/browser-smoke.ngc");
|
|
if (!independentMountPath.startsWith("/cnc-independent/")) {
|
|
throw new Error("independent OPFS mount point was not used for restored program parsing");
|
|
}
|
|
}, { mountPoint: "/cnc-independent", workspacePath: "cnc-simulator-smoke" });
|
|
});
|
|
}
|