Files
cfw-autumn/server/shell/run-parallel.sh
2025-10-22 22:03:52 +01:00

267 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Run Bun test files in parallel with proper error reporting
# Usage: ./run-parallel.sh <test_directory1> [test_directory2] [...] [--max=N]
if [ $# -eq 0 ]; then
echo "Error: No test directories specified"
echo "Usage: ./run-parallel.sh <test_directory1> [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