#!/usr/bin/env bash # Runs the headless GDScript gameplay tests. # # bash tests/run_tests.sh # everything # bash tests/run_tests.sh formation # only tests matching a substring # JOBS=1 bash tests/run_tests.sh # serial, for debugging a hang # VERBOSE=1 bash tests/run_tests.sh # print passing tests' output too # IN_PLACE=1 bash tests/run_tests.sh # run from the project dir itself # GODOT_BIN=/path/to/godot bash tests/run_tests.sh # pick the binary # # There are two ways to get a Godot to run them with, picked automatically: # # Termux the Godot 4.7 Linux arm64 build installed inside the # proot-distro Ubuntu container, which is the only way to run # a desktop Godot on the phone at all. # WSL the Windows Godot build, run straight from /mnt/c. A Linux # `godot` on PATH is used first if there is one. # # The platform decides which Godot runs and whether the project is copied # first; the worker count and tests/_run_inside.sh, which does the actual # running, are shared. # # On Termux, three things make this fast, in descending order of how much # they matter (measured, not guessed - see tests/_profile_startup.gd): # # 1. It runs from a COPY on Termux's own filesystem, not from the project # directory. The project lives under ~/storage/shared, which is Android's # FUSE bridge; read through proot on top of that, `load("res://world/Main.tscn")` # alone took 8.5s per test, against ~2s from a native path. Nearly every # test loads Main.tscn, and none of that time is gameplay. The copy is # ~1.4s for the whole suite and its .godot import cache is kept between # runs. WSL skips the copy: a Windows Godot reads C: natively, and the # only thing copying to ext4 would achieve is putting the project behind # the \\wsl$ share instead. # 2. Tests run JOBS at a time. Each is its own Godot process - they call # quit() and stamp on the GameState autoload, so they cannot share one - # which makes the suite a pile of independent, single-core-bound jobs on # an 8-core phone. # 3. The whole run happens in ONE `proot-distro login`. Entering the # container costs ~0.5s, which used to be paid once per test. # # Output is buffered per test and printed only for failures; six interleaved # logs are unreadable. set -uo pipefail PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PLATFORM="termux" if [[ -n "${WSL_DISTRO_NAME:-}" ]] || grep -qi microsoft /proc/version 2>/dev/null; then PLATFORM="wsl" fi # Finding a Godot on WSL. A Linux build on PATH wins - no interop, no drive # letters - but there usually is not one, and the Windows build the editor # already runs from is right there on /mnt/c. Two things about picking that # one up: # - it must be the *_console.exe. The plain .exe is built as a Windows GUI # subsystem binary: it detaches from the terminal, so every test would # produce an empty log and the runner would call all of them failures. # - sort -V so 4.7.2 beats 4.6.1 when several versions are downloaded. _find_godot_wsl() { local found found="$(command -v godot 2>/dev/null)" if [[ -n "$found" ]]; then echo "$found" return fi found="$(find /mnt/c/Users/*/Downloads /mnt/c/Users/*/AppData/Local/Programs \ "/mnt/c/Program Files" -maxdepth 2 -name 'Godot_v*_console.exe' \ 2>/dev/null | sort -V | tail -1)" echo "$found" } # Worker count comes from FREE MEMORY, not core count. Each Godot instance is # ~400MB and this phone shares its RAM with everything else running on it. # Getting it wrong is bad in both directions: # - too many and the kernel SIGKILLs the runner mid-suite (exit 137), which # looks exactly like a hang # - too many under swap pressure and every worker thrashes: the same suite # measured 100-190s with memory free, and 888s while 9GB sat in swap # One worker per ~900MB available, capped at 6, and forced to 1 once swap is # more than half consumed - past that, parallelism costs more than it buys. # Override with JOBS= when you know better. _pick_jobs() { local avail swap_total swap_free n avail=$(awk '/MemAvailable/ {print int($2 / 1024)}' /proc/meminfo 2>/dev/null) swap_total=$(awk '/SwapTotal/ {print $2}' /proc/meminfo 2>/dev/null) swap_free=$(awk '/SwapFree/ {print $2}' /proc/meminfo 2>/dev/null) if [[ -z "$avail" ]]; then echo 2 return fi if [[ -n "$swap_total" ]] && (( swap_total > 0 )) \ && (( (swap_total - swap_free) * 2 > swap_total )); then echo 1 return fi n=$(( avail / 900 )) (( n < 1 )) && n=1 (( n > 6 )) && n=6 echo "$n" } JOBS="${JOBS:-$(_pick_jobs)}" VERBOSE="${VERBOSE:-0}" # The copy only buys anything on Termux (see 1. above), so on WSL the project # directory IS the run directory whatever IN_PLACE says. if [[ "$PLATFORM" == "wsl" ]]; then IN_PLACE=1 else IN_PLACE="${IN_PLACE:-0}" fi WORK_DIR="${WORK_DIR:-$HOME/.cache/spaceship-tests}" if [[ -z "${GODOT_BIN:-}" ]]; then if [[ "$PLATFORM" == "wsl" ]]; then GODOT_BIN="$(_find_godot_wsl)" if [[ -z "$GODOT_BIN" ]]; then echo "no Godot found. Install one on PATH, or download a Windows" >&2 echo "build and point GODOT_BIN at its *_console.exe." >&2 exit 2 fi else GODOT_BIN="/root/Godot_v4.7.1-stable_linux.arm64" fi fi filter="${1:-}" tests=() for path in "$PROJECT_DIR"/tests/test_*.gd; do name="$(basename "$path")" if [[ -z "$filter" || "$name" == *"$filter"* ]]; then tests+=("$name") fi done if (( ${#tests[@]} == 0 )); then echo "no tests match '$filter'" exit 2 fi start=$(date +%s) run_dir="$PROJECT_DIR" if [[ "$IN_PLACE" != "1" ]]; then run_dir="$WORK_DIR" mkdir -p "$run_dir" # Clear last run's sources but keep .godot, so imports stay warm. Without # the clear, a test deleted from the project would live on in the copy and # keep being run. find "$run_dir" -mindepth 1 -maxdepth 1 ! -name '.godot' -exec rm -rf {} + # android/ and builds/ are export artefacts no test touches, and the .apk # is 30MB of nothing useful here. tar -C "$PROJECT_DIR" \ --exclude=./.godot --exclude=./android --exclude=./builds --exclude='*.apk' \ -cf - . 2>/dev/null | tar -C "$run_dir" -xf - fi if [[ "$PLATFORM" == "wsl" ]]; then bash "$run_dir/tests/_run_inside.sh" \ "$GODOT_BIN" "$run_dir" "$JOBS" "$VERBOSE" "${tests[@]}" else proot-distro login ubuntu -- bash "$run_dir/tests/_run_inside.sh" \ "$GODOT_BIN" "$run_dir" "$JOBS" "$VERBOSE" "${tests[@]}" fi status=$? echo "in $(( $(date +%s) - start ))s with JOBS=$JOBS" exit $status