479 lines
13 KiB
Bash
Executable File
479 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
WORKSPACE_ROOT="$(cd "$ROOT_DIR/.." && pwd)"
|
|
LINUXCNC_DIR="${LINUXCNC_DIR:-$WORKSPACE_ROOT/linuxcnc}"
|
|
SIM_DIR="$LINUXCNC_DIR/configs/sim"
|
|
BUILD_DIR="${SIM_CONFIG_BUILD_DIR:-$ROOT_DIR/build/native/sim-configs}"
|
|
RS274="${RS274:-$LINUXCNC_DIR/bin/rs274}"
|
|
TIMEOUT_SECONDS="${SIM_CONFIG_TIMEOUT_SECONDS:-30}"
|
|
ONLY_FILTER=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--only)
|
|
[[ $# -ge 2 ]] || { echo "missing value for --only" >&2; exit 2; }
|
|
ONLY_FILTER="$2"
|
|
shift 2
|
|
;;
|
|
--help|-h)
|
|
cat <<EOF
|
|
Usage: $0 [--only PATH_SUBSTRING]
|
|
|
|
Runs LinuxCNC configs/sim .ngc programs through LinuxCNC's rs274 standalone
|
|
entry point and writes logs plus summary.tsv, class-summary.tsv, and
|
|
path-matrix.tsv under:
|
|
$BUILD_DIR
|
|
EOF
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "unknown argument: $1" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ ! -x "$RS274" ]]; then
|
|
echo "missing executable rs274: $RS274" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -d "$SIM_DIR" ]]; then
|
|
echo "missing LinuxCNC sim config directory: $SIM_DIR" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$BUILD_DIR"
|
|
exec 9>"$BUILD_DIR/inventory.lock"
|
|
flock 9
|
|
|
|
"$ROOT_DIR/tools/build_native_probes.sh"
|
|
|
|
export LD_LIBRARY_PATH="$LINUXCNC_DIR/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
|
export PYTHONPATH="$LINUXCNC_DIR/lib/python${PYTHONPATH:+:$PYTHONPATH}"
|
|
|
|
mkdir -p "$BUILD_DIR"/{logs,out,wrapped,params}
|
|
: > "$BUILD_DIR/skipped.tsv"
|
|
SUMMARY_FILE="$BUILD_DIR/summary.tsv"
|
|
CLASS_SUMMARY_FILE="$BUILD_DIR/class-summary.tsv"
|
|
PATH_MATRIX_FILE="$BUILD_DIR/path-matrix.tsv"
|
|
printf 'path\tclass\tstatus\texpected_failure\texit_code\tseconds\tini\ttbl\toutput_lines\tstderr_summary\n' > "$SUMMARY_FILE"
|
|
|
|
relpath() {
|
|
local path="$1"
|
|
printf '%s\n' "${path#"$SIM_DIR"/}"
|
|
}
|
|
|
|
safe_name() {
|
|
local rel="$1"
|
|
rel="${rel//\//_}"
|
|
rel="${rel// /_}"
|
|
printf '%s\n' "$rel"
|
|
}
|
|
|
|
find_nearest_ini() {
|
|
local dir="$1"
|
|
local candidate
|
|
while [[ "$dir" == "$SIM_DIR"* ]]; do
|
|
candidate="$(find "$dir" -maxdepth 1 -type f -name '*.ini' | sort | sed -n '1p')"
|
|
if [[ -n "$candidate" ]]; then
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
[[ "$dir" == "$SIM_DIR" ]] && break
|
|
dir="$(dirname "$dir")"
|
|
done
|
|
return 1
|
|
}
|
|
|
|
find_ini_by_open_file() {
|
|
local ngc="$1"
|
|
local ngc_dir="$2"
|
|
local ini open_file resolved
|
|
while IFS= read -r ini; do
|
|
open_file="$(ini_value "$ini" DISPLAY OPEN_FILE || true)"
|
|
[[ -z "$open_file" ]] && continue
|
|
if [[ "$open_file" = /* ]]; then
|
|
resolved="$open_file"
|
|
else
|
|
resolved="$(realpath -m "$(dirname "$ini")/$open_file")"
|
|
fi
|
|
if [[ "$resolved" == "$(realpath -m "$ngc")" ]]; then
|
|
printf '%s\n' "$ini"
|
|
return 0
|
|
fi
|
|
done < <(find "$ngc_dir" -maxdepth 1 -type f -name '*.ini' | sort)
|
|
return 1
|
|
}
|
|
|
|
find_sim_ini() {
|
|
local rel="$1"
|
|
local ngc_dir="$2"
|
|
local ngc="$3"
|
|
if find_ini_by_open_file "$ngc" "$ngc_dir"; then
|
|
return 0
|
|
fi
|
|
case "$rel" in
|
|
axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/*)
|
|
printf '%s\n' "$SIM_DIR/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/xyzacb-trsrn_twp/xyzacb-trsrn.ini"
|
|
return 0
|
|
;;
|
|
esac
|
|
find_nearest_ini "$ngc_dir"
|
|
}
|
|
|
|
ini_value() {
|
|
local ini="$1"
|
|
local section="$2"
|
|
local key="$3"
|
|
awk -v section="$section" -v key="$key" '
|
|
BEGIN { in_section = 0 }
|
|
/^[[:space:]]*[#;]/ { next }
|
|
/^[[:space:]]*\[/ {
|
|
in_section = (toupper($0) ~ "\\[" toupper(section) "\\]")
|
|
next
|
|
}
|
|
in_section {
|
|
line = $0
|
|
sub(/[[:space:]]*[#;].*$/, "", line)
|
|
split(line, parts, "=")
|
|
name = parts[1]
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
|
|
if (toupper(name) == toupper(key)) {
|
|
value = substr(line, index(line, "=") + 1)
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
|
print value
|
|
exit
|
|
}
|
|
}
|
|
' "$ini"
|
|
}
|
|
|
|
find_upward_file_by_name() {
|
|
local start_dir="$1"
|
|
local basename="$2"
|
|
local dir="$start_dir"
|
|
while [[ "$dir" == "$SIM_DIR"* ]]; do
|
|
if [[ -f "$dir/$basename" ]]; then
|
|
printf '%s\n' "$dir/$basename"
|
|
return 0
|
|
fi
|
|
[[ "$dir" == "$SIM_DIR" ]] && break
|
|
dir="$(dirname "$dir")"
|
|
done
|
|
return 1
|
|
}
|
|
|
|
find_nearest_tbl() {
|
|
local ngc_dir="$1"
|
|
local ini="$2"
|
|
local tool_table
|
|
tool_table="$(ini_value "$ini" EMCIO TOOL_TABLE || true)"
|
|
if [[ -n "$tool_table" ]]; then
|
|
if [[ "$tool_table" = /* && -f "$tool_table" ]]; then
|
|
printf '%s\n' "$tool_table"
|
|
return 0
|
|
fi
|
|
if [[ -f "$(dirname "$ini")/$tool_table" ]]; then
|
|
printf '%s\n' "$(dirname "$ini")/$tool_table"
|
|
return 0
|
|
fi
|
|
if find_upward_file_by_name "$ngc_dir" "$(basename "$tool_table")"; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
local dir="$ngc_dir"
|
|
local candidate
|
|
while [[ "$dir" == "$SIM_DIR"* ]]; do
|
|
candidate="$(find "$dir" -maxdepth 1 -type f -name '*.tbl' | sort | sed -n '1p')"
|
|
if [[ -n "$candidate" ]]; then
|
|
printf '%s\n' "$candidate"
|
|
return 0
|
|
fi
|
|
[[ "$dir" == "$SIM_DIR" ]] && break
|
|
dir="$(dirname "$dir")"
|
|
done
|
|
return 1
|
|
}
|
|
|
|
copy_parameter_file() {
|
|
local ini="$1"
|
|
local dst="$2"
|
|
local parameter_file
|
|
parameter_file="$(ini_value "$ini" RS274NGC PARAMETER_FILE || true)"
|
|
rm -f "$dst" "$dst.bak"
|
|
if [[ -n "$parameter_file" ]]; then
|
|
if [[ "$parameter_file" = /* && -f "$parameter_file" ]]; then
|
|
cp "$parameter_file" "$dst"
|
|
return 0
|
|
fi
|
|
if [[ -f "$(dirname "$ini")/$parameter_file" ]]; then
|
|
cp "$(dirname "$ini")/$parameter_file" "$dst"
|
|
return 0
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
has_program_end() {
|
|
local ngc="$1"
|
|
awk '
|
|
BEGIN { found = 0 }
|
|
/^[[:space:]]*%[[:space:]]*$/ { found = 1 }
|
|
{
|
|
line = tolower($0)
|
|
gsub(/\([^)]*\)/, "", line)
|
|
if (line ~ /(^|[[:space:]])m0*(2|30)([[:space:]]|$)/) {
|
|
found = 1
|
|
}
|
|
}
|
|
END { exit(found ? 0 : 1) }
|
|
' "$ngc"
|
|
}
|
|
|
|
classify_program() {
|
|
local rel="$1"
|
|
local ngc="$2"
|
|
if [[ "$rel" == */remap_subs/* || "$rel" == */nc_subroutines/* ]]; then
|
|
printf 'remap_subroutine\n'
|
|
elif [[ "$rel" == */macros/* || "$rel" == */lathe_configs/* ]]; then
|
|
printf 'macro_load\n'
|
|
elif has_program_end "$ngc"; then
|
|
printf 'main\n'
|
|
else
|
|
printf 'macro_load\n'
|
|
fi
|
|
}
|
|
|
|
prepare_input() {
|
|
local class="$1"
|
|
local ngc="$2"
|
|
local wrapped="$3"
|
|
if [[ "$class" == "main" ]]; then
|
|
printf '%s\n' "$ngc"
|
|
return 0
|
|
fi
|
|
cp "$ngc" "$wrapped"
|
|
printf '\nM2\n' >> "$wrapped"
|
|
printf '%s\n' "$wrapped"
|
|
}
|
|
|
|
known_expected_failure() {
|
|
local rel="$1"
|
|
local combined="$2"
|
|
case "$rel" in
|
|
gmoccapy/*)
|
|
[[ "$combined" == *"python/toplevel.py"* ]] && { printf 'python-remap-runtime-edge\n'; return 0; }
|
|
;;
|
|
axis/laser/*)
|
|
[[ "$combined" == *"python/toplevel.py"* ]] && { printf 'python-remap-runtime-edge\n'; return 0; }
|
|
;;
|
|
axis/external_offsets/dyn_demo.ngc|axis/external_offsets/eoffsets.ngc|axis/external_offsets/jwp_z.ngc|axis/external_offsets/opa_demo.ngc)
|
|
[[ "$combined" == *"Unknown m code used: M111"* ]] && { printf 'user-m-code-M111\n'; return 0; }
|
|
;;
|
|
axis/geometry/xyzc.ngc)
|
|
[[ "$combined" == *"Unknown m code used: M110"* ]] && { printf 'user-m-code-M110\n'; return 0; }
|
|
;;
|
|
axis/foam/foam.ngc)
|
|
[[ "$combined" == *"Bad character 'u' used"* ]] && { printf 'ini-axis-mask-UV\n'; return 0; }
|
|
;;
|
|
axis/vismach/5axis/bridgemill/5axisgui.ngc)
|
|
[[ "$combined" == *"Bad character 'w' used"* ]] && { printf 'ini-axis-mask-W\n'; return 0; }
|
|
;;
|
|
axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition_g533.ngc)
|
|
[[ "$combined" == *"Cannot use axis values without a g code that uses them"* ]] && { printf 'upstream-demo-missing-motion-gcode\n'; return 0; }
|
|
;;
|
|
axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/*)
|
|
[[ "$combined" == *"python/toplevel.py"* ]] && { printf 'python-remap-runtime-edge\n'; return 0; }
|
|
;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
summarize_text() {
|
|
local file="$1"
|
|
if [[ ! -f "$file" ]]; then
|
|
return 0
|
|
fi
|
|
tr '\n\t' ' ' < "$file" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//' | cut -c 1-260
|
|
}
|
|
|
|
total=0
|
|
pass=0
|
|
fail=0
|
|
timeout_count=0
|
|
skip=0
|
|
expected_fail=0
|
|
unexpected_fail=0
|
|
|
|
while IFS= read -r ngc; do
|
|
rel="$(relpath "$ngc")"
|
|
if [[ -n "$ONLY_FILTER" && "$rel" != *"$ONLY_FILTER"* ]]; then
|
|
continue
|
|
fi
|
|
if [[ "$rel" == */python/*.ngc ]]; then
|
|
printf '%s\tunsupported_runtime_edge\tpython-ngc-helper\n' "$rel" >> "$BUILD_DIR/skipped.tsv"
|
|
skip=$((skip + 1))
|
|
continue
|
|
fi
|
|
|
|
ini="$(find_sim_ini "$rel" "$(dirname "$ngc")" "$ngc" || true)"
|
|
if [[ -z "$ini" ]]; then
|
|
printf '%s\tunsupported_runtime_edge\tmissing-ini\n' "$rel" >> "$BUILD_DIR/skipped.tsv"
|
|
skip=$((skip + 1))
|
|
continue
|
|
fi
|
|
tbl="$(find_nearest_tbl "$(dirname "$ngc")" "$ini" || true)"
|
|
if [[ -z "$tbl" ]]; then
|
|
printf '%s\tunsupported_runtime_edge\tmissing-tool-table\n' "$rel" >> "$BUILD_DIR/skipped.tsv"
|
|
skip=$((skip + 1))
|
|
continue
|
|
fi
|
|
|
|
total=$((total + 1))
|
|
class="$(classify_program "$rel" "$ngc")"
|
|
safe="$(safe_name "$rel")"
|
|
wrapped="$BUILD_DIR/wrapped/$safe"
|
|
input="$(prepare_input "$class" "$ngc" "$wrapped")"
|
|
out_file="$BUILD_DIR/out/$safe.out"
|
|
stdout_file="$BUILD_DIR/logs/$safe.stdout"
|
|
stderr_file="$BUILD_DIR/logs/$safe.stderr"
|
|
param_file="$BUILD_DIR/params/$safe.var"
|
|
copy_parameter_file "$ini" "$param_file"
|
|
|
|
start_time="$(date +%s)"
|
|
set +e
|
|
(
|
|
cd "$(dirname "$ini")"
|
|
INI_FILE_NAME="$ini" timeout "$TIMEOUT_SECONDS" "$RS274" \
|
|
-g \
|
|
-i "$(basename "$ini")" \
|
|
-t "$tbl" \
|
|
-v "$param_file" \
|
|
"$input" \
|
|
"$out_file"
|
|
) >"$stdout_file" 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
end_time="$(date +%s)"
|
|
seconds=$((end_time - start_time))
|
|
|
|
status="FAIL"
|
|
if [[ "$rc" -eq 0 ]]; then
|
|
status="PASS"
|
|
pass=$((pass + 1))
|
|
elif [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then
|
|
status="TIMEOUT"
|
|
timeout_count=$((timeout_count + 1))
|
|
else
|
|
fail=$((fail + 1))
|
|
fi
|
|
|
|
combined="$(cat "$stdout_file" "$stderr_file" 2>/dev/null || true)"
|
|
expected=""
|
|
if [[ "$status" != "PASS" ]]; then
|
|
expected="$(known_expected_failure "$rel" "$combined" || true)"
|
|
if [[ -n "$expected" ]]; then
|
|
expected_fail=$((expected_fail + 1))
|
|
else
|
|
unexpected_fail=$((unexpected_fail + 1))
|
|
fi
|
|
fi
|
|
|
|
output_lines=0
|
|
[[ -f "$out_file" ]] && output_lines="$(wc -l < "$out_file" | tr -d '[:space:]')"
|
|
stderr_summary="$(summarize_text "$stderr_file")"
|
|
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
"$rel" \
|
|
"$class" \
|
|
"$status" \
|
|
"$expected" \
|
|
"$rc" \
|
|
"$seconds" \
|
|
"$(relpath "$ini")" \
|
|
"$(relpath "$tbl")" \
|
|
"$output_lines" \
|
|
"$stderr_summary" >> "$SUMMARY_FILE"
|
|
done < <(find "$SIM_DIR" -type f -name '*.ngc' | sort)
|
|
|
|
{
|
|
printf 'class\tstatus\texpected_failure\tcount\n'
|
|
awk -F '\t' '
|
|
NR > 1 {
|
|
class[$2] += 1
|
|
status[$2 "\t" $3] += 1
|
|
if ($4 != "") {
|
|
expected[$2 "\t" $4] += 1
|
|
}
|
|
}
|
|
END {
|
|
for (key in class) {
|
|
print key "\tALL\t-\t" class[key]
|
|
}
|
|
for (key in status) {
|
|
split(key, parts, "\t")
|
|
print parts[1] "\t" parts[2] "\t-\t" status[key]
|
|
}
|
|
for (key in expected) {
|
|
split(key, parts, "\t")
|
|
print parts[1] "\tEXPECTED_FAIL\t" parts[2] "\t" expected[key]
|
|
}
|
|
}
|
|
' "$SUMMARY_FILE" | sort
|
|
} > "$CLASS_SUMMARY_FILE"
|
|
|
|
awk -F '\t' '
|
|
BEGIN {
|
|
OFS = "\t"
|
|
print "path", "class", "status", "expected_failure", "ini", "tbl", "runtime_family", "blocked"
|
|
}
|
|
NR > 1 {
|
|
runtime_family = $1
|
|
sub("/[^/]+$", "", runtime_family)
|
|
blocked = ""
|
|
if ($1 ~ /^axis\/db_demo\//) {
|
|
blocked = "L4-TOOL-DB"
|
|
} else if ($1 ~ /^axis\/vismach\/millturn\// && $2 == "main") {
|
|
blocked = "L4-USER-M-PROCESS"
|
|
} else if ($4 == "upstream-demo-missing-motion-gcode") {
|
|
blocked = "UPSTREAM-DEMO"
|
|
} else if ($1 ~ /^axis\/remap\// && $2 == "main") {
|
|
blocked = "L4-PYTHON-REMAP"
|
|
} else if ($1 ~ /^axis\/vismach\/VMC_toolchange\// && $2 == "main") {
|
|
blocked = "L4-PYTHON-REMAP"
|
|
} else if ($1 ~ /^gmoccapy\//) {
|
|
blocked = "L4-PYTHON-REMAP"
|
|
} else if ($1 ~ /^axis\/laser\//) {
|
|
blocked = "L4-PYTHON-REMAP"
|
|
} else if ($1 ~ /^axis\/vismach\/5axis\/table-rotary_spindle-rotary-nutating\/demos\//) {
|
|
blocked = "L4-PYTHON-REMAP"
|
|
} else if ($2 == "remap_subroutine") {
|
|
blocked = "ASSET-ONLY"
|
|
}
|
|
print $1, $2, $3, ($4 == "" ? "-" : $4), $7, $8, runtime_family, (blocked == "" ? "-" : blocked)
|
|
}
|
|
' "$SUMMARY_FILE" > "$PATH_MATRIX_FILE"
|
|
|
|
{
|
|
printf 'sim config harness summary\n'
|
|
printf 'summary: %s\n' "$SUMMARY_FILE"
|
|
printf 'class_summary: %s\n' "$CLASS_SUMMARY_FILE"
|
|
printf 'path_matrix: %s\n' "$PATH_MATRIX_FILE"
|
|
printf 'total: %s\n' "$total"
|
|
printf 'pass: %s\n' "$pass"
|
|
printf 'fail: %s\n' "$fail"
|
|
printf 'timeout: %s\n' "$timeout_count"
|
|
printf 'expected_fail: %s\n' "$expected_fail"
|
|
printf 'unexpected_fail: %s\n' "$unexpected_fail"
|
|
printf 'skipped: %s\n' "$skip"
|
|
} | tee "$BUILD_DIR/summary.txt"
|
|
|
|
if [[ "$unexpected_fail" -ne 0 ]]; then
|
|
echo "unexpected sim-config failures:" >&2
|
|
awk -F '\t' 'NR > 1 && $3 != "PASS" && $4 == "" { print $1 "\t" $3 "\t" $10 }' "$SUMMARY_FILE" >&2
|
|
exit 1
|
|
fi
|