#!/usr/bin/env bash # Run all bats test files in the repo. # Usage: bash tests/run-bats.sh set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BATS="$REPO_ROOT/tests/bats/bin/bats" if [[ ! -x "$BATS" ]]; then echo "bats not found at $BATS — initializing submodules..." >&2 git -C "$REPO_ROOT" submodule update --init --recursive fi if [[ ! -x "$BATS" ]]; then echo "Error: bats still not found at $BATS after submodule init" >&2 exit 1 fi mapfile -t TEST_FILES < <( find "$REPO_ROOT" -name "*.bats" \ -not -path "*/tests/bats/*" \ -not -path "*/test_helper/*" \ -not -path "*/.claude/worktrees/*" \ | sort ) if [[ ${#TEST_FILES[@]} -eq 0 ]]; then echo "No .bats test files found." >&2 exit 0 fi # Each file gets its own `bats` process, run concurrently (bounded by core # count) instead of one `bats` invocation working through all files serially. # A single test file is still serial internally -- this only overlaps the # fixed per-process startup cost (git/apm subprocess spawns dominate several # of these suites) across files, which is where the wall-clock actually goes. # Output is buffered per file so concurrent TAP streams can't interleave, then # flushed in stable sorted order once every job has finished. SCRATCH_ROOT="$(mktemp -d)" trap 'rm -rf "$SCRATCH_ROOT"' EXIT # A failing test is the normal case a CI runner must handle, and a failing # background job makes `wait`/`wait -n` return non-zero -- under `set -e` that # would abort the script right here, before the per-file report below ever # runs. Every wait in this dispatcher is therefore explicitly guarded. JOBS="$(nproc 2>/dev/null || echo 4)" running=0 i=0 for f in "${TEST_FILES[@]}"; do i=$((i + 1)) ( "$BATS" "$f" >"$SCRATCH_ROOT/$i.log" 2>&1; echo $? >"$SCRATCH_ROOT/$i.status" ) & running=$((running + 1)) if [[ "$running" -ge "$JOBS" ]]; then wait -n || true running=$((running - 1)) fi done wait || true FAIL=0 TOTAL_OK=0 TOTAL_NOT_OK=0 i=0 for f in "${TEST_FILES[@]}"; do i=$((i + 1)) rel="${f#"$REPO_ROOT"/}" echo "=== $rel ===" cat "$SCRATCH_ROOT/$i.log" echo "" file_ok="$(grep -c '^ok ' "$SCRATCH_ROOT/$i.log" || true)" file_not_ok="$(grep -c '^not ok ' "$SCRATCH_ROOT/$i.log" || true)" status="$(cat "$SCRATCH_ROOT/$i.status" 2>/dev/null || echo 1)" TOTAL_OK=$((TOTAL_OK + file_ok)) TOTAL_NOT_OK=$((TOTAL_NOT_OK + file_not_ok)) if [[ "$file_not_ok" -gt 0 || "$status" -ne 0 ]]; then FAIL=1 fi done echo "$((TOTAL_OK + TOTAL_NOT_OK)) tests, $TOTAL_NOT_OK failures" exit "$FAIL"