Expand LinuxCNC interpreter regression coverage

This commit is contained in:
2026-06-09 14:54:54 +08:00
parent 50757e45ba
commit 5bd8f12872
143 changed files with 10895 additions and 219 deletions

View File

@@ -25,6 +25,7 @@ Use `src/index.js` for stable imports:
import {
createLinuxCncIniSdk,
createLinuxCncInterpSdk,
planIniFileContextStaging,
planSimConfigStaging,
} from "./src/index.js";
```
@@ -86,14 +87,22 @@ into the Emscripten filesystem, applies executable bits for user M-code files,
then forwards to `runFileWithIni()` by default or to `runFiveAxisRemapFile()`
when `executionMode: "fiveAxisRemap"` is explicitly requested.
`planSimConfigStaging()` is the companion file-staging planner for
`planIniFileContextStaging()` is the generic file-staging planner for
LinuxCNC INI-driven interpreter runs. Callers provide vendored source-manifest
text, a source root, an INI file name, INI text, and a target WASM directory.
The planner reads only INI file references and the manifest, then returns the
INI, `[DISPLAY]OPEN_FILE`, `[EMCIO]TOOL_TABLE`,
`[RS274NGC]PARAMETER_FILE`, `[RS274NGC]SUBROUTINE_PATH`,
`[RS274NGC]USER_M_PATH`, and remap-NGC files that should be copied into the
Emscripten filesystem. Each planned file includes `sourceRel`, `wasmPath`, the
legacy alias `path`, and `executable`. It does not implement G-code, tool,
parameter, remap, or user-M semantics.
`planSimConfigStaging()` is the companion convenience wrapper for
representative LinuxCNC `configs/sim` programs. Callers provide the vendored
source manifest text, machine relative path, INI file name, and INI text. The
planner reads only INI file references and the manifest, then returns the INI,
`[DISPLAY]OPEN_FILE`, `[EMCIO]TOOL_TABLE`, `[RS274NGC]PARAMETER_FILE`,
`[RS274NGC]SUBROUTINE_PATH`, `[RS274NGC]USER_M_PATH`, and remap-NGC files that
should be copied into the Emscripten filesystem. It does not implement G-code,
tool, parameter, remap, or user-M semantics.
source manifest text, machine relative path, INI file name, and INI text. It
uses `planIniFileContextStaging()` with a `configs/sim/<machine>` source root
and `configs/sim` upward-search boundary.
`runFileWithIniContinueOnError()` uses the same LinuxCNC-backed file execution
path but keeps the runner loop going after LinuxCNC reports an error, matching

View File

@@ -1,3 +1,6 @@
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";
export { planSimConfigStaging } from "./sim-config-staging.js";
export {
planIniFileContextStaging,
planSimConfigStaging,
} from "./sim-config-staging.js";

View File

@@ -98,14 +98,14 @@ function isSubroutinePath(path) {
return path.toLowerCase().endsWith(".ngc");
}
function findUpwardByBasename(manifestSet, sourceDir, fileName) {
function findUpwardByBasename(manifestSet, sourceDir, fileName, searchRootRel) {
let current = sourceDir;
while (current.startsWith("configs/sim")) {
while (!searchRootRel || current === searchRootRel || current.startsWith(`${searchRootRel}/`)) {
const candidate = normalizeRel(`${current}/${fileName}`);
if (manifestSet.has(candidate)) {
return candidate;
}
if (current === "configs/sim") {
if (!current || current === searchRootRel) {
break;
}
current = dirname(current);
@@ -113,7 +113,7 @@ function findUpwardByBasename(manifestSet, sourceDir, fileName) {
return null;
}
function sourceForReference(manifestSet, sourceDir, reference) {
function sourceForReference(manifestSet, sourceDir, reference, searchRootRel) {
if (!reference || reference.startsWith("/")) {
return null;
}
@@ -121,7 +121,7 @@ function sourceForReference(manifestSet, sourceDir, reference) {
if (manifestSet.has(sourceRel)) {
return sourceRel;
}
return findUpwardByBasename(manifestSet, sourceDir, basename(reference));
return findUpwardByBasename(manifestSet, sourceDir, basename(reference), searchRootRel);
}
function addPlannedFile(plan, manifestSet, sourceRel, targetRel, options = {}) {
@@ -141,12 +141,13 @@ function addPlannedFile(plan, manifestSet, sourceRel, targetRel, options = {}) {
plan.set(sourceRel, {
sourceRel,
wasmPath: targetPathFor(options.wasmDir, targetRel),
path: targetPathFor(options.wasmDir, targetRel),
executable,
});
}
function addDirectoryFiles(plan, manifestEntries, sourceDir, targetDir, predicate, options) {
function addDirectoryFiles(plan, manifestEntries, manifestSet, sourceDir, targetDir, predicate, options) {
const prefix = sourceDir ? `${sourceDir}/` : "";
for (const sourceRel of manifestEntries) {
if (!sourceRel.startsWith(prefix)) {
@@ -156,7 +157,7 @@ function addDirectoryFiles(plan, manifestEntries, sourceDir, targetDir, predicat
if (childRel.includes("/") || !predicate(sourceRel)) {
continue;
}
addPlannedFile(plan, new Set(manifestEntries), sourceRel, normalizeRel(`${targetDir}/${childRel}`), options);
addPlannedFile(plan, manifestSet, sourceRel, normalizeRel(`${targetDir}/${childRel}`), options);
}
}
@@ -178,18 +179,45 @@ export function planSimConfigStaging({
iniText,
programFile = null,
wasmDir = `/work/sim/${machineRel}`,
}) {
return planIniFileContextStaging({
manifestText,
sourceRootRel: `configs/sim/${machineRel}`,
sourceSearchRootRel: "configs/sim",
iniFile,
iniText,
programFile,
wasmDir,
});
}
export function planIniFileContextStaging({
manifestText,
sourceRootRel,
iniFile,
iniText,
programFile = null,
wasmDir,
sourceSearchRootRel = sourceRootRel,
}) {
const manifestEntries = cleanManifest(manifestText);
const manifestSet = new Set(manifestEntries);
const iniValues = parseIni(iniText);
const plan = new Map();
const sourceDir = sourceRelFor(machineRel, dirname(iniFile));
const iniSourceRel = sourceRelFor(machineRel, iniFile);
const normalizedSourceRoot = normalizeRel(sourceRootRel);
const normalizedSearchRoot = normalizeRel(sourceSearchRootRel);
const sourceDir = normalizeRel(`${normalizedSourceRoot}/${dirname(iniFile)}`);
const iniSourceRel = normalizeRel(`${normalizedSourceRoot}/${iniFile}`);
const programReference = programFile ?? firstIniValue(iniValues, "DISPLAY", "OPEN_FILE");
addPlannedFile(plan, manifestSet, iniSourceRel, iniFile, { wasmDir, required: true });
if (programReference) {
const programSourceRel = sourceForReference(manifestSet, sourceDir, programReference);
const programSourceRel = sourceForReference(
manifestSet,
sourceDir,
programReference,
normalizedSearchRoot,
);
addPlannedFile(plan, manifestSet, programSourceRel, programReference, {
wasmDir,
required: true,
@@ -204,39 +232,54 @@ export function planSimConfigStaging({
if (!reference) {
continue;
}
const sourceRel = sourceForReference(manifestSet, sourceDir, reference);
const sourceRel = sourceForReference(manifestSet, sourceDir, reference, normalizedSearchRoot);
addPlannedFile(plan, manifestSet, sourceRel, reference, { wasmDir });
}
const subroutineDirs = splitSearchPath(
firstIniValue(iniValues, "RS274NGC", "SUBROUTINE_PATH") ?? "",
);
const subroutineDirs = allIniValues(iniValues, "RS274NGC", "SUBROUTINE_PATH")
.flatMap(splitSearchPath);
for (const dirEntry of subroutineDirs) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceSubdir = normalizeRel(`${sourceDir}/${dirEntry}`);
const targetSubdir = normalizeRel(dirEntry);
addDirectoryFiles(plan, manifestEntries, sourceSubdir, targetSubdir, isSubroutinePath, {
wasmDir,
});
addDirectoryFiles(
plan,
manifestEntries,
manifestSet,
sourceSubdir,
targetSubdir,
isSubroutinePath,
{ wasmDir },
);
}
for (const dirEntry of splitSearchPath(firstIniValue(iniValues, "RS274NGC", "USER_M_PATH") ?? "")) {
for (const dirEntry of allIniValues(iniValues, "RS274NGC", "USER_M_PATH").flatMap(splitSearchPath)) {
if (dirEntry.startsWith("/")) {
continue;
}
const sourceUserMDir = normalizeRel(`${sourceDir}/${dirEntry}`);
const targetUserMDir = normalizeRel(dirEntry);
addDirectoryFiles(plan, manifestEntries, sourceUserMDir, targetUserMDir, isUserMCodePath, {
wasmDir,
executable: true,
});
addDirectoryFiles(
plan,
manifestEntries,
manifestSet,
sourceUserMDir,
targetUserMDir,
isUserMCodePath,
{ wasmDir, executable: true },
);
}
for (const remapName of remapNgcNames(iniValues)) {
for (const dirEntry of subroutineDirs) {
const sourceRel = sourceForReference(manifestSet, normalizeRel(`${sourceDir}/${dirEntry}`), remapName);
const sourceRel = sourceForReference(
manifestSet,
normalizeRel(`${sourceDir}/${dirEntry}`),
remapName,
normalizedSearchRoot,
);
addPlannedFile(plan, manifestSet, sourceRel, normalizeRel(`${dirEntry}/${remapName}`), {
wasmDir,
});