#!/usr/bin/env bash # Run Bun test files in parallel with proper error reporting # Usage: ./run-parallel.sh [test_directory2] [...] [--max=N] if [ $# -eq 0 ]; then echo "Error: No test directories specified" echo "Usage: ./run-parallel.sh [test_directory2] [...] [--max=N]" exit 1 fi # Parse arguments TEST_DIRS=() MAX_PARALLEL=6 for arg in "$@"; do if [[ "$arg" == --max=* ]]; then MAX_PARALLEL="${arg#*=}" else if [ ! -d "$arg" ]; then echo "Error: Directory '$arg' not found" exit 1 fi TEST_DIRS+=("$arg") fi done if [ ${#TEST_DIRS[@]} -eq 0 ]; then echo "Error: No valid test directories specified" exit 1 fi TEMP_DIR=$(mktemp -d) FAILED_DIR="$TEMP_DIR/failed" STATUS_DIR="$TEMP_DIR/status" mkdir -p "$FAILED_DIR" "$STATUS_DIR" # Colors GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' # No Color # Spinner frames SPINNER_FRAMES=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') SPINNER_FRAME=0 NUM_SPINNER_FRAMES=${#SPINNER_FRAMES[@]} # Collect all test files first TEST_FILES=() for TEST_DIR in "${TEST_DIRS[@]}"; do for test_file in "$TEST_DIR"/*.test.ts; do if [ -f "$test_file" ]; then TEST_FILES+=("$test_file") # Create status file echo "pending" > "$STATUS_DIR/$(basename "$test_file").status" fi done done # Check if any tests were found if [ ${#TEST_FILES[@]} -eq 0 ]; then echo "No test files found in specified directories" rm -rf "$TEMP_DIR" exit 0 fi # Helper function to get status get_status() { local test_file="$1" local test_name=$(basename "$test_file") local status_file="$STATUS_DIR/${test_name}.status" if [ -f "$status_file" ]; then cat "$status_file" else echo "pending" fi } # Helper function to set status set_status() { local test_file="$1" local status="$2" local test_name=$(basename "$test_file") echo "$status" > "$STATUS_DIR/${test_name}.status" } # Cleanup function cleanup() { # Stop spinner if [ ! -z "$SPINNER_PID" ]; then kill $SPINNER_PID 2>/dev/null || true wait $SPINNER_PID 2>/dev/null || true fi # Kill all descendant processes pkill -P $$ 2>/dev/null || true pkill -f "bun test" 2>/dev/null || true jobs -p | while read pid; do kill -9 $pid 2>/dev/null || true; done # Show cursor again tput cnorm 2>/dev/null || true # Clean up temp directory rm -rf "$TEMP_DIR" exit 130 } # Set up signal handlers trap cleanup SIGINT SIGTERM EXIT # Function to render the test list render_tests() { local line_num=1 # Save cursor position tput sc 2>/dev/null || true for test_file in "${TEST_FILES[@]}"; do local test_name=$(basename "$test_file") local status=$(get_status "$test_file") local display_name="${test_name}" # Move to the line tput cup $((line_num - 1)) 0 2>/dev/null || true # Clear line tput el 2>/dev/null || true case "$status" in "pending") echo -ne "${DIM}⋯${NC} ${DIM}${display_name}${NC}" ;; "running") local frame_idx=$((SPINNER_FRAME % NUM_SPINNER_FRAMES)) local spinner_char="${SPINNER_FRAMES[$frame_idx]}" echo -ne "${CYAN}${spinner_char}${NC} ${display_name}" ;; "passed") echo -ne "${GREEN}✓${NC} ${DIM}${display_name}${NC}" ;; "failed") echo -ne "${RED}✗${NC} ${display_name}" ;; esac ((line_num++)) done # Restore cursor position tput rc 2>/dev/null || true } # Function to update spinner animation animate_spinner() { while true; do SPINNER_FRAME=$((SPINNER_FRAME + 1)) render_tests sleep 0.1 done } # Function to run a test run_test() { local test_file=$1 local test_name=$(basename "$test_file") local output_file="$TEMP_DIR/$test_name.log" # Mark as running set_status "$test_file" "running" # Run the test if script -q /dev/null bash -c "FORCE_COLOR=3 bun test --timeout 0 '$test_file' 2>&1" > "$output_file"; then set_status "$test_file" "passed" return 0 else set_status "$test_file" "failed" echo "$test_file|$output_file" > "$FAILED_DIR/$test_name.failed" return 1 fi } # Hide cursor tput civis 2>/dev/null || true # Initial render - create space for all tests echo "" for test_file in "${TEST_FILES[@]}"; do echo "" done # Move cursor back up tput cuu ${#TEST_FILES[@]} 2>/dev/null || true # Start spinner animation in background animate_spinner & SPINNER_PID=$! # Run tests in parallel count=0 for test_file in "${TEST_FILES[@]}"; do # Wait if we've hit max parallel while [ $(jobs -r | wc -l) -ge $((MAX_PARALLEL + 1)) ]; do sleep 0.1 done run_test "$test_file" & ((count++)) done # Wait for all tests to complete (exclude spinner process) for job in $(jobs -p); do if [ "$job" != "$SPINNER_PID" ]; then wait $job 2>/dev/null || true fi done # Stop spinner if [ ! -z "$SPINNER_PID" ]; then kill $SPINNER_PID 2>/dev/null || true wait $SPINNER_PID 2>/dev/null || true fi # Final render render_tests # Move cursor below test list echo "" echo "" # Show cursor again tput cnorm 2>/dev/null || true # Report failures FAILED_COUNT=$(ls "$FAILED_DIR"/*.failed 2>/dev/null | wc -l) if [ $FAILED_COUNT -gt 0 ]; then echo -e "${RED}${BOLD}========================================" echo -e "FAILED TESTS ($FAILED_COUNT/${count}):" echo -e "========================================${NC}" echo "" # Show detailed errors for failure_file in "$FAILED_DIR"/*.failed; do IFS='|' read -r test_file output_file < "$failure_file" echo -e "${RED}${BOLD}✗ $(basename $test_file)${NC}" echo -e "${DIM}─────────────────────────────────────────${NC}" cat "$output_file" echo "" done rm -rf "$TEMP_DIR" # Remove trap before exit to prevent double cleanup trap - SIGINT SIGTERM EXIT exit 1 else echo -e "${GREEN}${BOLD}✓ All tests passed!${NC} ${CYAN}($count tests)${NC}" rm -rf "$TEMP_DIR" # Remove trap before exit to prevent double cleanup trap - SIGINT SIGTERM EXIT exit 0 fi