diff --git a/tests/run-bats.sh b/tests/run-bats.sh index 5aa0221..9059661 100755 --- a/tests/run-bats.sh +++ b/tests/run-bats.sh @@ -29,4 +29,53 @@ if [[ ${#TEST_FILES[@]} -eq 0 ]]; then exit 0 fi -"$BATS" "${TEST_FILES[@]}" +# 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" diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 1e73765..26de1d4 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -49,14 +49,42 @@ done < <( | sort ) -# bash before 4.4 treats "${arr[@]}" on an empty array as unbound under -# `set -u`, so every array expansion here uses the ${arr[@]+"${arr[@]}"} guard, -# including the SKIPPED/FAILED loops already fenced by a count check. +# 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 ===" - rc=0 - bash "$script" || rc=$? + 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