Skip to content

Commit ec07563

Browse files
committed
Merge remote-tracking branch 'upstream/develop' into 4.8
2 parents 5f89740 + e66d025 commit ec07563

15 files changed

Lines changed: 160 additions & 112 deletions

File tree

.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/reusable-serviceless-phpunit-test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ jobs:
7676
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
7777
with:
7878
persist-credentials: false
79+
fetch-depth: 0
7980

8081
- name: Setup PHP
8182
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,16 @@ jobs:
169169
170170
- name: Checkout
171171
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
172+
with:
173+
persist-credentials: false
174+
fetch-depth: 0
172175

173176
- name: Setup PHP ${{ matrix.php-version }}
174177
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0
175178
with:
176179
php-version: ${{ matrix.php-version }}
177180
extensions: gd, curl, iconv, json, mbstring, openssl, sodium
181+
ini-values: opcache.enable_cli=0
178182
coverage: none
179183

180184
- name: Get composer cache directory

.github/workflows/test-scss.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3737

3838
- name: Setup Node
39-
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
39+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
4040
with:
4141
node-version: '24'
4242

system/CLI/CLI.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ public static function generateDimensions()
750750
static::$width = (int) $matches[2];
751751
}
752752
}
753-
} elseif (($size = exec('stty size')) && preg_match('/(\d+)\s+(\d+)/', $size, $matches)) {
753+
} elseif (($size = exec('stty size 2>/dev/null')) && preg_match('/(\d+)\s+(\d+)/', $size, $matches)) {
754754
static::$height = (int) $matches[1];
755755
static::$width = (int) $matches[2];
756756
} else {

system/Commands/Housekeeping/ClearLogs.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ public function run(array $params)
6868

6969
if (! $force && CLI::prompt('Are you sure you want to delete the logs?', ['n', 'y']) === 'n') {
7070
CLI::error('Deleting logs aborted.');
71-
CLI::error('If you want, use the "--force" option to force delete all log files.');
71+
72+
// @todo to re-add under non-interactive mode
73+
// CLI::error('If you want, use the "--force" option to force delete all log files.');
7274

7375
return EXIT_ERROR;
7476
}

system/Commands/Utilities/Routes.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,21 @@ class Routes extends BaseCommand
7373
* @var array<string, string>
7474
*/
7575
protected $options = [
76-
'--handler' => 'Sort by Handler.',
77-
'--host' => 'Specify hostname in request URI.',
76+
'--sort-by-handler' => 'Sort by handler.',
77+
'--host' => 'Specify hostname in request URI.',
7878
];
7979

8080
/**
8181
* Displays the help for the spark cli script itself.
8282
*/
8383
public function run(array $params)
8484
{
85-
$sortByHandler = array_key_exists('handler', $params);
85+
$sortByHandler = array_key_exists('sort-by-handler', $params);
8686

8787
if (! $sortByHandler && array_key_exists('h', $params)) {
88+
// @todo to remove support in v4.8.0
8889
// Support -h as a shortcut but print a warning that it is not the intended use of -h.
89-
CLI::write('Warning: -h will be used as shortcut for --help in v4.8.0. Please use --handler to sort by handler.', 'yellow');
90+
CLI::write('Warning: -h will be used as shortcut for --help in v4.8.0. Please use --sort-by-handler to sort by handler.', 'yellow');
9091
CLI::newLine();
9192

9293
$sortByHandler = true;

tests/system/AutoReview/CreateNewChangelogTest.php

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,6 @@ final class CreateNewChangelogTest extends TestCase
2727
{
2828
private string $currentVersion;
2929

30-
public static function setUpBeforeClass(): void
31-
{
32-
parent::setUpBeforeClass();
33-
34-
if (getenv('GITHUB_ACTIONS') !== false) {
35-
exec('git fetch --unshallow 2>&1', $output, $exitCode);
36-
exec('git fetch --tags 2>&1', $output, $exitCode);
37-
38-
if ($exitCode !== 0) {
39-
self::fail(sprintf(
40-
"Failed to fetch git history and tags.\nOutput: %s",
41-
implode("\n", $output),
42-
));
43-
}
44-
}
45-
}
46-
4730
protected function setUp(): void
4831
{
4932
parent::setUp();

tests/system/AutoReview/FrameworkCodeTest.php

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -157,17 +157,9 @@ private static function getTestClasses(): array
157157

158158
$testClasses = array_map(
159159
static function (SplFileInfo $file) use ($directory): string {
160-
$relativePath = substr_replace(
161-
$file->getPathname(),
162-
'',
163-
0,
164-
strlen($directory),
165-
);
166-
$relativePath = substr_replace(
167-
$relativePath,
168-
'',
169-
strlen($relativePath) - strlen(DIRECTORY_SEPARATOR . $file->getBasename()),
170-
);
160+
$relativePath = substr($file->getPathname(), strlen($directory));
161+
$separatorPos = strrpos($relativePath, DIRECTORY_SEPARATOR);
162+
$relativePath = $separatorPos === false ? '' : substr($relativePath, 0, $separatorPos);
171163

172164
return sprintf(
173165
'CodeIgniter\\%s%s%s',
@@ -178,17 +170,15 @@ static function (SplFileInfo $file) use ($directory): string {
178170
},
179171
array_filter(
180172
iterator_to_array($iterator, false),
173+
// Filename-based heuristic: avoids the is_subclass_of() cold-autoload issue
174+
// by only considering files that end with "Test.php" or "TestCase.php".
181175
static fn (SplFileInfo $file): bool => $file->isFile()
176+
&& (str_ends_with($file->getBasename(), 'Test.php') || str_ends_with($file->getBasename(), 'TestCase.php'))
182177
&& ! str_contains($file->getPathname(), DIRECTORY_SEPARATOR . 'fixtures' . DIRECTORY_SEPARATOR)
183178
&& ! str_contains($file->getPathname(), DIRECTORY_SEPARATOR . 'Views' . DIRECTORY_SEPARATOR),
184179
),
185180
);
186181

187-
$testClasses = array_filter(
188-
$testClasses,
189-
static fn (string $class): bool => is_subclass_of($class, TestCase::class),
190-
);
191-
192182
sort($testClasses);
193183

194184
self::$testClasses = $testClasses;

tests/system/CLI/CLITest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use CodeIgniter\Test\StreamFilterTrait;
2222
use PHPUnit\Framework\Attributes\DataProvider;
2323
use PHPUnit\Framework\Attributes\Group;
24+
use PHPUnit\Framework\Attributes\RequiresOperatingSystem;
2425
use ReflectionProperty;
2526

2627
/**
@@ -677,6 +678,33 @@ public function testWindow(): void
677678
$this->assertIsInt(CLI::getWidth());
678679
}
679680

681+
#[RequiresOperatingSystem('Darwin|Linux')]
682+
public function testGenerateDimensionsDoesNotLeakSttyErrorToStderr(): void
683+
{
684+
$code = <<<'PHP'
685+
require __DIR__ . '/system/Test/bootstrap.php';
686+
CodeIgniter\CLI\CLI::generateDimensions();
687+
PHP;
688+
689+
$cmd = sprintf('%s -r %s < /dev/null', PHP_BINARY, escapeshellarg($code));
690+
691+
$proc = proc_open(
692+
$cmd,
693+
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
694+
$pipes,
695+
ROOTPATH,
696+
);
697+
$this->assertIsResource($proc);
698+
699+
stream_get_contents($pipes[1]);
700+
$stderr = stream_get_contents($pipes[2]);
701+
fclose($pipes[1]);
702+
fclose($pipes[2]);
703+
proc_close($proc);
704+
705+
$this->assertSame('', $stderr);
706+
}
707+
680708
/**
681709
* @param array $tbody
682710
* @param array $thead

0 commit comments

Comments
 (0)