- 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).
145 lines
6.0 KiB
Bash
Executable File
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
|