#!/usr/bin/env bash # Run all test-*.sh files in the repo (including plugins) and the bats suite. # Usage: bash tests/run-tests.sh [--bats-only] # # A script exiting 77 (the automake convention) is reported as SKIPPED, not # passed — a suite that can't run for lack of a binary must not read as green. # # TEST_DIR — override root to search for test-*.sh (default: REPO_ROOT); used by tests. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BATS="$REPO_ROOT/tests/run-bats.sh" BATS_ONLY=false [[ "${1:-}" == "--bats-only" ]] && BATS_ONLY=true SEARCH_ROOT="${TEST_DIR:-$REPO_ROOT}" FAILED=() SKIPPED=() PASSED=0 SKIP_EXIT=77 run_bats() { if [[ -x "$BATS" ]]; then echo "=== bats ===" bash "$BATS" echo "" fi } if $BATS_ONLY; then run_bats exit 0 fi run_bats # Collected with a `while read` loop rather than `mapfile` — macOS ships # /bin/bash 3.2, which has no `mapfile`. Process substitution (not a pipe) # keeps the loop in this shell so the appends survive. `sort` is still fed # newline-delimited output, exactly as before. SCRIPTS=() while IFS= read -r script; do SCRIPTS+=("$script") done < <( find "$SEARCH_ROOT" -name "test-*.sh" \ -not -path "*/.git/*" \ -not -path "*/.claude/worktrees/*" \ | sort ) # Each test-*.sh is independent (fixtures live under its own mktemp dir, none # write back into the live repo tree -- verified before adding this), so they # run concurrently in fixed-size batches instead of one at a time. Batches # (not a rolling pool) because a bounded rolling pool needs `wait -n`, which # is bash 4.3+ and this script is explicitly bash-3.2-safe (see the hazard # scan in test-vale-wrap.sh); plain `wait` -- waiting for every job in the # current batch -- is available since ancient bash. `getconf` over `nproc` # for the same reason: `nproc` doesn't exist on macOS. SCRATCH_ROOT="$(mktemp -d)" trap 'rm -rf "$SCRATCH_ROOT"' EXIT JOBS_LIMIT="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)" running=0 idx=0 for script in ${SCRIPTS[@]+"${SCRIPTS[@]}"}; do idx=$((idx + 1)) ( rc=0 bash "$script" >"$SCRATCH_ROOT/$idx.log" 2>&1 || rc=$? echo "$rc" >"$SCRATCH_ROOT/$idx.status" ) & running=$((running + 1)) if [[ $running -ge $JOBS_LIMIT ]]; then wait running=0 fi done wait idx=0 for script in ${SCRIPTS[@]+"${SCRIPTS[@]}"}; do idx=$((idx + 1)) rel="${script#"$SEARCH_ROOT/"}" echo "=== $rel ===" cat "$SCRATCH_ROOT/$idx.log" rc="$(cat "$SCRATCH_ROOT/$idx.status" 2>/dev/null || echo 1)" if [[ $rc -eq 0 ]]; then PASSED=$((PASSED + 1)) elif [[ $rc -eq $SKIP_EXIT ]]; then SKIPPED+=("$rel") else FAILED+=("$rel") fi echo "" done echo "=== Summary: $PASSED passed, ${#SKIPPED[@]} skipped, ${#FAILED[@]} failed ===" if [[ ${#SKIPPED[@]} -gt 0 ]]; then echo "Skipped scripts:" for s in ${SKIPPED[@]+"${SKIPPED[@]}"}; do echo " $s" done fi if [[ ${#FAILED[@]} -gt 0 ]]; then echo "Failed scripts:" for s in ${FAILED[@]+"${FAILED[@]}"}; do echo " $s" done exit 1 fi