Files
wasm-simulator/check-linuxcnc-switchkins-remap-table.sh

1080 lines
38 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
linuxcnc_root=${LINUXCNC_ROOT:-../linuxcnc}
manifest=${1:-linuxcnc-kinematics-source-files.txt}
usage() {
echo "usage: $0 [manifest]" >&2
}
if [[ "${manifest:-}" == "--help" || "${manifest:-}" == "-h" ]]; then
usage
exit 0
fi
if [[ "${manifest:-}" == --* ]]; then
usage
echo "unknown switchkins remap table checker option: $manifest" >&2
exit 1
fi
if [[ -n "${2:-}" ]]; then
usage
echo "too many switchkins remap table checker arguments" >&2
exit 1
fi
if [[ ! -d "$linuxcnc_root" ]]; then
echo "missing LinuxCNC root: $linuxcnc_root" >&2
exit 1
fi
if [[ ! -f "$manifest" ]]; then
echo "missing kinematics manifest: $manifest" >&2
exit 1
fi
validate_manifest_remap_kinstypes() {
local manifest_file=$1
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$linuxcnc_root" "$manifest_file" <<'PY'
import os
import re
import sys
linuxcnc_root, manifest_file = sys.argv[1:]
remap_re = re.compile(r'^\s*#<kinstype>\s*=\s*(-?\d+)\b')
seen = {}
with open(manifest_file, encoding="utf-8") as manifest:
for line_number, line in enumerate(manifest, 1):
parts = line.rstrip("\n").split(":", 2)
if len(parts) < 2 or parts[0] != "remap":
continue
path = parts[1]
basename = os.path.basename(path)
if basename not in {"428remap.ngc", "429remap.ngc", "430remap.ngc"}:
raise SystemExit(f"unexpected switchkins remap source at {manifest_file}:{line_number}: {path}")
if path in seen:
raise SystemExit(f"duplicate switchkins remap source at {manifest_file}:{line_number}: {path}")
seen[path] = line_number
source = os.path.join(linuxcnc_root, path)
with open(source, encoding="utf-8") as handle:
kinstypes = [int(match.group(1)) for match in map(remap_re.match, handle) if match]
if len(kinstypes) != 1:
raise SystemExit(f"expected exactly one #<kinstype> assignment in {path}, found {len(kinstypes)}")
if kinstypes[0] < 0:
raise SystemExit(f"invalid negative #<kinstype> assignment in {path}: {kinstypes[0]}")
if not seen:
raise SystemExit(f"no switchkins remap sources listed in {manifest_file}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$linuxcnc_root" "$manifest_file" <<'JS'
const fs = require("fs");
const path = require("path");
const [linuxcncRoot, manifestFile] = process.argv.slice(2);
const remapRe = /^\s*#<kinstype>\s*=\s*(-?\d+)\b/;
const seen = new Map();
for (const [index, line] of fs.readFileSync(manifestFile, "utf8").split(/\r?\n/).entries()) {
const parts = line.split(":", 3);
if (parts.length < 2 || parts[0] !== "remap") {
continue;
}
const sourcePath = parts[1];
const basename = path.basename(sourcePath);
if (!["428remap.ngc", "429remap.ngc", "430remap.ngc"].includes(basename)) {
throw new Error(`unexpected switchkins remap source at ${manifestFile}:${index + 1}: ${sourcePath}`);
}
if (seen.has(sourcePath)) {
throw new Error(`duplicate switchkins remap source at ${manifestFile}:${index + 1}: ${sourcePath}`);
}
seen.set(sourcePath, index + 1);
const kinstypes = fs.readFileSync(path.join(linuxcncRoot, sourcePath), "utf8")
.split(/\r?\n/)
.map((sourceLine) => remapRe.exec(sourceLine))
.filter(Boolean)
.map((match) => Number(match[1]));
if (kinstypes.length !== 1) {
throw new Error(`expected exactly one #<kinstype> assignment in ${sourcePath}, found ${kinstypes.length}`);
}
if (kinstypes[0] < 0) {
throw new Error(`invalid negative #<kinstype> assignment in ${sourcePath}: ${kinstypes[0]}`);
}
}
if (seen.size === 0) {
throw new Error(`no switchkins remap sources listed in ${manifestFile}`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap manifest validation" >&2
exit 1
fi
}
validate_manifest_remap_kinstypes "$manifest"
validate_manifest_config_remaps() {
local manifest_file=$1
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$linuxcnc_root" "$manifest_file" <<'PY'
import os
import re
import sys
linuxcnc_root, manifest_file = sys.argv[1:]
remap_re = re.compile(r'REMAP\s*=\s*M(428|429|430)(?:\D|$)', re.I)
seen = {}
with open(manifest_file, encoding="utf-8") as manifest:
for line_number, line in enumerate(manifest, 1):
parts = line.rstrip("\n").split(":", 2)
if len(parts) < 2 or parts[0] != "config" or not parts[1].endswith(".ini"):
continue
source_path = parts[1]
source = os.path.join(linuxcnc_root, source_path)
with open(source, encoding="utf-8") as handle:
mcodes = {int(match.group(1)) for match in remap_re.finditer(handle.read())}
if not mcodes:
continue
seen[source_path] = mcodes
missing = {428, 429} - mcodes
if missing:
raise SystemExit(f"switchkins config {source_path} is missing required M-code remap(s): {sorted(missing)}")
if not seen:
raise SystemExit(f"no switchkins REMAP config sources listed in {manifest_file}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$linuxcnc_root" "$manifest_file" <<'JS'
const fs = require("fs");
const path = require("path");
const [linuxcncRoot, manifestFile] = process.argv.slice(2);
const remapRe = /REMAP\s*=\s*M(428|429|430)(?:\D|$)/ig;
const seen = new Map();
for (const line of fs.readFileSync(manifestFile, "utf8").split(/\r?\n/)) {
const parts = line.split(":", 3);
if (parts.length < 2 || parts[0] !== "config" || !parts[1].endsWith(".ini")) {
continue;
}
const sourcePath = parts[1];
const text = fs.readFileSync(path.join(linuxcncRoot, sourcePath), "utf8");
const mcodes = new Set();
for (const match of text.matchAll(remapRe)) {
mcodes.add(Number(match[1]));
}
if (mcodes.size === 0) {
continue;
}
seen.set(sourcePath, mcodes);
const missing = [428, 429].filter((mcode) => !mcodes.has(mcode));
if (missing.length > 0) {
throw new Error(`switchkins config ${sourcePath} is missing required M-code remap(s): ${missing.join(",")}`);
}
}
if (seen.size === 0) {
throw new Error(`no switchkins REMAP config sources listed in ${manifestFile}`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap config validation" >&2
exit 1
fi
}
validate_manifest_config_remaps "$manifest"
validate_generator_cli_contract() {
local manifest_file=$1
local cli_dir
cli_dir=$(mktemp -d "${TMPDIR:-/tmp}/linuxcnc_switchkins_remap_cli.XXXXXX")
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --help >/dev/null 2>&1
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --unknown "$manifest_file" >/dev/null 2>&1; then
echo "switchkins remap generator accepted unknown option" >&2
exit 1
fi
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --all-output-dir >/dev/null 2>&1; then
echo "switchkins remap generator accepted --all-output-dir without directory" >&2
exit 1
fi
if LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh "$manifest_file" extra >/dev/null 2>&1; then
echo "switchkins remap generator accepted too many arguments" >&2
exit 1
fi
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --all-output-dir "$cli_dir/out" "$manifest_file"
for generated_file in \
linuxcnc_switchkins_remap_table.inc \
linuxcnc_switchkins_remap_config_cases.inc \
linuxcnc_switchkins_remap_config_cases.json
do
if [[ ! -s "$cli_dir/out/$generated_file" ]]; then
echo "switchkins remap generator --all-output-dir did not write $generated_file" >&2
exit 1
fi
done
rm -rf "$cli_dir"
}
validate_generator_cli_contract "$manifest"
generated_dir=$(mktemp -d "${TMPDIR:-/tmp}/linuxcnc_switchkins_remap.XXXXXX")
trap 'rm -rf "$generated_dir"' EXIT
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --all-output-dir "$generated_dir" "$manifest"
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh "$manifest" \
>"$generated_dir/linuxcnc_switchkins_remap_table.stdout.inc"
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --config-cases "$manifest" \
>"$generated_dir/linuxcnc_switchkins_remap_config_cases.stdout.inc"
LINUXCNC_ROOT="$linuxcnc_root" ./generate-linuxcnc-switchkins-remap-table.sh --json-cases "$manifest" \
>"$generated_dir/linuxcnc_switchkins_remap_config_cases.stdout.json"
validate_json() {
local file=$1
local label=$2
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
data = json.load(handle)
if not isinstance(data, list) or not data:
raise SystemExit("expected non-empty JSON array")
seen = set()
seen_values = {}
for index, entry in enumerate(data):
if not isinstance(entry, dict):
raise SystemExit(f"entry {index} is not an object")
if set(entry) != {"field", "value", "m428", "m429", "m430"}:
raise SystemExit(f"entry {index} has unexpected keys")
if not isinstance(entry["field"], str) or not entry["field"]:
raise SystemExit(f"entry {index} has invalid field")
if not isinstance(entry["value"], str) or not entry["value"]:
raise SystemExit(f"entry {index} has invalid value")
for key in ("m428", "m429", "m430"):
if not isinstance(entry[key], int):
raise SystemExit(f"entry {index} has non-integer {key}")
case_key = (entry["field"], entry["value"], entry["m428"], entry["m429"], entry["m430"])
if case_key in seen:
raise SystemExit(f"entry {index} duplicates an earlier case")
seen.add(case_key)
value_key = (entry["field"], entry["value"])
types = (entry["m428"], entry["m429"], entry["m430"])
if value_key in seen_values and seen_values[value_key] != types:
raise SystemExit(f"entry {index} has ambiguous field/value mapping")
seen_values[value_key] = types
PY
then
echo "invalid switchkins remap JSON: $label" >&2
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$file" <<'JS'
const fs = require("fs");
const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (!Array.isArray(data) || data.length === 0) {
throw new Error("expected non-empty JSON array");
}
const seen = new Set();
const seenValues = new Map();
for (const [index, entry] of data.entries()) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
throw new Error(`entry ${index} is not an object`);
}
const keys = Object.keys(entry).sort();
const expected = ["field", "m428", "m429", "m430", "value"];
if (keys.length !== expected.length || keys.some((key, i) => key !== expected[i])) {
throw new Error(`entry ${index} has unexpected keys`);
}
if (typeof entry.field !== "string" || entry.field.length === 0) {
throw new Error(`entry ${index} has invalid field`);
}
if (typeof entry.value !== "string" || entry.value.length === 0) {
throw new Error(`entry ${index} has invalid value`);
}
for (const key of ["m428", "m429", "m430"]) {
if (!Number.isInteger(entry[key])) {
throw new Error(`entry ${index} has non-integer ${key}`);
}
}
const caseKey = JSON.stringify([entry.field, entry.value, entry.m428, entry.m429, entry.m430]);
if (seen.has(caseKey)) {
throw new Error(`entry ${index} duplicates an earlier case`);
}
seen.add(caseKey);
const valueKey = JSON.stringify([entry.field, entry.value]);
const types = JSON.stringify([entry.m428, entry.m429, entry.m430]);
if (seenValues.has(valueKey) && seenValues.get(valueKey) !== types) {
throw new Error(`entry ${index} has ambiguous field/value mapping`);
}
seenValues.set(valueKey, types);
}
JS
then
echo "invalid switchkins remap JSON: $label" >&2
exit 1
fi
else
echo "missing python3 or node for switchkins remap JSON validation" >&2
exit 1
fi
}
count_cpp_config_cases() {
local file=$1
grep -Ec '^[[:space:]]*\{"[^"]+", "[^"]+", -?[0-9]+, -?[0-9]+, -?[0-9]+\},?$' "$file"
}
count_table_rows() {
local file=$1
grep -Ec '^[[:space:]]*\{"[^"]+", -?[0-9]+, -?[0-9]+, -?[0-9]+\},?$' "$file"
}
count_json_cases() {
local file=$1
if command -v python3 >/dev/null 2>&1; then
python3 - "$file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
print(len(json.load(handle)))
PY
elif command -v node >/dev/null 2>&1; then
node -e 'console.log(JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).length)' "$file"
else
echo "missing python3 or node for switchkins remap JSON counting" >&2
exit 1
fi
}
validate_generated_header() {
local file=$1
local label=$2
local expected=$3
if [[ "$(head -n 1 "$file")" != "$expected" ]]; then
echo "unexpected generated switchkins remap $label header in $file" >&2
exit 1
fi
}
validate_cpp_table() {
local file=$1
local label=$2
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$file" "$label" <<'PY'
import json
import re
import sys
file, label = sys.argv[1:]
uppercase_or_space_re = re.compile(r'[A-Z\s]')
row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
aliases = {}
row_count = 0
with open(file, encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
match = row_re.match(line)
if not match:
continue
row_count += 1
alias = json.loads(match.group(1))
if not alias:
raise SystemExit(f"{label} table has empty alias at line {line_number}")
if uppercase_or_space_re.search(alias):
raise SystemExit(f"{label} table has unnormalized alias {alias!r} at line {line_number}")
if alias in aliases:
raise SystemExit(
f"{label} table has duplicate alias {alias!r} at lines "
f"{aliases[alias]} and {line_number}"
)
aliases[alias] = line_number
types = (int(match.group(2)), int(match.group(3)), int(match.group(4)))
if types[0] < 0 or types[1] < 0:
raise SystemExit(f"{label} table has missing M428/M429 kinstype at line {line_number}")
for type_value in types:
if type_value < -1:
raise SystemExit(f"{label} table has invalid kinstype {type_value} at line {line_number}")
if row_count == 0:
raise SystemExit(f"{label} table has no rows")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$file" "$label" <<'JS'
const fs = require("fs");
const [file, label] = process.argv.slice(2);
const rowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const uppercaseOrSpaceRe = /[A-Z\s]/;
const aliases = new Map();
let rowCount = 0;
for (const [index, line] of fs.readFileSync(file, "utf8").split(/\r?\n/).entries()) {
const lineNumber = index + 1;
const match = rowRe.exec(line);
if (!match) {
continue;
}
rowCount += 1;
const alias = JSON.parse(match[1]);
if (!alias) {
throw new Error(`${label} table has empty alias at line ${lineNumber}`);
}
if (uppercaseOrSpaceRe.test(alias)) {
throw new Error(`${label} table has unnormalized alias ${JSON.stringify(alias)} at line ${lineNumber}`);
}
if (aliases.has(alias)) {
throw new Error(`${label} table has duplicate alias ${JSON.stringify(alias)} at lines ${aliases.get(alias)} and ${lineNumber}`);
}
aliases.set(alias, lineNumber);
const types = [Number(match[2]), Number(match[3]), Number(match[4])];
if (types[0] < 0 || types[1] < 0) {
throw new Error(`${label} table has missing M428/M429 kinstype at line ${lineNumber}`);
}
for (const typeValue of types) {
if (typeValue < -1) {
throw new Error(`${label} table has invalid kinstype ${typeValue} at line ${lineNumber}`);
}
}
}
if (rowCount === 0) {
throw new Error(`${label} table has no rows`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap table validation" >&2
exit 1
fi
}
validate_cpp_cases() {
local file=$1
local label=$2
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$file" "$label" <<'PY'
import json
import re
import sys
file, label = sys.argv[1:]
row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
seen = set()
seen_values = {}
row_count = 0
with open(file, encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
match = row_re.match(line)
if not match:
continue
row_count += 1
field = json.loads(match.group(1))
value = json.loads(match.group(2))
types = (int(match.group(3)), int(match.group(4)), int(match.group(5)))
if types[0] < 0 or types[1] < 0:
raise SystemExit(f"{label} cases have missing M428/M429 kinstype at line {line_number}")
if any(type_value < -1 for type_value in types):
raise SystemExit(f"{label} cases have invalid kinstype at line {line_number}")
if not field or not value:
raise SystemExit(f"{label} cases have empty field/value at line {line_number}")
case_key = (field, value, *types)
if case_key in seen:
raise SystemExit(f"{label} cases duplicate an earlier case at line {line_number}")
seen.add(case_key)
value_key = (field, value)
if value_key in seen_values and seen_values[value_key] != types:
raise SystemExit(f"{label} cases have ambiguous field/value mapping at line {line_number}")
seen_values[value_key] = types
if row_count == 0:
raise SystemExit(f"{label} cases have no rows")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$file" "$label" <<'JS'
const fs = require("fs");
const [file, label] = process.argv.slice(2);
const rowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const seen = new Set();
const seenValues = new Map();
let rowCount = 0;
for (const [index, line] of fs.readFileSync(file, "utf8").split(/\r?\n/).entries()) {
const lineNumber = index + 1;
const match = rowRe.exec(line);
if (!match) {
continue;
}
rowCount += 1;
const field = JSON.parse(match[1]);
const value = JSON.parse(match[2]);
const types = [Number(match[3]), Number(match[4]), Number(match[5])];
if (types[0] < 0 || types[1] < 0) {
throw new Error(`${label} cases have missing M428/M429 kinstype at line ${lineNumber}`);
}
if (types.some((typeValue) => typeValue < -1)) {
throw new Error(`${label} cases have invalid kinstype at line ${lineNumber}`);
}
if (!field || !value) {
throw new Error(`${label} cases have empty field/value at line ${lineNumber}`);
}
const caseKey = JSON.stringify([field, value, ...types]);
if (seen.has(caseKey)) {
throw new Error(`${label} cases duplicate an earlier case at line ${lineNumber}`);
}
seen.add(caseKey);
const valueKey = JSON.stringify([field, value]);
const typeKey = JSON.stringify(types);
if (seenValues.has(valueKey) && seenValues.get(valueKey) !== typeKey) {
throw new Error(`${label} cases have ambiguous field/value mapping at line ${lineNumber}`);
}
seenValues.set(valueKey, typeKey);
}
if (rowCount === 0) {
throw new Error(`${label} cases have no rows`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap C++ case validation" >&2
exit 1
fi
}
compare_cpp_json_cases() {
local cpp_file=$1
local json_file=$2
local label=$3
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$cpp_file" "$json_file" "$label" <<'PY'
import json
import re
import sys
cpp_file, json_file, label = sys.argv[1:]
row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
cpp_cases = []
with open(cpp_file, encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
match = row_re.match(line)
if not match:
continue
cpp_cases.append({
"field": json.loads(match.group(1)),
"value": json.loads(match.group(2)),
"m428": int(match.group(3)),
"m429": int(match.group(4)),
"m430": int(match.group(5)),
})
with open(json_file, encoding="utf-8") as handle:
json_cases = json.load(handle)
if cpp_cases != json_cases:
raise SystemExit(f"{label} C++/JSON switchkins remap cases differ")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$cpp_file" "$json_file" "$label" <<'JS'
const fs = require("fs");
const [cppFile, jsonFile, label] = process.argv.slice(2);
const rowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const cppCases = [];
for (const line of fs.readFileSync(cppFile, "utf8").split(/\r?\n/)) {
const match = rowRe.exec(line);
if (!match) {
continue;
}
cppCases.push({
field: JSON.parse(match[1]),
value: JSON.parse(match[2]),
m428: Number(match[3]),
m429: Number(match[4]),
m430: Number(match[5]),
});
}
const jsonCases = JSON.parse(fs.readFileSync(jsonFile, "utf8"));
if (JSON.stringify(cppCases) !== JSON.stringify(jsonCases)) {
throw new Error(`${label} C++/JSON switchkins remap cases differ`);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap C++/JSON comparison" >&2
exit 1
fi
}
extract_api_switchkins_alias_keys() {
local file=$1
if command -v python3 >/dev/null 2>&1; then
python3 - "$file" <<'PY'
import re
import sys
file = sys.argv[1]
pattern = re.compile(r'contains_string_value\(compact,\s*"([^"]+)",\s*alias\)')
function_started = False
brace_depth = 0
keys = []
with open(file, encoding="utf-8") as handle:
for line in handle:
if not function_started:
if line.startswith("bool contains_switchkins_config_alias("):
function_started = True
brace_depth += line.count("{") - line.count("}")
continue
brace_depth += line.count("{") - line.count("}")
keys.extend(pattern.findall(line))
if brace_depth <= 0:
break
if not keys:
raise SystemExit("missing switchkins config alias keys in cnc_sim_api.cpp")
for key in keys:
print(key)
PY
elif command -v node >/dev/null 2>&1; then
node - "$file" <<'JS'
const fs = require("fs");
const file = process.argv[2];
const pattern = /contains_string_value\(compact,\s*"([^"]+)",\s*alias\)/g;
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
let started = false;
let braceDepth = 0;
const keys = [];
for (const line of lines) {
if (!started) {
if (line.startsWith("bool contains_switchkins_config_alias(")) {
started = true;
braceDepth += (line.match(/{/g) || []).length - (line.match(/}/g) || []).length;
}
continue;
}
braceDepth += (line.match(/{/g) || []).length - (line.match(/}/g) || []).length;
for (const match of line.matchAll(pattern)) {
keys.push(match[1]);
}
if (braceDepth <= 0) {
break;
}
}
if (keys.length === 0) {
throw new Error("missing switchkins config alias keys in cnc_sim_api.cpp");
}
for (const key of keys) {
console.log(key);
}
JS
else
echo "missing python3 or node for switchkins remap alias extraction" >&2
exit 1
fi
}
validate_switchkins_case_fields_against_api() {
local api_keys_csv=$1
local cpp_file=$2
local json_file=$3
local label=$4
if [[ -z "$api_keys_csv" ]]; then
echo "missing switchkins config alias keys in core/src/cnc_sim_api.cpp" >&2
exit 1
fi
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$api_keys_csv" "$cpp_file" "$json_file" "$label" <<'PY'
import json
import re
import sys
api_keys_csv, cpp_file, json_file, label = sys.argv[1:]
allowed = {key for key in api_keys_csv.split(",") if key}
if not allowed:
raise SystemExit("missing switchkins config alias keys in cnc_sim_api.cpp")
row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
def load_cases(path):
if path.endswith(".json"):
with open(path, encoding="utf-8") as handle:
return json.load(handle)
with open(path, encoding="utf-8") as handle:
return [
{
"field": json.loads(match.group(1)),
"value": json.loads(match.group(2)),
"m428": int(match.group(3)),
"m429": int(match.group(4)),
"m430": int(match.group(5)),
}
for match in row_re.finditer(handle.read())
]
cases = load_cases(cpp_file) + load_cases(json_file)
for index, entry in enumerate(cases):
field = entry["field"]
if field.lower() not in allowed:
raise SystemExit(f"{label} case {index} uses unsupported switchkins field {field!r}")
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$api_keys_csv" "$cpp_file" "$json_file" "$label" <<'JS'
const fs = require("fs");
const [apiKeysCsv, cppFile, jsonFile, label] = process.argv.slice(2);
const allowed = new Set(apiKeysCsv.split(",").filter(Boolean));
if (allowed.size === 0) {
throw new Error("missing switchkins config alias keys in cnc_sim_api.cpp");
}
const rowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const cases = [];
for (const [path, isJson] of [[cppFile, false], [jsonFile, true]]) {
if (isJson) {
for (const entry of JSON.parse(fs.readFileSync(path, "utf8"))) {
cases.push(entry);
}
continue;
}
for (const line of fs.readFileSync(path, "utf8").split(/\r?\n/)) {
const match = rowRe.exec(line);
if (!match) {
continue;
}
cases.push({
field: JSON.parse(match[1]),
value: JSON.parse(match[2]),
m428: Number(match[3]),
m429: Number(match[4]),
m430: Number(match[5]),
});
}
}
cases.forEach((entry, index) => {
if (!allowed.has(entry.field.toLowerCase())) {
throw new Error(`${label} case ${index} uses unsupported switchkins field ${JSON.stringify(entry.field)}`);
}
});
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap API alias validation" >&2
exit 1
fi
}
validate_case_values_against_table() {
local table_file=$1
local cpp_file=$2
local json_file=$3
local label=$4
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$table_file" "$cpp_file" "$json_file" "$label" <<'PY'
import json
import re
import sys
table_file, cpp_file, json_file, label = sys.argv[1:]
table_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
case_row_re = re.compile(
r'^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*'
r'("(?:(?:\\.)|[^"\\])*"),\s*'
r'(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$'
)
aliases = {}
with open(table_file, encoding="utf-8") as handle:
for line in handle:
match = table_row_re.match(line)
if not match:
continue
aliases[json.loads(match.group(1))] = (
int(match.group(2)),
int(match.group(3)),
int(match.group(4)),
)
if not aliases:
raise SystemExit(f"{label} table has no aliases")
case_entries = []
with open(cpp_file, encoding="utf-8") as handle:
for line_number, line in enumerate(handle, 1):
match = case_row_re.match(line)
if not match:
continue
case_entries.append((
f"{cpp_file}:{line_number}",
json.loads(match.group(1)),
json.loads(match.group(2)),
(int(match.group(3)), int(match.group(4)), int(match.group(5))),
))
with open(json_file, encoding="utf-8") as handle:
for index, entry in enumerate(json.load(handle)):
case_entries.append((
f"{json_file}:{index}",
entry["field"],
entry["value"],
(entry["m428"], entry["m429"], entry["m430"]),
))
for location, field, value, types in case_entries:
normalized = "".join(ch for ch in value.lower() if not ch.isspace())
if normalized not in aliases:
raise SystemExit(f"{label} case value {value!r} at {location} is not covered by the alias table")
if aliases[normalized] != types:
raise SystemExit(
f"{label} case {field}={value!r} at {location} has kinstypes "
f"{types}, but alias table has {aliases[normalized]}"
)
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$table_file" "$cpp_file" "$json_file" "$label" <<'JS'
const fs = require("fs");
const [tableFile, cppFile, jsonFile, label] = process.argv.slice(2);
const tableRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const caseRowRe = /^\s*\{("(?:(?:\\.)|[^"\\])*"),\s*("(?:(?:\\.)|[^"\\])*"),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)\},?\s*$/;
const aliases = new Map();
for (const line of fs.readFileSync(tableFile, "utf8").split(/\r?\n/)) {
const match = tableRowRe.exec(line);
if (match) {
aliases.set(JSON.parse(match[1]), [Number(match[2]), Number(match[3]), Number(match[4])]);
}
}
if (aliases.size === 0) {
throw new Error(`${label} table has no aliases`);
}
const caseEntries = [];
for (const [index, line] of fs.readFileSync(cppFile, "utf8").split(/\r?\n/).entries()) {
const match = caseRowRe.exec(line);
if (!match) {
continue;
}
caseEntries.push({
location: `${cppFile}:${index + 1}`,
field: JSON.parse(match[1]),
value: JSON.parse(match[2]),
types: [Number(match[3]), Number(match[4]), Number(match[5])],
});
}
JSON.parse(fs.readFileSync(jsonFile, "utf8")).forEach((entry, index) => {
caseEntries.push({
location: `${jsonFile}:${index}`,
field: entry.field,
value: entry.value,
types: [entry.m428, entry.m429, entry.m430],
});
});
for (const entry of caseEntries) {
const { location, field, value, types } = entry;
const normalized = value.toLowerCase().replace(/\s+/g, "");
if (!aliases.has(normalized)) {
throw new Error(`${label} case value ${JSON.stringify(value)} at ${location} is not covered by the alias table`);
}
const aliasTypes = aliases.get(normalized);
if (JSON.stringify(aliasTypes) !== JSON.stringify(types)) {
throw new Error(`${label} case ${field}=${JSON.stringify(value)} at ${location} has kinstypes ${JSON.stringify(types)}, but alias table has ${JSON.stringify(aliasTypes)}`);
}
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap case/table validation" >&2
exit 1
fi
}
api_keys_csv=$(printf '%s,' $(extract_api_switchkins_alias_keys core/src/cnc_sim_api.cpp))
api_keys_csv=${api_keys_csv%,}
validate_api_switchkins_alias_keys() {
local keys_csv=$1
if command -v python3 >/dev/null 2>&1; then
if ! python3 - "$keys_csv" <<'PY'
import sys
keys = [key for key in sys.argv[1].split(",") if key]
if not keys:
raise SystemExit("missing switchkins config alias keys in cnc_sim_api.cpp")
seen = set()
for key in keys:
if key != key.lower():
raise SystemExit(f"switchkins config alias key is not lowercase: {key}")
if any(ch.isspace() for ch in key):
raise SystemExit(f"switchkins config alias key contains whitespace: {key}")
if key in seen:
raise SystemExit(f"duplicate switchkins config alias key in cnc_sim_api.cpp: {key}")
seen.add(key)
PY
then
exit 1
fi
elif command -v node >/dev/null 2>&1; then
if ! node - "$keys_csv" <<'JS'
const keys = process.argv[2].split(",").filter(Boolean);
if (keys.length === 0) {
throw new Error("missing switchkins config alias keys in cnc_sim_api.cpp");
}
const seen = new Set();
for (const key of keys) {
if (key !== key.toLowerCase()) {
throw new Error(`switchkins config alias key is not lowercase: ${key}`);
}
if (/\s/.test(key)) {
throw new Error(`switchkins config alias key contains whitespace: ${key}`);
}
if (seen.has(key)) {
throw new Error(`duplicate switchkins config alias key in cnc_sim_api.cpp: ${key}`);
}
seen.add(key);
}
JS
then
exit 1
fi
else
echo "missing python3 or node for switchkins remap API key validation" >&2
exit 1
fi
}
validate_api_switchkins_alias_keys "$api_keys_csv"
for generated_file in \
linuxcnc_switchkins_remap_table.inc \
linuxcnc_switchkins_remap_table.stdout.inc \
linuxcnc_switchkins_remap_config_cases.inc \
linuxcnc_switchkins_remap_config_cases.stdout.inc \
linuxcnc_switchkins_remap_config_cases.json \
linuxcnc_switchkins_remap_config_cases.stdout.json
do
if [[ ! -f "$generated_dir/$generated_file" ]]; then
echo "missing generated switchkins remap output: $generated_file" >&2
exit 1
fi
if [[ ! -s "$generated_dir/$generated_file" ]]; then
echo "empty generated switchkins remap output: $generated_file" >&2
exit 1
fi
done
diff -u "$generated_dir/linuxcnc_switchkins_remap_table.inc" \
"$generated_dir/linuxcnc_switchkins_remap_table.stdout.inc"
diff -u "$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.stdout.inc"
diff -u "$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.stdout.json"
validate_generated_header \
"$generated_dir/linuxcnc_switchkins_remap_table.inc" \
"table" \
"// Generated by ./generate-linuxcnc-switchkins-remap-table.sh."
validate_generated_header \
core/src/linuxcnc_switchkins_remap_table.inc \
"tracked table" \
"// Generated by ./generate-linuxcnc-switchkins-remap-table.sh."
validate_generated_header \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"config cases" \
"// Generated by ./generate-linuxcnc-switchkins-remap-table.sh --config-cases."
validate_generated_header \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
"tracked config cases" \
"// Generated by ./generate-linuxcnc-switchkins-remap-table.sh --config-cases."
validate_json "$generated_dir/linuxcnc_switchkins_remap_config_cases.json" "generated linuxcnc_switchkins_remap_config_cases.json"
validate_json "$generated_dir/linuxcnc_switchkins_remap_config_cases.stdout.json" "generated stdout linuxcnc_switchkins_remap_config_cases.json"
validate_json web/public/linuxcnc_switchkins_remap_config_cases.json "web/public/linuxcnc_switchkins_remap_config_cases.json"
validate_cpp_table "$generated_dir/linuxcnc_switchkins_remap_table.inc" "generated"
validate_cpp_table core/src/linuxcnc_switchkins_remap_table.inc "tracked"
validate_cpp_cases "$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" "generated"
validate_cpp_cases core/tests/linuxcnc_switchkins_remap_config_cases.inc "tracked"
generated_table_rows=$(count_table_rows "$generated_dir/linuxcnc_switchkins_remap_table.inc")
if ((generated_table_rows == 0)); then
echo "empty generated switchkins remap table rows" >&2
exit 1
fi
generated_cpp_cases=$(count_cpp_config_cases "$generated_dir/linuxcnc_switchkins_remap_config_cases.inc")
generated_json_cases=$(count_json_cases "$generated_dir/linuxcnc_switchkins_remap_config_cases.json")
tracked_cpp_cases=$(count_cpp_config_cases core/tests/linuxcnc_switchkins_remap_config_cases.inc)
tracked_json_cases=$(count_json_cases web/public/linuxcnc_switchkins_remap_config_cases.json)
if [[ "$generated_cpp_cases" != "$generated_json_cases" ]]; then
echo "generated switchkins remap C++/JSON case count mismatch: $generated_cpp_cases != $generated_json_cases" >&2
exit 1
fi
if [[ "$tracked_cpp_cases" != "$tracked_json_cases" ]]; then
echo "tracked switchkins remap C++/JSON case count mismatch: $tracked_cpp_cases != $tracked_json_cases" >&2
exit 1
fi
compare_cpp_json_cases \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
"generated"
compare_cpp_json_cases \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
web/public/linuxcnc_switchkins_remap_config_cases.json \
"tracked"
validate_switchkins_case_fields_against_api \
"$api_keys_csv" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
"generated"
validate_case_values_against_table \
"$generated_dir/linuxcnc_switchkins_remap_table.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.inc" \
"$generated_dir/linuxcnc_switchkins_remap_config_cases.json" \
"generated"
validate_switchkins_case_fields_against_api \
"$api_keys_csv" \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
web/public/linuxcnc_switchkins_remap_config_cases.json \
"tracked"
validate_case_values_against_table \
core/src/linuxcnc_switchkins_remap_table.inc \
core/tests/linuxcnc_switchkins_remap_config_cases.inc \
web/public/linuxcnc_switchkins_remap_config_cases.json \
"tracked"
diff -u core/src/linuxcnc_switchkins_remap_table.inc "$generated_dir/linuxcnc_switchkins_remap_table.inc"
diff -u core/tests/linuxcnc_switchkins_remap_config_cases.inc "$generated_dir/linuxcnc_switchkins_remap_config_cases.inc"
diff -u web/public/linuxcnc_switchkins_remap_config_cases.json "$generated_dir/linuxcnc_switchkins_remap_config_cases.json"