Files
settled-reach/tests/run-godot
T
jpmschweitzer 50ac9041ad fix(tests): PR #134 review round 3 — seed start-state + PID log path
- test_character_creation_sprint28: before_each now seeds
  _selected_bookmark_id and _selected_location_id so the new disabled-
  guard in _on_start() (round 2) doesn't silently block 5 existing
  tests that call _on_start()/KEY_ENTER without setting up a valid
  bookmark selection. Restores the 2 tests Hoshe flagged as R2-H1 plus
  3 siblings that would have degraded the same way under the guard.
- tests/run-godot: LOG_FILE now includes $$ (PID) so concurrent runs
  across worktrees don't clobber each other's logs. Path is echoed
  back via the stdout JSON "log" field and the stderr hint line, so
  callers never need to predict it (R2-H2).
2026-04-21 14:30:38 +02:00

145 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# tests/run-godot: Run Godot client test suite via gdUnit4 (D-030)
#
# Exit: 0 = all pass, non-zero = failure.
# Stdout: JSON summary on ONE line, plus a pointer to the full log.
# {"suite":"godot","total":N,"passed":N,"failed":N,"duration_ms":N,"log":"/tmp/sr-run-godot.log"}
# (on timeout: same JSON + "timeout":true, "timeout_sec":N; exit code 124)
# Stderr: a short hint line pointing at the log. The Godot/gdUnit4 output
# does NOT stream to stdout or stderr — it is captured to the log file.
# This is intentional: streaming 20k+ lines of test log into an LLM
# caller's context is unworkable. Inspect the log with the commands the
# hint line suggests.
#
# --filter <stem|filename|res-path>: narrow the test run to a single file.
set -euo pipefail
# Hard wall-clock cap. Sprint 36 lost an hour to a hung test suite that
# silently consumed CPU forever. 300s is generous for the full suite
# (which currently runs in ~60s) and well above the slowest single suite
# (~40s for the compositor build). If you need longer for an unusual
# workload (fixture regen, etc.), prefer adding a dedicated script over
# extending this cap — the cap is the point.
TIMEOUT_SEC=300
# Per-process log path. PID suffix prevents concurrent runs across worktrees
# from clobbering each other's logs and producing summary JSON that mixes
# counts from different suites (R2-Hoshe-2). The actual path is echoed back
# via the JSON "log" field and the stderr hint line, so callers don't need
# to predict it.
LOG_FILE="/tmp/sr-run-godot.$$.log"
FILTER=""
while [[ $# -gt 0 ]]; do
case "$1" in
--filter) FILTER="${2:-}"; shift 2 ;;
--filter=*) FILTER="${1#--filter=}"; shift ;;
*) echo "Unknown argument: $1" >&2; exit 2 ;;
esac
done
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GODOT=$(command -v godot4 2>/dev/null || command -v godot 2>/dev/null || echo "")
if [[ -z "$GODOT" ]]; then
printf '{"suite":"godot","total":0,"passed":0,"failed":0,"duration_ms":0,"error":"godot not found in PATH"}\n'
exit 1
fi
# Resolve the test target: directory or specific file
if [[ -n "$FILTER" ]]; then
# Support bare name (test_protocol) or full path (test_protocol.gd)
if [[ "$FILTER" == res://* ]]; then
TEST_TARGET="$FILTER"
elif [[ "$FILTER" == *.gd ]]; then
TEST_TARGET="res://tests/$FILTER"
else
TEST_TARGET="res://tests/${FILTER}.gd"
fi
else
TEST_TARGET="res://tests/"
fi
START_MS=$(date +%s%3N)
# Redirect Godot+gdUnit4 output to the log file. Nothing streams to the
# caller — the summary JSON (stdout) and the hint line (stderr) are the
# only things the caller ever sees. See header comment for rationale.
set +e
timeout --foreground --kill-after=10 "$TIMEOUT_SEC" \
"$GODOT" --headless --path "$REPO_ROOT/client" \
-s res://addons/gdUnit4/bin/GdUnitCmdTool.gd \
--ignoreHeadlessMode \
-c \
-a "$TEST_TARGET" \
> "$LOG_FILE" 2>&1
EXIT_CODE=$?
set -e
END_MS=$(date +%s%3N)
DURATION_MS=$((END_MS - START_MS))
# timeout(1) exit code 124 = wall clock exceeded; 137 = needed SIGKILL.
TIMED_OUT=false
if [[ "$EXIT_CODE" -eq 124 || "$EXIT_CODE" -eq 137 ]]; then
TIMED_OUT=true
fi
# gdUnit4 outputs per-suite statistics: "N test cases | X errors | Y failures | ..."
TOTAL=0; PASSED=0; FAILED=0
STATS_LINES=$(grep -oE "[0-9]+ test cases \| [0-9]+ errors \| [0-9]+ failures" "$LOG_FILE" || true)
if [[ -n "$STATS_LINES" ]]; then
TOTAL=$(echo "$STATS_LINES" | grep -oE '^[0-9]+' | awk '{s+=$1} END {print s}')
ERRORS=$(echo "$STATS_LINES" | grep -oE '[0-9]+ errors' | grep -oE '^[0-9]+' | awk '{s+=$1} END {print s}')
FAILURES=$(echo "$STATS_LINES" | grep -oE '[0-9]+ failures' | grep -oE '^[0-9]+' | awk '{s+=$1} END {print s}')
FAILED=$(( ${ERRORS:-0} + ${FAILURES:-0} ))
PASSED=$(( TOTAL - FAILED ))
fi
# Fallback: parse "Executed test cases : (X/N)" for total if stats parse failed
if [[ "$TOTAL" -eq 0 ]]; then
EXEC_LINE=$(grep -oE "Executed test cases : \([0-9]+/[0-9]+\)" "$LOG_FILE" | tail -1 || true)
if [[ -n "$EXEC_LINE" ]]; then
TOTAL=$(echo "$EXEC_LINE" | grep -oE '/[0-9]+\)' | grep -oE '[0-9]+')
PASSED=$(echo "$EXEC_LINE" | grep -oE '\([0-9]+/' | grep -oE '[0-9]+')
FAILED=$(( TOTAL - PASSED ))
fi
fi
LOG_LINES=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)
# Single-line JSON summary on stdout — machine-parseable, small.
if [[ "$TIMED_OUT" == "true" ]]; then
printf '{"suite":"godot","total":%d,"passed":%d,"failed":%d,"duration_ms":%d,"timeout":true,"timeout_sec":%d,"log":"%s"}\n' \
"${TOTAL:-0}" "${PASSED:-0}" "${FAILED:-0}" "$DURATION_MS" "$TIMEOUT_SEC" "$LOG_FILE"
else
printf '{"suite":"godot","total":%d,"passed":%d,"failed":%d,"duration_ms":%d,"log":"%s"}\n' \
"${TOTAL:-0}" "${PASSED:-0}" "${FAILED:-0}" "$DURATION_MS" "$LOG_FILE"
fi
# Hint line on stderr. Stays short. LLM callers: read this literally.
if [[ "$TIMED_OUT" == "true" ]]; then
cat >&2 <<EOF
TEST_TIMEOUT: tests/run-godot exceeded ${TIMEOUT_SEC}s wall-clock cap (killed=${EXIT_CODE}).
filter=${FILTER:-<none>} target=${TEST_TARGET} log=${LOG_FILE} (${LOG_LINES} lines)
This is NOT a regular test failure — the process was force-terminated.
A hung test almost always means a cleanup hook froze (e.g. after_test
freeing GdUnit4 infrastructure) or an assertion waits on a signal that
never fires. Bisect:
grep -E 'STARTED|PASSED|FAILED' ${LOG_FILE} | tail -20
The last STARTED without a matching PASSED/FAILED is the hang site.
EOF
elif [[ "${FAILED:-0}" -gt 0 ]]; then
cat >&2 <<EOF
Tests finished with failures. log=${LOG_FILE} (${LOG_LINES} lines)
Summarize failures per suite:
sed 's/\x1b\[[0-9;]*m//g' ${LOG_FILE} | grep -E 'FAILED [0-9]+ms\$' | awk -F'>' '{print \$1}' | sort | uniq -c | sort -rn
First failure block with context:
sed 's/\x1b\[[0-9;]*m//g' ${LOG_FILE} | grep -n 'FAILED\|Expecting\|Godot Runtime Error' | head -40
EOF
else
echo "Tests passed. log=${LOG_FILE} (${LOG_LINES} lines)" >&2
fi
exit $EXIT_CODE