export async function runBrowserOpfsParameterSections(context, simulator) { const { expectErrorContaining, expectEvent, expectIncludes, expectLinuxCncParameterScratchMissing, expectMissingOpfsFile, expectMissingWasmPath, expectNoEvent, expectOpfsDirectoryStat, expectText, expectWasmFilesMissing, near, readOpfsText, readWasmText, removeWasmPathIfExists, runSection, withSmokeSimulator, } = context; const backend = { backend: "linuxcnc-rs274" }; const parseWithParameters = (program, path) => simulator.parseWithParameterFile(program, path, "linuxcnc", backend); const g28BranchProgram = "G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n"; const writeDefaultScratch = (main, temporary, backup) => { simulator.fs.module.FS.writeFile("rs274ngc.var", main); simulator.fs.module.FS.writeFile("rs274ngc.var.new", temporary); simulator.fs.module.FS.writeFile("rs274ngc.var.bak", backup); }; await runSection("opfs parameter save", async () => { await simulator.parseWithParameterFile("G21 G90\nG0 X1\nG28.1\nM30\n", "parameters/rs274ngc.var", "linuxcnc", backend); const opfsParameterFile = await readOpfsText(simulator, "parameters/rs274ngc.var"); expectIncludes(opfsParameterFile, "5161\t1.000000", "LinuxCNC parameter file was not persisted into OPFS"); expectWasmFilesMissing(simulator.fs.module.FS, ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"], (parameterPath) => `LinuxCNC parameter scratch file remained outside OPFS after save: ${parameterPath}`); }); await runSection("opfs parameter restore in new instance", async () => { await withSmokeSimulator(async (restoredWorkspaceSimulator) => { // LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() // loads persisted numbered parameters before interpreting blocks. const restoredWorkspaceEvents = await restoredWorkspaceSimulator.parseWithParameterFile( "G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X8\nO10 endif\n", "parameters/rs274ngc.var", "linuxcnc", backend, ); expectEvent(restoredWorkspaceEvents, (event) => event.type === "linear-feed" && near(event.end.x, 8), "LinuxCNC OPFS parameter file was not restored across WASM simulator instances"); }); }); await runSection("opfs default parameter backup", async () => { // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() // preserves the previous default parameter file as filename + ".bak". await parseWithParameters("G21 G90\nM30\n", "parameters/rs274ngc.var"); const defaultParameterBackup = await readOpfsText(simulator, "parameters/rs274ngc.var.bak"); expectIncludes(defaultParameterBackup, "5161\t1.000000", "LinuxCNC default OPFS parameter backup did not preserve the previous file"); }); await runSection("opfs parameter temporary cleanup", async () => { await simulator.fs.opfs.writeFile("parameters/rs274ngc.var.new", "stale temporary parameter file\n"); // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() writes // filename + ".new", then renames it over the parameter file. simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale wasm temporary parameter file\n"); const restoredParameterEvents = await parseWithParameters( "G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X9\nO10 endif\n", "parameters/rs274ngc.var", ); expectEvent(restoredParameterEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9), "LinuxCNC parameter file was not restored from OPFS"); await expectMissingOpfsFile( () => simulator.fs.opfs.readFile("parameters/rs274ngc.var.new"), "stale LinuxCNC temporary parameter file remained in OPFS", ); expectMissingWasmPath( () => simulator.fs.module.FS.readFile("rs274ngc.var.new"), "stale LinuxCNC temporary parameter file remained in WASM FS", ); simulator.fs.module.FS.mkdir(simulator.fs.opfs.resolvePath("parameters/new-directory.var.new")); await simulator.fs.opfs.persistDirectory("parameters/new-directory.var.new"); await parseWithParameters("G21 G90\nG0 X5\nG28.1\nM30\n", "parameters/new-directory.var"); expectIncludes(await readOpfsText(simulator, "parameters/new-directory.var"), "5161\t5.000000", "LinuxCNC OPFS parameter file was not saved when stale temporary cleanup saw a directory"); await expectOpfsDirectoryStat(simulator, "parameters/new-directory.var.new", "LinuxCNC temporary file cleanup removed an existing OPFS directory"); await simulator.fs.opfs.removeDirectory("parameters/new-directory.var.new"); }); await runSection("opfs parameter backup bridge", async () => { // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() // attempts link(filename, filename + ".bak") but treats link // failure as non-fatal. The browser bridge must persist a backup // when the WASM filesystem exposes one. simulator.fs.module.FS.writeFile("rs274ngc.var", "5161\t2.000000\n"); simulator.fs.module.FS.writeFile("rs274ngc.var.bak", "5161\t1.000000\n"); await simulator.fs.opfs.persistParameterFile("parameters/backup-bridge.var"); const opfsParameterBackup = await readOpfsText(simulator, "parameters/backup-bridge.var.bak"); expectIncludes(opfsParameterBackup, "5161\t1.000000", "LinuxCNC parameter backup file was not bridged into OPFS"); expectWasmFilesMissing(simulator.fs.module.FS, ["rs274ngc.var", "rs274ngc.var.new", "rs274ngc.var.bak"], (parameterPath) => `LinuxCNC parameter scratch file remained outside OPFS after backup bridge: ${parameterPath}`); }); await runSection("opfs parameter backup after existing save", async () => { await simulator.fs.opfs.writeFile("parameters/existing-save.var", "5161\t3.000000\n"); await simulator.fs.opfs.writeFile("parameters/existing-save.var.bak", "stale previous backup\n"); // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() unlinks // filename + ".bak", links the current parameter file to it, // then renames filename + ".new" over filename. await parseWithParameters("G21 G90\nG0 X4\nG28.1\nM30\n", "parameters/existing-save.var"); await withSmokeSimulator(async (restoredBackupSimulator) => { expectIncludes(await readOpfsText(restoredBackupSimulator, "parameters/existing-save.var"), "5161\t4.000000", "LinuxCNC existing OPFS parameter file was not restored in a new WASM instance"); expectIncludes(await readOpfsText(restoredBackupSimulator, "parameters/existing-save.var.bak"), "5161\t3.000000", "LinuxCNC existing OPFS parameter backup was not restored in a new WASM instance"); }); await expectMissingOpfsFile( () => simulator.fs.opfs.readFile("parameters/existing-save.var.new"), "LinuxCNC existing OPFS parameter temporary file remained after save", ); await simulator.fs.opfs.writeFile("parameters/backup-directory.var", "5161\t6.000000\n"); const backupDirectory = simulator.fs.opfs.resolvePath("parameters/backup-directory.var.bak"); simulator.fs.module.FS.mkdir(backupDirectory); await simulator.fs.opfs.persistDirectory("parameters/backup-directory.var.bak"); await parseWithParameters("G21 G90\nG0 X7\nG28.1\nM30\n", "parameters/backup-directory.var"); expectIncludes(await readOpfsText(simulator, "parameters/backup-directory.var"), "5161\t7.000000", "LinuxCNC OPFS parameter file was not saved when backup link failed"); await expectOpfsDirectoryStat(simulator, "parameters/backup-directory.var.bak", "LinuxCNC non-fatal OPFS backup failure did not leave the existing backup path intact"); }); await runSection("opfs parameter stale backup cleanup", async () => { await simulator.fs.opfs.removeFile("parameters/stale-backup-cleanup.var"); await simulator.fs.opfs.removeFile("parameters/stale-backup-cleanup.var.new"); await simulator.fs.opfs.removeFile("parameters/stale-backup-cleanup.var.bak"); await simulator.fs.opfs.writeFile("parameters/stale-backup-cleanup.var.bak", "stale backup without main file\n"); removeWasmPathIfExists(simulator, "rs274ngc.var.bak"); removeWasmPathIfExists(simulator, "/rs274ngc.var.bak"); await simulator.parseWithParameterFile( "G21 G90\nM30\n", "parameters/stale-backup-cleanup.var", "linuxcnc", backend, ); await expectMissingOpfsFile( () => simulator.fs.opfs.readFile("parameters/stale-backup-cleanup.var.bak"), "stale LinuxCNC parameter backup file remained in OPFS", ); // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() // unlinks stale backup files before replacing the parameter file. await simulator.fs.opfs.removeFile("parameters/missing-cleanup/rs274ngc.var.bak"); }); await runSection("opfs program file with parameter persistence", async () => { await simulator.fs.opfs.writeFile("programs/parameter-file-smoke.ngc", "G21 G90\nF100\nG0 X1\nG28.1\nM30\n"); const fileParameterEvents = await simulator.parseFileWithParameterFile( "programs/parameter-file-smoke.ngc", "parameters/rs274ngc.var", "linuxcnc", backend, ); expectEvent(fileParameterEvents, (event) => event.type === "program-end", "OPFS program file with LinuxCNC parameter persistence did not parse"); await withSmokeSimulator(async (restoredFileParameterSimulator) => expectEvent(await restoredFileParameterSimulator.parseWithParameterFile("G21 G90\nF100\nO10 if [#5161 EQ 1]\nG1 X10\nO10 endif\n", "parameters/rs274ngc.var", "linuxcnc", backend), (event) => event.type === "linear-feed" && near(event.end.x, 10), "LinuxCNC OPFS parameter file saved by parseFileWithParameterFile was not restored in a new WASM instance")); }); await runSection("opfs missing parameter file creation", async () => { await simulator.fs.opfs.removeFile("parameters/missing-main.var"); await simulator.fs.opfs.removeFile("parameters/missing-main.var.new"); await simulator.fs.opfs.removeFile("parameters/missing-main.var.bak"); await simulator.fs.opfs.writeFile("parameters/missing-main.var.bak", "stale backup without main file\n"); await simulator.fs.opfs.writeFile("parameters/missing-main.var.new", "stale temporary without main file\n"); // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() unlinks // filename + RS274NGC_PARAMETER_FILE_BACKUP_SUFFIX before linking, // then renames filename + ".new" over the parameter file. simulator.fs.module.FS.writeFile("rs274ngc.var.bak", "stale wasm backup without main file\n"); simulator.fs.module.FS.writeFile("rs274ngc.var.new", "stale wasm temporary without main file\n"); await simulator.parseWithParameterFile( "G21 G90\nM30\n", "parameters/missing-main.var", "linuxcnc", backend, ); const missingMainParameterFile = await readOpfsText(simulator, "parameters/missing-main.var"); expectIncludes(missingMainParameterFile, "5220\t1.000000", "LinuxCNC missing parameter file was not created through OPFS"); expectIncludes(missingMainParameterFile, "5161\t0.000000", "LinuxCNC missing parameter file did not include required G28 parameter"); await expectMissingOpfsFile( () => simulator.fs.opfs.readFile("parameters/missing-main.var.bak"), "stale LinuxCNC missing-parameter backup remained in OPFS after first creation", ); const missingMainRestoreEvents = await simulator.parseWithParameterFile( "G21 G90\nF100\nO10 if [#5220 EQ 1]\nG1 X6\nO10 endif\n", "parameters/missing-main.var", "linuxcnc", backend, ); expectEvent(missingMainRestoreEvents, (event) => event.type === "linear-feed" && near(event.end.x, 6), "LinuxCNC-created missing OPFS parameter file was not restored on the next parse"); const missingMainParameterBackup = await readOpfsText(simulator, "parameters/missing-main.var.bak"); expectIncludes(missingMainParameterBackup, "5220\t1.000000", "LinuxCNC-created OPFS parameter file was not backed up on the next successful save"); expectMissingWasmPath( () => simulator.fs.module.FS.readFile("rs274ngc.var.bak"), "stale LinuxCNC missing-parameter backup remained in WASM FS", ); await expectMissingOpfsFile( () => simulator.fs.opfs.readFile("parameters/missing-main.var.new"), "stale LinuxCNC missing-parameter temporary file remained in OPFS", ); expectMissingWasmPath( () => simulator.fs.module.FS.readFile("rs274ngc.var.new"), "stale LinuxCNC missing-parameter temporary file remained in WASM FS", ); }); await runSection("opfs invalid parameter restore failures", async () => { await simulator.fs.opfs.writeFile("parameters/out-of-order.var", "5162\t1.000000\n5161\t1.000000\n"); await expectErrorContaining( () => simulator.parseWithParameterFile( "G21 G90\nM30\n", "parameters/out-of-order.var", "linuxcnc", backend, ), "Parameter file out of order", "LinuxCNC accepted an out-of-order OPFS parameter file", ); const outOfOrderParameterFile = await readOpfsText(simulator, "parameters/out-of-order.var"); expectText(outOfOrderParameterFile, "5162\t1.000000\n5161\t1.000000\n", "LinuxCNC persisted an out-of-order OPFS parameter file after restore failure"); expectMissingWasmPath( () => simulator.fs.module.FS.readFile("rs274ngc.var"), "LinuxCNC out-of-order parameter file remained in WASM FS after restore failure", ); await simulator.fs.opfs.writeFile("parameters/out-of-range.var", "0\t1.000000\n"); await expectErrorContaining( () => simulator.parseWithParameterFile( "G21 G90\nM30\n", "parameters/out-of-range.var", "linuxcnc", backend, ), "Parameter number out of range", "LinuxCNC accepted an out-of-range OPFS parameter file", ); const outOfRangeParameterFile = await readOpfsText(simulator, "parameters/out-of-range.var"); expectText(outOfRangeParameterFile, "0\t1.000000\n", "LinuxCNC persisted an out-of-range OPFS parameter file after restore failure"); expectMissingWasmPath( () => simulator.fs.module.FS.readFile("rs274ngc.var"), "LinuxCNC out-of-range parameter file remained in WASM FS after restore failure", ); await simulator.fs.opfs.writeFile("parameters/failed-parse.var", "1\t0.000000\n"); await expectErrorContaining( () => simulator.parseWithParameterFile( "G21 G90\nG28.1\n#0 = 1\n", "parameters/failed-parse.var", "linuxcnc", backend, ), "", "LinuxCNC accepted a failing parse that should not persist parameters", ); const failedParseParameterFile = await readOpfsText(simulator, "parameters/failed-parse.var"); if (failedParseParameterFile.includes("5161\t1.000000")) { throw new Error("LinuxCNC persisted failed parse parameter updates into OPFS"); } // LinuxCNC source basis: save_parameters() runs only after successful interpretation. await simulator.fs.opfs.writeFile("parameters/failed-backup.var", "5161\t5.000000\n"); await simulator.fs.opfs.writeFile("parameters/failed-backup.var.bak", "5161\t4.000000\n"); await expectErrorContaining( () => simulator.parseWithParameterFile("G21 G90\n#0 = 1\n", "parameters/failed-backup.var", "linuxcnc", backend), "", "LinuxCNC accepted a failing parse that should not replace the parameter backup", ); expectText(await readOpfsText(simulator, "parameters/failed-backup.var.bak"), "5161\t4.000000\n", "LinuxCNC replaced the OPFS parameter backup after a failed parse"); }); await runSection("opfs stale parameter state cleanup", async () => { await simulator.fs.opfs.writeFile("parameters/stale-state.var", "5161\t1.000000\n"); const staleStateEvents = await simulator.parseWithParameterFile(g28BranchProgram, "parameters/stale-state.var", "linuxcnc", backend); expectEvent(staleStateEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9), "LinuxCNC did not honor the loaded OPFS parameter state"); await simulator.fs.opfs.removeFile("parameters/stale-state.var"); const missingStateEvents = await simulator.parseWithParameterFile(g28BranchProgram, "parameters/stale-state.var", "linuxcnc", backend); expectNoEvent(missingStateEvents, (event) => event.type === "linear-feed" && near(event.end.x, 9), "LinuxCNC stale OPFS parameter state was not cleared when the file was missing"); // LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() ignores // missing files and reads only the configured parameter file. writeDefaultScratch("5161\t1.000000\n", "stale temporary parameter file\n", "stale backup parameter file\n"); const missingLoad = await simulator.fs.opfs.loadParameterFile("parameters/load-missing.var"); if (missingLoad !== null) { throw new Error("missing OPFS parameter file did not return null"); } expectLinuxCncParameterScratchMissing(simulator, (parameterPath) => `missing OPFS parameter load left stale WASM file ${parameterPath}`); writeDefaultScratch("5161\t7.000000\n", "stale successful-load temporary parameter file\n", "stale successful-load backup parameter file\n"); await simulator.fs.opfs.writeFile("parameters/load-existing.var", "5161\t2.000000\n"); await simulator.fs.opfs.loadParameterFile("parameters/load-existing.var"); expectText(readWasmText(simulator, "rs274ngc.var"), "5161\t2.000000\n", "existing OPFS parameter load did not replace the LinuxCNC default parameter file"); expectWasmFilesMissing(simulator.fs.module.FS, ["rs274ngc.var.new", "rs274ngc.var.bak"], (parameterPath) => `existing OPFS parameter load left stale WASM file ${parameterPath}`); }); }