Skip to content

Commit e7823b1

Browse files
committed
chore: fix transient random test failures
1 parent 7b109bb commit e7823b1

File tree

2 files changed

+90
-62
lines changed

2 files changed

+90
-62
lines changed

.github/scripts/run-random-tests.sh

Lines changed: 89 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -477,78 +477,105 @@ run_component_tests() {
477477

478478
local output_file="$results_dir/random_test_output_${component}_$$.log"
479479
local events_file="$results_dir/random_test_events_${component}_$$.log"
480-
local random_seed=$(generate_phpunit_random_seed)
481480
local exit_code=0
482-
483-
# Security: Use array to avoid eval and prevent command injection
484-
local -a phpunit_args=(
485-
"vendor/bin/phpunit"
486-
"$test_dir"
487-
"--colors=never"
488-
"--no-coverage"
489-
"--do-not-cache-result"
490-
"--order-by=random"
491-
"--random-order-seed=${random_seed}"
492-
"--log-events-text"
493-
"$events_file"
494-
)
495-
496-
if [[ $timeout_seconds -gt 0 ]] && command -v timeout >/dev/null 2>&1; then
497-
(cd "$project_root" && timeout --kill-after=2s "${timeout_seconds}s" "${phpunit_args[@]}") > "$output_file" 2>&1
498-
exit_code=$?
499-
elif [[ $timeout_seconds -gt 0 ]] && command -v gtimeout >/dev/null 2>&1; then
500-
(cd "$project_root" && gtimeout --kill-after=2s "${timeout_seconds}s" "${phpunit_args[@]}") > "$output_file" 2>&1
501-
exit_code=$?
502-
else
503-
local timeout_marker="$output_file.timeout"
504-
(cd "$project_root" && "${phpunit_args[@]}") > "$output_file" 2>&1 &
505-
local test_pid=$!
506-
507-
if [[ $timeout_seconds -gt 0 ]]; then
508-
# Watchdog: monitors test process and kills it after timeout
509-
# Uses 1-second sleep intervals to respond quickly when test finishes early
510-
(
511-
local elapsed=0
512-
while [[ $elapsed -lt $timeout_seconds ]]; do
513-
sleep 1
514-
elapsed=$((elapsed + 1))
515-
kill -0 "$test_pid" 2>/dev/null || exit 0
516-
done
517-
518-
if kill -0 "$test_pid" 2>/dev/null; then
519-
touch "$timeout_marker"
520-
local pids_to_kill=$(pgrep -P "$test_pid" 2>/dev/null)
521-
522-
kill -TERM "$test_pid" 2>/dev/null || true
523-
if [[ -n "$pids_to_kill" ]]; then
524-
echo "$pids_to_kill" | xargs kill -TERM 2>/dev/null || true
525-
fi
526-
527-
sleep 2
481+
local attempt=1
482+
local -r max_attempts=2
483+
local random_seed
484+
local -a phpunit_args
485+
486+
# Retry loop: the Composer classmap autoloader occasionally fails to load
487+
# CodeIgniter\CodeIgniter under parallel CI load — a transient infra race,
488+
# not a real test failure. Retry once on that signature with a fresh random
489+
# seed; a second miss is reported as genuine failure.
490+
while true; do
491+
random_seed=$(generate_phpunit_random_seed)
492+
493+
# Security: Use array to avoid eval and prevent command injection
494+
phpunit_args=(
495+
"vendor/bin/phpunit"
496+
"$test_dir"
497+
"--colors=never"
498+
"--no-coverage"
499+
"--do-not-cache-result"
500+
"--order-by=random"
501+
"--random-order-seed=${random_seed}"
502+
"--log-events-text"
503+
"$events_file"
504+
)
505+
506+
if [[ $timeout_seconds -gt 0 ]] && command -v timeout >/dev/null 2>&1; then
507+
(cd "$project_root" && timeout --kill-after=2s "${timeout_seconds}s" "${phpunit_args[@]}") > "$output_file" 2>&1
508+
exit_code=$?
509+
elif [[ $timeout_seconds -gt 0 ]] && command -v gtimeout >/dev/null 2>&1; then
510+
(cd "$project_root" && gtimeout --kill-after=2s "${timeout_seconds}s" "${phpunit_args[@]}") > "$output_file" 2>&1
511+
exit_code=$?
512+
else
513+
local timeout_marker="$output_file.timeout"
514+
(cd "$project_root" && "${phpunit_args[@]}") > "$output_file" 2>&1 &
515+
local test_pid=$!
516+
517+
if [[ $timeout_seconds -gt 0 ]]; then
518+
# Watchdog: monitors test process and kills it after timeout
519+
# Uses 1-second sleep intervals to respond quickly when test finishes early
520+
(
521+
local elapsed=0
522+
while [[ $elapsed -lt $timeout_seconds ]]; do
523+
sleep 1
524+
elapsed=$((elapsed + 1))
525+
kill -0 "$test_pid" 2>/dev/null || exit 0
526+
done
528527

529528
if kill -0 "$test_pid" 2>/dev/null; then
530-
kill -KILL "$test_pid" 2>/dev/null || true
529+
touch "$timeout_marker"
530+
local pids_to_kill=$(pgrep -P "$test_pid" 2>/dev/null)
531+
532+
kill -TERM "$test_pid" 2>/dev/null || true
531533
if [[ -n "$pids_to_kill" ]]; then
532-
echo "$pids_to_kill" | xargs kill -KILL 2>/dev/null || true
534+
echo "$pids_to_kill" | xargs kill -TERM 2>/dev/null || true
535+
fi
536+
537+
sleep 2
538+
539+
if kill -0 "$test_pid" 2>/dev/null; then
540+
kill -KILL "$test_pid" 2>/dev/null || true
541+
if [[ -n "$pids_to_kill" ]]; then
542+
echo "$pids_to_kill" | xargs kill -KILL 2>/dev/null || true
543+
fi
544+
# Security: Quote and escape test_dir for safe pattern matching
545+
pkill -KILL -f "phpunit.*${test_dir//\//\\/}" 2>/dev/null || true
533546
fi
534-
# Security: Quote and escape test_dir for safe pattern matching
535-
pkill -KILL -f "phpunit.*${test_dir//\//\\/}" 2>/dev/null || true
536547
fi
537-
fi
538-
) &
539-
disown $! 2>/dev/null || true
548+
) &
549+
disown $! 2>/dev/null || true
550+
fi
551+
552+
wait "$test_pid" 2>/dev/null
553+
exit_code=$?
554+
555+
if [[ -f "$timeout_marker" ]]; then
556+
exit_code=124
557+
rm -f "$timeout_marker"
558+
elif [[ $exit_code -eq 143 || $exit_code -eq 137 ]]; then
559+
exit_code=124
560+
fi
540561
fi
541562

542-
wait "$test_pid" 2>/dev/null
543-
exit_code=$?
563+
# Success, exhausted attempts, or a non-infra failure: stop retrying.
564+
if [[ $exit_code -eq 0 ]] || [[ $attempt -ge $max_attempts ]]; then
565+
break
566+
fi
544567

545-
if [[ -f "$timeout_marker" ]]; then
546-
exit_code=124
547-
rm -f "$timeout_marker"
548-
elif [[ $exit_code -eq 143 || $exit_code -eq 137 ]]; then
549-
exit_code=124
568+
# Only retry on the known transient autoload race signatures.
569+
# Matching on error messages (not line numbers) so the pattern survives
570+
# unrelated edits to MockCodeIgniter/CIUnitTestCase.
571+
if ! grep -qE 'Failed to open stream: No such file or directory|Class "CodeIgniter.CodeIgniter" not found' "$output_file" 2>/dev/null; then
572+
break
550573
fi
551-
fi
574+
575+
print_debug "Transient autoload failure detected in $component; retrying (attempt $((attempt + 1))/${max_attempts})"
576+
((attempt++))
577+
rm -f "$events_file"
578+
done
552579

553580
local elapsed=$((($(date +%s%N) - $start_time) / 1000000))
554581
local result_file="$results_dir/random_test_result_${elapsed}_${component}.txt"

.github/workflows/test-random-execution.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ jobs:
175175
with:
176176
php-version: ${{ matrix.php-version }}
177177
extensions: gd, curl, iconv, json, mbstring, openssl, sodium
178+
ini-values: opcache.enable_cli=0
178179
coverage: none
179180

180181
- name: Get composer cache directory

0 commit comments

Comments
 (0)