mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* test: parallel unit-test wrapper + failure-first logging (commit 1/8) Lay foundation for v0.26.4 parallel test loop: - scripts/run-unit-parallel.sh: spawns N shards (default min(8, cpu_count)) via run-unit-shard.sh, captures per-shard logs, post-shard single-writer failure-log aggregation at .context/test-failures.log, 10s heartbeat to stderr, per-shard 600s timeout (gtimeout/timeout/bg-pid fallback chain), loud final banner with absolute path + tail-30 of failures, summary file for at-a-glance status. Single writer eliminates concurrent-write hazards on the failure log. - scripts/run-serial-tests.sh: discovers *.serial.test.ts files (concurrency- unsafe by design), runs them with --max-concurrency=1. Invoked after the parallel pass. - scripts/run-unit-shard.sh: now accepts --max-concurrency=N (forwarded to bun test); --dry-run-list moved into argv parsing alongside; excludes *.serial.test.ts in addition to *.slow.test.ts. - bunfig.toml: trim stale comment about typecheck-chained timeout. - .gitignore: add .context/ (Conductor workspace artifacts directory; the failure log + summary + per-shard logs all live here). No package.json changes yet (commit 2). No test reorganization yet (commits 4-7). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: split package.json scripts; bun run test = parallel fast loop (commit 2/8) Per Codex Tension #4 (verify scope), distinguish three tiers cleanly: - `bun run test` = fast loop, file-level parallel fan-out via the new wrapper (scripts/run-unit-parallel.sh). No pre-checks, no typecheck, no wasm compile in the hot path. ~15s of pre-test gates removed. - `bun run verify` = CI's authoritative gate set: check:jsonb + check:progress + check:wasm + typecheck. Matches what .github/workflows/test.yml runs on shard 1, no scope drift. The 4 checks not in CI (privacy, no-legacy-getconnection, trailing-newline, exports-count) move to `bun run check:all` for opt-in local use. - `bun run test:full` = verify + parallel + slow + smart e2e (runs e2e only if DATABASE_URL is set; else loud skip notice to stderr per Open Item #7). The local equivalent of "everything CI runs." Adds `bun run test:serial` for the *.serial.test.ts subset (concurrency- unsafe files run with --max-concurrency=1). Bumps VERSION + package.json to 0.26.4. Both move together per the CI version-gate contract in CLAUDE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: fix-wave for parallel wrapper + tighten privacy gate (commit 3/5) Wave: makes the new wrapper actually green and tightens the CI gate it exposed. Wrapper bug fixes (scripts/run-unit-parallel.sh): - grep_count helper: avoids the `grep -c | echo 0` double-output bug where 0 matches yields a 2-line "0\n0" string and breaks arithmetic. - bun_summary_count helper: parses Bun's actual end-of-shard summary format (`N pass` / `N fail` / `N skip`), not the per-test markers (which are `✓` / `(fail)`, never `(pass)` / `(skip)`). - Heartbeat now reads `^\s+✓` (Bun's per-test pass marker) for live progress mid-run; final summary still uses the summary-line counts for accuracy. Privacy gate tightening: - Move scripts/check-privacy.sh into `bun run verify` (was previously only in the now-removed `bun run test` chain). Without this, after commit 2 the privacy check ran in nothing automatic. - .github/workflows/test.yml now calls `bun run verify` instead of inlining the gate list. Single source of truth for "what's the ship gate." This is what verify == CI was supposed to mean per Codex T#4. - Pre-existing `Wintermute` references in src/core/mounts-cache.ts:6 and :324 caught by the now-running gate; replaced with `your OpenClaw` per CLAUDE.md privacy rule (verify gate now passes on master HEAD). - test/privacy-script-wired.test.ts updated: regression guard now asserts verify includes check:privacy AND that test.yml runs `bun run verify`, replacing the obsolete "test script includes check-privacy.sh" assertion. Quarantine 2 cross-file-contention flakes: - test/brain-registry.test.ts: 28 tests pass alone (41ms); 1 test ("empty/null/undefined id routes to host") fails when run alongside other files in the same shard. Renamed → *.serial.test.ts so it runs in scripts/run-serial-tests.sh's serial pass after the parallel pass completes. - test/reconcile-links.test.ts: 6 tests pass alone (1s); a beforeEach hook times out (~896s) under cross-file contention. Same treatment. Both flakes are bun-process-level shared-state leaks (PGLite singletons or top-level imports). Fixing them properly is the v0.27.0+ intra-file parallelism project (TODO P0 — see commit 5). Measurement after this commit: bun run test = 94s (was 18 min sequential) 3639 pass, 0 fail, 0 skip across 8 parallel shards + 34 serial tests Failure-log + heartbeat + summary all working Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: regression tests for parallel wrapper + serial-test contracts (commit 4/5) Three regression suites pin the v0.26.4 contracts. Without these, future refactors of the wrapper or shard scripts could silently regress the work in commits 1-3. test/scripts/run-unit-shard.test.ts (4 cases — gap b): - Asserts the unit-shard `--dry-run-list` output excludes every *.slow.test.ts and *.serial.test.ts file, plus the test/e2e/ subtree. - Catches a future `find` expression that drops one of the `-not -name` clauses and silently un-quarantines slow/serial files into the parallel pass. test/scripts/serial-files.test.ts (3 cases — gap e): - Every checked-in *.serial.test.ts (via `git ls-files`) is listed by scripts/run-serial-tests.sh's `--dry-run-list`. - The script's source contains `bun test --max-concurrency=1` (the serial-pass guarantee that quarantined files don't run intra-file concurrent and reintroduce the contention they were quarantined for). - Disjoint set: a file is never in both the unit-shard list AND the serial list — pins the carve-out contract. test/scripts/run-unit-parallel.test.ts (6 cases — gaps a + d): - Exit-code propagation (a): wrapper exits non-zero when ANY shard has a failing test; exits zero when all pass. The hardest contract to silently break in a fan-out wrapper (`for ... &; wait` returns the LAST child's status, not any failure's). - Failure-log contract (d): on failure, .context/test-failures.log exists, is non-empty, contains the `--- shard N:` prefix and the failing test's describe text. Stderr banner contains the absolute log path. On success, the log is cleared (no stale content). - Summary file format: `shard N/M: pass=X fail=Y skip=Z rc=W` per shard, machine-parseable for future tooling. The wrapper test runs against a 4-file tempdir (3 pass + 1 fail) so it executes in ~500ms; spawning the wrapper against the real test suite would take ~90s and isn't worth the cost in a regression suite. All 13 cases pass on first run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(v0.26.4): testing tier docs + CHANGELOG + intra-file P0 TODO (commit 5/5) Closes the v0.26.4 ship. CLAUDE.md Testing section rewritten: - New tier table: test (fast loop, 85s) / verify (CI gates, 12s) / test:full (everything local) / test:slow / test:serial / test:e2e / check:all. Each row names its scope, wallclock, and when to use. - Intentional CI vs local divergence section: CI matrix (test-shard.sh, hash-bucketed, includes slow) vs local fast loop (run-unit-shard.sh, round-robin, excludes slow + serial). Codex correctly flagged that a parity test would always fail by design — this is the documentation that explains why. - Failure-first logging contract: .context/test-failures.log format, stderr banner, summary file, wedge handling. - File taxonomy: *.test.ts / *.slow.test.ts / *.serial.test.ts / test/e2e/. Names the two currently-quarantined files and points at the intra-file P0 TODO for the proper fix. CHANGELOG.md `## [0.26.4]` entry per voice rules: - Two-line headline: "bun run test finishes in 85 seconds. Was 18 minutes." + failure-log directive. - Lead paragraph names what shipped and why. - Numbers-that-matter table: BEFORE / AFTER / Δ for wallclock, pre-test gates, failure visibility, shards, pipe-survival. - "What this means for you" closing tied to the inner-loop user. - "To take advantage of v0.26.4" block per the v0.13+ self-repair template (gbrain upgrade + contributor steps). - Itemized changes by area (new scripts, script extensions, package.json tier split, CI tightening, failure-first logging, quarantine, regression tests, bunfig). - "What did NOT ship" section names the intra-file project + E2E template-DB project as P0/P1 follow-ups with concrete acceptance criteria. - Process section names the codex review + scope-correction loop honestly: "snapped back to ship today once empirical measurement showed Bun's --max-concurrency does nothing on tests not marked test.concurrent()." - For-contributors note on portability + single-writer + fallback paths. TODOS.md adds two P-rated entries: - P0: intra-file parallelism via --concurrent flag. Sweep ~58 PGLite sites + ~40 env mutations + 2 mock.module sites. Target: bun run test < 30s. ~1-2 weeks. Detailed acceptance criteria. References Codex findings and plan-file rationale. - P1: E2E parallelism via Postgres template databases. CREATE DATABASE TEMPLATE gbrain_template per test file. ~1-2 days. llms.txt + llms-full.txt regenerated via `bun run build:llms` to absorb the CLAUDE.md changes (per CLAUDE.md's "After any release ship that touches the Key Files annotations in CLAUDE.md, run bun run build:llms" rule). The build-llms regression test was firing in shard 7 of the parallel pass — caught the drift, regeneration cleared it. Final measurement after fix: 94s wallclock, 3652 pass, 0 fail across 8 parallel shards + 34 serial tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
342 lines
16 KiB
Bash
Executable File
342 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# scripts/run-unit-parallel.sh — fast unit-test loop, parallel fan-out.
|
|
#
|
|
# Spawns N parallel `bun test` processes, each running a hash-disjoint shard
|
|
# of the unit-test set (files only — no e2e, no .slow, no .serial). After
|
|
# all shards complete, runs serial-only files (*.serial.test.ts) with
|
|
# --max-concurrency=1. Failure-first logging: extracts failure blocks from
|
|
# each shard's log, writes to .context/test-failures.log with --- shard $i:
|
|
# prefixes, prints loud stderr banner if any failures, exit non-zero.
|
|
#
|
|
# Usage:
|
|
# bash scripts/run-unit-parallel.sh [--shards N] [--max-concurrency N] [--dry-run]
|
|
#
|
|
# Env overrides:
|
|
# SHARDS=N same as --shards
|
|
# GBRAIN_TEST_SHARD_TIMEOUT per-shard wallclock cap, seconds (default 600)
|
|
# GBRAIN_TEST_MAX_CONCURRENCY passed through to bun test (default 4)
|
|
#
|
|
# Output files (workspace-local; falls back to /tmp if .context/ unwritable):
|
|
# .context/test-failures.log failure blocks (cleared at start)
|
|
# .context/test-summary.txt per-shard pass/fail/skip/duration (cleared at start)
|
|
# .context/test-shards/ per-shard logs + exit codes (cleared at start)
|
|
|
|
set -uo pipefail
|
|
|
|
cd "$(dirname "$0")/.."
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# CPU detection: Apple Silicon perf cores → Mac total physical → nproc → 4.
|
|
# Returns a single positive integer.
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
detect_cpus() {
|
|
local n=""
|
|
n=$(sysctl -n hw.perflevel0.physicalcpu 2>/dev/null) && [ -n "$n" ] && [ "$n" -gt 0 ] && echo "$n" && return
|
|
n=$(sysctl -n hw.physicalcpu 2>/dev/null) && [ -n "$n" ] && [ "$n" -gt 0 ] && echo "$n" && return
|
|
n=$(nproc 2>/dev/null) && [ -n "$n" ] && [ "$n" -gt 0 ] && echo "$n" && return
|
|
echo 4
|
|
}
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# Argument parsing. --shards N override wins over $SHARDS; both are clamped.
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
SHARDS_OVERRIDE=""
|
|
MAX_CONCURRENCY_OVERRIDE=""
|
|
DRY_RUN=0
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--shards) SHARDS_OVERRIDE="$2"; shift 2 ;;
|
|
--shards=*) SHARDS_OVERRIDE="${1#*=}"; shift ;;
|
|
--max-concurrency) MAX_CONCURRENCY_OVERRIDE="$2"; shift 2 ;;
|
|
--max-concurrency=*) MAX_CONCURRENCY_OVERRIDE="${1#*=}"; shift ;;
|
|
--dry-run) DRY_RUN=1; shift ;;
|
|
*) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
N="${SHARDS_OVERRIDE:-${SHARDS:-$(detect_cpus)}}"
|
|
if ! printf '%s' "$N" | grep -qE '^[0-9]+$' || [ "$N" -lt 1 ]; then
|
|
echo "ERROR: invalid shard count: $N" >&2; exit 2
|
|
fi
|
|
[ "$N" -gt 8 ] && N=8
|
|
|
|
INTRA_CONC="${MAX_CONCURRENCY_OVERRIDE:-${GBRAIN_TEST_MAX_CONCURRENCY:-4}}"
|
|
SHARD_TIMEOUT="${GBRAIN_TEST_SHARD_TIMEOUT:-600}"
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# Output directories. Prefer workspace-local .context/, fall back to /tmp.
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
LOG_DIR=""
|
|
if mkdir -p .context/test-shards 2>/dev/null; then
|
|
LOG_DIR=".context/test-shards"
|
|
FAILURES_LOG=".context/test-failures.log"
|
|
SUMMARY_FILE=".context/test-summary.txt"
|
|
else
|
|
LOG_DIR="/tmp/gbrain-test-shards-$$"
|
|
FAILURES_LOG="/tmp/gbrain-test-failures.log"
|
|
SUMMARY_FILE="/tmp/gbrain-test-summary.txt"
|
|
mkdir -p "$LOG_DIR" || { echo "ERROR: cannot create log dir" >&2; exit 2; }
|
|
fi
|
|
# Clear from prior run.
|
|
rm -f "$LOG_DIR"/shard-*.log "$LOG_DIR"/shard-*.exit "$LOG_DIR"/shard-*.wedged 2>/dev/null
|
|
: > "$FAILURES_LOG"
|
|
: > "$SUMMARY_FILE"
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# Resolve `timeout` command. macOS without coreutils has neither; we degrade
|
|
# to bg-pid + sleep cap. For now, prefer gtimeout (brew coreutils) → timeout.
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
TIMEOUT_BIN=""
|
|
if command -v gtimeout >/dev/null 2>&1; then TIMEOUT_BIN="gtimeout"
|
|
elif command -v timeout >/dev/null 2>&1; then TIMEOUT_BIN="timeout"
|
|
fi
|
|
|
|
START_TS=$(date +%s)
|
|
echo "[unit-parallel] N=$N shards | --max-concurrency=$INTRA_CONC | timeout=${SHARD_TIMEOUT}s | logs=$LOG_DIR" >&2
|
|
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
echo "[unit-parallel] dry-run: would spawn $N shards with the above settings."
|
|
for i in $(seq 1 "$N"); do
|
|
SHARD="$i/$N" bash scripts/run-unit-shard.sh --dry-run-list 2>/dev/null \
|
|
| sed "s|^| [s$i] |"
|
|
done
|
|
exit 0
|
|
fi
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# Spawn shards. Each child captures its own exit code into a sentinel file
|
|
# so $? is recoverable per-shard (we never trust `wait`'s aggregate value).
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
SHARD_PIDS=()
|
|
for i in $(seq 1 "$N"); do
|
|
(
|
|
SHARD_LOG="$LOG_DIR/shard-$i.log"
|
|
if [ -n "$TIMEOUT_BIN" ]; then
|
|
"$TIMEOUT_BIN" "${SHARD_TIMEOUT}s" \
|
|
env SHARD="$i/$N" \
|
|
bash scripts/run-unit-shard.sh --max-concurrency="$INTRA_CONC" \
|
|
> "$SHARD_LOG" 2>&1
|
|
else
|
|
env SHARD="$i/$N" \
|
|
bash scripts/run-unit-shard.sh --max-concurrency="$INTRA_CONC" \
|
|
> "$SHARD_LOG" 2>&1 &
|
|
pid=$!
|
|
( sleep "$SHARD_TIMEOUT" && kill -TERM "$pid" 2>/dev/null && \
|
|
sleep 5 && kill -KILL "$pid" 2>/dev/null ) &
|
|
cap_pid=$!
|
|
wait "$pid" 2>/dev/null
|
|
kill "$cap_pid" 2>/dev/null
|
|
wait "$cap_pid" 2>/dev/null
|
|
fi
|
|
rc=$?
|
|
echo "$rc" > "$LOG_DIR/shard-$i.exit"
|
|
[ "$rc" = "124" ] && echo "WEDGED" > "$LOG_DIR/shard-$i.wedged"
|
|
) &
|
|
SHARD_PIDS+=($!)
|
|
done
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# Heartbeat: every 10s, print per-shard progress to stderr by tailing logs
|
|
# and counting Bun's `(pass)` / `(fail)` / `(skip)` markers. Read-only.
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# grep_count: returns 0 (single integer) if file is missing or zero matches,
|
|
# otherwise the match count. Avoids the `grep -c | echo 0` double-output bug
|
|
# where 0 matches produces a 2-line "0\n0" string that breaks arithmetic.
|
|
grep_count() {
|
|
local pattern="$1"; local file="$2"
|
|
if [ ! -f "$file" ]; then echo 0; return; fi
|
|
local n
|
|
n=$(grep -cE "$pattern" "$file" 2>/dev/null) || n=0
|
|
echo "${n:-0}"
|
|
}
|
|
|
|
# bun_summary_count: parses Bun's summary lines (one per `bun test` invocation
|
|
# inside a shard — there's only one when we pass an explicit file list).
|
|
# Looks for ` N pass` / ` N fail` / ` N skip` patterns and sums them across
|
|
# all summary blocks the shard emitted. `bun test` prints these near the end
|
|
# of its output. Format: leading whitespace + integer + space + label.
|
|
bun_summary_count() {
|
|
local label="$1"; local file="$2"
|
|
if [ ! -f "$file" ]; then echo 0; return; fi
|
|
awk -v label="$label" '
|
|
$1 ~ /^[0-9]+$/ && $2 == label { total += $1 }
|
|
END { print total + 0 }
|
|
' "$file"
|
|
}
|
|
|
|
heartbeat() {
|
|
while true; do
|
|
sleep 10
|
|
local line=""
|
|
for i in $(seq 1 "$N"); do
|
|
if [ -f "$LOG_DIR/shard-$i.exit" ]; then
|
|
local rc; rc=$(cat "$LOG_DIR/shard-$i.exit" 2>/dev/null || echo "?")
|
|
local status="✓"
|
|
[ "$rc" != "0" ] && status="✗"
|
|
line="$line [s$i: done $status]"
|
|
else
|
|
local lf="$LOG_DIR/shard-$i.log"
|
|
if [ -f "$lf" ]; then
|
|
# Heartbeat: prefer Bun's per-test "✓" (passed) and "(fail)" markers
|
|
# so we see live progress; the "N pass" summary line only appears at
|
|
# the very end of the shard and would always show 0 mid-run.
|
|
local p f
|
|
p=$(grep_count '^[[:space:]]+✓' "$lf")
|
|
f=$(grep_count '^\(fail\)' "$lf")
|
|
line="$line [s$i: ${p}p ${f}f ...]"
|
|
else
|
|
line="$line [s$i: starting]"
|
|
fi
|
|
fi
|
|
done
|
|
printf '[heartbeat] %s\n' "$line" >&2
|
|
done
|
|
}
|
|
heartbeat &
|
|
HB_PID=$!
|
|
trap 'kill "$HB_PID" 2>/dev/null; wait "$HB_PID" 2>/dev/null' EXIT
|
|
|
|
# Wait for every shard. Don't care about wait's exit code.
|
|
for pid in "${SHARD_PIDS[@]}"; do wait "$pid" 2>/dev/null || true; done
|
|
|
|
kill "$HB_PID" 2>/dev/null
|
|
wait "$HB_PID" 2>/dev/null
|
|
trap - EXIT
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# Aggregate failures (single writer; serial; never concurrent).
|
|
# Bun failure block format: from `(fail) ...` line through next `(pass)`,
|
|
# `(skip)`, blank line, or `__bun_test_summary__` marker.
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
TOTAL_FAILURES=0
|
|
TOTAL_PASS=0
|
|
TOTAL_SKIP=0
|
|
TOTAL_RC=0
|
|
for i in $(seq 1 "$N"); do
|
|
SHARD_LOG="$LOG_DIR/shard-$i.log"
|
|
EXIT_FILE="$LOG_DIR/shard-$i.exit"
|
|
WEDGED_FILE="$LOG_DIR/shard-$i.wedged"
|
|
rc=1
|
|
[ -f "$EXIT_FILE" ] && rc=$(cat "$EXIT_FILE" 2>/dev/null || echo 1)
|
|
|
|
pass_count=$(bun_summary_count "pass" "$SHARD_LOG")
|
|
fail_count=$(bun_summary_count "fail" "$SHARD_LOG")
|
|
skip_count=$(bun_summary_count "skip" "$SHARD_LOG")
|
|
TOTAL_PASS=$((TOTAL_PASS + pass_count))
|
|
TOTAL_FAILURES=$((TOTAL_FAILURES + fail_count))
|
|
TOTAL_SKIP=$((TOTAL_SKIP + skip_count))
|
|
|
|
if [ -f "$WEDGED_FILE" ]; then
|
|
TOTAL_RC=1
|
|
{
|
|
echo "--- shard $i: WEDGED after ${SHARD_TIMEOUT}s ---"
|
|
[ -f "$SHARD_LOG" ] && tail -50 "$SHARD_LOG"
|
|
echo ""
|
|
} >> "$FAILURES_LOG"
|
|
echo "shard $i/$N: WEDGED after ${SHARD_TIMEOUT}s (rc=$rc)" >> "$SUMMARY_FILE"
|
|
continue
|
|
fi
|
|
|
|
echo "shard $i/$N: pass=$pass_count fail=$fail_count skip=$skip_count rc=$rc" >> "$SUMMARY_FILE"
|
|
|
|
if [ "$rc" != "0" ]; then
|
|
TOTAL_RC=1
|
|
if [ "$fail_count" -gt 0 ] && [ -f "$SHARD_LOG" ]; then
|
|
# Extract each (fail) block: from `(fail)` line through next `(pass)`,
|
|
# `(skip)`, blank line, or `__bun_test_summary__`. Single awk pass.
|
|
awk -v shard="$i" '
|
|
/^\(fail\) / { in_block=1; print "--- shard " shard ": " $0; next }
|
|
in_block {
|
|
if (/^\(pass\)/ || /^\(skip\)/ || /^[[:space:]]*$/ || /__bun_test_summary__/) { in_block=0; print ""; next }
|
|
print $0
|
|
}
|
|
' "$SHARD_LOG" >> "$FAILURES_LOG"
|
|
elif [ -f "$SHARD_LOG" ]; then
|
|
# Non-zero rc but no (fail) line found — extraction couldn't pinpoint.
|
|
# Dump the full shard log so we never silently lose the failure cause.
|
|
{
|
|
echo "--- shard $i: rc=$rc, no (fail) markers — full log follows ---"
|
|
cat "$SHARD_LOG"
|
|
echo ""
|
|
} >> "$FAILURES_LOG"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# Print each shard's full output to stdout (developer expects to scroll
|
|
# through it). Print summary file last for one-glance overview.
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
for i in $(seq 1 "$N"); do
|
|
SHARD_LOG="$LOG_DIR/shard-$i.log"
|
|
echo ""
|
|
echo "════════════ shard $i/$N ════════════"
|
|
[ -f "$SHARD_LOG" ] && cat "$SHARD_LOG"
|
|
done
|
|
echo ""
|
|
echo "════════════ summary ════════════"
|
|
cat "$SUMMARY_FILE"
|
|
echo ""
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# Serial pass: any *.serial.test.ts files run after parallel pass.
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
SERIAL_RC=0
|
|
SERIAL_FILES_COUNT=0
|
|
SERIAL_FILES_COUNT=$(find test -name '*.serial.test.ts' -not -path 'test/e2e/*' 2>/dev/null | wc -l | tr -d ' ')
|
|
if [ "$SERIAL_FILES_COUNT" -gt 0 ]; then
|
|
echo "════════════ serial pass ($SERIAL_FILES_COUNT files) ════════════"
|
|
bash scripts/run-serial-tests.sh > "$LOG_DIR/serial.log" 2>&1
|
|
SERIAL_RC=$?
|
|
cat "$LOG_DIR/serial.log"
|
|
if [ "$SERIAL_RC" != "0" ]; then
|
|
TOTAL_RC=1
|
|
s_fail=$(bun_summary_count "fail" "$LOG_DIR/serial.log")
|
|
TOTAL_FAILURES=$((TOTAL_FAILURES + s_fail))
|
|
if [ "$s_fail" -gt 0 ]; then
|
|
awk '
|
|
/^\(fail\) / { in_block=1; print "--- shard serial: " $0; next }
|
|
in_block {
|
|
if (/^\(pass\)/ || /^\(skip\)/ || /^[[:space:]]*$/ || /__bun_test_summary__/) { in_block=0; print ""; next }
|
|
print $0
|
|
}
|
|
' "$LOG_DIR/serial.log" >> "$FAILURES_LOG"
|
|
else
|
|
{
|
|
echo "--- shard serial: rc=$SERIAL_RC, no (fail) markers — full log follows ---"
|
|
cat "$LOG_DIR/serial.log"
|
|
echo ""
|
|
} >> "$FAILURES_LOG"
|
|
fi
|
|
echo "serial: rc=$SERIAL_RC fail=$s_fail" >> "$SUMMARY_FILE"
|
|
else
|
|
s_pass=$(bun_summary_count "pass" "$LOG_DIR/serial.log")
|
|
TOTAL_PASS=$((TOTAL_PASS + s_pass))
|
|
echo "serial: pass=$s_pass rc=0" >> "$SUMMARY_FILE"
|
|
fi
|
|
fi
|
|
|
|
END_TS=$(date +%s)
|
|
ELAPSED=$((END_TS - START_TS))
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# Loud banner if anything failed. To stderr so it survives `| head`/`| tail`.
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
if [ "$TOTAL_RC" != "0" ]; then
|
|
ABS_FAIL=$(cd "$(dirname "$FAILURES_LOG")" && pwd)/$(basename "$FAILURES_LOG")
|
|
{
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo "❌ $TOTAL_FAILURES TEST FAILURES — full details:"
|
|
echo " $ABS_FAIL"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
tail -30 "$FAILURES_LOG"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo "[unit-parallel] elapsed=${ELAPSED}s | pass=$TOTAL_PASS fail=$TOTAL_FAILURES skip=$TOTAL_SKIP"
|
|
} >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "[unit-parallel] elapsed=${ELAPSED}s | pass=$TOTAL_PASS fail=$TOTAL_FAILURES skip=$TOTAL_SKIP" >&2
|
|
exit 0
|