#!/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 <&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