Files
holocron/tests/run-bats.sh
Defame1297 a8beff7d2c perf(kyberforge): parallelize the bats and shell test runners
Both runners spawned one process at a time and let it finish before starting
the next, so wall time scaled with file count even though each file's own
work (bats: mostly repeated apm/git subprocess startup; the vale-heavy
shell scripts: repeated vale binary startup) rarely used a full core.

run-bats.sh now backgrounds one `bats` invocation per file, bounded by core
count, buffering each file's TAP output so concurrent streams can't
interleave, then flushes in stable sorted order once every job finishes.
run-tests.sh does the same for test-*.sh in fixed-size batches (plain `wait`,
not `wait -n`, to stay on the bash-3.2-safe path test-vale-wrap.sh already
enforces for this file) -- verified beforehand that every test-*.sh keeps its
fixtures under its own mktemp dir rather than mutating the live repo tree, so
running them concurrently is safe.

Confirmed correct on both the pass and fail paths (a deliberately failing
bats test and a scratch TEST_DIR with pass/fail/skip-77 scripts) before
measuring: full bats suite 26-30s -> 7.5s, tests/run-tests.sh 68s -> ~45-50s,
full pre-push gate 2m40s -> 1m12s.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-13 18:35:18 +00:00

82 lines
2.6 KiB
Bash
Executable File

#!/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"