#!/bin/bash
show_help() {
echo "Usage: $0 [options] [test_path]"
echo "Options:"
echo " -h, --help Show this help message"
echo " -v, --verbose Show detailed test output"
echo " -s Show test output content"
echo " -x Stop immediately on first failure"
echo " -n NUM Run tests in parallel using NUM processes (default: 6, requires pytest-xdist)"
echo " --serial Run tests serially (disable parallel execution)"
echo " --durations=N Show slowest N test durations"
echo " --cov Enable code coverage statistics"
echo " --cov-report= Specify coverage report format (term/html/xml)"
echo " --exclude= Exclude files/directories from coverage (supports wildcards)"
echo " -k EXPRESSION Only run tests matching the expression"
echo ""
echo "Default exclusion rules:"
echo " - Test files: */tests/*"
echo " - Cache files: */__pycache__/*"
echo " - gRPC generated files: */cluster_grpc/*_pb2*.py, */cluster_grpc/*_grpc.py"
echo " - Proto generated files: */motor/common/utils/proto/*_pb2*.py, */motor/common/utils/proto/*_grpc.py"
echo " - Proto cache files: */motor/common/utils/proto/__pycache__/*"
echo ""
echo "Examples:"
echo " $0 --cov --cov-report=html tests/controller/"
echo " $0 --cov --cov-report=html tests/coordinator/"
echo " $0 -v -k 'test_register'"
echo " $0 --cov --exclude='motor/config/*' --exclude='motor/utils/logger.py'"
}
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT_DIR"
rm -f .coverage
rm -rf htmlcov
PYTEST_ARGS=""
COVERAGE_ENABLED=false
COVERAGE_REPORT="term"
COVERAGE_EXCLUDE=()
PARALLEL_PROCESSES="6"
DISABLE_PARALLEL=false
HAS_WARNINGS=false
DEFAULT_EXCLUDES=(
"*/tests/*"
"*/__pycache__/*"
"*/cluster_grpc/*_pb2*.py"
"*/cluster_grpc/*_grpc.py"
"*/motor/common/utils/proto/*_pb2*.py"
"*/motor/common/utils/proto/*_grpc.py"
"*/motor/common/utils/proto/__pycache__/*"
)
export PYTHONPATH="$ROOT_DIR:$PYTHONPATH"
export PYTHONPATH="$ROOT_DIR/motor:$PYTHONPATH"
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
-n)
PARALLEL_PROCESSES="$2"
shift 2
;;
-n*)
PARALLEL_PROCESSES="${1#-n}"
shift
;;
--serial)
DISABLE_PARALLEL=true
shift
;;
--cov)
COVERAGE_ENABLED=true
shift
;;
--cov-report=*)
COVERAGE_REPORT="${1#*=}"
shift
;;
--exclude=*)
COVERAGE_EXCLUDE+=("${1#*=}")
shift
;;
*)
PYTEST_ARGS="$PYTEST_ARGS $1"
shift
;;
esac
done
check_dependencies() {
echo "Checking test dependencies..."
python3 -c "import pytest" 2>/dev/null || { echo "Installing pytest..."; pip install pytest; }
python3 -c "import pytest_cov" 2>/dev/null || { echo "Installing pytest-cov..."; pip install pytest-cov; }
python3 -c "import pytest_asyncio" 2>/dev/null || { echo "Installing pytest-asyncio..."; pip install pytest-asyncio; }
python3 -c "import pytest_xdist" 2>/dev/null || { echo "Installing pytest-xdist..."; pip install pytest-xdist; }
echo "Checking project core dependencies..."
python3 -c "import psutil" 2>/dev/null || { echo "Installing psutil..."; pip install psutil>=5.9.8; }
python3 -c "import fastapi" 2>/dev/null || { echo "Installing fastapi..."; pip install fastapi>=0.68.0; }
python3 -c "import uvicorn" 2>/dev/null || { echo "Installing uvicorn..."; pip install "uvicorn[standard]>=0.15.0"; }
python3 -c "import grpc" 2>/dev/null || { echo "Installing grpcio..."; pip install grpcio>=1.40.0; }
python3 -c "import grpc_tools" 2>/dev/null || { echo "Installing grpcio-tools..."; pip install grpcio-tools>=1.40.0; }
python3 -c "import pydantic" 2>/dev/null || { echo "Installing pydantic..."; pip install pydantic>=1.8.0; }
python3 -c "from OpenSSL import crypto" 2>/dev/null || { echo "Installing pyOpenSSL..."; pip install pyOpenSSL>=21.0.0; }
echo "Checking HTTP client dependencies..."
python3 -c "import requests" 2>/dev/null || { echo "Installing requests..."; pip install requests>=2.25.0; }
python3 -c "import httpx" 2>/dev/null || { echo "Installing httpx..."; pip install httpx>=0.24.0; }
python3 -c "import asyncio" 2>/dev/null || { echo "asyncio is not available, this may affect async tests"; }
python3 -c "import tempfile" 2>/dev/null || { echo "tempfile is not available, this may affect temporary file tests"; }
echo "Dependency check completed"
}
check_dependencies
generate_proto_files() {
echo "Checking and generating protobuf files..."
if [ -f "$ROOT_DIR/scripts/generate_proto.sh" ]; then
"$ROOT_DIR/scripts/generate_proto.sh"
else
echo "Warning: generate_proto.sh not found. Skipping protobuf generation."
echo "If you encounter import errors, please run: scripts/generate_proto.sh"
fi
}
generate_proto_files
CMD="pytest"
if ! echo " $PYTEST_ARGS " | grep -qE " (--color=|--color )"; then
CMD="$CMD --color=yes"
fi
if [ "$DISABLE_PARALLEL" = false ] && [ -n "$PARALLEL_PROCESSES" ]; then
CMD="$CMD -n $PARALLEL_PROCESSES"
fi
if [ "$COVERAGE_ENABLED" = true ]; then
COVERAGERC_FILE=".coveragerc.tmp"
cat > "$COVERAGERC_FILE" << EOF
[run]
source = motor
omit =
EOF
for exclude_pattern in "${DEFAULT_EXCLUDES[@]}"; do
echo " $exclude_pattern" >> "$COVERAGERC_FILE"
done
if [ ${#COVERAGE_EXCLUDE[@]} -gt 0 ]; then
for exclude_pattern in "${COVERAGE_EXCLUDE[@]}"; do
echo " $exclude_pattern" >> "$COVERAGERC_FILE"
done
fi
CMD="$CMD --cov=motor --cov-report=$COVERAGE_REPORT --cov-config=$COVERAGERC_FILE"
fi
if [ ! -z "$PYTEST_ARGS" ]; then
CMD="$CMD $PYTEST_ARGS"
else
CMD="$CMD tests/"
fi
show_test_summary() {
local exit_code=$1
local output_file=$2
echo ""
echo "=========================================="
echo "Test Result Summary"
echo "=========================================="
if [ -f "$output_file" ]; then
local stats=$(grep -E "(passed|failed|error|skipped|warnings|warnings summary)" "$output_file" | tail -1)
local last_lines=$(tail -10 "$output_file")
local summary_line=$(echo "$last_lines" | grep -E "[0-9]+ (passed|failed|error)" | grep -E "in [0-9]+\.[0-9]+s" | tail -1)
if [ -z "$summary_line" ]; then
summary_line=$(grep -E "[0-9]+ (passed|failed|error)" "$output_file" | grep -E "in [0-9]+\.[0-9]+s" | tail -1)
fi
passed=0
failed=0
errors=0
skipped=0
warnings=0
if [ -n "$summary_line" ]; then
passed=$(echo "$summary_line" | grep -oE "[0-9]+ passed" | grep -oE "[0-9]+" | head -1 || echo "0")
failed=$(echo "$summary_line" | grep -oE "[0-9]+ failed" | grep -oE "[0-9]+" | head -1 || echo "0")
errors=$(echo "$summary_line" | grep -oE "[0-9]+ error" | grep -oE "[0-9]+" | head -1 || echo "0")
skipped=$(echo "$summary_line" | grep -oE "[0-9]+ skipped" | grep -oE "[0-9]+" | head -1 || echo "0")
warnings=$(echo "$summary_line" | grep -oE "[0-9]+ warnings?" | grep -oE "[0-9]+" | head -1 || echo "0")
else
passed=$(echo "$last_lines" | grep -oE "[0-9]+ passed" | grep -oE "[0-9]+" | head -1 || echo "0")
failed=$(echo "$last_lines" | grep -oE "[0-9]+ failed" | grep -oE "[0-9]+" | head -1 || echo "0")
errors=$(echo "$last_lines" | grep -oE "[0-9]+ error" | grep -oE "[0-9]+" | head -1 || echo "0")
skipped=$(echo "$last_lines" | grep -oE "[0-9]+ skipped" | grep -oE "[0-9]+" | head -1 || echo "0")
warnings=$(echo "$last_lines" | grep -oE "[0-9]+ warnings?" | grep -oE "[0-9]+" | head -1 || echo "0")
fi
if [ -z "$passed" ] || [ "$passed" = "0" ]; then
passed=$(grep -oE "[0-9]+ passed" "$output_file" | grep -oE "[0-9]+" | head -1 || echo "0")
failed=$(grep -oE "[0-9]+ failed" "$output_file" | grep -oE "[0-9]+" | head -1 || echo "0")
errors=$(grep -oE "[0-9]+ error" "$output_file" | grep -oE "[0-9]+" | head -1 || echo "0")
skipped=$(grep -oE "[0-9]+ skipped" "$output_file" | grep -oE "[0-9]+" | head -1 || echo "0")
if [ -z "$warnings" ] || [ "$warnings" = "0" ]; then
warnings=$(grep -oE "[0-9]+ warnings?" "$output_file" | grep -oE "[0-9]+" | head -1 || echo "0")
fi
fi
if [ -n "$warnings" ] && [ "$warnings" != "0" ]; then
HAS_WARNINGS=true
fi
echo "Test Status:"
local has_stats=false
if [ -n "$passed" ] && [ "$passed" != "0" ]; then
echo " ✓ Passed: $passed"
has_stats=true
fi
if [ -n "$failed" ] && [ "$failed" != "0" ]; then
echo " ✗ Failed: $failed"
has_stats=true
fi
if [ -n "$errors" ] && [ "$errors" != "0" ]; then
echo " ✗ Errors: $errors"
has_stats=true
fi
if [ -n "$skipped" ] && [ "$skipped" != "0" ]; then
echo " ⊘ Skipped: $skipped"
has_stats=true
fi
if [ "$HAS_WARNINGS" = true ]; then
echo " ⚠ Warnings: $warnings"
has_stats=true
fi
if [ "$has_stats" = false ]; then
echo " (Unable to extract detailed statistics from output)"
fi
echo ""
echo "Overall Status:"
if [ $exit_code -eq 0 ]; then
if [ "$HAS_WARNINGS" = true ]; then
echo " ⚠ All tests passed, but with warnings"
else
echo " ✓ All tests passed"
fi
else
case $exit_code in
1)
echo " ✗ Tests failed"
;;
2)
echo " ✗ Tests interrupted"
;;
3)
echo " ✗ Internal error"
;;
4)
echo " ✗ pytest usage error"
;;
5)
echo " ✗ No tests collected"
;;
*)
echo " ✗ Unknown error (exit code: $exit_code)"
;;
esac
fi
if [ "$failed" -gt 0 ] 2>/dev/null || [ "$errors" -gt 0 ] 2>/dev/null; then
echo ""
echo "Hint: Check the output above for detailed information about failed tests"
fi
if [ "$HAS_WARNINGS" = true ]; then
echo ""
echo "Hint: Check the output above for detailed warning information"
fi
else
echo "Test Status:"
if [ $exit_code -eq 0 ]; then
echo " ✓ All tests passed"
else
echo " ✗ Tests did not pass completely (exit code: $exit_code)"
fi
fi
echo "=========================================="
echo ""
}
echo "Executing command: $CMD"
OUTPUT_FILE=$(mktemp)
trap "rm -f $OUTPUT_FILE" EXIT
TERM_WIDTH=${COLUMNS:-$(tput cols 2>/dev/null || echo 80)}
export COLUMNS=$TERM_WIDTH
$CMD 2>&1 | tee "$OUTPUT_FILE"
TEST_EXIT_CODE=${PIPESTATUS[0]}
show_test_summary $TEST_EXIT_CODE "$OUTPUT_FILE"
if [ $TEST_EXIT_CODE -eq 0 ]; then
echo ""
if [ "$HAS_WARNINGS" = true ]; then
echo "✓ All tests passed ($warnings warning(s) — see warnings summary above)"
else
echo "✓ All tests passed"
fi
fi
if [ -f ".coveragerc.tmp" ]; then
rm -f ".coveragerc.tmp"
fi
exit $TEST_EXIT_CODE