#!/bin/bash # The half of the runner that actually runs Godot - do not call this # directly, call tests/run_tests.sh instead. # # On Termux it runs INSIDE the proot-distro Ubuntu container. It exists as # its own file so the whole suite runs in ONE `proot-distro login`: entering # the container costs ~0.5s, which used to be paid 38 times over. On WSL # there is no container to enter and run_tests.sh calls it directly - the # scheduling, timeouts and verdicts below are the same either way, which is # why the Godot binary arrives as an argument rather than being hardcoded. # # Args: ... set -uo pipefail GODOT_BIN="$1"; shift PROJECT_DIR="$1"; shift JOBS="$1"; shift VERBOSE="$1"; shift # A broken test can otherwise leave SceneTree alive after an initialization # error, printing the same error every frame forever. Gameplay tests normally # complete in seconds, even on a busy phone, so make that failure explicit # rather than making the suite look hung. Override for deliberate long runs. TEST_TIMEOUT_SECONDS="${TEST_TIMEOUT_SECONDS:-120}" cd "$PROJECT_DIR" || exit 2 tests=("$@") total=${#tests[@]} if (( total == 0 )); then echo "no tests matched" exit 2 fi outdir="$(mktemp -d)" trap 'rm -rf "$outdir"' EXIT REPORT_DIR="${TEST_REPORT_DIR:-$PROJECT_DIR/.test-results}" mkdir -p "$REPORT_DIR" # Import once, serially, before anything runs in parallel. Godot writes to # .godot/ while importing; several instances discovering a cold cache at the # same moment would race on it. After this the workers only read it. "$GODOT_BIN" --headless --import --quit >/dev/null 2>&1 run_one() { local name="$1" local log="$outdir/$name.log" local start end start=$(date +%s%N) # --fixed-fps pins delta to exactly 1/60 instead of measuring the wall # clock. Without it a test's `elapsed` counts REAL seconds, so running # several at once stretches every frame and the gameplay under test sees # deltas it never sees in the game: ships overshoot their braking curve, # settle late, and tests time out for no reason but load. That made # test_camera_settles and test_planet_prompt_map_interaction fail on 2 of # 3 parallel runs. Pinned, a test simulates the same 60fps whatever else # the phone is doing, and its result no longer depends on how busy the # machine was. timeout "$TEST_TIMEOUT_SECONDS" "$GODOT_BIN" --headless --fixed-fps 60 \ -s "res://tests/$name" >"$log" 2>&1 local rc=$? end=$(date +%s%N) local ms=$(( (end - start) / 1000000 )) # Two independent criteria, because either one alone can lie: a test that # crashes before reaching its verdict exits non-zero without ever printing # FAIL, and a hypothetical `quit(0)` on a failing path would exit clean # while the log says otherwise. if (( rc == 0 )) && grep -q ": PASS" "$log"; then printf 'PASS %6dms %s\n' "$ms" "$name" > "$outdir/$name.status" elif (( rc == 124 )); then printf 'TIMEOUT %4dms %s (limit %ss)\n' \ "$ms" "$name" "$TEST_TIMEOUT_SECONDS" > "$outdir/$name.status" elif (( rc == 137 || rc == 139 )); then # Killed, not failed - almost always the kernel reclaiming memory. Worth # saying plainly: a killed worker and a failing one both simply produced # no verdict before, so a whole suite dying to the OOM killer was # indistinguishable from a hang. printf 'KILLED %4dms %s (signal %d - out of memory? try JOBS=1)\n' \ "$ms" "$name" "$(( rc - 128 ))" > "$outdir/$name.status" else printf 'FAIL %6dms %s (exit %d)\n' "$ms" "$name" "$rc" > "$outdir/$name.status" fi cat "$outdir/$name.status" } running=0 for name in "${tests[@]}"; do run_one "$name" & running=$(( running + 1 )) if (( running >= JOBS )); then wait -n running=$(( running - 1 )) fi done wait failed=0 for name in "${tests[@]}"; do if ! grep -q '^PASS' "$outdir/$name.status" 2>/dev/null; then failed=$(( failed + 1 )) echo echo "=== $name FAILED, full output ===" cat "$outdir/$name.log" 2>/dev/null elif [[ "$VERBOSE" == "1" ]]; then echo echo "=== $name ===" cat "$outdir/$name.log" 2>/dev/null fi done echo echo "$(( total - failed ))/$total passed" # Keep the concise verdicts after the temporary logs are removed. This is # especially useful from Termux, where a long-running child can outlive the # terminal command that launched it and its final buffered output is lost. rm -f "$REPORT_DIR"/*.status cp "$outdir"/*.status "$REPORT_DIR"/ (( failed == 0 ))