Skip to content

Commit acf18b5

Browse files
authored
Parallel testing for faster local testing (#360)
* Parallel testing for faster local testing (up to 4a4f2ca) * Address Copilot PR feedback and add seriallocal marking
1 parent 87e79a4 commit acf18b5

5 files changed

Lines changed: 795 additions & 15 deletions

File tree

pyproject.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ namespaces = false # to disable scanning PEP 420 namespaces (true by default)
9090

9191
# === Linting & Formatting ===
9292

93+
[tool.black]
94+
line-length = 120
95+
9396
# --- ruff ---
9497

9598
[tool.ruff]
@@ -146,6 +149,7 @@ lint.per-file-ignores."tests/**" = [
146149
"D401",
147150
"S101",
148151
"S105",
152+
"S110",
149153
"S311",
150154
"S603",
151155
]
@@ -188,6 +192,7 @@ addopts = [
188192
"-v",
189193
"-s",
190194
"-W error",
195+
# Note: parallel execution is opt-in via -n/--numprocesses (pytest-xdist)
191196
]
192197
markers = [
193198
"mongo: test the MongoDB core",
@@ -199,6 +204,8 @@ markers = [
199204
"maxage: test the max_age functionality",
200205
"asyncio: marks tests as async",
201206
"smoke: fast smoke tests with no external service dependencies",
207+
"flaky: tests that are known to be flaky and should be retried",
208+
"seriallocal: local core tests that should run serially",
202209
]
203210

204211
[tool.coverage.report]
@@ -209,10 +216,17 @@ exclude_lines = [
209216
"raise NotImplementedError", # Don't complain if tests don't hit defensive assertion code:
210217
"if TYPE_CHECKING:", # Is only true when running mypy, not tests
211218
]
219+
# Parallel test execution configuration
220+
# Use: pytest -n auto (for automatic worker detection)
221+
# Or: pytest -n 4 (for specific number of workers)
222+
# Memory tests are safe to run in parallel by default
223+
# Pickle tests require isolation (handled by conftest.py fixture)
224+
212225
# --- coverage ---
213226

214227
[tool.coverage.run]
215228
branch = true
229+
parallel = true
216230
# dynamic_context = "test_function"
217231
omit = [
218232
"tests/*",

scripts/test-local.sh

Lines changed: 97 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ COVERAGE_REPORT="term"
2525
KEEP_RUNNING=false
2626
SELECTED_CORES=""
2727
INCLUDE_LOCAL_CORES=false
28-
TEST_FILES=""
28+
TEST_FILES=()
29+
PARALLEL=false
30+
PARALLEL_WORKERS="auto"
2931

3032
# Function to print colored messages
3133
print_message() {
@@ -57,6 +59,8 @@ OPTIONS:
5759
-k, --keep-running Keep containers running after tests
5860
-h, --html-coverage Generate HTML coverage report
5961
-f, --files Specify test files to run (can be used multiple times)
62+
-p, --parallel Run tests in parallel using pytest-xdist
63+
-w, --workers Number of parallel workers (default: auto)
6064
--help Show this help message
6165
6266
EXAMPLES:
@@ -66,6 +70,8 @@ EXAMPLES:
6670
$0 external -k # Run external backends, keep containers
6771
$0 mongo memory -v # Run MongoDB and memory tests verbosely
6872
$0 all -f tests/test_main.py -f tests/test_redis_core_coverage.py # Run specific test files
73+
$0 memory pickle -p # Run local tests in parallel
74+
$0 all -p -w 4 # Run all tests with 4 parallel workers
6975
7076
ENVIRONMENT:
7177
You can also set cores via CACHIER_TEST_CORES environment variable:
@@ -96,13 +102,27 @@ while [[ $# -gt 0 ]]; do
96102
usage
97103
exit 1
98104
fi
99-
TEST_FILES="$TEST_FILES $1"
105+
TEST_FILES+=("$1")
100106
shift
101107
;;
102108
--help)
103109
usage
104110
exit 0
105111
;;
112+
-p|--parallel)
113+
PARALLEL=true
114+
shift
115+
;;
116+
-w|--workers)
117+
shift
118+
if [[ $# -eq 0 ]] || [[ "$1" == -* ]]; then
119+
print_message $RED "Error: -w/--workers requires a number argument"
120+
usage
121+
exit 1
122+
fi
123+
PARALLEL_WORKERS="$1"
124+
shift
125+
;;
106126
-*)
107127
print_message $RED "Unknown option: $1"
108128
usage
@@ -234,6 +254,17 @@ check_dependencies() {
234254
}
235255
fi
236256

257+
# Check for pytest-xdist if parallel testing is requested
258+
if [ "$PARALLEL" = true ]; then
259+
if ! python -c "import xdist" 2>/dev/null; then
260+
print_message $YELLOW "Installing pytest-xdist for parallel testing..."
261+
pip install pytest-xdist || {
262+
print_message $RED "Failed to install pytest-xdist"
263+
exit 1
264+
}
265+
fi
266+
fi
267+
237268
# Check MongoDB dependencies if testing MongoDB
238269
if echo "$SELECTED_CORES" | grep -qw "mongo"; then
239270
if ! python -c "import pymongo" 2>/dev/null; then
@@ -423,14 +454,20 @@ main() {
423454
# Check and install dependencies
424455
check_dependencies
425456

426-
# Check if we need Docker
457+
# Check if we need Docker, and if we should run serial pickle tests
427458
needs_docker=false
459+
run_serial_local_tests=false
428460
for core in $SELECTED_CORES; do
429461
case $core in
430462
mongo|redis|sql)
431463
needs_docker=true
432464
;;
433465
esac
466+
case $core in
467+
pickle|all)
468+
run_serial_local_tests=true
469+
;;
470+
esac
434471
done
435472

436473
if [ "$needs_docker" = true ]; then
@@ -497,26 +534,46 @@ main() {
497534
sql) test_sql ;;
498535
esac
499536
done
537+
if [ -n "$pytest_markers" ]; then
538+
pytest_markers="($pytest_markers) and not seriallocal"
539+
else
540+
pytest_markers="not seriallocal"
541+
fi
500542

501543
# Run pytest
502544
# Build pytest command
503-
PYTEST_CMD="pytest"
545+
PYTEST_ARGS=(pytest)
546+
# and the specific pytest command for running serial pickle tests
547+
SERIAL_PYTEST_ARGS=(pytest -m seriallocal)
548+
# Only add -n0 if pytest-xdist is available; otherwise, plain pytest is already serial
549+
if python - << 'EOF' >/dev/null 2>&1
550+
import xdist # noqa: F401
551+
EOF
552+
then
553+
SERIAL_PYTEST_ARGS+=(-n0)
554+
fi
504555

505556
# Add test files if specified
506-
if [ -n "$TEST_FILES" ]; then
507-
PYTEST_CMD="$PYTEST_CMD $TEST_FILES"
508-
print_message $BLUE "Test files specified: $TEST_FILES"
557+
if [ ${#TEST_FILES[@]} -gt 0 ]; then
558+
PYTEST_ARGS+=("${TEST_FILES[@]}")
559+
print_message $BLUE "Test files specified: ${TEST_FILES[*]}"
560+
# and turn off serial local tests, so we run only selected files
561+
run_serial_local_tests=false
509562
fi
510563

511564
# Add markers if needed (only if no specific test files were given)
512-
if [ -z "$TEST_FILES" ]; then
565+
if [ ${#TEST_FILES[@]} -eq 0 ]; then
513566
# Check if we selected all cores - if so, run all tests without marker filtering
514567
all_cores="memory mongo pickle redis s3 sql"
515568
selected_sorted=$(echo "$SELECTED_CORES" | tr ' ' '\n' | sort | tr '\n' ' ' | xargs)
516569
all_sorted=$(echo "$all_cores" | tr ' ' '\n' | sort | tr '\n' ' ' | xargs)
517570

518571
if [ "$selected_sorted" != "$all_sorted" ]; then
519-
PYTEST_CMD="$PYTEST_CMD -m \"$pytest_markers\""
572+
PYTEST_ARGS+=(-m "$pytest_markers")
573+
else
574+
print_message $BLUE "Running all tests without markers since all cores are selected"
575+
PYTEST_ARGS+=(-m "not seriallocal")
576+
run_serial_local_tests=true
520577
fi
521578
else
522579
# When test files are specified, still apply markers if not running all cores
@@ -525,21 +582,47 @@ main() {
525582
all_sorted=$(echo "$all_cores" | tr ' ' '\n' | sort | tr '\n' ' ' | xargs)
526583

527584
if [ "$selected_sorted" != "$all_sorted" ]; then
528-
PYTEST_CMD="$PYTEST_CMD -m \"$pytest_markers\""
585+
PYTEST_ARGS+=(-m "$pytest_markers")
529586
fi
530587
fi
531588

532589
# Add verbose flag if needed
533590
if [ "$VERBOSE" = true ]; then
534-
PYTEST_CMD="$PYTEST_CMD -v"
591+
PYTEST_ARGS+=(-v)
592+
SERIAL_PYTEST_ARGS+=(-v)
593+
fi
594+
595+
# Add parallel testing options if requested
596+
if [ "$PARALLEL" = true ]; then
597+
PYTEST_ARGS+=(-n "$PARALLEL_WORKERS")
598+
599+
# Show parallel testing info
600+
if [ "$PARALLEL_WORKERS" = "auto" ]; then
601+
print_message $BLUE "Running tests in parallel with automatic worker detection"
602+
else
603+
print_message $BLUE "Running tests in parallel with $PARALLEL_WORKERS workers"
604+
fi
605+
606+
# Special note for pickle tests
607+
if echo "$SELECTED_CORES" | grep -qw "pickle"; then
608+
print_message $YELLOW "Note: Pickle tests will use isolated cache directories for parallel safety"
609+
fi
535610
fi
536611

537612
# Add coverage options
538-
PYTEST_CMD="$PYTEST_CMD --cov=cachier --cov-report=$COVERAGE_REPORT"
613+
PYTEST_ARGS+=(--cov=cachier --cov-report="$COVERAGE_REPORT")
614+
SERIAL_PYTEST_ARGS+=(--cov=cachier --cov-report="$COVERAGE_REPORT" --cov-append)
539615

540616
# Print and run the command
541-
print_message $BLUE "Running: $PYTEST_CMD"
542-
eval $PYTEST_CMD
617+
print_message $BLUE "Running: $(printf '%q ' "${PYTEST_ARGS[@]}")"
618+
"${PYTEST_ARGS[@]}"
619+
620+
if [ "$run_serial_local_tests" = true ]; then
621+
print_message $BLUE "Running serial local tests (pickle, memory) with: $(printf '%q ' "${SERIAL_PYTEST_ARGS[@]}")"
622+
"${SERIAL_PYTEST_ARGS[@]}"
623+
else
624+
print_message $BLUE "Skipping serial local tests (pickle, memory) since not requested"
625+
fi
543626

544627
TEST_EXIT_CODE=$?
545628

0 commit comments

Comments
 (0)