From f913ab6acffc3b27c79227b3afdc578f02e85664 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:29 +0700 Subject: [PATCH 01/26] Run tests in parallel by default --- README.md | 8 +- docs/source/miscellaneous/writing-tests.rst | 5 ++ ext/gd/tests/createfromwbmp2.phpt | 2 +- ext/gd/tests/createfromwbmp2_extern.phpt | 4 +- ext/zip/tests/oo_addglob_leak.phpt | 4 +- run-tests.php | 84 +++++++++++++++++--- sapi/cli/tests/010-2.phpt | 2 +- sapi/cli/tests/010.phpt | 4 +- tests/run-test/automatic_worker_limit.phpt | 85 +++++++++++++++++++++ 9 files changed, 175 insertions(+), 23 deletions(-) create mode 100644 tests/run-test/automatic_worker_limit.phpt diff --git a/README.md b/README.md index d83203d74c0b..80618f2d6f94 100644 --- a/README.md +++ b/README.md @@ -97,15 +97,15 @@ can be determined using `nproc`. PHP ships with an extensive test suite, the command `make test` is used after successful compilation of the sources to run this test suite. -It is possible to run tests using multiple cores by setting `-jN` in -`TEST_PHP_ARGS` or `TESTS`: +Tests run in parallel by default, using up to 10 detected logical processors. +Set `-jN` in `TEST_PHP_ARGS` or `TESTS` to override the worker count: ```shell make TEST_PHP_ARGS=-j4 test ``` -Shall run `make test` with a maximum of 4 concurrent jobs: Generally the maximum -number of jobs should not exceed the number of cores available. +This runs `make test` with a maximum of 4 concurrent jobs. Alternatively, +use `-j1` to run tests sequentially. Use the `TEST_PHP_ARGS` or `TESTS` variable to test only specific directories: diff --git a/docs/source/miscellaneous/writing-tests.rst b/docs/source/miscellaneous/writing-tests.rst index 7365267648a9..433f87b941b8 100644 --- a/docs/source/miscellaneous/writing-tests.rst +++ b/docs/source/miscellaneous/writing-tests.rst @@ -192,6 +192,11 @@ When you are testing your test case it's really important to make sure that you temporary resources (eg files) that you used in the test. There is a special ``--CLEAN--`` section to help you do this — see `here <#clean>`_. +Tests run in parallel by default. Mutable resources such as files, directories, ports, database +objects, and IPC identifiers must therefore be unique to each test. Read-only fixtures may be +shared. If a resource cannot be isolated, declare the narrowest applicable conflict using +``--CONFLICTS--`` or a ``SCOPED_CONFLICTS`` file. + Another good check is to look at what lines of code in the PHP source your test case covers. This is easy to do, there are some instructions on the `PHP Wiki `_. diff --git a/ext/gd/tests/createfromwbmp2.phpt b/ext/gd/tests/createfromwbmp2.phpt index 4608c861323f..7007ff545d74 100644 --- a/ext/gd/tests/createfromwbmp2.phpt +++ b/ext/gd/tests/createfromwbmp2.phpt @@ -8,7 +8,7 @@ gd ?> --FILE-- "); diff --git a/ext/gd/tests/createfromwbmp2_extern.phpt b/ext/gd/tests/createfromwbmp2_extern.phpt index 68895f9a3570..711f2e8ca3ed 100644 --- a/ext/gd/tests/createfromwbmp2_extern.phpt +++ b/ext/gd/tests/createfromwbmp2_extern.phpt @@ -4,7 +4,7 @@ imagecreatefromwbmp with invalid wbmp gd --FILE-- "); @@ -41,4 +41,4 @@ unlink($filename); --EXPECTF-- Warning: imagecreatefromwbmp(): %croduct of memory allocation multiplication would exceed INT_MAX, failing operation gracefully%win %s on line %d -Warning: imagecreatefromwbmp(): "%s_tmp.wbmp" is not a valid WBMP file in %s on line %d +Warning: imagecreatefromwbmp(): "%s_tmp_createfromwbmp2_extern.wbmp" is not a valid WBMP file in %s on line %d diff --git a/ext/zip/tests/oo_addglob_leak.phpt b/ext/zip/tests/oo_addglob_leak.phpt index 9040c5565f84..be7f92dccb90 100644 --- a/ext/zip/tests/oo_addglob_leak.phpt +++ b/ext/zip/tests/oo_addglob_leak.phpt @@ -12,7 +12,7 @@ if(!defined("GLOB_BRACE")) die ('skip requires GLOB_BRACE'); $dirname = __DIR__ . '/'; include $dirname . 'utils.inc'; -$dirname = __DIR__ . '/__tmp_oo_addglob2/'; +$dirname = __DIR__ . '/__tmp_oo_addglob_leak/'; $file = $dirname . 'test.zip'; @mkdir($dirname); @@ -38,7 +38,7 @@ var_dump($zip->addGlob($dirname . 'bar.*', GLOB_BRACE, $options)); --EXPECTF-- array(1) { diff --git a/run-tests.php b/run-tests.php index 998e7e24c337..7ceef857bab2 100755 --- a/run-tests.php +++ b/run-tests.php @@ -34,9 +34,9 @@ function show_usage(): void php run-tests.php [options] [files] [directories] Options: - -j Run up to simultaneous testing processes in parallel for - quicker testing on systems with multiple logical processors. - Note that this is experimental feature. + -j Run up to simultaneous testing processes. By default, + the worker count is detected automatically. Use -j1 to run + tests sequentially. -l Read the testfiles to be executed from . After the test has finished all failed tests are written to the same . @@ -351,6 +351,7 @@ function main(): void $shuffle = false; $bless = false; $workers = null; + $workersExplicit = false; $context_line_count = 3; $num_repeats = 1; $show_progress = true; @@ -412,6 +413,7 @@ function main(): void switch ($switch) { case 'j': + $workersExplicit = true; $workers = substr($argv[$i], 2); if ($workers == 0 || !preg_match('/^\d+$/', $workers)) { error("'$workers' is not a valid number of workers, try e.g. -j16 for 16 workers"); @@ -641,6 +643,17 @@ function main(): void } } + if (!$workersExplicit && $valgrind === null && !isset($environment['SKIP_ASAN'])) { + $workers = get_default_worker_count(); + if ( + $workers !== null + && (!$selected_tests || count($test_files) > 1) + && !can_create_parallel_worker_socket() + ) { + $workers = null; + } + } + if ($online === null && !isset($environment['SKIP_ONLINE_TESTS'])) { $online = false; } @@ -800,6 +813,53 @@ function main(): void } } +function get_default_worker_count(): ?int +{ + if (IS_WINDOWS) { + $workerCount = getenv('NUMBER_OF_PROCESSORS'); + return is_string($workerCount) ? parse_default_worker_count($workerCount) : null; + } + + $commands = [ + 'nproc 2>/dev/null', + 'getconf _NPROCESSORS_ONLN 2>/dev/null', + 'getconf NPROCESSORS_ONLN 2>/dev/null', + 'sysctl -n hw.logicalcpu 2>/dev/null', + 'sysctl -n hw.ncpu 2>/dev/null', + ]; + foreach ($commands as $command) { + $workerCount = shell_exec($command); + if ( + is_string($workerCount) + && ($workerCount = parse_default_worker_count($workerCount)) !== null + ) { + return $workerCount; + } + } + + return null; +} + +function parse_default_worker_count(string $workerCount): ?int +{ + $workerCount = filter_var(trim($workerCount), FILTER_VALIDATE_INT, [ + 'options' => ['min_range' => 2], + ]); + + return $workerCount !== false ? min($workerCount, 10) : null; +} + +function can_create_parallel_worker_socket(): bool +{ + $socket = @stream_socket_server('tcp://127.0.0.1:0'); + if ($socket === false) { + return false; + } + + fclose($socket); + return true; +} + function verify_config(string $php): void { if (empty($php) || !file_exists($php)) { @@ -1420,14 +1480,15 @@ function run_all_tests_parallel(array $test_files, array $env, ?string $redir_te echo "Spawning $workers workers... "; - // We use sockets rather than STDIN/STDOUT for comms because on Windows, - // those can't be non-blocking for some reason. - $listenSock = stream_socket_server("tcp://127.0.0.1:0") or error("Couldn't create socket on localhost."); + // We use sockets rather than STDIN/STDOUT for comms because those can't + // be non-blocking on Windows. + $listenSock = stream_socket_server("tcp://127.0.0.1:0") + or error("Couldn't create socket on localhost."); $sockName = stream_socket_get_name($listenSock, false); - // PHP is terrible and returns IPv6 addresses not enclosed by [] + // PHP returns IPv6 addresses without enclosing them in square brackets. $portPos = strrpos($sockName, ":"); $sockHost = substr($sockName, 0, $portPos); - if (false !== strpos($sockHost, ":")) { + if (strpos($sockHost, ":") !== false) { $sockHost = "[$sockHost]"; } $sockPort = substr($sockName, $portPos + 1); @@ -1438,7 +1499,7 @@ function run_all_tests_parallel(array $test_files, array $env, ?string $redir_te for ($i = 1; $i <= $workers; $i++) { $proc = proc_open( [$thisPHP, $thisScript], - [], // Inherit our stdin, stdout and stderr + [], $pipes, null, $GLOBALS['environment'] + [ @@ -1497,6 +1558,7 @@ function run_all_tests_parallel(array $test_files, array $env, ?string $redir_te $workerID = $reply["workerID"]; $workerSocks[$workerID] = $workerSock; } + fclose($listenSock); printf("Done in %.2fs\n", microtime(true) - $startTime); echo "=====================================================================\n"; echo "\n"; @@ -1735,8 +1797,8 @@ function run_worker(): void global $junit; $sockUri = getenv("TEST_PHP_URI"); - - $workerSock = stream_socket_client($sockUri, $_, $_, 5) or error("Couldn't connect to $sockUri"); + $workerSock = stream_socket_client($sockUri, $_, $_, 5) + or error("Couldn't connect to $sockUri"); $greeting = fgets($workerSock); $greeting = unserialize(base64_decode($greeting)) or die("Could not decode greeting\n"); diff --git a/sapi/cli/tests/010-2.phpt b/sapi/cli/tests/010-2.phpt index 88fe1c832a11..ddf7315c8298 100644 --- a/sapi/cli/tests/010-2.phpt +++ b/sapi/cli/tests/010-2.phpt @@ -12,7 +12,7 @@ if (substr(PHP_OS, 0, 3) == 'WIN') { $php = getenv('TEST_PHP_EXECUTABLE_ESCAPED'); -$filename_txt = __DIR__."/010.test.txt"; +$filename_txt = __DIR__."/010-R.test.txt"; $filename_txt_escaped = escapeshellarg($filename_txt); $txt = ' diff --git a/sapi/cli/tests/010.phpt b/sapi/cli/tests/010.phpt index 356b69bebf91..80758f368ab9 100644 --- a/sapi/cli/tests/010.phpt +++ b/sapi/cli/tests/010.phpt @@ -14,7 +14,7 @@ $php = getenv('TEST_PHP_EXECUTABLE_ESCAPED'); $filename = __DIR__."/010.test.php"; $filename_escaped = escapeshellarg($filename); -$filename_txt = __DIR__."/010.test.txt"; +$filename_txt = __DIR__."/010-F.test.txt"; $filename_txt_escaped = escapeshellarg($filename_txt); $code = ' @@ -37,7 +37,7 @@ var_dump(shell_exec("cat $filename_txt_escaped | $php -n -F $filename_escaped")) --CLEAN-- --EXPECT-- string(25) " diff --git a/tests/run-test/automatic_worker_limit.phpt b/tests/run-test/automatic_worker_limit.phpt new file mode 100644 index 000000000000..021c0373ea42 --- /dev/null +++ b/tests/run-test/automatic_worker_limit.phpt @@ -0,0 +1,85 @@ +--TEST-- +Automatic worker detection is capped +--SKIPIF-- + +--ENV-- +TEST_PHP_FORK_SERVER=0 +--FILE-- + + --EXPECT-- + ok + PHPT); +} + +$command = [ + getenv('TEST_PHP_EXECUTABLE'), + dirname(__DIR__, 2) . '/run-tests.php', + '-q', + '--no-progress', + ...$testFiles, +]; +$environment = [ + 'PATH' => $bin . PATH_SEPARATOR . getenv('PATH'), + 'TEST_PHP_EXECUTABLE' => getenv('TEST_PHP_EXECUTABLE'), + 'TEST_PHP_FORK_SERVER' => '0', +]; +foreach (['TEMP', 'TMPDIR'] as $name) { + if (($value = getenv($name)) !== false) { + $environment[$name] = $value; + } +} + +$process = proc_open( + $command, + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $environment, +); +fclose($pipes[0]); +$output = stream_get_contents($pipes[1]); +fclose($pipes[1]); +$exitCode = proc_close($process); + +var_dump($exitCode); +var_dump(str_contains($output, 'Spawning 10 workers...')); +var_dump(str_contains($output, 'Spawning 11 workers...')); + +foreach ($testFiles as $file) { + unlink($file); +} +unlink($nproc); +rmdir($tests); +rmdir($bin); +rmdir($root); +?> +--EXPECT-- +int(0) +bool(true) +bool(false) From c9ffc95ec52df29d3a2acf0c487742c6220f7a7c Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:29 +0700 Subject: [PATCH 02/26] Select Windows test workers automatically --- .github/workflows/test-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index e269ed336408..2375ed01114b 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -857,7 +857,7 @@ jobs: PLATFORM: ${{ matrix.x64 && 'x64' || 'x86' }} THREAD_SAFE: "${{ matrix.zts && '1' || '0' }}" INTRINSICS: "${{ matrix.zts && 'AVX2' || '' }}" - PARALLEL: -j2 + PARALLEL: ${{ matrix.asan && '-j2' || '' }} OPCACHE: "${{ matrix.opcache && '1' || '0' }}" ASAN: "${{ matrix.asan && '1' || '0' }}" CLANG_TOOLSET: "${{ matrix.clang && '1' || '0' }}" From c4105dfb2de6042bd5569bc761afaa61f4590f8e Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:30 +0700 Subject: [PATCH 03/26] Parallelize new_oom subprocesses --- Zend/tests/new_oom.phpt | 100 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 6 deletions(-) diff --git a/Zend/tests/new_oom.phpt b/Zend/tests/new_oom.phpt index 6d4ba3d760b4..7f891f233617 100644 --- a/Zend/tests/new_oom.phpt +++ b/Zend/tests/new_oom.phpt @@ -1,5 +1,5 @@ --TEST-- -Test OOM on new of each class +Test OOM on new of each instantiable class --SKIPIF-- /dev/null'); + if (!is_string($processCount)) { + return 1; + } + + $processCount = filter_var(trim($processCount), FILTER_VALIDATE_INT, [ + 'options' => ['min_range' => 1], + ]); + + return $processCount === false ? 1 : min($processCount, 4); +} + +function startOomTest(string $php, string $file, string $class): ?array +{ + $output = tmpfile(); + if ($output === false) { + echo "Class $class failed\nUnable to create output file\n"; + return null; + } + + $process = proc_open( + [$php, '--no-php-ini', $file, $class], + [ + 0 => ['null'], + 1 => $output, + 2 => ['redirect', 1], + ], + $pipes, + ); + if (!is_resource($process)) { + fclose($output); + echo "Class $class failed\nUnable to start process\n"; + return null; + } + + return [ + 'class' => $class, + 'output' => $output, + 'process' => $process, + ]; +} + +function finishOomTest(array $test): bool +{ + $status = proc_get_status($test['process']); + if ($status['running']) { + return false; + } + + proc_close($test['process']); + rewind($test['output']); + $output = stream_get_contents($test['output']); + fclose($test['output']); + + if ($status['signaled']) { + echo "Class {$test['class']} failed\n"; + echo "Process terminated by signal {$status['termsig']}\n"; + } elseif ($output && preg_match('(^\nFatal error: Allowed memory size of [0-9]+ bytes exhausted[^\r\n]* \(tried to allocate [0-9]+ bytes\) in [^\r\n]+ on line [0-9]+\nStack trace:\n(#[0-9]+ [^\r\n]+\n)+$)', $output) !== 1) { + echo "Class {$test['class']} failed\n"; + echo $output, "\n"; + } + + return true; +} + $file = __DIR__ . '/new_oom.inc'; $php = PHP_BINARY; +$classes = array_filter( + get_declared_classes(), + static fn(string $class): bool => (new ReflectionClass($class))->isInstantiable(), +); +$tests = []; +$processCount = getOomProcessCount(); -foreach (get_declared_classes() as $class) { - $output = shell_exec("$php --no-php-ini $file $class 2>&1"); - if ($output && preg_match('(^\nFatal error: Allowed memory size of [0-9]+ bytes exhausted[^\r\n]* \(tried to allocate [0-9]+ bytes\) in [^\r\n]+ on line [0-9]+\nStack trace:\n(#[0-9]+ [^\r\n]+\n)+$)', $output) !== 1) { - echo "Class $class failed\n"; - echo $output, "\n"; +while ($classes || $tests) { + while ($classes && count($tests) < $processCount) { + $class = array_shift($classes); + $test = startOomTest($php, $file, $class); + if ($test !== null) { + $tests[] = $test; + } + } + + $testFinished = false; + foreach ($tests as $index => $test) { + if (finishOomTest($test)) { + unset($tests[$index]); + $testFinished = true; + } + } + if (!$testFinished) { + usleep(1000); } } From 82e0811b122cdd4be5a7798671e59b3fd3c58f26 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:30 +0700 Subject: [PATCH 04/26] Reduce parallel test batch size --- run-tests.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run-tests.php b/run-tests.php index 7ceef857bab2..31c718fef607 100755 --- a/run-tests.php +++ b/run-tests.php @@ -1633,10 +1633,10 @@ function run_all_tests_parallel(array $test_files, array $env, ?string $redir_te // - If this is running a small enough number of tests, // reduce the batch size to give batches to more workers. $files = []; - $maxBatchSize = $valgrind ? 1 : ($shuffle ? 4 : 32); + $maxBatchSize = $valgrind ? 1 : 4; $averageFilesPerWorker = max(1, (int) ceil($totalFileCount / count($workerProcs))); $batchSize = min($maxBatchSize, $averageFilesPerWorker); - while (count($files) <= $batchSize && $file = array_pop($test_files)) { + while (count($files) < $batchSize && $file = array_pop($test_files)) { foreach ($fileConflictsWith[$file] as $conflictKey) { if (isset($activeConflicts[$conflictKey])) { $waitingTests[$conflictKey][] = $file; From c0b796152ecd18b2f101b0f14573cc2fa631556b Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:30 +0700 Subject: [PATCH 05/26] Reuse CLI startup with an isolated fork server --- run-tests.php | 439 +++++++++++++++++++++- sapi/cli/php_cli.c | 429 ++++++++++++++++++++- sapi/cli/tests/test_fork_server.phpt | 122 ++++++ tests/run-test/auxiliary_environment.phpt | 26 ++ tests/run-test/fork_server_lifecycle.phpt | 189 ++++++++++ tests/run-test/fork_server_opcache.phpt | 128 +++++++ 6 files changed, 1321 insertions(+), 12 deletions(-) create mode 100644 sapi/cli/tests/test_fork_server.phpt create mode 100644 tests/run-test/auxiliary_environment.phpt create mode 100644 tests/run-test/fork_server_lifecycle.phpt create mode 100644 tests/run-test/fork_server_opcache.phpt diff --git a/run-tests.php b/run-tests.php index 31c718fef607..5a62be7b29ef 100755 --- a/run-tests.php +++ b/run-tests.php @@ -147,6 +147,7 @@ function main(): void $end_time, $environment, $exts_skipped, $exts_tested, $exts_to_test, $failed_tests_file, $ignored_by_ext, $ini_overwrites, $colorize, + $cli_opcache_enabled, $log_format, $no_clean, $no_file_cache, $pass_options, $php, $php_cgi, $preload, $result_tests_file, $slow_min_ms, $start_time, @@ -348,6 +349,7 @@ function main(): void $slow_min_ms = INF; $preload = false; $file_cache = null; + $cli_opcache_enabled = true; $shuffle = false; $bless = false; $workers = null; @@ -877,6 +879,7 @@ function verify_config(string $php): void function write_information(array $user_tests, $phpdbg): void { global $php, $php_cgi, $php_info, $ini_overwrites, $pass_options, $exts_to_test, $valgrind, $no_file_cache; + global $cli_opcache_enabled; $php_escaped = escapeshellarg($php); // Get info from php @@ -887,6 +890,8 @@ function write_information(array $user_tests, $phpdbg): void PHP_VERSION : " , phpversion() , " ZEND_VERSION: " , zend_version() , " PHP_OS : " , PHP_OS , " - " , php_uname() , " +OPCACHE CLI : " , filter_var(ini_get("opcache.enable"), FILTER_VALIDATE_BOOL) + && filter_var(ini_get("opcache.enable_cli"), FILTER_VALIDATE_BOOL) ? "enabled" : "disabled" , " INI actual : " , realpath(get_cfg_var("cfg_file_path")) , " More .INIs : " , (function_exists(\'php_ini_scanned_files\') ? str_replace("\n","", php_ini_scanned_files()) : "** not determined **"); ?>'; save_text($info_file, $php_info); @@ -894,6 +899,7 @@ function write_information(array $user_tests, $phpdbg): void settings2array($ini_overwrites, $info_params); $info_params = settings2params($info_params); $php_info = shell_exec("$php_escaped $pass_options $info_params $no_file_cache \"$info_file\""); + $cli_opcache_enabled = !str_contains($php_info, "\nOPCACHE CLI : disabled\n"); define('TESTED_PHP_VERSION', shell_exec("$php_escaped -n -r \"echo PHP_VERSION;\"")); if ($php_cgi && $php != $php_cgi) { @@ -1337,6 +1343,360 @@ function system_with_timeout( return $data; } +final class TestForkServer +{ + public const MAX_EXTRA_ARGS_LENGTH = 1_048_576; + + /** @var resource */ + private $process; + /** @var array */ + private array $pipes = []; + private string $token; + private string $buffer = ''; + private int $index = 0; + private bool $stopped = false; + private ?string $startupOutput = null; + + public function __construct(string|array $command, array $env, bool $captureStdErr = true) + { + $this->token = bin2hex(random_bytes(16)); + $descriptors = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => $captureStdErr + ? ['redirect', 1] + : ['file', '/dev/null', 'a'], + ]; + if (is_array($command)) { + $command[] = '--test-fork-server'; + $command[] = $this->token; + } else { + $command = "exec $command --test-fork-server {$this->token}"; + } + $this->process = proc_open( + $command, + $descriptors, + $this->pipes, + TEST_PHP_SRCDIR, + $env, + ['suppress_errors' => true] + ); + if (!is_resource($this->process)) { + throw new RuntimeException('Unable to start test fork server'); + } + stream_set_blocking($this->pipes[1], false); + } + + public function run( + string $file, + int $timeout, + bool $setScriptEnvironment = true, + bool $captureStdErr = true, + ?string $testPhpExtraArgs = null + ): ?string + { + $environmentMode = $setScriptEnvironment ? '+' : '-'; + $errorMode = $captureStdErr ? '+' : '-'; + $request = "$environmentMode$errorMode\t$file\n"; + if ($testPhpExtraArgs !== null) { + // The length prefix supports values containing newlines or exceeding MAXPATHLEN. + $request = "@$environmentMode$errorMode\t" . strlen($testPhpExtraArgs) + . "\n$testPhpExtraArgs\n$file\n"; + } + if ($this->stopped || fwrite($this->pipes[0], $request) === false) { + $this->abort(); + return null; + } + fflush($this->pipes[0]); + + $beginMarker = "\0{$this->token}:B:{$this->index}\0"; + $endMarker = "\0{$this->token}:E:{$this->index}:"; + $deadline = microtime(true) + $timeout; + + while (true) { + try { + $result = $this->extractResult($beginMarker, $endMarker); + } catch (UnexpectedValueException) { + $this->abort(); + return null; + } + if ($result !== null) { + [$output, $status] = $result; + $this->index++; + if ($status > 128 && $status < 160) { + $output .= "\nTermsig=" . ($status - 128) . "\n"; + } + return $output; + } + + $remaining = $deadline - microtime(true); + if ($remaining <= 0) { + $this->abort(); + return "\n ** ERROR: process timed out **\n"; + } + + $read = [$this->pipes[1]]; + $write = null; + $except = null; + $seconds = (int) $remaining; + $microseconds = (int) (($remaining - $seconds) * 1_000_000); + $ready = @stream_select($read, $write, $except, $seconds, $microseconds); + if ($ready === false) { + $this->abort(); + return null; + } + if ($ready === 0) { + continue; + } + + $chunk = fread($this->pipes[1], 8192); + if ($chunk === false || ($chunk === '' && feof($this->pipes[1]))) { + $this->abort(); + return null; + } + $this->buffer .= $chunk; + } + } + + /** + * @return null|array{string, int} + */ + private function extractResult(string $beginMarker, string $endMarker): ?array + { + $begin = strpos($this->buffer, $beginMarker); + if ($begin === false) { + return null; + } + + $outputStart = $begin + strlen($beginMarker); + $end = strpos($this->buffer, $endMarker, $outputStart); + if ($end === false) { + return null; + } + + $statusStart = $end + strlen($endMarker); + $statusEnd = strpos($this->buffer, "\0", $statusStart); + if ($statusEnd === false) { + return null; + } + + $status = substr($this->buffer, $statusStart, $statusEnd - $statusStart); + if (!ctype_digit($status)) { + throw new UnexpectedValueException('Invalid test fork server status'); + } + + $prefix = substr($this->buffer, 0, $begin); + if ($this->startupOutput === null) { + $this->startupOutput = $prefix; + $prefix = ''; + } + $output = $this->startupOutput + . $prefix + . substr($this->buffer, $outputStart, $end - $outputStart); + $this->buffer = substr($this->buffer, $statusEnd + 1); + return [$output, (int) $status]; + } + + private function abort(): void + { + if ($this->stopped) { + return; + } + $this->stopped = true; + proc_terminate($this->process); + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + proc_close($this->process); + } + + public function __destruct() + { + if ($this->stopped) { + return; + } + fclose($this->pipes[0]); + fclose($this->pipes[1]); + proc_close($this->process); + } +} + +function can_use_test_fork_server(): bool +{ + global $cli_opcache_enabled, $environment, $file_cache, $IN_REDIRECT, $num_repeats, $preload, $valgrind; + + return !IS_WINDOWS + && !PHP_ZTS + && !$cli_opcache_enabled + && getenv('TEST_PHP_FORK_SERVER') !== '0' + && !isset($environment['SKIP_ASAN']) + && !$valgrind + && !$preload + && $file_cache === null + && $num_repeats === 1 + && !is_array($IN_REDIRECT); +} + +function can_run_in_test_fork_server(TestFile $test, bool $uses_cgi): bool +{ + return can_use_test_fork_server() + && !$uses_cgi + && !($test->hasSection('SKIPIF') && str_contains($test->getSection('SKIPIF'), 'SKIP_REPEAT')) + && has_only_fork_safe_ini_settings($test) + && !$test->hasAnySections( + 'ARGS', + 'CAPTURE_STDIO', + 'CGI', + 'COOKIE', + 'DEFLATE_POST', + 'ENV', + 'GET', + 'GZIP_POST', + 'PHPDBG', + 'POST', + 'POST_RAW', + 'PUT', + 'REDIRECTTEST', + 'STDIN', + 'XLEAK' + ); +} + +function can_run_auxiliary_script_in_test_fork_server(TestFile $test, bool $usesNonCliSapi): bool +{ + return can_use_test_fork_server() + && !$usesNonCliSapi + && !$test->hasSection('ENV'); +} + +function has_only_fork_safe_ini_settings(TestFile $test): bool +{ + if (!$test->sectionNotEmpty('INI')) { + return true; + } + // Only settings that are safe to initialize in the parent process belong here. + static $safeSettings = [ + 'allow_url_fopen' => true, + 'assert.active' => true, + 'assert.bail' => true, + 'assert.callback' => true, + 'assert.exception' => true, + 'assert.warning' => true, + 'bcmath.scale' => true, + 'date.timezone' => true, + 'default_charset' => true, + 'ffi.enable' => true, + 'internal_encoding' => true, + 'intl.default_locale' => true, + 'open_basedir' => true, + 'output_handler' => true, + 'phar.readonly' => true, + 'phar.require_hash' => true, + 'precision' => true, + 'serialize_precision' => true, + 'soap.wsdl_cache_enabled' => true, + 'zend.assertions' => true, + 'zend.enable_gc' => true, + 'zlib.output_compression' => true, + ]; + + foreach (preg_split('/[\r\n]+/', $test->getSection('INI')) as $setting) { + $setting = trim($setting); + if ($setting === '' || $setting[0] === ';') { + continue; + } + $name = strtolower(trim(explode('=', $setting, 2)[0])); + // Session request state is initialized in the child after the fork. + if (str_starts_with($name, 'session.')) { + continue; + } + if (!isset($safeSettings[$name])) { + return false; + } + } + + return true; +} + +function test_fork_server_cache_key( + string|array $command, + array $env, + bool $captureStdErr, + ?string $testPhpExtraArgs +): string { + // A server inherits its environment only when it is created. These values are sent + // explicitly with each request and therefore do not belong to the inherited state. + unset($env['PATH_TRANSLATED'], $env['SCRIPT_FILENAME']); + if ($testPhpExtraArgs !== null) { + unset($env['TEST_PHP_EXTRA_ARGS']); + } + ksort($env); + return serialize([$command, $captureStdErr, $env, $testPhpExtraArgs !== null]); +} + +function system_with_test_fork_server( + string|array $command, + string $file, + array $env, + string $channel = 'test', + bool $setScriptEnvironment = true, + bool $captureStdErr = true, + bool $deferUntilRepeated = false, + ?string $testPhpExtraArgs = null +): ?string { + static $servers = []; + static $serverKeys = []; + static $pendingKeys = []; + static $unsupported = []; + + $serverKey = test_fork_server_cache_key($command, $env, $captureStdErr, $testPhpExtraArgs); + if ( + strpbrk($file, "\r\n") !== false + || ($testPhpExtraArgs !== null + && strlen($testPhpExtraArgs) > TestForkServer::MAX_EXTRA_ARGS_LENGTH) + || isset($unsupported[$serverKey]) + ) { + return null; + } + if (($serverKeys[$channel] ?? null) !== $serverKey) { + if ($deferUntilRepeated && ($pendingKeys[$channel] ?? null) !== $serverKey) { + $pendingKeys[$channel] = $serverKey; + return null; + } + unset($pendingKeys[$channel]); + unset($servers[$channel]); + $serverKeys[$channel] = $serverKey; + } else { + unset($pendingKeys[$channel]); + } + if (!isset($servers[$channel])) { + try { + $servers[$channel] = new TestForkServer($command, $env, $captureStdErr); + } catch (Throwable) { + $unsupported[$serverKey] = true; + return null; + } + } + + $timeout = (int) ($env['TEST_TIMEOUT'] ?? 60); + if (isset($env['SKIP_ASAN'])) { + $timeout *= 3; + } + + $output = $servers[$channel]->run( + $file, + $timeout, + $setScriptEnvironment, + $captureStdErr, + $testPhpExtraArgs, + ); + if ($output === null) { + unset($servers[$channel]); + $unsupported[$serverKey] = true; + } + return $output; +} + function run_all_tests(array $test_files, array $env, ?string $redir_tested = null): void { global $test_results, $failed_tests_file, $result_tests_file, $php, $test_idx, $file_cache, $shuffle; @@ -2230,7 +2590,17 @@ function run_test(string $php, $file, array $env): string $startTime = microtime(true); $commandLine = "$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache -d display_errors=1 -d display_startup_errors=0"; - $output = $skipCache->checkSkip($commandLine, $test->getSection('SKIPIF'), $test_skipif, $temp_skipif, $env); + $output = $skipCache->checkSkip( + $commandLine, + $test->getSection('SKIPIF'), + $test_skipif, + $temp_skipif, + $env, + can_run_auxiliary_script_in_test_fork_server( + $test, + $uses_cgi || $test->hasSection('PHPDBG') + ), + ); $time = microtime(true) - $startTime; $junit->stopTimer($shortname); @@ -2560,7 +2930,20 @@ function run_test(string $php, $file, array $env): string $startTime = $hrtime[0] * 1000000000 + $hrtime[1]; $stdin = $test->hasSection('STDIN') ? $test->getSection('STDIN') : null; - $out = system_with_timeout($cmd, $env, $stdin, $captureStdIn, $captureStdOut, $captureStdErr); + $out = null; + if (can_run_in_test_fork_server($test, $uses_cgi)) { + $hasTestIni = $test->sectionNotEmpty('INI'); + $out = system_with_test_fork_server( + "$php $pass_options $ini_settings", + $test_file, + $env, + channel: $hasTestIni ? 'test-ini' : 'test', + deferUntilRepeated: $hasTestIni, + ); + } + if ($out === null) { + $out = system_with_timeout($cmd, $env, $stdin, $captureStdIn, $captureStdOut, $captureStdErr); + } $junit->stopTimer($shortname); $hrtime = hrtime(); @@ -2584,7 +2967,31 @@ function run_test(string $php, $file, array $env): string if (!$no_clean) { $extra = !IS_WINDOWS ? "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : ""; - $clean_output = system_with_timeout("$extra $orig_php $pass_options -q $orig_ini_settings $no_file_cache \"$test_clean\"", $env); + $cleanCommand = "$extra $orig_php $pass_options -q $orig_ini_settings $no_file_cache"; + $cleanEnv = $env; + if (!IS_WINDOWS) { + unset( + $cleanEnv['REQUEST_METHOD'], + $cleanEnv['QUERY_STRING'], + $cleanEnv['PATH_TRANSLATED'], + $cleanEnv['SCRIPT_FILENAME'], + ); + } + $clean_output = null; + if (can_run_auxiliary_script_in_test_fork_server($test, $uses_cgi)) { + $clean_output = system_with_test_fork_server( + $cleanCommand, + $test_clean, + $cleanEnv, + channel: 'clean', + setScriptEnvironment: false, + captureStdErr: false, + testPhpExtraArgs: $cleanEnv['TEST_PHP_EXTRA_ARGS'] ?? '', + ); + } + if ($clean_output === null) { + $clean_output = system_with_timeout("$cleanCommand \"$test_clean\"", $cleanEnv); + } } if (!$cfg['keep']['clean']) { @@ -3674,7 +4081,14 @@ public function __construct(bool $enable, bool $keepFile) $this->keepFile = $keepFile; } - public function checkSkip(string $php, string $code, string $checkFile, string $tempFile, array $env): string + public function checkSkip( + string $php, + string $code, + string $checkFile, + string $tempFile, + array $env, + bool $useForkServer + ): string { // Extension tests frequently use something like enable) { diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c index 9095fb10dd41..8fdc766a94ba 100644 --- a/sapi/cli/php_cli.c +++ b/sapi/cli/php_cli.c @@ -42,6 +42,12 @@ #ifdef HAVE_UNISTD_H #include #endif +#ifdef HAVE_SYS_WAIT_H +#include +#endif +#ifndef PHP_WIN32 +#include +#endif #include #include @@ -151,9 +157,364 @@ const opt_struct OPTIONS[] = { /* Internal testing option -- may be changed or removed without notice, * including in patch releases. */ {16, 1, "repeat"}, + {17, 1, "test-fork-server"}, {'-', 0, NULL} /* end of args */ }; +typedef struct { + char file[MAXPATHLEN]; + char *test_php_extra_args; + size_t test_php_extra_args_length; + size_t index; + const char *token; + bool set_script_environment; + bool capture_stderr; + bool has_test_php_extra_args; +} php_cli_test_batch; + +#define PHP_CLI_TEST_BATCH_MAX_EXTRA_ARGS (1024 * 1024) + +typedef enum { + PHP_CLI_TEST_BATCH_ERROR = -1, + PHP_CLI_TEST_BATCH_CHILD, + PHP_CLI_TEST_BATCH_PARENT, + PHP_CLI_TEST_BATCH_DONE, +} php_cli_test_batch_result; + +#if defined(HAVE_FORK) && !defined(ZTS) +static volatile sig_atomic_t php_cli_test_child_pid; + +static void php_cli_test_batch_signal(int signal) /* {{{ */ +{ + if (php_cli_test_child_pid > 0) { + kill(php_cli_test_child_pid, SIGKILL); + } + _exit(128 + signal); +} +/* }}} */ +#endif + +static bool php_cli_test_batch_init(php_cli_test_batch *batch, const char *token) /* {{{ */ +{ + size_t token_length = strlen(token); + + if (token_length == 0 || token_length > 64) { + fprintf(stderr, "Invalid test token length\n"); + return false; + } + for (size_t i = 0; i < token_length; i++) { + if (!isalnum((unsigned char) token[i])) { + fprintf(stderr, "Invalid test token\n"); + return false; + } + } + + batch->token = token; + return true; +} +/* }}} */ + +#if defined(HAVE_FORK) && !defined(ZTS) +static int php_cli_test_batch_read_line(char *buffer, size_t size) /* {{{ */ +{ + while (fgets(buffer, size, stdin)) { + size_t length = strlen(buffer); + + while (length > 0 && (buffer[length - 1] == '\n' || buffer[length - 1] == '\r')) { + buffer[--length] = '\0'; + } + if (length == 0) { + continue; + } + if (length == size - 1 && !feof(stdin)) { + fprintf(stderr, "Test request exceeds MAXPATHLEN\n"); + return -1; + } + return 1; + } + + return ferror(stdin) ? -1 : 0; +} +/* }}} */ + +static bool php_cli_test_batch_read_bytes(char *buffer, size_t length) /* {{{ */ +{ + while (length > 0) { + size_t bytes_read = fread(buffer, 1, length, stdin); + if (bytes_read == 0) { + return false; + } + buffer += bytes_read; + length -= bytes_read; + } + return true; +} +/* }}} */ + +static int php_cli_test_batch_next(php_cli_test_batch *batch) /* {{{ */ +{ + free(batch->test_php_extra_args); + batch->test_php_extra_args = NULL; + batch->test_php_extra_args_length = 0; + batch->has_test_php_extra_args = false; + + int has_line = php_cli_test_batch_read_line(batch->file, sizeof(batch->file)); + + if (has_line <= 0) { + return has_line; + } + + size_t length = strlen(batch->file); + batch->set_script_environment = true; + batch->capture_stderr = true; + /* A length-prefixed request is followed by extra args, a newline, then the test path. */ + if (batch->file[0] == '@' + && (batch->file[1] == '+' || batch->file[1] == '-') + && (batch->file[2] == '+' || batch->file[2] == '-') + && batch->file[3] == '\t') { + char *end; + errno = 0; + unsigned long long extra_args_length = strtoull(batch->file + 4, &end, 10); + if (errno || end == batch->file + 4 || *end != '\0' + || extra_args_length > PHP_CLI_TEST_BATCH_MAX_EXTRA_ARGS) { + fprintf(stderr, "Invalid TEST_PHP_EXTRA_ARGS length\n"); + return -1; + } + + batch->set_script_environment = batch->file[1] == '+'; + batch->capture_stderr = batch->file[2] == '+'; + batch->test_php_extra_args_length = (size_t) extra_args_length; + batch->test_php_extra_args = malloc(batch->test_php_extra_args_length + 1); + if (!batch->test_php_extra_args + || !php_cli_test_batch_read_bytes( + batch->test_php_extra_args, batch->test_php_extra_args_length) + || fgetc(stdin) != '\n') { + fprintf(stderr, "Unable to read TEST_PHP_EXTRA_ARGS\n"); + free(batch->test_php_extra_args); + batch->test_php_extra_args = NULL; + batch->test_php_extra_args_length = 0; + return -1; + } + batch->test_php_extra_args[batch->test_php_extra_args_length] = '\0'; + batch->has_test_php_extra_args = true; + + has_line = php_cli_test_batch_read_line(batch->file, sizeof(batch->file)); + if (has_line <= 0) { + fprintf(stderr, "Test path is missing\n"); + return -1; + } + return 1; + } + if ((batch->file[0] == '+' || batch->file[0] == '-') + && (batch->file[1] == '+' || batch->file[1] == '-') + && batch->file[2] == '\t') { + batch->set_script_environment = batch->file[0] == '+'; + batch->capture_stderr = batch->file[1] == '+'; + memmove(batch->file, batch->file + 3, length - 2); + if (length == 3) { + fprintf(stderr, "Test path is empty\n"); + return -1; + } + } + return 1; +} +/* }}} */ + +static void php_cli_test_batch_marker( + const php_cli_test_batch *batch, + char type, + int exit_status +) /* {{{ */ +{ + fputc('\0', stdout); + if (type == 'B') { + fprintf(stdout, "%s:B:%zu", batch->token, batch->index); + } else { + fprintf(stdout, "%s:E:%zu:%d", batch->token, batch->index, exit_status); + } + fputc('\0', stdout); + fflush(stdout); +} +/* }}} */ + +static bool php_cli_test_batch_relay_output(int fd) /* {{{ */ +{ + char buffer[8192]; + ssize_t bytes_read; + + while (true) { + bytes_read = read(fd, buffer, sizeof(buffer)); + if (bytes_read > 0) { + if (fwrite(buffer, 1, bytes_read, stdout) != (size_t) bytes_read) { + return false; + } + fflush(stdout); + continue; + } + if (bytes_read < 0 && errno == EINTR) { + continue; + } + return bytes_read == 0; + } +} +/* }}} */ +#endif + +static php_cli_test_batch_result php_cli_test_batch_start( + php_cli_test_batch *batch, + char **script_file +) /* {{{ */ +{ +#if !defined(HAVE_FORK) || defined(ZTS) + fprintf(stderr, "--test-fork-server requires a non-ZTS build with fork()\n"); + return PHP_CLI_TEST_BATCH_ERROR; +#else + int test_output[2]; + sigset_t termination_signals; + sigset_t previous_signal_mask; + int has_test = php_cli_test_batch_next(batch); + + if (has_test < 0) { + fprintf(stderr, "Unable to read test path\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + if (has_test == 0) { + return PHP_CLI_TEST_BATCH_DONE; + } + + *script_file = batch->file; + if (pipe(test_output) < 0) { + fprintf(stderr, "Unable to create test output pipe\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + php_cli_test_batch_marker(batch, 'B', 0); + + sigemptyset(&termination_signals); + sigaddset(&termination_signals, SIGINT); + sigaddset(&termination_signals, SIGTERM); + if (sigprocmask(SIG_BLOCK, &termination_signals, &previous_signal_mask) < 0) { + close(test_output[0]); + close(test_output[1]); + fprintf(stderr, "Unable to block termination signals\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + + pid_t test_pid = fork(); + if (test_pid < 0) { + close(test_output[0]); + close(test_output[1]); + sigprocmask(SIG_SETMASK, &previous_signal_mask, NULL); + fprintf(stderr, "Unable to fork test process\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + if (test_pid > 0) { + int status; + pid_t waited; + + close(test_output[1]); + php_cli_test_child_pid = test_pid; + if (sigprocmask(SIG_SETMASK, &previous_signal_mask, NULL) < 0) { + kill(test_pid, SIGKILL); + php_cli_test_child_pid = 0; + close(test_output[0]); + do { + waited = waitpid(test_pid, &status, 0); + } while (waited < 0 && errno == EINTR); + fprintf(stderr, "Unable to restore termination signals\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + bool output_relayed = php_cli_test_batch_relay_output(test_output[0]); + close(test_output[0]); + do { + waited = waitpid(test_pid, &status, 0); + } while (waited < 0 && errno == EINTR); + php_cli_test_child_pid = 0; + + if (!output_relayed || waited < 0) { + fprintf(stderr, "Unable to collect test process output\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + + if (!WIFEXITED(status) && !WIFSIGNALED(status)) { + fprintf(stderr, "Unexpected test process status\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + int exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status); + php_cli_test_batch_marker(batch, 'E', exit_status); + batch->index++; + return PHP_CLI_TEST_BATCH_PARENT; + } + + php_cli_test_child_pid = 0; + signal(SIGINT, SIG_DFL); + signal(SIGTERM, SIG_DFL); + if (sigprocmask(SIG_SETMASK, &previous_signal_mask, NULL) < 0) { + fprintf(stderr, "Unable to restore termination signals\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + close(test_output[0]); + if (dup2(test_output[1], STDOUT_FILENO) < 0) { + fprintf(stderr, "Unable to redirect test output\n"); + close(test_output[1]); + return PHP_CLI_TEST_BATCH_ERROR; + } + if (batch->capture_stderr) { + if (dup2(test_output[1], STDERR_FILENO) < 0) { + fprintf(stderr, "Unable to redirect test error output\n"); + close(test_output[1]); + return PHP_CLI_TEST_BATCH_ERROR; + } + } else { + int null_fd = open("/dev/null", O_WRONLY); + if (null_fd < 0 || dup2(null_fd, STDERR_FILENO) < 0) { + fprintf(stderr, "Unable to discard test error output\n"); + if (null_fd >= 0) { + close(null_fd); + } + close(test_output[1]); + return PHP_CLI_TEST_BATCH_ERROR; + } + close(null_fd); + } + close(test_output[1]); + if (batch->set_script_environment) { + if (setenv("PATH_TRANSLATED", *script_file, 1) < 0 + || setenv("SCRIPT_FILENAME", *script_file, 1) < 0) { + fprintf(stderr, "Unable to set test environment\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + } else if (unsetenv("PATH_TRANSLATED") < 0 || unsetenv("SCRIPT_FILENAME") < 0) { + fprintf(stderr, "Unable to unset test environment\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + if (batch->has_test_php_extra_args) { + if (memchr(batch->test_php_extra_args, '\0', batch->test_php_extra_args_length) + || setenv("TEST_PHP_EXTRA_ARGS", batch->test_php_extra_args, 1) < 0) { + fprintf(stderr, "Unable to set TEST_PHP_EXTRA_ARGS\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + } + php_child_init(); + + /* Match proc_open() with a closed write end: an empty pipe, not /dev/null. */ + int test_input[2]; + if (pipe(test_input) < 0) { + fprintf(stderr, "Unable to create test input pipe\n"); + return PHP_CLI_TEST_BATCH_ERROR; + } + close(test_input[1]); + if (dup2(test_input[0], STDIN_FILENO) < 0) { + fprintf(stderr, "Unable to redirect test input\n"); + close(test_input[0]); + return PHP_CLI_TEST_BATCH_ERROR; + } + close(test_input[0]); + + return PHP_CLI_TEST_BATCH_CHILD; +#endif +} +/* }}} */ + static int module_name_cmp(Bucket *f, Bucket *s) /* {{{ */ { return strcasecmp(((zend_module_entry *)Z_PTR(f->val))->name, @@ -592,6 +953,11 @@ static int do_cli(int argc, char **argv) /* {{{ */ char *exec_direct = NULL, *exec_run = NULL, *exec_begin = NULL, *exec_end = NULL; char *arg_free = NULL, **arg_excp = &arg_free; char *script_file = NULL, *translated_path = NULL; + char *test_argv[2] = {NULL, NULL}; + char *test_batch_token = NULL; + php_cli_test_batch test_batch = {0}; + bool test_batch_complete = false; + bool test_batch_failed = false; bool interactive = false; const char *param_error = NULL; bool hide_argv = false; @@ -817,11 +1183,29 @@ static int do_cli(int argc, char **argv) /* {{{ */ case 16: num_repeats = atoi(php_optarg); break; + case 17: + test_batch_token = php_optarg; + break; default: break; } } + if (test_batch_token) { + if (zend_ini_bool_literal("opcache.enable") + && zend_ini_bool_literal("opcache.enable_cli")) { + param_error = "--test-fork-server cannot be used when opcache.enable_cli is enabled.\n"; + } else if (!php_cli_test_batch_init(&test_batch, test_batch_token)) { + param_error = "Unable to initialize test batch.\n"; + } + } +#if defined(HAVE_FORK) && !defined(ZTS) + if (test_batch.token) { + signal(SIGINT, php_cli_test_batch_signal); + signal(SIGTERM, php_cli_test_batch_signal); + } +#endif + if (param_error) { PUTS(param_error); EG(exit_status) = 1; @@ -853,6 +1237,22 @@ static int do_cli(int argc, char **argv) /* {{{ */ } do_repeat: + if (test_batch.token) { + php_cli_test_batch_result result = php_cli_test_batch_start(&test_batch, &script_file); + if (result == PHP_CLI_TEST_BATCH_PARENT) { + goto do_repeat; + } + if (result == PHP_CLI_TEST_BATCH_DONE) { + test_batch_complete = true; + goto out; + } + if (result == PHP_CLI_TEST_BATCH_ERROR) { + EG(exit_status) = 1; + test_batch_failed = true; + goto out; + } + } + /* only set script_file if not set already and not in direct mode and not at end of parameter list */ if (argc > php_optind && !script_file @@ -890,16 +1290,24 @@ static int do_cli(int argc, char **argv) /* {{{ */ /* before registering argv to module exchange the *new* argv[0] */ /* we can achieve this without allocating more memory */ - SG(request_info).argc = argc - php_optind + 1; - arg_excp = argv + php_optind - 1; - arg_free = argv[php_optind - 1]; SG(request_info).path_translated = translated_path ? translated_path : php_self; - argv[php_optind - 1] = php_self; - SG(request_info).argv = argv + php_optind - 1; + if (test_batch.token) { + test_argv[0] = php_self; + SG(request_info).argc = 1; + SG(request_info).argv = test_argv; + } else { + SG(request_info).argc = argc - php_optind + 1; + arg_excp = argv + php_optind - 1; + arg_free = argv[php_optind - 1]; + argv[php_optind - 1] = php_self; + SG(request_info).argv = argv + php_optind - 1; + } SG(server_context) = &context; if (php_request_startup() == FAILURE) { - *arg_excp = arg_free; + if (!test_batch.token) { + *arg_excp = arg_free; + } PUTS("Could not startup.\n"); goto err; } @@ -911,7 +1319,9 @@ static int do_cli(int argc, char **argv) /* {{{ */ is_ps_title_available() == PS_TITLE_SUCCESS, 0, 0); - *arg_excp = arg_free; /* reconstruct argv */ + if (!test_batch.token) { + *arg_excp = arg_free; /* reconstruct argv */ + } if (hide_argv) { int i; @@ -1154,6 +1564,11 @@ static int do_cli(int argc, char **argv) /* {{{ */ free(translated_path); translated_path = NULL; } + if (test_batch.token && (test_batch_complete || test_batch_failed || pid != getpid())) { + int exit_status = EG(exit_status); + free(test_batch.test_php_extra_args); + return test_batch_complete ? 0 : exit_status; + } if (context.mode == PHP_CLI_MODE_LINT && argc > php_optind && strcmp(argv[php_optind], "--")) { script_file = NULL; goto do_repeat; diff --git a/sapi/cli/tests/test_fork_server.phpt b/sapi/cli/tests/test_fork_server.phpt new file mode 100644 index 000000000000..a12022b179a2 --- /dev/null +++ b/sapi/cli/tests/test_fork_server.phpt @@ -0,0 +1,122 @@ +--TEST-- +CLI test fork server isolates requests and continues after a nonzero exit +--SKIPIF-- + ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, +); +fclose($pipes[0]); +stream_get_contents($pipes[1]); +fclose($pipes[1]); +if (proc_close($process) !== 0) { + die('skip fork server is not available'); +} +?> +--FILE-- + <<<'PHP' + <<<'PHP' + <<<'PHP' + <<<'PHP' + <<<'PHP' + <<<'PHP' + $code) { + file_put_contents($file, $code); +} + +$token = 'TESTTOKEN'; +$processEnv = getenv(); +$processEnv['TEST_PHP_EXTRA_ARGS'] = 'server defaults'; +$process = proc_open( + [$php, '-n', '--test-fork-server', $token], + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $processEnv, +); +$files = array_keys($scripts); +foreach ($files as $index => $file) { + if ($index === array_key_last($files) - 1) { + $extraArgs = '-d test=' . str_repeat('x', 1500); + fwrite($pipes[0], "@--\t" . strlen($extraArgs) . "\n$extraArgs\n$file\n"); + } else { + fwrite($pipes[0], "$file\n"); + } +} +fclose($pipes[0]); +$output = stream_get_contents($pipes[1]); +fclose($pipes[1]); +$exitCode = proc_close($process); + +foreach (array_keys($scripts) as $file) { + unlink($file); +} + +echo str_replace("\0", '|', $output); +var_dump($exitCode); +?> +--EXPECT-- +|TESTTOKEN:B:0|first +pipe +empty +script env +|TESTTOKEN:E:0:0||TESTTOKEN:B:1|isolated +|TESTTOKEN:E:1:0||TESTTOKEN:B:2||TESTTOKEN:E:2:23||TESTTOKEN:B:3|after +|TESTTOKEN:E:3:0||TESTTOKEN:B:4|unset env +extra args +|TESTTOKEN:E:4:0||TESTTOKEN:B:5|restored extra args +|TESTTOKEN:E:5:0|int(0) diff --git a/tests/run-test/auxiliary_environment.phpt b/tests/run-test/auxiliary_environment.phpt new file mode 100644 index 000000000000..1a324a5c3d20 --- /dev/null +++ b/tests/run-test/auxiliary_environment.phpt @@ -0,0 +1,26 @@ +--TEST-- +SKIPIF and CLEAN receive test-specific extra arguments +--INI-- +error_reporting=123 +--SKIPIF-- + +--FILE-- + +--CLEAN-- + +--EXPECT-- +set diff --git a/tests/run-test/fork_server_lifecycle.phpt b/tests/run-test/fork_server_lifecycle.phpt new file mode 100644 index 000000000000..35258e60ee2a --- /dev/null +++ b/tests/run-test/fork_server_lifecycle.phpt @@ -0,0 +1,189 @@ +--TEST-- +Test fork server timeout, termination, and failure fallback +--EXTENSIONS-- +posix +--SKIPIF-- + ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, +); +fclose($pipes[0]); +stream_get_contents($pipes[1]); +fclose($pipes[1]); +if (proc_close($process) !== 0) { + die('skip fork server is not available'); +} +?> +--ENV-- +TEST_PHP_FORK_SERVER=0 +--FILE-- + + --EXPECT-- + $expected + PHPT); +} + +/** + * @return array{int, string, array} + */ +function runTests(array $files, string $results, array $arguments = []): array +{ + $command = [ + getenv('TEST_PHP_EXECUTABLE'), + dirname(__DIR__, 2) . '/run-tests.php', + '-n', + '-q', + '-j1', + ...$arguments, + '-W', + $results, + ...$files, + ]; + $environment = [ + 'PATH' => getenv('PATH'), + 'TEST_PHP_EXECUTABLE' => getenv('TEST_PHP_EXECUTABLE'), + 'TEST_PHP_FORK_SERVER' => '1', + ]; + foreach (['SystemRoot', 'TEMP', 'TMPDIR'] as $name) { + if (($value = getenv($name)) !== false) { + $environment[$name] = $value; + } + } + $process = proc_open( + $command, + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $environment, + ); + fclose($pipes[0]); + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + $exitCode = proc_close($process); + + $statuses = []; + foreach (file($results, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $resultLine) { + [$status, $file] = explode("\t", $resultLine, 2); + $statuses[basename($file)] = $status; + } + ksort($statuses); + return [$exitCode, $output, $statuses]; +} + +function processExited(int $pid): bool +{ + $deadline = microtime(true) + 2; + do { + if (!@posix_kill($pid, 0)) { + return true; + } + usleep(10_000); + } while (microtime(true) < $deadline); + return false; +} + +$root = __DIR__ . '/fork_server_lifecycle_' . getmypid(); +mkdir($root); + +$timeoutPid = $root . '/timeout.pid'; +$timeoutPidExpression = var_export($timeoutPid, true); +$timeoutTest = $root . '/timeout.phpt'; +$afterTest = $root . '/after.phpt'; +writeTest( + $timeoutTest, + 'fork server timeout', + "file_put_contents($timeoutPidExpression, getmypid());\nsleep(10);\necho \"finished\\n\";", + 'finished', +); +writeTest($afterTest, 'run after timeout', 'echo "after\n";', 'after'); +$timeoutResults = $root . '/timeout-results.txt'; +[$timeoutExit, $timeoutOutput, $timeoutStatuses] = runTests( + [$timeoutTest, $afterTest], + $timeoutResults, + ['--set-timeout', '1'], +); +foreach ($timeoutStatuses as $file => $status) { + echo "timeout $status $file\n"; +} +var_dump($timeoutExit); + +$timeoutChildExited = false; +if (file_exists($timeoutPid)) { + $timeoutChild = (int) file_get_contents($timeoutPid); + $timeoutChildExited = processExited($timeoutChild); + if (!$timeoutChildExited) { + posix_kill($timeoutChild, 9); + } +} +echo $timeoutChildExited ? "timeout child terminated\n" : "timeout child leaked\n"; +if ($timeoutExit !== 1 || count($timeoutStatuses) !== 2) { + echo $timeoutOutput; +} + +$failureMarker = $root . '/failure.marker'; +$failureMarkerExpression = var_export($failureMarker, true); +$failureTest = $root . '/failure.phpt'; +writeTest( + $failureTest, + 'fork server failure fallback', + << $status) { + echo "failure $status $file\n"; +} +var_dump($failureExit); +if ($failureExit !== 0 || count($failureStatuses) !== 1) { + echo $failureOutput; +} + +foreach (glob($root . '/*') as $file) { + unlink($file); +} +rmdir($root); +?> +--EXPECT-- +timeout PASSED after.phpt +timeout FAILED timeout.phpt +int(1) +timeout child terminated +failure PASSED failure.phpt +int(0) diff --git a/tests/run-test/fork_server_opcache.phpt b/tests/run-test/fork_server_opcache.phpt new file mode 100644 index 000000000000..6cd771fea7ef --- /dev/null +++ b/tests/run-test/fork_server_opcache.phpt @@ -0,0 +1,128 @@ +--TEST-- +Test fork server is disabled when CLI OPcache is enabled +--EXTENSIONS-- +opcache +--SKIPIF-- + ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, +); +fclose($pipes[0]); +stream_get_contents($pipes[1]); +fclose($pipes[1]); +if (proc_close($process) !== 0) { + die('skip fork server is not available'); +} + +$process = proc_open( + [ + getenv('TEST_PHP_EXECUTABLE'), + '-n', + '-r', + 'exit(extension_loaded("Zend OPcache") ? 0 : 1);', + ], + [], + $pipes, +); +if (proc_close($process) !== 0) { + die('skip requires OPcache without additional arguments'); +} +?> +--ENV-- +TEST_PHP_FORK_SERVER=0 +--FILE-- + ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, +); +fclose($pipes[0]); +echo stream_get_contents($pipes[1]); +fclose($pipes[1]); +var_dump(proc_close($process)); + +$results = __DIR__ . '/fork_server_opcache_results.txt'; +$command = [ + $php, + dirname(__DIR__, 2) . '/run-tests.php', + '-q', + '-j1', + '-d', + 'opcache.enable=1', + '-d', + 'opcache.enable_cli=1', + '-W', + $results, + dirname(__DIR__, 2) . '/ext/opcache/tests/gh17422/001.phpt', + dirname(__DIR__, 2) . '/ext/opcache/tests/gh17422/003.phpt', +]; +$environment = [ + 'PATH' => getenv('PATH'), + 'TEST_PHP_EXECUTABLE' => getenv('TEST_PHP_EXECUTABLE'), + 'TEST_PHP_FORK_SERVER' => '1', +]; +foreach (['SystemRoot', 'TEMP', 'TMPDIR'] as $name) { + if (($value = getenv($name)) !== false) { + $environment[$name] = $value; + } +} +$process = proc_open( + $command, + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $environment, +); +fclose($pipes[0]); +$output = stream_get_contents($pipes[1]); +fclose($pipes[1]); +$exitCode = proc_close($process); + +foreach (file($results, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $resultLine) { + [$status, $file] = explode("\t", $resultLine, 2); + echo $status, ' ', basename($file), "\n"; +} +var_dump($exitCode); +if ($exitCode !== 0) { + echo $output; +} +?> +--CLEAN-- + +--EXPECT-- +--test-fork-server cannot be used when opcache.enable_cli is enabled. +int(1) +PASSED 001.phpt +PASSED 003.phpt +int(0) From 9105140d150fda5a9238ba5c4afdf8d28e1bdbe5 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:30 +0700 Subject: [PATCH 06/26] Cache and bound failed PDO connection probes --- ext/pdo/tests/bug_73234.phpt | 2 +- ext/pdo/tests/bug_79106.phpt | 2 +- ext/pdo/tests/bug_79106_collision.phpt | 2 +- ext/pdo/tests/debug_emulated_prepares.phpt | 2 +- ext/pdo/tests/gh8626.phpt | 2 +- ext/pdo/tests/pdo_017.phpt | 2 +- ext/pdo/tests/pdo_test.inc | 88 ++++++++++++++++--- ext/pdo/tests/pdo_test_skip_cache.phpt | 72 +++++++++++++++ ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt | 2 +- .../tests/batch_stmt_ins_sel_up_del.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_ins_up.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_rowcount.phpt | 2 +- .../tests/batch_stmt_transaction.phpt | 2 +- ext/pdo_dblib/tests/batch_stmt_try.phpt | 2 +- ext/pdo_dblib/tests/bug_38955.phpt | 2 +- ext/pdo_dblib/tests/bug_45876.phpt | 2 +- ext/pdo_dblib/tests/bug_47588.phpt | 2 +- ext/pdo_dblib/tests/bug_50755.phpt | 2 +- ext/pdo_dblib/tests/bug_54648.phpt | 2 +- ext/pdo_dblib/tests/bug_67130.phpt | 2 +- ext/pdo_dblib/tests/bug_68957.phpt | 2 +- ext/pdo_dblib/tests/bug_69592.phpt | 2 +- ext/pdo_dblib/tests/bug_69757.phpt | 2 +- ext/pdo_dblib/tests/bug_71667.phpt | 2 +- ext/pdo_dblib/tests/bug_73396.phpt | 2 +- ext/pdo_dblib/tests/common.phpt | 10 ++- ext/pdo_dblib/tests/config.inc | 7 ++ ext/pdo_dblib/tests/datetime2.phpt | 2 +- ext/pdo_dblib/tests/datetime_convert.phpt | 2 +- ext/pdo_dblib/tests/dbtds.phpt | 2 +- ext/pdo_dblib/tests/dbversion.phpt | 2 +- .../tests/pdo_dblib_param_str_natl.phpt | 2 +- ext/pdo_dblib/tests/pdo_dblib_quote.phpt | 2 +- ext/pdo_dblib/tests/pdodblib_001.phpt | 2 +- ext/pdo_dblib/tests/pdodblib_002.phpt | 2 +- .../tests/stringify_uniqueidentifier.phpt | 2 +- ext/pdo_dblib/tests/timeout.phpt | 2 +- ext/pdo_dblib/tests/types.phpt | 2 +- run-tests.php | 40 ++++++++- 40 files changed, 238 insertions(+), 49 deletions(-) create mode 100644 ext/pdo/tests/pdo_test_skip_cache.phpt diff --git a/ext/pdo/tests/bug_73234.phpt b/ext/pdo/tests/bug_73234.phpt index f291df704bbc..628feda50f42 100644 --- a/ext/pdo/tests/bug_73234.phpt +++ b/ext/pdo/tests/bug_73234.phpt @@ -10,7 +10,7 @@ if (str_starts_with(getenv('PDOTEST_DSN'), "firebird")) die('xfail firebird driv require_once $dir . 'pdo_test.inc'; PDOTest::skip(); -$db = PDOTest::factory(); +$db = PDOTest::factoryForSkip(); if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'oci') { die("xfail PDO::PARAM_NULL is not honored by OCI driver, related with bug #81586"); } diff --git a/ext/pdo/tests/bug_79106.phpt b/ext/pdo/tests/bug_79106.phpt index c3d13914e373..7e756a35ff25 100644 --- a/ext/pdo/tests/bug_79106.phpt +++ b/ext/pdo/tests/bug_79106.phpt @@ -8,7 +8,7 @@ $dir = getenv('REDIR_TEST_DIR'); if (!$dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; try { - $db = PDOTest::factory(); + $db = PDOTest::factoryForSkip(); } catch (PDOException $e) { die('skip ' . $e->getMessage()); } diff --git a/ext/pdo/tests/bug_79106_collision.phpt b/ext/pdo/tests/bug_79106_collision.phpt index dc895f017016..403b76ff512f 100644 --- a/ext/pdo/tests/bug_79106_collision.phpt +++ b/ext/pdo/tests/bug_79106_collision.phpt @@ -8,7 +8,7 @@ $dir = getenv('REDIR_TEST_DIR'); if (!$dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; try { - $db = PDOTest::factory(); + $db = PDOTest::factoryForSkip(); } catch (PDOException $e) { die('skip ' . $e->getMessage()); } diff --git a/ext/pdo/tests/debug_emulated_prepares.phpt b/ext/pdo/tests/debug_emulated_prepares.phpt index fba878eeed8b..7c7056323b96 100644 --- a/ext/pdo/tests/debug_emulated_prepares.phpt +++ b/ext/pdo/tests/debug_emulated_prepares.phpt @@ -9,7 +9,7 @@ if (false == $dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; PDOTest::skip(); -$db = PDOTest::factory(); +$db = PDOTest::factoryForSkip(); if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') die('skip pgsql has its own test for this feature'); if (!@$db->getAttribute(PDO::ATTR_EMULATE_PREPARES) && !@$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true)) die('skip driver cannot emulate prepared statements'); ?> diff --git a/ext/pdo/tests/gh8626.phpt b/ext/pdo/tests/gh8626.phpt index 3515a30d77fb..b39ff30e40f1 100644 --- a/ext/pdo/tests/gh8626.phpt +++ b/ext/pdo/tests/gh8626.phpt @@ -9,7 +9,7 @@ if (false == $dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; PDOTest::skip(); -$db = PDOTest::factory(); +$db = PDOTest::factoryForSkip(); if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'oci') { die("xfail OCI driver errorInfo is inconsistent with other PDO drivers"); } diff --git a/ext/pdo/tests/pdo_017.phpt b/ext/pdo/tests/pdo_017.phpt index b9171ce0cd58..fd4b8674669d 100644 --- a/ext/pdo/tests/pdo_017.phpt +++ b/ext/pdo/tests/pdo_017.phpt @@ -9,7 +9,7 @@ if (false == $dir) die('skip no driver'); require_once $dir . 'pdo_test.inc'; PDOTest::skip(); -$db = PDOTest::factory(); +$db = PDOTest::factoryForSkip(); try { $db->beginTransaction(); $db->rollback(); diff --git a/ext/pdo/tests/pdo_test.inc b/ext/pdo/tests/pdo_test.inc index b44d0b88e77b..7b88184da1ad 100644 --- a/ext/pdo/tests/pdo_test.inc +++ b/ext/pdo/tests/pdo_test.inc @@ -16,26 +16,31 @@ if (getenv('PDOTEST_DSN') === false) { } class PDOTest { + private static function getAttributes(string $environmentVariable): ?array { + $attributes = getenv($environmentVariable); + if (is_string($attributes) && strlen($attributes)) { + return unserialize($attributes); + } + return null; + } + // create an instance of the PDO driver, based on // the current environment - static function factory($classname = PDO::class, bool $useConnectMethod = false) { + static function factory($classname = PDO::class, bool $useConnectMethod = false, ?array $attributes = null) { $dsn = getenv('PDOTEST_DSN'); $user = getenv('PDOTEST_USER'); $pass = getenv('PDOTEST_PASS'); - $attr = getenv('PDOTEST_ATTR'); - if (is_string($attr) && strlen($attr)) { - $attr = unserialize($attr); - } else { - $attr = null; + if ($attributes === null) { + $attributes = self::getAttributes('PDOTEST_ATTR'); } if ($user === false) $user = NULL; if ($pass === false) $pass = NULL; if ($useConnectMethod) { - $db = $classname::connect($dsn, $user, $pass, $attr); + $db = $classname::connect($dsn, $user, $pass, $attributes); } else { - $db = new $classname($dsn, $user, $pass, $attr); + $db = new $classname($dsn, $user, $pass, $attributes); } if (!$db) { @@ -50,14 +55,77 @@ class PDOTest { return $db; } - static function skip() { + private static function getSkipCacheFile(): ?string { + $directory = getenv('TEST_PHP_SHARED_CACHE_DIR'); + if (!is_string($directory) || !is_dir($directory)) { + return null; + } + + $configuration = [ + getenv('PDOTEST_DSN'), + getenv('PDOTEST_USER'), + getenv('PDOTEST_PASS'), + getenv('PDOTEST_ATTR'), + getenv('PDOTEST_SKIP_ATTR'), + ]; + return $directory . DIRECTORY_SEPARATOR . 'pdo-' . hash('sha256', serialize($configuration)); + } + + static function factoryForSkip() { + $attributes = self::getAttributes('PDOTEST_ATTR'); + $skipAttributes = self::getAttributes('PDOTEST_SKIP_ATTR'); + if ($skipAttributes !== null) { + $attributes = $skipAttributes + ($attributes ?? []); + } + return PDOTest::factory(PDO::class, false, $attributes); + } + + private static function connectOrSkip(): void { try { - $db = PDOTest::factory(); + self::factoryForSkip(); } catch (PDOException $e) { die("skip " . $e->getMessage()); } } + static function skip() { + $cacheFile = self::getSkipCacheFile(); + if ($cacheFile === null) { + self::connectOrSkip(); + return; + } + + $cache = @fopen($cacheFile, 'c+'); + if ($cache === false || !flock($cache, LOCK_EX)) { + if (is_resource($cache)) { + fclose($cache); + } + self::connectOrSkip(); + return; + } + + $cached = stream_get_contents($cache); + $reason = $cached !== '' ? $cached : null; + if ($reason === null) { + // Only failures are shared; successful checks still create their own connection. + try { + self::factoryForSkip(); + } catch (PDOException $e) { + $reason = $e->getMessage(); + rewind($cache); + ftruncate($cache, 0); + fwrite($cache, $reason); + fflush($cache); + } + } + + flock($cache, LOCK_UN); + fclose($cache); + if (is_string($reason)) { + die("skip $reason"); + } + } + static function test_factory($file, $classname = PDO::class, bool $useConnectMethod = false) { $config = self::get_config($file); foreach ($config['ENV'] as $k => $v) { diff --git a/ext/pdo/tests/pdo_test_skip_cache.phpt b/ext/pdo/tests/pdo_test_skip_cache.phpt new file mode 100644 index 000000000000..1f44a3b50c2f --- /dev/null +++ b/ext/pdo/tests/pdo_test_skip_cache.phpt @@ -0,0 +1,72 @@ +--TEST-- +PDO test helper caches connection failures for one test run +--EXTENSIONS-- +pdo +--FILE-- + ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $environment, + ['bypass_shell' => true], + ); + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + if (0 !== $exitCode = proc_close($process)) { + throw new Exception("PHP subprocess exited with code $exitCode: $output"); + } + + return $output; +} + +$dsn = 'missing_' . getmypid() . ':'; +$cacheDirectory = __DIR__ . '/pdo_test_skip_cache_' . getmypid(); +mkdir($cacheDirectory); + +$environment = getenv(); +$environment['PDOTEST_DSN'] = $dsn; +$environment['PDOTEST_USER'] = 'test'; +$environment['PDOTEST_PASS'] = 'test'; +$environment['TEST_PHP_SHARED_CACHE_DIR'] = $cacheDirectory; +unset($environment['PDOTEST_ATTR']); + +$helperDirectory = getenv('REDIR_TEST_DIR') ?: __DIR__; +$helper = var_export($helperDirectory . '/pdo_test.inc', true); +$code = "require $helper; PDOTest::skip();"; +try { + $first = run_pdo_skip_check($code, $environment); + + $cacheFiles = glob($cacheDirectory . '/pdo-*'); + if (count($cacheFiles) !== 1) { + throw new Exception('Expected exactly one cache file'); + } + $cacheFile = $cacheFiles[0]; + $cachedReason = file_get_contents($cacheFile); + file_put_contents($cacheFile, 'cached connection failure'); + + $second = run_pdo_skip_check($code, $environment); + echo "$first\n$cachedReason\n$second\n"; +} finally { + foreach (glob($cacheDirectory . '/*') as $file) { + unlink($file); + } + rmdir($cacheDirectory); +} +?> +--EXPECT-- +skip could not find driver +could not find driver +skip cached connection failure diff --git a/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt b/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt index 95b33ddb7f3c..812a715177e7 100644 --- a/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt +++ b/ext/pdo_dblib/tests/GHSA-5hqh-c84r-qjcv.phpt @@ -9,7 +9,7 @@ if (PHP_INT_SIZE != 4) die("skip for 32bit platforms only"); if (PHP_OS_FAMILY === "Windows") die("skip not for Windows because the virtual address space for application is only 2GiB"); if (getenv("SKIP_SLOW_TESTS")) die("skip slow test"); require __DIR__ . '/config.inc'; -getDbConnection(); +skipIfNoDbConnection(); ?> --INI-- memory_limit=-1 diff --git a/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt b/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt index 5b5c35252b0f..df9e0ac12d7a 100644 --- a/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_ins_exec.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt b/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt index 80bc8ab533f0..c94b560dbed0 100644 --- a/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_ins_sel_up_del.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt b/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt index afc87dd05d7c..3eda5b6733d6 100644 --- a/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_ins_up.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt b/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt index 03f8456c7876..126bb5a20746 100644 --- a/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_rowcount.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_transaction.phpt b/ext/pdo_dblib/tests/batch_stmt_transaction.phpt index 2fab8dcda742..a9ed9762ea45 100644 --- a/ext/pdo_dblib/tests/batch_stmt_transaction.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_transaction.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/batch_stmt_try.phpt b/ext/pdo_dblib/tests/batch_stmt_try.phpt index 9e735ff87715..0383e2feb00c 100644 --- a/ext/pdo_dblib/tests/batch_stmt_try.phpt +++ b/ext/pdo_dblib/tests/batch_stmt_try.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- diff --git a/ext/pdo_dblib/tests/bug_38955.phpt b/ext/pdo_dblib/tests/bug_38955.phpt index cd244ba1ec33..256b11f590f4 100644 --- a/ext/pdo_dblib/tests/bug_38955.phpt +++ b/ext/pdo_dblib/tests/bug_38955.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --CONFLICTS-- all diff --git a/ext/pdo_dblib/tests/bug_47588.phpt b/ext/pdo_dblib/tests/bug_47588.phpt index 218088b1e11f..3f58844b6a7f 100644 --- a/ext/pdo_dblib/tests/bug_47588.phpt +++ b/ext/pdo_dblib/tests/bug_47588.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --CONFLICTS-- all diff --git a/ext/pdo_dblib/tests/bug_54648.phpt b/ext/pdo_dblib/tests/bug_54648.phpt index 9128b8c25404..111d79692c84 100644 --- a/ext/pdo_dblib/tests/bug_54648.phpt +++ b/ext/pdo_dblib/tests/bug_54648.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- getAttribute(Pdo\Dblib::ATTR_TDS_VERSION), ['4.2', '4.6', '5.0', '6.0', '7.0'])) die('skip bigint type is unsupported by active TDS version'); ?> --FILE-- diff --git a/ext/pdo_dblib/tests/common.phpt b/ext/pdo_dblib/tests/common.phpt index 293597b623e3..f3a963ef9b63 100644 --- a/ext/pdo_dblib/tests/common.phpt +++ b/ext/pdo_dblib/tests/common.phpt @@ -5,7 +5,7 @@ pdo_dblib --REDIRECTTEST-- # magic auto-configuration -return [ +$config = [ 'ENV' => [ 'PDOTEST_DSN' => getenv('PDO_DBLIB_TEST_DSN') ?: 'dblib:host=localhost;dbname=test', 'PDOTEST_USER' => getenv('PDO_DBLIB_TEST_USER') ?: 'php', @@ -13,3 +13,11 @@ return [ ], 'TESTS' => __DIR__ . '/ext/pdo/tests', ]; + +if (getenv('PDO_DBLIB_TEST_DSN') === false) { + $config['ENV']['PDOTEST_SKIP_ATTR'] = serialize([ + Pdo\Dblib::ATTR_CONNECTION_TIMEOUT => 1, + ]); +} + +return $config; diff --git a/ext/pdo_dblib/tests/config.inc b/ext/pdo_dblib/tests/config.inc index 1612a80a9336..4f9605e1ba5c 100644 --- a/ext/pdo_dblib/tests/config.inc +++ b/ext/pdo_dblib/tests/config.inc @@ -55,6 +55,13 @@ function getDbConnection(string $class = PDO::class, ?array $attributes = null) return $db; } +function skipIfNoDbConnection(): PDO { + $attributes = getenv('PDO_DBLIB_TEST_DSN') === false + ? [Pdo\Dblib::ATTR_CONNECTION_TIMEOUT => 1] + : null; + return getDbConnection(PDO::class, $attributes); +} + function connectToDb() { [$dsn, $user, $pass] = getCredentials(); diff --git a/ext/pdo_dblib/tests/datetime2.phpt b/ext/pdo_dblib/tests/datetime2.phpt index 2b54361a30e5..6564b398513c 100644 --- a/ext/pdo_dblib/tests/datetime2.phpt +++ b/ext/pdo_dblib/tests/datetime2.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- getAttribute(Pdo\Dblib::ATTR_TDS_VERSION), ['4.2', '4.6', '5.0', '6.0', '7.0', '7.1', '7.2'])) die('skip feature unsupported by this TDS version'); ?> --FILE-- diff --git a/ext/pdo_dblib/tests/datetime_convert.phpt b/ext/pdo_dblib/tests/datetime_convert.phpt index 0934dd7f83e7..18244a181957 100644 --- a/ext/pdo_dblib/tests/datetime_convert.phpt +++ b/ext/pdo_dblib/tests/datetime_convert.phpt @@ -5,7 +5,7 @@ pdo_dblib --SKIPIF-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- --FILE-- getAttribute(Pdo\Dblib::ATTR_TDS_VERSION), ['4.2', '4.6'])) die('skip feature unsupported by this TDS version'); ?> --FILE-- diff --git a/ext/pdo_dblib/tests/timeout.phpt b/ext/pdo_dblib/tests/timeout.phpt index 5f935a3d01b8..9bc5eb9bd5c3 100644 --- a/ext/pdo_dblib/tests/timeout.phpt +++ b/ext/pdo_dblib/tests/timeout.phpt @@ -6,7 +6,7 @@ pdo_dblib --FILE-- --FILE-- Date: Tue, 28 Jul 2026 20:29:30 +0700 Subject: [PATCH 07/26] Account for redirected tests in parallel progress --- run-tests.php | 23 ++++- tests/run-test/redirected_parallel.phpt | 107 ++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 tests/run-test/redirected_parallel.phpt diff --git a/run-tests.php b/run-tests.php index ab3618ef0b46..4d6e92cdf8d1 100755 --- a/run-tests.php +++ b/run-tests.php @@ -1761,7 +1761,12 @@ function run_all_tests(array $test_files, array $env, ?string $redir_tested = nu } /* Ignore -jN if there is only one file to analyze. */ - if ($workers !== null && count($test_files) > 1 && !$workerID) { + if ( + $workers !== null + && count($test_files) > 1 + && !$workerID + && $redir_tested === null + ) { run_all_tests_parallel($test_files, $env, $redir_tested); return; } @@ -1816,7 +1821,7 @@ function run_all_tests(array $test_files, array $env, ?string $redir_tested = nu function run_all_tests_parallel(array $test_files, array $env, ?string $redir_tested): void { - global $workers, $test_idx, $test_results, $failed_tests_file, $result_tests_file, $PHP_FAILED_TESTS, $shuffle, $valgrind, $show_progress; + global $workers, $test_cnt, $test_idx, $test_results, $failed_tests_file, $result_tests_file, $PHP_FAILED_TESTS, $shuffle, $valgrind, $show_progress; global $junit; @@ -2000,6 +2005,9 @@ function run_all_tests_parallel(array $test_files, array $env, ?string $redir_te } switch ($message["type"]) { + case "test_count_delta": + $test_cnt += $message["delta"]; + break; case "tests_finished": $testsInProgress--; foreach ($activeConflicts as $key => $workerId) { @@ -2302,7 +2310,7 @@ function run_test(string $php, $file, array $env): string global $preload, $file_cache; global $num_repeats; // Parallel testing - global $workerID; + global $workerID, $workerSock; global $show_progress; // Temporary @@ -2721,7 +2729,14 @@ function run_test(string $php, $file, array $env): string $test_files[] = [$f, $file]; } } - $test_cnt += count($test_files) - 1; + $testCountDelta = count($test_files) - 1; + $test_cnt += $testCountDelta; + if ($workerID && $testCountDelta !== 0) { + send_message($workerSock, [ + "type" => "test_count_delta", + "delta" => $testCountDelta, + ]); + } $test_idx--; show_redirect_start($IN_REDIRECT['TESTS'], $tested, $tested_file); diff --git a/tests/run-test/redirected_parallel.phpt b/tests/run-test/redirected_parallel.phpt new file mode 100644 index 000000000000..692fe0a19e75 --- /dev/null +++ b/tests/run-test/redirected_parallel.phpt @@ -0,0 +1,107 @@ +--TEST-- +Redirected tests work in parallel runs and update the progress total +--ENV-- +TEST_PHP_FORK_SERVER=0 +--FILE-- + + --EXPECT-- + ok + PHPT); +} + +function runRedirectedTests(array $testFiles): array +{ + $command = [ + getenv('TEST_PHP_EXECUTABLE'), + '-n', + dirname(__DIR__, 2) . '/run-tests.php', + '-n', + '-q', + '-j2', + '--progress', + ...$testFiles, + ]; + $environment = [ + 'PATH' => getenv('PATH'), + 'TEST_PHP_EXECUTABLE' => getenv('TEST_PHP_EXECUTABLE'), + 'TEST_PHP_FORK_SERVER' => '0', + ]; + foreach (['SystemRoot', 'TEMP', 'TMPDIR'] as $name) { + if (($value = getenv($name)) !== false) { + $environment[$name] = $value; + } + } + + $process = proc_open( + $command, + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $environment, + ); + fclose($pipes[0]); + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + return [proc_close($process), str_replace("\r", "\n", $output)]; +} + +$root = __DIR__ . '/redirected_parallel_' . getmypid(); +$targets = $root . '/targets'; +mkdir($targets, recursive: true); + +writeRedirectedTest($targets . '/one.phpt', 'redirected one'); +writeRedirectedTest($targets . '/two.phpt', 'redirected two'); + +$targetExpression = var_export($targets, true); +$redirect = $root . '/redirect.phpt'; +file_put_contents($redirect, << [], 'TESTS' => $targetExpression]; + PHPT); +writeRedirectedTest($root . '/companion.phpt', 'companion'); + +[$singleExitCode, $singleOutput] = runRedirectedTests([$redirect]); +if ($singleExitCode !== 0) { + echo $singleOutput; +} +var_dump($singleExitCode); +var_dump(str_contains($singleOutput, 'Fatal error')); + +[$parallelExitCode, $parallelOutput] = runRedirectedTests([ + $redirect, + $root . '/companion.phpt', +]); +if ($parallelExitCode !== 0) { + echo $parallelOutput; +} +var_dump($parallelExitCode); +var_dump(str_contains($parallelOutput, 'TEST 3/3')); +var_dump(str_contains($parallelOutput, 'TEST 3/2')); + +foreach (glob($targets . '/*') as $file) { + unlink($file); +} +rmdir($targets); +unlink($root . '/redirect.phpt'); +unlink($root . '/companion.phpt'); +rmdir($root); +?> +--EXPECT-- +int(0) +bool(false) +int(0) +bool(true) +bool(false) From c0bfc2b0f0209c91a7a852ad1b139a1248f89f5a Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:30 +0700 Subject: [PATCH 08/26] Support bounded directory concurrency --- docs/source/miscellaneous/writing-tests.rst | 30 ++++ run-tests.php | 145 ++++++++++++++++-- tests/run-test/max_concurrency_conflicts.phpt | 102 ++++++++++++ tests/run-test/scoped_conflicts.phpt | 141 +++++++++++++++++ 4 files changed, 405 insertions(+), 13 deletions(-) create mode 100644 tests/run-test/max_concurrency_conflicts.phpt create mode 100644 tests/run-test/scoped_conflicts.phpt diff --git a/docs/source/miscellaneous/writing-tests.rst b/docs/source/miscellaneous/writing-tests.rst index 433f87b941b8..efa4f4ac6505 100644 --- a/docs/source/miscellaneous/writing-tests.rst +++ b/docs/source/miscellaneous/writing-tests.rst @@ -688,6 +688,9 @@ An alternative to have a ``--CONFLICTS--`` section is to add a file named ``CONF directory containing the tests. The contents of the ``CONFLICTS`` file must have the same format as the contents of the ``--CONFLICTS--`` section. +Unlike a ``SCOPED_CONFLICTS`` file, a ``CONFLICTS`` file also prevents tests in its own directory +from running concurrently, regardless of whether the directory has a ``MAX_CONCURRENCY`` file. + **Required:** No. **Format:** One conflict key per line. Comment lines starting with # are also allowed. @@ -701,6 +704,33 @@ Example 1 (snippet): Example 1 (full): :ref:`conflicts_1.phpt` +``SCOPED_CONFLICTS`` file +------------------------- + +**Description:** This file is only relevant for parallel test execution. It specifies directory +conflict keys that tests in the same directory may share. Tests outside the directory that use the +same key remain mutually exclusive with the directory. Use this when a directory may run internally +in parallel but must not overlap another user of the same shared resource. + +This does not limit concurrency within the directory. Add a ``MAX_CONCURRENCY`` file separately if a +numerical cap is also required. + +**Required:** No. + +**Format:** One conflict key per line. Comment lines starting with # are also allowed. + +``MAX_CONCURRENCY`` file +------------------------ + +**Description:** This file is only relevant for parallel test execution. It limits how many workers +may simultaneously execute tests from its directory. It does not change the behavior of +``CONFLICTS`` or ``SCOPED_CONFLICTS``. This allows a bounded amount of parallelism for tests that +are safe to overlap but would otherwise contend excessively for a shared resource. + +**Required:** No. + +**Format:** A positive integer. Comment lines starting with # are also allowed. + ``--WHITESPACE_SENSITIVE--`` ---------------------------- diff --git a/run-tests.php b/run-tests.php index 4d6e92cdf8d1..fa3bb16588c6 100755 --- a/run-tests.php +++ b/run-tests.php @@ -1835,17 +1835,38 @@ function run_all_tests_parallel(array $test_files, array $env, ?string $redir_te // Each test may specify a list of conflict keys. While a test that conflicts with // key K is running, no other test that conflicts with K may run. Conflict keys are - // specified either in the --CONFLICTS-- section, or CONFLICTS file inside a directory. + // specified either in the --CONFLICTS-- section, or a CONFLICTS or SCOPED_CONFLICTS + // file inside a directory. $dirConflictsWith = []; + $dirScopedConflictsWith = []; $fileConflictsWith = []; + $fileConflictGroups = []; + // A MAX_CONCURRENCY file limits how many workers may run tests from that directory. + $dirMaxConcurrency = []; + $fileConcurrencyDirectory = []; $sequentialTests = []; foreach ($test_files as $i => $file) { + $dir = dirname($file); + if (!array_key_exists($dir, $dirMaxConcurrency)) { + $maxConcurrencyFile = $dir . '/MAX_CONCURRENCY'; + $dirMaxConcurrency[$dir] = null; + if (file_exists($maxConcurrencyFile)) { + $dirMaxConcurrency[$dir] = parse_max_concurrency( + file_get_contents($maxConcurrencyFile), + $maxConcurrencyFile, + ); + } + } + if ($dirMaxConcurrency[$dir] !== null) { + $fileConcurrencyDirectory[$file] = $dir; + } + $contents = file_get_contents($file); + $conflictGroups = []; if (preg_match('/^--CONFLICTS--(.+?)^--/ms', $contents, $matches)) { $conflicts = parse_conflicts($matches[1]); } else { // Cache per-directory conflicts in a separate map, so we compute these only once. - $dir = dirname($file); if (!isset($dirConflictsWith[$dir])) { $dirConflicts = []; if (file_exists($dir . '/CONFLICTS')) { @@ -1855,6 +1876,22 @@ function run_all_tests_parallel(array $test_files, array $env, ?string $redir_te $dirConflictsWith[$dir] = $dirConflicts; } $conflicts = $dirConflictsWith[$dir]; + + if (!isset($dirScopedConflictsWith[$dir])) { + $dirScopedConflicts = []; + $scopedConflictsFile = $dir . '/SCOPED_CONFLICTS'; + if (file_exists($scopedConflictsFile)) { + $dirScopedConflicts = parse_conflicts(file_get_contents($scopedConflictsFile)); + } + $dirScopedConflictsWith[$dir] = $dirScopedConflicts; + } + foreach ($dirScopedConflictsWith[$dir] as $conflictKey) { + if (in_array($conflictKey, $conflicts, true)) { + error("Conflict key '$conflictKey' is listed in both CONFLICTS and SCOPED_CONFLICTS in $dir"); + } + $conflicts[] = $conflictKey; + $conflictGroups[$conflictKey] = $dir; + } } // For tests conflicting with "all", no other tests may run in parallel. We'll run these @@ -1865,6 +1902,7 @@ function run_all_tests_parallel(array $test_files, array $env, ?string $redir_te } $fileConflictsWith[$file] = $conflicts; + $fileConflictGroups[$file] = $conflictGroups; } // Some tests assume that they are executed in a certain order. We will be popping from @@ -1965,10 +2003,16 @@ function run_all_tests_parallel(array $test_files, array $env, ?string $redir_te $rawMessageBuffers = []; $testsInProgress = 0; - // Map from conflict key to worker ID. + // Maps conflict keys to worker IDs and their optional internally compatible group. $activeConflicts = []; // Tests waiting due to conflicts. Map from conflict key to array. $waitingTests = []; + // Maps capped directories to the workers currently running tests from them. + $activeConcurrencyDirectories = []; + // Maps workers to the capped directories acquired for their current batch. + $workerConcurrencyDirectories = []; + // Tests waiting for capacity in a capped directory. + $waitingConcurrencyTests = []; escape: while ($test_files || $sequentialTests || $testsInProgress > 0) { @@ -2010,15 +2054,28 @@ function run_all_tests_parallel(array $test_files, array $env, ?string $redir_te break; case "tests_finished": $testsInProgress--; - foreach ($activeConflicts as $key => $workerId) { - if ($workerId === $i) { - unset($activeConflicts[$key]); - if (isset($waitingTests[$key])) { - while ($test = array_pop($waitingTests[$key])) { - $test_files[] = $test; - } - unset($waitingTests[$key]); + foreach ($workerConcurrencyDirectories[$i] ?? [] as $dir) { + unset($activeConcurrencyDirectories[$dir][$i]); + if (isset($waitingConcurrencyTests[$dir])) { + while ($test = array_pop($waitingConcurrencyTests[$dir])) { + $test_files[] = $test; } + unset($waitingConcurrencyTests[$dir]); + } + } + unset($workerConcurrencyDirectories[$i]); + foreach ($activeConflicts as $key => $activeGroups) { + unset($activeGroups[$i]); + if ($activeGroups) { + $activeConflicts[$key] = $activeGroups; + continue; + } + unset($activeConflicts[$key]); + if (isset($waitingTests[$key])) { + while ($test = array_pop($waitingTests[$key])) { + $test_files[] = $test; + } + unset($waitingTests[$key]); } } $junit->mergeResults($message["junit"]); @@ -2035,24 +2092,57 @@ function run_all_tests_parallel(array $test_files, array $env, ?string $redir_te // - If this is running a small enough number of tests, // reduce the batch size to give batches to more workers. $files = []; + $batchConcurrencyDirectories = []; $maxBatchSize = $valgrind ? 1 : 4; $averageFilesPerWorker = max(1, (int) ceil($totalFileCount / count($workerProcs))); $batchSize = min($maxBatchSize, $averageFilesPerWorker); while (count($files) < $batchSize && $file = array_pop($test_files)) { foreach ($fileConflictsWith[$file] as $conflictKey) { - if (isset($activeConflicts[$conflictKey])) { + $conflictGroup = $fileConflictGroups[$file][$conflictKey] ?? null; + if ( + isset($activeConflicts[$conflictKey]) + && has_active_conflict( + $activeConflicts[$conflictKey], + $conflictGroup, + ) + ) { $waitingTests[$conflictKey][] = $file; continue 2; } } + $concurrencyDirectory = $fileConcurrencyDirectory[$file] ?? null; + if ( + $concurrencyDirectory !== null + && !isset($batchConcurrencyDirectories[$concurrencyDirectory]) + && count($activeConcurrencyDirectories[$concurrencyDirectory] ?? []) + >= $dirMaxConcurrency[$concurrencyDirectory] + ) { + $waitingConcurrencyTests[$concurrencyDirectory][] = $file; + continue; + } $files[] = $file; + if ($concurrencyDirectory !== null) { + $batchConcurrencyDirectories[$concurrencyDirectory] = true; + } } if ($files) { foreach ($files as $file) { foreach ($fileConflictsWith[$file] as $conflictKey) { - $activeConflicts[$conflictKey] = $i; + $conflictGroup = $fileConflictGroups[$file][$conflictKey] ?? null; + if ( + array_key_exists($i, $activeConflicts[$conflictKey] ?? []) + && $activeConflicts[$conflictKey][$i] !== $conflictGroup + ) { + $activeConflicts[$conflictKey][$i] = null; + } else { + $activeConflicts[$conflictKey][$i] = $conflictGroup; + } } } + $workerConcurrencyDirectories[$i] = array_keys($batchConcurrencyDirectories); + foreach ($workerConcurrencyDirectories[$i] as $dir) { + $activeConcurrencyDirectories[$dir][$i] = true; + } $testsInProgress++; send_message($workerSocks[$i], [ "type" => "run_tests", @@ -3785,6 +3875,35 @@ function parse_conflicts(string $text): array return array_map('trim', explode("\n", trim($text))); } +/** + * @param array $activeGroups + */ +function has_active_conflict(array $activeGroups, ?string $candidateGroup): bool +{ + if ($candidateGroup === null) { + return true; + } + foreach ($activeGroups as $activeGroup) { + if ($activeGroup !== $candidateGroup) { + return true; + } + } + return false; +} + +function parse_max_concurrency(string $text, string $file): int +{ + // Strip comments + $text = trim(preg_replace('/#.*/', '', $text)); + $maxConcurrency = filter_var($text, FILTER_VALIDATE_INT, [ + 'options' => ['min_range' => 1], + ]); + if ($maxConcurrency === false) { + error("Invalid positive integer in $file"); + } + return $maxConcurrency; +} + function show_result( string $result, string $tested, diff --git a/tests/run-test/max_concurrency_conflicts.phpt b/tests/run-test/max_concurrency_conflicts.phpt new file mode 100644 index 000000000000..6dc86483e69d --- /dev/null +++ b/tests/run-test/max_concurrency_conflicts.phpt @@ -0,0 +1,102 @@ +--TEST-- +MAX_CONCURRENCY does not weaken directory conflicts +--ENV-- +TEST_PHP_FORK_SERVER=0 +--FILE-- + + --EXPECT-- + ok + PHPT); +} + +$root = __DIR__ . '/max_concurrency_conflicts_' . getmypid(); +$results = $root . '/results.txt'; +mkdir($root); +file_put_contents($root . '/CONFLICTS', "shared\n"); +file_put_contents($root . '/MAX_CONCURRENCY', "2\n"); + +$rootExpression = var_export($root, true); +$testCode = <<<'PHP' + $root = %s; + $marker = $root . '/active-' . getmypid(); + file_put_contents($marker, ''); + $deadline = microtime(true) + 0.5; + do { + $overlap = count(glob($root . '/active-*')) > 1; + if ($overlap) { + break; + } + usleep(10_000); + } while (microtime(true) < $deadline); + unlink($marker); + echo $overlap ? "overlap\n" : "ok\n"; + PHP; +$testCode = sprintf($testCode, $rootExpression); +writeParallelTest($root . '/one.phpt', 'conflict one', $testCode); +writeParallelTest($root . '/two.phpt', 'conflict two', $testCode); + +$command = [ + getenv('TEST_PHP_EXECUTABLE'), + dirname(__DIR__, 2) . '/run-tests.php', + '-q', + '-j2', + '-W', + $results, + $root . '/one.phpt', + $root . '/two.phpt', +]; +$environment = [ + 'PATH' => getenv('PATH'), + 'TEST_PHP_EXECUTABLE' => getenv('TEST_PHP_EXECUTABLE'), + 'TEST_PHP_FORK_SERVER' => '0', +]; +foreach (['SystemRoot', 'TEMP', 'TMPDIR'] as $name) { + if (($value = getenv($name)) !== false) { + $environment[$name] = $value; + } +} +$process = proc_open( + $command, + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $environment, +); +fclose($pipes[0]); +$output = stream_get_contents($pipes[1]); +fclose($pipes[1]); +$exitCode = proc_close($process); + +$resultLines = file($results, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); +sort($resultLines); +foreach ($resultLines as $resultLine) { + [$status, $file] = explode("\t", $resultLine, 2); + echo "$status ", basename($file), "\n"; +} +var_dump($exitCode); +if ($exitCode !== 0) { + echo $output; +} + +foreach (glob($root . '/*') as $file) { + unlink($file); +} +rmdir($root); +?> +--EXPECT-- +PASSED one.phpt +PASSED two.phpt +int(0) diff --git a/tests/run-test/scoped_conflicts.phpt b/tests/run-test/scoped_conflicts.phpt new file mode 100644 index 000000000000..bb4bcb7c2f18 --- /dev/null +++ b/tests/run-test/scoped_conflicts.phpt @@ -0,0 +1,141 @@ +--TEST-- +Scoped directory conflicts allow internal concurrency +--ENV-- +TEST_PHP_FORK_SERVER=0 +--FILE-- + + --EXPECT-- + ok + PHPT); +} + +$root = __DIR__ . '/scoped_conflicts_' . getmypid(); +$group = $root . '/a-group'; +$external = $root . '/z-external'; +$results = $root . '/results.txt'; +mkdir($group, recursive: true); +mkdir($external); +file_put_contents($group . '/SCOPED_CONFLICTS', "shared\n"); +file_put_contents($group . '/MAX_CONCURRENCY', "2\n"); + +$rootExpression = var_export($root, true); +$groupCode = <<<'PHP' + $root = %s; + $marker = $root . '/active-group-' . getmypid(); + file_put_contents($marker, ''); + $deadline = microtime(true) + 10; + do { + $groupIsParallel = count(glob($root . '/active-group-*')) === 2; + if ($groupIsParallel) { + break; + } + usleep(10_000); + } while (microtime(true) < $deadline); + if ($groupIsParallel) { + // Give the peer time to observe both markers before either is removed. + usleep(100_000); + } + $externalOverlaps = file_exists($root . '/active-external'); + unlink($marker); + echo $groupIsParallel && !$externalOverlaps ? "ok\n" : "not isolated\n"; + PHP; +$groupCode = sprintf($groupCode, $rootExpression); +writeParallelTest($group . '/one.phpt', 'scoped conflict one', $groupCode); +writeParallelTest($group . '/two.phpt', 'scoped conflict two', $groupCode); + +$externalCode = << + --EXPECT-- + ok + PHPT); + +$command = [ + getenv('TEST_PHP_EXECUTABLE'), + dirname(__DIR__, 2) . '/run-tests.php', + '-q', + '-j3', + '-W', + $results, + $group . '/one.phpt', + $group . '/two.phpt', + $external . '/external.phpt', +]; +$environment = [ + 'PATH' => getenv('PATH'), + 'TEST_PHP_EXECUTABLE' => getenv('TEST_PHP_EXECUTABLE'), + 'TEST_PHP_FORK_SERVER' => '0', +]; +foreach (['SystemRoot', 'TEMP', 'TMPDIR'] as $name) { + if (($value = getenv($name)) !== false) { + $environment[$name] = $value; + } +} +$process = proc_open( + $command, + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $environment, +); +fclose($pipes[0]); +$output = stream_get_contents($pipes[1]); +fclose($pipes[1]); +$exitCode = proc_close($process); + +$resultLines = file($results, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); +sort($resultLines); +foreach ($resultLines as $resultLine) { + [$status, $file] = explode("\t", $resultLine, 2); + echo "$status ", basename($file), "\n"; +} +var_dump($exitCode); +if ($exitCode !== 0) { + echo $output; +} + +foreach (glob($root . '/active-*') as $marker) { + unlink($marker); +} +unlink($results); +foreach ([$group, $external] as $directory) { + foreach (glob($directory . '/*') as $file) { + unlink($file); + } +} +rmdir($group); +rmdir($external); +rmdir($root); +?> +--EXPECT-- +PASSED one.phpt +PASSED two.phpt +PASSED external.phpt +int(0) From 765c4d1075a2ead36b8a067b4982df1057955605 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:54 +0700 Subject: [PATCH 09/26] Cache failed SNMP agent probes --- ext/snmp/tests/skipif.inc | 51 +++++++++++++++++++- ext/snmp/tests/snmp_skip_cache.phpt | 73 +++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 ext/snmp/tests/snmp_skip_cache.phpt diff --git a/ext/snmp/tests/skipif.inc b/ext/snmp/tests/skipif.inc index 0ae4ee16e5b8..4beb3b28080d 100644 --- a/ext/snmp/tests/skipif.inc +++ b/ext/snmp/tests/skipif.inc @@ -1,10 +1,57 @@ + @snmpget($hostname, $community, '.1.3.6.1.2.1.1.1.0', $timeout) === false + ? 'NO SNMPD on this host or community invalid' + : null; + + $directory = getenv('TEST_PHP_SHARED_CACHE_DIR'); + if (!is_string($directory) || !is_dir($directory)) { + return $probe(); + } + + $configuration = [$hostname, $community, $timeout]; + $cacheFile = $directory + . DIRECTORY_SEPARATOR + . 'snmp-' + . hash('sha256', serialize($configuration)); + $cache = @fopen($cacheFile, 'c+'); + if ($cache === false || !flock($cache, LOCK_EX)) { + if (is_resource($cache)) { + fclose($cache); + } + return $probe(); + } + + $cached = stream_get_contents($cache); + $reason = $cached !== '' ? $cached : null; + if ($reason === null) { + // Only failures are shared; successful checks still probe the configured agent. + $reason = $probe(); + if (is_string($reason)) { + rewind($cache); + ftruncate($cache, 0); + fwrite($cache, $reason); + fflush($cache); + } + } + + flock($cache, LOCK_UN); + fclose($cache); + return $reason; +} + //test server is available // this require snmpget to work ... //snmpget ( string $hostname , string $community , //string $object_id [, int $timeout [, int $retries ]] ) -if (@snmpget($hostname, $community, '.1.3.6.1.2.1.1.1.0', $timeout) === false) - die('skip NO SNMPD on this host or community invalid'); +$reason = get_snmp_test_agent_unavailable_reason(); +if (is_string($reason)) { + die("skip $reason"); +} diff --git a/ext/snmp/tests/snmp_skip_cache.phpt b/ext/snmp/tests/snmp_skip_cache.phpt new file mode 100644 index 000000000000..3534d78ca1bf --- /dev/null +++ b/ext/snmp/tests/snmp_skip_cache.phpt @@ -0,0 +1,73 @@ +--TEST-- +SNMP test helper caches agent availability for one test run +--EXTENSIONS-- +snmp +--FILE-- + ['pipe', 'w'], + 2 => ['redirect', 1], + ], + $pipes, + null, + $environment, + ['bypass_shell' => true], + ); + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + if (0 !== $exitCode = proc_close($process)) { + throw new Exception("PHP subprocess exited with code $exitCode: $output"); + } + + return $output; +} + +$cacheDirectory = __DIR__ . '/snmp_skip_cache_' . getmypid(); +mkdir($cacheDirectory); + +$environment = getenv(); +$environment['SNMP_HOSTNAME'] = '127.0.0.1'; +$environment['SNMP_PORT'] = '65000'; +$environment['SNMP_COMMUNITY'] = 'php_test_cache'; +$environment['SNMP_TIMEOUT'] = '1'; +$environment['SNMP_RETRIES'] = '0'; +$environment['TEST_PHP_SHARED_CACHE_DIR'] = $cacheDirectory; + +$helper = var_export(__DIR__ . '/skipif.inc', true); +$code = "require $helper; echo \"available\\n\";"; +try { + $first = run_snmp_skip_check($code, $environment); + + $cacheFiles = glob($cacheDirectory . '/snmp-*'); + if (count($cacheFiles) !== 1) { + throw new Exception('Expected exactly one cache file'); + } + $cacheFile = $cacheFiles[0]; + $cachedReason = file_get_contents($cacheFile); + file_put_contents($cacheFile, 'cached agent failure'); + + $second = run_snmp_skip_check($code, $environment); + echo "$first\n"; + echo "$cachedReason\n"; + echo $second; +} finally { + foreach (glob($cacheDirectory . '/*') as $file) { + unlink($file); + } + rmdir($cacheDirectory); +} +?> +--EXPECT-- +skip NO SNMPD on this host or community invalid +NO SNMPD on this host or community invalid +skip cached agent failure From 295aa2daa5120960f70aed71f602e66d2f7d1f72 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:54 +0700 Subject: [PATCH 10/26] Avoid external DNS in addrinfo test --- ext/sockets/tests/gh20532.phpt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ext/sockets/tests/gh20532.phpt b/ext/sockets/tests/gh20532.phpt index 360f9e7e11a8..1252d6145ad9 100644 --- a/ext/sockets/tests/gh20532.phpt +++ b/ext/sockets/tests/gh20532.phpt @@ -7,10 +7,9 @@ sockets $error_code = 0; var_dump(socket_addrinfo_lookup(".whynot", null, [], $error_code) === false && in_array($error_code, [EAI_NONAME, EAI_FAIL], true)); var_dump(socket_addrinfo_lookup("2001:db8::1", null, ['ai_family' => AF_INET], $error_code) === false && in_array($error_code, [EAI_FAMILY, EAI_ADDRFAMILY, EAI_NONAME, EAI_NODATA])); -var_dump(socket_addrinfo_lookup("example.com", "http", ['ai_socktype' => SOCK_RAW, 'ai_flags' => 2147483647], $error_code) === false && in_array($error_code, [EAI_SOCKTYPE, EAI_SERVICE, EAI_BADFLAGS, EAI_NONAME])); +var_dump(socket_addrinfo_lookup("localhost", "http", ['ai_socktype' => SOCK_RAW, 'ai_flags' => 2147483647], $error_code) === false && in_array($error_code, [EAI_SOCKTYPE, EAI_SERVICE, EAI_BADFLAGS, EAI_NONAME])); ?> --EXPECT-- bool(true) bool(true) bool(true) - From 2f9f6c3759c1791ce81a675e45e8a798188830fa Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:54 +0700 Subject: [PATCH 11/26] Run scandir overflow test on Windows only --- ext/standard/tests/file/bug36365.phpt | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/standard/tests/file/bug36365.phpt b/ext/standard/tests/file/bug36365.phpt index be1a6c516555..c3aeb8045d6e 100644 --- a/ext/standard/tests/file/bug36365.phpt +++ b/ext/standard/tests/file/bug36365.phpt @@ -2,6 +2,7 @@ Bug #36365 (scandir duplicates file name at every 65535th file) --SKIPIF-- --FILE-- From 14a62b10d18884627ff92b5ed3142f9f04ae555f Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:54 +0700 Subject: [PATCH 12/26] Remove redundant filesystem test delays --- ext/standard/tests/file/001.phpt | 2 +- ext/standard/tests/file/lstat_stat_basic.phpt | 2 -- ext/standard/tests/file/lstat_stat_variation10.phpt | 2 +- ext/standard/tests/file/lstat_stat_variation11.phpt | 2 +- ext/standard/tests/file/lstat_stat_variation13.phpt | 2 +- ext/standard/tests/file/lstat_stat_variation15.phpt | 1 - ext/standard/tests/file/lstat_stat_variation4.phpt | 2 +- 7 files changed, 5 insertions(+), 8 deletions(-) diff --git a/ext/standard/tests/file/001.phpt b/ext/standard/tests/file/001.phpt index 8fe6946d72c2..01a07e19182c 100644 --- a/ext/standard/tests/file/001.phpt +++ b/ext/standard/tests/file/001.phpt @@ -24,7 +24,7 @@ if (file_exists('test.file')) { } else { echo "test.file does not exist\n"; } -sleep (2); +touch('test.file', 946684800); symlink('test.file','test.link'); if (file_exists('test.link')) { echo "test.link exists\n"; diff --git a/ext/standard/tests/file/lstat_stat_basic.phpt b/ext/standard/tests/file/lstat_stat_basic.phpt index 3d5e56b412ff..4cdd3d3051c7 100644 --- a/ext/standard/tests/file/lstat_stat_basic.phpt +++ b/ext/standard/tests/file/lstat_stat_basic.phpt @@ -31,7 +31,6 @@ $file_handle = fopen($filename, "w"); fclose($file_handle); // stat of the file created $file_stat = stat($filename); -sleep(2); // now new stat of the dir after file is created $new_dir_stat = stat($dirname); @@ -42,7 +41,6 @@ $sym_linkname = "$file_path/lstat_stat_basic_link.tmp"; symlink($filename, $sym_linkname); // stat of the link created $link_stat = lstat($sym_linkname); -sleep(2); // new stat of the file, after a softlink to this file is created $new_file_stat = stat($filename); clearstatcache(); diff --git a/ext/standard/tests/file/lstat_stat_variation10.phpt b/ext/standard/tests/file/lstat_stat_variation10.phpt index d4d142405da6..4a3a65e834d3 100644 --- a/ext/standard/tests/file/lstat_stat_variation10.phpt +++ b/ext/standard/tests/file/lstat_stat_variation10.phpt @@ -15,13 +15,13 @@ require "$file_path/file.inc"; /* create temp file, link and directory */ $dirname = "$file_path/lstat_stat_variation10"; mkdir($dirname); // temp dir +touch($dirname, 946684800); // is_dir() on a directory echo "*** Testing stat() on directory after using is_dir() on it ***\n"; $old_stat = stat($dirname); // clear the cache clearstatcache(); -sleep(1); var_dump( is_dir($dirname) ); $new_stat = stat($dirname); diff --git a/ext/standard/tests/file/lstat_stat_variation11.phpt b/ext/standard/tests/file/lstat_stat_variation11.phpt index 91510209a6f5..cde2a226fd8d 100644 --- a/ext/standard/tests/file/lstat_stat_variation11.phpt +++ b/ext/standard/tests/file/lstat_stat_variation11.phpt @@ -15,13 +15,13 @@ require "$file_path/file.inc"; $filename = "$file_path/lstat_stat_variation11.tmp"; $fp = fopen($filename, "w"); // temp file fclose($fp); +touch($filename, 946684800); // is_file() on a file echo "*** Testing stat() on a file after using is_file() on it ***\n"; $old_stat = stat($filename); // clear the stat clearstatcache(); -sleep(1); var_dump( is_file($filename) ); $new_stat = stat($filename); // compare self stats diff --git a/ext/standard/tests/file/lstat_stat_variation13.phpt b/ext/standard/tests/file/lstat_stat_variation13.phpt index 45f7a17c570f..a69a9441ed54 100644 --- a/ext/standard/tests/file/lstat_stat_variation13.phpt +++ b/ext/standard/tests/file/lstat_stat_variation13.phpt @@ -17,10 +17,10 @@ $filename = "$file_path/lstat_stat_variation13.tmp"; echo "*** Checking stat() on a file opened using read/write mode ***\n"; $file_handle = fopen($filename, "w"); // create file fclose($file_handle); +touch($filename, 946684800); $old_stat = stat($filename); // clear the stat clearstatcache(); -sleep(1); // opening file again in read mode $file_handle = fopen($filename, "r"); // read file fclose($file_handle); diff --git a/ext/standard/tests/file/lstat_stat_variation15.phpt b/ext/standard/tests/file/lstat_stat_variation15.phpt index 2d1157d401b5..99c567767d99 100644 --- a/ext/standard/tests/file/lstat_stat_variation15.phpt +++ b/ext/standard/tests/file/lstat_stat_variation15.phpt @@ -31,7 +31,6 @@ $old_stat = lstat($linkname); var_dump( chmod($linkname, 0777) ); // clear the stat clearstatcache(); -sleep(2); $new_stat = lstat($linkname); // compare self stats var_dump( compare_self_stat($old_stat) ); diff --git a/ext/standard/tests/file/lstat_stat_variation4.phpt b/ext/standard/tests/file/lstat_stat_variation4.phpt index 27ea0bf24801..8813cc61590a 100644 --- a/ext/standard/tests/file/lstat_stat_variation4.phpt +++ b/ext/standard/tests/file/lstat_stat_variation4.phpt @@ -17,13 +17,13 @@ require "$file_path/file.inc"; $file_name = "$file_path/lstat_stat_variation4.tmp"; $fp = fopen($file_name, "w"); // temp file fclose($fp); +touch($file_name, 946684800); // touch a file check stat, there should be difference in atime echo "*** Testing stat() for file after using touch() on the file ***\n"; $old_stat = stat($file_name); // clear the cache clearstatcache(); -sleep(1); var_dump( touch($file_name) ); $new_stat = stat($file_name); From bafe4d875f678b27b733a6f84aa692813b2be4ca Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:54 +0700 Subject: [PATCH 13/26] Reduce password test work factors --- ext/sodium/tests/php_password_hash_argon2i.phpt | 15 ++++++++------- .../tests/php_password_hash_argon2id.phpt | 13 +++++++------ ext/sodium/tests/php_password_verify.phpt | 17 +++++++++-------- ext/standard/tests/password/password_hash.phpt | 4 ++-- .../tests/password/password_hash_argon2.phpt | 3 ++- .../password/password_removed_salt_option.phpt | 4 ++-- ext/standard/tests/strings/crypt_chars.phpt | 8 ++++---- 7 files changed, 34 insertions(+), 30 deletions(-) diff --git a/ext/sodium/tests/php_password_hash_argon2i.phpt b/ext/sodium/tests/php_password_hash_argon2i.phpt index 23f6b730dbb3..ef1b7cb0535d 100644 --- a/ext/sodium/tests/php_password_hash_argon2i.phpt +++ b/ext/sodium/tests/php_password_hash_argon2i.phpt @@ -17,11 +17,12 @@ if (!in_array('argon2i', password_algos(), true /* strict */)) { echo 'Argon2 provider: '; var_dump(PASSWORD_ARGON2_PROVIDER); +// Interoperability does not depend on using production-strength costs. foreach([1, 2] as $mem) { - foreach([1, 2] as $time) { + foreach([3, 4] as $time) { $opts = [ - 'memory_cost' => PASSWORD_ARGON2_DEFAULT_MEMORY_COST * $mem, - 'time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST * $time, + 'memory_cost' => 8 * 1024 * $mem, + 'time_cost' => $time, 'threads' => PASSWORD_ARGON2_DEFAULT_THREADS, ]; $password = random_bytes(32); @@ -40,18 +41,18 @@ foreach([1, 2] as $mem) { --EXPECTF-- Argon2 provider: string(%d) "%s" Using password: string(44) "%s" -Hash: string(96) "$argon2i$v=19$m=65536,t=4,p=1$%s$%s" +Hash: string(95) "$argon2i$v=19$m=8192,t=3,p=1$%s$%s" bool(true) bool(false) Using password: string(44) "%s" -Hash: string(96) "$argon2i$v=19$m=65536,t=8,p=1$%s$%s" +Hash: string(95) "$argon2i$v=19$m=8192,t=4,p=1$%s$%s" bool(true) bool(false) Using password: string(44) "%s" -Hash: string(97) "$argon2i$v=19$m=131072,t=4,p=1$%s$%s" +Hash: string(96) "$argon2i$v=19$m=16384,t=3,p=1$%s$%s" bool(true) bool(false) Using password: string(44) "%s" -Hash: string(97) "$argon2i$v=19$m=131072,t=8,p=1$%s$%s" +Hash: string(96) "$argon2i$v=19$m=16384,t=4,p=1$%s$%s" bool(true) bool(false) diff --git a/ext/sodium/tests/php_password_hash_argon2id.phpt b/ext/sodium/tests/php_password_hash_argon2id.phpt index 94197f319bd6..d0bcbee629be 100644 --- a/ext/sodium/tests/php_password_hash_argon2id.phpt +++ b/ext/sodium/tests/php_password_hash_argon2id.phpt @@ -17,11 +17,12 @@ if (!in_array('argon2id', password_algos(), true /* strict */)) { echo 'Argon2 provider: '; var_dump(PASSWORD_ARGON2_PROVIDER); +// Interoperability does not depend on using production-strength costs. foreach([1, 2] as $mem) { foreach([1, 2] as $time) { $opts = [ - 'memory_cost' => PASSWORD_ARGON2_DEFAULT_MEMORY_COST * $mem, - 'time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST * $time, + 'memory_cost' => 8 * 1024 * $mem, + 'time_cost' => $time, 'threads' => PASSWORD_ARGON2_DEFAULT_THREADS, ]; $password = random_bytes(32); @@ -40,18 +41,18 @@ foreach([1, 2] as $mem) { --EXPECTF-- Argon2 provider: string(%d) "%s" Using password: string(44) "%s" -Hash: string(97) "$argon2id$v=19$m=65536,t=4,p=1$%s$%s" +Hash: string(96) "$argon2id$v=19$m=8192,t=1,p=1$%s$%s" bool(true) bool(false) Using password: string(44) "%s" -Hash: string(97) "$argon2id$v=19$m=65536,t=8,p=1$%s$%s" +Hash: string(96) "$argon2id$v=19$m=8192,t=2,p=1$%s$%s" bool(true) bool(false) Using password: string(44) "%s" -Hash: string(98) "$argon2id$v=19$m=131072,t=4,p=1$%s$%s" +Hash: string(97) "$argon2id$v=19$m=16384,t=1,p=1$%s$%s" bool(true) bool(false) Using password: string(44) "%s" -Hash: string(98) "$argon2id$v=19$m=131072,t=8,p=1$%s$%s" +Hash: string(97) "$argon2id$v=19$m=16384,t=2,p=1$%s$%s" bool(true) bool(false) diff --git a/ext/sodium/tests/php_password_verify.phpt b/ext/sodium/tests/php_password_verify.phpt index f65c34b1278d..3aa603afa4e7 100644 --- a/ext/sodium/tests/php_password_verify.phpt +++ b/ext/sodium/tests/php_password_verify.phpt @@ -19,13 +19,14 @@ if (!in_array($algo, password_algos(), true /* strict */)) { --FILE-- 4]); + var_dump($hash === crypt("foo", $hash)); } echo "OK!"; diff --git a/ext/standard/tests/password/password_hash_argon2.phpt b/ext/standard/tests/password/password_hash_argon2.phpt index b179ee202318..b5c778cf1cc1 100644 --- a/ext/standard/tests/password/password_hash_argon2.phpt +++ b/ext/standard/tests/password/password_hash_argon2.phpt @@ -9,6 +9,7 @@ if (!defined('PASSWORD_ARGON2ID')) die('skip password_hash not built with Argon2 8, 'time_cost' => 3]; $algos = [ PASSWORD_ARGON2I, @@ -19,7 +20,7 @@ $algos = [ 3, ]; foreach ($algos as $algo) { - $hash = password_hash($password, $algo); + $hash = password_hash($password, $algo, $options); var_dump(password_verify($password, $hash)); var_dump(password_get_info($hash)['algo']); } diff --git a/ext/standard/tests/password/password_removed_salt_option.phpt b/ext/standard/tests/password/password_removed_salt_option.phpt index f802e162e3ea..25cee9f61b4b 100644 --- a/ext/standard/tests/password/password_removed_salt_option.phpt +++ b/ext/standard/tests/password/password_removed_salt_option.phpt @@ -7,9 +7,9 @@ Test removed support for explicit salt option //-=-=-=- -var_dump(strlen(password_hash("rasmuslerdorf", PASSWORD_BCRYPT, array("cost" => 7, "salt" => "usesomesillystringforsalt")))); +var_dump(strlen(password_hash("rasmuslerdorf", PASSWORD_BCRYPT, array("cost" => 4, "salt" => "usesomesillystringforsalt")))); -var_dump(strlen(password_hash("test", PASSWORD_BCRYPT, array("salt" => "123456789012345678901" . chr(0))))); +var_dump(strlen(password_hash("test", PASSWORD_BCRYPT, array("cost" => 4, "salt" => "123456789012345678901" . chr(0))))); echo "OK!"; ?> diff --git a/ext/standard/tests/strings/crypt_chars.phpt b/ext/standard/tests/strings/crypt_chars.phpt index c0ef4a39da8e..68e461e00c14 100644 --- a/ext/standard/tests/strings/crypt_chars.phpt +++ b/ext/standard/tests/strings/crypt_chars.phpt @@ -4,11 +4,11 @@ crypt() function - characters > 0x80 --EXPECT-- string(13) "99PxawtsTfX56" string(13) "99jcVcGxUZOWk" -string(20) "_01234567IBjxKliXXRQ" -string(20) "_012345678OSGpGQRVHA" +string(20) "_J9..4567q0YG9xIr3M6" +string(20) "_J9..4567xcl/AKtT5rI" From 8c2430bde850044362f27b0cb20b43d8c399b689 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:55 +0700 Subject: [PATCH 14/26] Optimize Phar regression fixtures --- ext/phar/tests/bug13727.phpt | 4134 +----------------- ext/phar/tests/phar_buildfromiterator10.phpt | 96 +- 2 files changed, 37 insertions(+), 4193 deletions(-) diff --git a/ext/phar/tests/bug13727.phpt b/ext/phar/tests/bug13727.phpt index 10427ef1f42a..9d51ce12fc99 100644 --- a/ext/phar/tests/bug13727.phpt +++ b/ext/phar/tests/bug13727.phpt @@ -1,5 +1,5 @@ --TEST-- -Phar: SLOW TEST bug #13727: "Number of files in the Phar" limited to 2042 +Phar: SLOW TEST bug #13727: addFile() closes source streams --EXTENSIONS-- phar --SKIPIF-- @@ -9,4127 +9,23 @@ phar.require_hash=0 phar.readonly=0 --FILE-- addFile("$fileDir/$i", "$dirName"); +$archive = __DIR__ . '/bug13727.phar.php'; +$source = __DIR__ . '/bug13727.tmp'; +file_put_contents($source, ''); + +$phar = new Phar($archive, 0, 'DataArchive.phar'); +$phar->startBuffering(); +for ($i = 0; $i < 2 * 1024; $i++) { + $phar->addFile($source, 'entry'); } -echo("\n Written Files($i)\n"); +$phar->stopBuffering(); + +var_dump($i); ?> --CLEAN-- --EXPECT-- -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505 -506 -507 -508 -509 -510 -511 -512 -513 -514 -515 -516 -517 -518 -519 -520 -521 -522 -523 -524 -525 -526 -527 -528 -529 -530 -531 -532 -533 -534 -535 -536 -537 -538 -539 -540 -541 -542 -543 -544 -545 -546 -547 -548 -549 -550 -551 -552 -553 -554 -555 -556 -557 -558 -559 -560 -561 -562 -563 -564 -565 -566 -567 -568 -569 -570 -571 -572 -573 -574 -575 -576 -577 -578 -579 -580 -581 -582 -583 -584 -585 -586 -587 -588 -589 -590 -591 -592 -593 -594 -595 -596 -597 -598 -599 -600 -601 -602 -603 -604 -605 -606 -607 -608 -609 -610 -611 -612 -613 -614 -615 -616 -617 -618 -619 -620 -621 -622 -623 -624 -625 -626 -627 -628 -629 -630 -631 -632 -633 -634 -635 -636 -637 -638 -639 -640 -641 -642 -643 -644 -645 -646 -647 -648 -649 -650 -651 -652 -653 -654 -655 -656 -657 -658 -659 -660 -661 -662 -663 -664 -665 -666 -667 -668 -669 -670 -671 -672 -673 -674 -675 -676 -677 -678 -679 -680 -681 -682 -683 -684 -685 -686 -687 -688 -689 -690 -691 -692 -693 -694 -695 -696 -697 -698 -699 -700 -701 -702 -703 -704 -705 -706 -707 -708 -709 -710 -711 -712 -713 -714 -715 -716 -717 -718 -719 -720 -721 -722 -723 -724 -725 -726 -727 -728 -729 -730 -731 -732 -733 -734 -735 -736 -737 -738 -739 -740 -741 -742 -743 -744 -745 -746 -747 -748 -749 -750 -751 -752 -753 -754 -755 -756 -757 -758 -759 -760 -761 -762 -763 -764 -765 -766 -767 -768 -769 -770 -771 -772 -773 -774 -775 -776 -777 -778 -779 -780 -781 -782 -783 -784 -785 -786 -787 -788 -789 -790 -791 -792 -793 -794 -795 -796 -797 -798 -799 -800 -801 -802 -803 -804 -805 -806 -807 -808 -809 -810 -811 -812 -813 -814 -815 -816 -817 -818 -819 -820 -821 -822 -823 -824 -825 -826 -827 -828 -829 -830 -831 -832 -833 -834 -835 -836 -837 -838 -839 -840 -841 -842 -843 -844 -845 -846 -847 -848 -849 -850 -851 -852 -853 -854 -855 -856 -857 -858 -859 -860 -861 -862 -863 -864 -865 -866 -867 -868 -869 -870 -871 -872 -873 -874 -875 -876 -877 -878 -879 -880 -881 -882 -883 -884 -885 -886 -887 -888 -889 -890 -891 -892 -893 -894 -895 -896 -897 -898 -899 -900 -901 -902 -903 -904 -905 -906 -907 -908 -909 -910 -911 -912 -913 -914 -915 -916 -917 -918 -919 -920 -921 -922 -923 -924 -925 -926 -927 -928 -929 -930 -931 -932 -933 -934 -935 -936 -937 -938 -939 -940 -941 -942 -943 -944 -945 -946 -947 -948 -949 -950 -951 -952 -953 -954 -955 -956 -957 -958 -959 -960 -961 -962 -963 -964 -965 -966 -967 -968 -969 -970 -971 -972 -973 -974 -975 -976 -977 -978 -979 -980 -981 -982 -983 -984 -985 -986 -987 -988 -989 -990 -991 -992 -993 -994 -995 -996 -997 -998 -999 -1000 -1001 -1002 -1003 -1004 -1005 -1006 -1007 -1008 -1009 -1010 -1011 -1012 -1013 -1014 -1015 -1016 -1017 -1018 -1019 -1020 -1021 -1022 -1023 -1024 -1025 -1026 -1027 -1028 -1029 -1030 -1031 -1032 -1033 -1034 -1035 -1036 -1037 -1038 -1039 -1040 -1041 -1042 -1043 -1044 -1045 -1046 -1047 -1048 -1049 -1050 -1051 -1052 -1053 -1054 -1055 -1056 -1057 -1058 -1059 -1060 -1061 -1062 -1063 -1064 -1065 -1066 -1067 -1068 -1069 -1070 -1071 -1072 -1073 -1074 -1075 -1076 -1077 -1078 -1079 -1080 -1081 -1082 -1083 -1084 -1085 -1086 -1087 -1088 -1089 -1090 -1091 -1092 -1093 -1094 -1095 -1096 -1097 -1098 -1099 -1100 -1101 -1102 -1103 -1104 -1105 -1106 -1107 -1108 -1109 -1110 -1111 -1112 -1113 -1114 -1115 -1116 -1117 -1118 -1119 -1120 -1121 -1122 -1123 -1124 -1125 -1126 -1127 -1128 -1129 -1130 -1131 -1132 -1133 -1134 -1135 -1136 -1137 -1138 -1139 -1140 -1141 -1142 -1143 -1144 -1145 -1146 -1147 -1148 -1149 -1150 -1151 -1152 -1153 -1154 -1155 -1156 -1157 -1158 -1159 -1160 -1161 -1162 -1163 -1164 -1165 -1166 -1167 -1168 -1169 -1170 -1171 -1172 -1173 -1174 -1175 -1176 -1177 -1178 -1179 -1180 -1181 -1182 -1183 -1184 -1185 -1186 -1187 -1188 -1189 -1190 -1191 -1192 -1193 -1194 -1195 -1196 -1197 -1198 -1199 -1200 -1201 -1202 -1203 -1204 -1205 -1206 -1207 -1208 -1209 -1210 -1211 -1212 -1213 -1214 -1215 -1216 -1217 -1218 -1219 -1220 -1221 -1222 -1223 -1224 -1225 -1226 -1227 -1228 -1229 -1230 -1231 -1232 -1233 -1234 -1235 -1236 -1237 -1238 -1239 -1240 -1241 -1242 -1243 -1244 -1245 -1246 -1247 -1248 -1249 -1250 -1251 -1252 -1253 -1254 -1255 -1256 -1257 -1258 -1259 -1260 -1261 -1262 -1263 -1264 -1265 -1266 -1267 -1268 -1269 -1270 -1271 -1272 -1273 -1274 -1275 -1276 -1277 -1278 -1279 -1280 -1281 -1282 -1283 -1284 -1285 -1286 -1287 -1288 -1289 -1290 -1291 -1292 -1293 -1294 -1295 -1296 -1297 -1298 -1299 -1300 -1301 -1302 -1303 -1304 -1305 -1306 -1307 -1308 -1309 -1310 -1311 -1312 -1313 -1314 -1315 -1316 -1317 -1318 -1319 -1320 -1321 -1322 -1323 -1324 -1325 -1326 -1327 -1328 -1329 -1330 -1331 -1332 -1333 -1334 -1335 -1336 -1337 -1338 -1339 -1340 -1341 -1342 -1343 -1344 -1345 -1346 -1347 -1348 -1349 -1350 -1351 -1352 -1353 -1354 -1355 -1356 -1357 -1358 -1359 -1360 -1361 -1362 -1363 -1364 -1365 -1366 -1367 -1368 -1369 -1370 -1371 -1372 -1373 -1374 -1375 -1376 -1377 -1378 -1379 -1380 -1381 -1382 -1383 -1384 -1385 -1386 -1387 -1388 -1389 -1390 -1391 -1392 -1393 -1394 -1395 -1396 -1397 -1398 -1399 -1400 -1401 -1402 -1403 -1404 -1405 -1406 -1407 -1408 -1409 -1410 -1411 -1412 -1413 -1414 -1415 -1416 -1417 -1418 -1419 -1420 -1421 -1422 -1423 -1424 -1425 -1426 -1427 -1428 -1429 -1430 -1431 -1432 -1433 -1434 -1435 -1436 -1437 -1438 -1439 -1440 -1441 -1442 -1443 -1444 -1445 -1446 -1447 -1448 -1449 -1450 -1451 -1452 -1453 -1454 -1455 -1456 -1457 -1458 -1459 -1460 -1461 -1462 -1463 -1464 -1465 -1466 -1467 -1468 -1469 -1470 -1471 -1472 -1473 -1474 -1475 -1476 -1477 -1478 -1479 -1480 -1481 -1482 -1483 -1484 -1485 -1486 -1487 -1488 -1489 -1490 -1491 -1492 -1493 -1494 -1495 -1496 -1497 -1498 -1499 -1500 -1501 -1502 -1503 -1504 -1505 -1506 -1507 -1508 -1509 -1510 -1511 -1512 -1513 -1514 -1515 -1516 -1517 -1518 -1519 -1520 -1521 -1522 -1523 -1524 -1525 -1526 -1527 -1528 -1529 -1530 -1531 -1532 -1533 -1534 -1535 -1536 -1537 -1538 -1539 -1540 -1541 -1542 -1543 -1544 -1545 -1546 -1547 -1548 -1549 -1550 -1551 -1552 -1553 -1554 -1555 -1556 -1557 -1558 -1559 -1560 -1561 -1562 -1563 -1564 -1565 -1566 -1567 -1568 -1569 -1570 -1571 -1572 -1573 -1574 -1575 -1576 -1577 -1578 -1579 -1580 -1581 -1582 -1583 -1584 -1585 -1586 -1587 -1588 -1589 -1590 -1591 -1592 -1593 -1594 -1595 -1596 -1597 -1598 -1599 -1600 -1601 -1602 -1603 -1604 -1605 -1606 -1607 -1608 -1609 -1610 -1611 -1612 -1613 -1614 -1615 -1616 -1617 -1618 -1619 -1620 -1621 -1622 -1623 -1624 -1625 -1626 -1627 -1628 -1629 -1630 -1631 -1632 -1633 -1634 -1635 -1636 -1637 -1638 -1639 -1640 -1641 -1642 -1643 -1644 -1645 -1646 -1647 -1648 -1649 -1650 -1651 -1652 -1653 -1654 -1655 -1656 -1657 -1658 -1659 -1660 -1661 -1662 -1663 -1664 -1665 -1666 -1667 -1668 -1669 -1670 -1671 -1672 -1673 -1674 -1675 -1676 -1677 -1678 -1679 -1680 -1681 -1682 -1683 -1684 -1685 -1686 -1687 -1688 -1689 -1690 -1691 -1692 -1693 -1694 -1695 -1696 -1697 -1698 -1699 -1700 -1701 -1702 -1703 -1704 -1705 -1706 -1707 -1708 -1709 -1710 -1711 -1712 -1713 -1714 -1715 -1716 -1717 -1718 -1719 -1720 -1721 -1722 -1723 -1724 -1725 -1726 -1727 -1728 -1729 -1730 -1731 -1732 -1733 -1734 -1735 -1736 -1737 -1738 -1739 -1740 -1741 -1742 -1743 -1744 -1745 -1746 -1747 -1748 -1749 -1750 -1751 -1752 -1753 -1754 -1755 -1756 -1757 -1758 -1759 -1760 -1761 -1762 -1763 -1764 -1765 -1766 -1767 -1768 -1769 -1770 -1771 -1772 -1773 -1774 -1775 -1776 -1777 -1778 -1779 -1780 -1781 -1782 -1783 -1784 -1785 -1786 -1787 -1788 -1789 -1790 -1791 -1792 -1793 -1794 -1795 -1796 -1797 -1798 -1799 -1800 -1801 -1802 -1803 -1804 -1805 -1806 -1807 -1808 -1809 -1810 -1811 -1812 -1813 -1814 -1815 -1816 -1817 -1818 -1819 -1820 -1821 -1822 -1823 -1824 -1825 -1826 -1827 -1828 -1829 -1830 -1831 -1832 -1833 -1834 -1835 -1836 -1837 -1838 -1839 -1840 -1841 -1842 -1843 -1844 -1845 -1846 -1847 -1848 -1849 -1850 -1851 -1852 -1853 -1854 -1855 -1856 -1857 -1858 -1859 -1860 -1861 -1862 -1863 -1864 -1865 -1866 -1867 -1868 -1869 -1870 -1871 -1872 -1873 -1874 -1875 -1876 -1877 -1878 -1879 -1880 -1881 -1882 -1883 -1884 -1885 -1886 -1887 -1888 -1889 -1890 -1891 -1892 -1893 -1894 -1895 -1896 -1897 -1898 -1899 -1900 -1901 -1902 -1903 -1904 -1905 -1906 -1907 -1908 -1909 -1910 -1911 -1912 -1913 -1914 -1915 -1916 -1917 -1918 -1919 -1920 -1921 -1922 -1923 -1924 -1925 -1926 -1927 -1928 -1929 -1930 -1931 -1932 -1933 -1934 -1935 -1936 -1937 -1938 -1939 -1940 -1941 -1942 -1943 -1944 -1945 -1946 -1947 -1948 -1949 -1950 -1951 -1952 -1953 -1954 -1955 -1956 -1957 -1958 -1959 -1960 -1961 -1962 -1963 -1964 -1965 -1966 -1967 -1968 -1969 -1970 -1971 -1972 -1973 -1974 -1975 -1976 -1977 -1978 -1979 -1980 -1981 -1982 -1983 -1984 -1985 -1986 -1987 -1988 -1989 -1990 -1991 -1992 -1993 -1994 -1995 -1996 -1997 -1998 -1999 -2000 -2001 -2002 -2003 -2004 -2005 -2006 -2007 -2008 -2009 -2010 -2011 -2012 -2013 -2014 -2015 -2016 -2017 -2018 -2019 -2020 -2021 -2022 -2023 -2024 -2025 -2026 -2027 -2028 -2029 -2030 -2031 -2032 -2033 -2034 -2035 -2036 -2037 -2038 -2039 -2040 -2041 -2042 -2043 -2044 -2045 -2046 -2047 -2048 -2049 -2050 -2051 -2052 -2053 -2054 -2055 -2056 -2057 -2058 -2059 -2060 -2061 -2062 -2063 -2064 -2065 -2066 -2067 -2068 -2069 -2070 -2071 -2072 -2073 -2074 -2075 -2076 -2077 -2078 -2079 -2080 -2081 -2082 -2083 -2084 -2085 -2086 -2087 -2088 -2089 -2090 -2091 -2092 -2093 -2094 -2095 -2096 -2097 -2098 -2099 -2100 -2101 -2102 -2103 -2104 -2105 -2106 -2107 -2108 -2109 -2110 -2111 -2112 -2113 -2114 -2115 -2116 -2117 -2118 -2119 -2120 -2121 -2122 -2123 -2124 -2125 -2126 -2127 -2128 -2129 -2130 -2131 -2132 -2133 -2134 -2135 -2136 -2137 -2138 -2139 -2140 -2141 -2142 -2143 -2144 -2145 -2146 -2147 -2148 -2149 -2150 -2151 -2152 -2153 -2154 -2155 -2156 -2157 -2158 -2159 -2160 -2161 -2162 -2163 -2164 -2165 -2166 -2167 -2168 -2169 -2170 -2171 -2172 -2173 -2174 -2175 -2176 -2177 -2178 -2179 -2180 -2181 -2182 -2183 -2184 -2185 -2186 -2187 -2188 -2189 -2190 -2191 -2192 -2193 -2194 -2195 -2196 -2197 -2198 -2199 -2200 -2201 -2202 -2203 -2204 -2205 -2206 -2207 -2208 -2209 -2210 -2211 -2212 -2213 -2214 -2215 -2216 -2217 -2218 -2219 -2220 -2221 -2222 -2223 -2224 -2225 -2226 -2227 -2228 -2229 -2230 -2231 -2232 -2233 -2234 -2235 -2236 -2237 -2238 -2239 -2240 -2241 -2242 -2243 -2244 -2245 -2246 -2247 -2248 -2249 -2250 -2251 -2252 -2253 -2254 -2255 -2256 -2257 -2258 -2259 -2260 -2261 -2262 -2263 -2264 -2265 -2266 -2267 -2268 -2269 -2270 -2271 -2272 -2273 -2274 -2275 -2276 -2277 -2278 -2279 -2280 -2281 -2282 -2283 -2284 -2285 -2286 -2287 -2288 -2289 -2290 -2291 -2292 -2293 -2294 -2295 -2296 -2297 -2298 -2299 -2300 -2301 -2302 -2303 -2304 -2305 -2306 -2307 -2308 -2309 -2310 -2311 -2312 -2313 -2314 -2315 -2316 -2317 -2318 -2319 -2320 -2321 -2322 -2323 -2324 -2325 -2326 -2327 -2328 -2329 -2330 -2331 -2332 -2333 -2334 -2335 -2336 -2337 -2338 -2339 -2340 -2341 -2342 -2343 -2344 -2345 -2346 -2347 -2348 -2349 -2350 -2351 -2352 -2353 -2354 -2355 -2356 -2357 -2358 -2359 -2360 -2361 -2362 -2363 -2364 -2365 -2366 -2367 -2368 -2369 -2370 -2371 -2372 -2373 -2374 -2375 -2376 -2377 -2378 -2379 -2380 -2381 -2382 -2383 -2384 -2385 -2386 -2387 -2388 -2389 -2390 -2391 -2392 -2393 -2394 -2395 -2396 -2397 -2398 -2399 -2400 -2401 -2402 -2403 -2404 -2405 -2406 -2407 -2408 -2409 -2410 -2411 -2412 -2413 -2414 -2415 -2416 -2417 -2418 -2419 -2420 -2421 -2422 -2423 -2424 -2425 -2426 -2427 -2428 -2429 -2430 -2431 -2432 -2433 -2434 -2435 -2436 -2437 -2438 -2439 -2440 -2441 -2442 -2443 -2444 -2445 -2446 -2447 -2448 -2449 -2450 -2451 -2452 -2453 -2454 -2455 -2456 -2457 -2458 -2459 -2460 -2461 -2462 -2463 -2464 -2465 -2466 -2467 -2468 -2469 -2470 -2471 -2472 -2473 -2474 -2475 -2476 -2477 -2478 -2479 -2480 -2481 -2482 -2483 -2484 -2485 -2486 -2487 -2488 -2489 -2490 -2491 -2492 -2493 -2494 -2495 -2496 -2497 -2498 -2499 -2500 -2501 -2502 -2503 -2504 -2505 -2506 -2507 -2508 -2509 -2510 -2511 -2512 -2513 -2514 -2515 -2516 -2517 -2518 -2519 -2520 -2521 -2522 -2523 -2524 -2525 -2526 -2527 -2528 -2529 -2530 -2531 -2532 -2533 -2534 -2535 -2536 -2537 -2538 -2539 -2540 -2541 -2542 -2543 -2544 -2545 -2546 -2547 -2548 -2549 -2550 -2551 -2552 -2553 -2554 -2555 -2556 -2557 -2558 -2559 -2560 -2561 -2562 -2563 -2564 -2565 -2566 -2567 -2568 -2569 -2570 -2571 -2572 -2573 -2574 -2575 -2576 -2577 -2578 -2579 -2580 -2581 -2582 -2583 -2584 -2585 -2586 -2587 -2588 -2589 -2590 -2591 -2592 -2593 -2594 -2595 -2596 -2597 -2598 -2599 -2600 -2601 -2602 -2603 -2604 -2605 -2606 -2607 -2608 -2609 -2610 -2611 -2612 -2613 -2614 -2615 -2616 -2617 -2618 -2619 -2620 -2621 -2622 -2623 -2624 -2625 -2626 -2627 -2628 -2629 -2630 -2631 -2632 -2633 -2634 -2635 -2636 -2637 -2638 -2639 -2640 -2641 -2642 -2643 -2644 -2645 -2646 -2647 -2648 -2649 -2650 -2651 -2652 -2653 -2654 -2655 -2656 -2657 -2658 -2659 -2660 -2661 -2662 -2663 -2664 -2665 -2666 -2667 -2668 -2669 -2670 -2671 -2672 -2673 -2674 -2675 -2676 -2677 -2678 -2679 -2680 -2681 -2682 -2683 -2684 -2685 -2686 -2687 -2688 -2689 -2690 -2691 -2692 -2693 -2694 -2695 -2696 -2697 -2698 -2699 -2700 -2701 -2702 -2703 -2704 -2705 -2706 -2707 -2708 -2709 -2710 -2711 -2712 -2713 -2714 -2715 -2716 -2717 -2718 -2719 -2720 -2721 -2722 -2723 -2724 -2725 -2726 -2727 -2728 -2729 -2730 -2731 -2732 -2733 -2734 -2735 -2736 -2737 -2738 -2739 -2740 -2741 -2742 -2743 -2744 -2745 -2746 -2747 -2748 -2749 -2750 -2751 -2752 -2753 -2754 -2755 -2756 -2757 -2758 -2759 -2760 -2761 -2762 -2763 -2764 -2765 -2766 -2767 -2768 -2769 -2770 -2771 -2772 -2773 -2774 -2775 -2776 -2777 -2778 -2779 -2780 -2781 -2782 -2783 -2784 -2785 -2786 -2787 -2788 -2789 -2790 -2791 -2792 -2793 -2794 -2795 -2796 -2797 -2798 -2799 -2800 -2801 -2802 -2803 -2804 -2805 -2806 -2807 -2808 -2809 -2810 -2811 -2812 -2813 -2814 -2815 -2816 -2817 -2818 -2819 -2820 -2821 -2822 -2823 -2824 -2825 -2826 -2827 -2828 -2829 -2830 -2831 -2832 -2833 -2834 -2835 -2836 -2837 -2838 -2839 -2840 -2841 -2842 -2843 -2844 -2845 -2846 -2847 -2848 -2849 -2850 -2851 -2852 -2853 -2854 -2855 -2856 -2857 -2858 -2859 -2860 -2861 -2862 -2863 -2864 -2865 -2866 -2867 -2868 -2869 -2870 -2871 -2872 -2873 -2874 -2875 -2876 -2877 -2878 -2879 -2880 -2881 -2882 -2883 -2884 -2885 -2886 -2887 -2888 -2889 -2890 -2891 -2892 -2893 -2894 -2895 -2896 -2897 -2898 -2899 -2900 -2901 -2902 -2903 -2904 -2905 -2906 -2907 -2908 -2909 -2910 -2911 -2912 -2913 -2914 -2915 -2916 -2917 -2918 -2919 -2920 -2921 -2922 -2923 -2924 -2925 -2926 -2927 -2928 -2929 -2930 -2931 -2932 -2933 -2934 -2935 -2936 -2937 -2938 -2939 -2940 -2941 -2942 -2943 -2944 -2945 -2946 -2947 -2948 -2949 -2950 -2951 -2952 -2953 -2954 -2955 -2956 -2957 -2958 -2959 -2960 -2961 -2962 -2963 -2964 -2965 -2966 -2967 -2968 -2969 -2970 -2971 -2972 -2973 -2974 -2975 -2976 -2977 -2978 -2979 -2980 -2981 -2982 -2983 -2984 -2985 -2986 -2987 -2988 -2989 -2990 -2991 -2992 -2993 -2994 -2995 -2996 -2997 -2998 -2999 -3000 -3001 -3002 -3003 -3004 -3005 -3006 -3007 -3008 -3009 -3010 -3011 -3012 -3013 -3014 -3015 -3016 -3017 -3018 -3019 -3020 -3021 -3022 -3023 -3024 -3025 -3026 -3027 -3028 -3029 -3030 -3031 -3032 -3033 -3034 -3035 -3036 -3037 -3038 -3039 -3040 -3041 -3042 -3043 -3044 -3045 -3046 -3047 -3048 -3049 -3050 -3051 -3052 -3053 -3054 -3055 -3056 -3057 -3058 -3059 -3060 -3061 -3062 -3063 -3064 -3065 -3066 -3067 -3068 -3069 -3070 -3071 -3072 -3073 -3074 -3075 -3076 -3077 -3078 -3079 -3080 -3081 -3082 -3083 -3084 -3085 -3086 -3087 -3088 -3089 -3090 -3091 -3092 -3093 -3094 -3095 -3096 -3097 -3098 -3099 -3100 -3101 -3102 -3103 -3104 -3105 -3106 -3107 -3108 -3109 -3110 -3111 -3112 -3113 -3114 -3115 -3116 -3117 -3118 -3119 -3120 -3121 -3122 -3123 -3124 -3125 -3126 -3127 -3128 -3129 -3130 -3131 -3132 -3133 -3134 -3135 -3136 -3137 -3138 -3139 -3140 -3141 -3142 -3143 -3144 -3145 -3146 -3147 -3148 -3149 -3150 -3151 -3152 -3153 -3154 -3155 -3156 -3157 -3158 -3159 -3160 -3161 -3162 -3163 -3164 -3165 -3166 -3167 -3168 -3169 -3170 -3171 -3172 -3173 -3174 -3175 -3176 -3177 -3178 -3179 -3180 -3181 -3182 -3183 -3184 -3185 -3186 -3187 -3188 -3189 -3190 -3191 -3192 -3193 -3194 -3195 -3196 -3197 -3198 -3199 -3200 -3201 -3202 -3203 -3204 -3205 -3206 -3207 -3208 -3209 -3210 -3211 -3212 -3213 -3214 -3215 -3216 -3217 -3218 -3219 -3220 -3221 -3222 -3223 -3224 -3225 -3226 -3227 -3228 -3229 -3230 -3231 -3232 -3233 -3234 -3235 -3236 -3237 -3238 -3239 -3240 -3241 -3242 -3243 -3244 -3245 -3246 -3247 -3248 -3249 -3250 -3251 -3252 -3253 -3254 -3255 -3256 -3257 -3258 -3259 -3260 -3261 -3262 -3263 -3264 -3265 -3266 -3267 -3268 -3269 -3270 -3271 -3272 -3273 -3274 -3275 -3276 -3277 -3278 -3279 -3280 -3281 -3282 -3283 -3284 -3285 -3286 -3287 -3288 -3289 -3290 -3291 -3292 -3293 -3294 -3295 -3296 -3297 -3298 -3299 -3300 -3301 -3302 -3303 -3304 -3305 -3306 -3307 -3308 -3309 -3310 -3311 -3312 -3313 -3314 -3315 -3316 -3317 -3318 -3319 -3320 -3321 -3322 -3323 -3324 -3325 -3326 -3327 -3328 -3329 -3330 -3331 -3332 -3333 -3334 -3335 -3336 -3337 -3338 -3339 -3340 -3341 -3342 -3343 -3344 -3345 -3346 -3347 -3348 -3349 -3350 -3351 -3352 -3353 -3354 -3355 -3356 -3357 -3358 -3359 -3360 -3361 -3362 -3363 -3364 -3365 -3366 -3367 -3368 -3369 -3370 -3371 -3372 -3373 -3374 -3375 -3376 -3377 -3378 -3379 -3380 -3381 -3382 -3383 -3384 -3385 -3386 -3387 -3388 -3389 -3390 -3391 -3392 -3393 -3394 -3395 -3396 -3397 -3398 -3399 -3400 -3401 -3402 -3403 -3404 -3405 -3406 -3407 -3408 -3409 -3410 -3411 -3412 -3413 -3414 -3415 -3416 -3417 -3418 -3419 -3420 -3421 -3422 -3423 -3424 -3425 -3426 -3427 -3428 -3429 -3430 -3431 -3432 -3433 -3434 -3435 -3436 -3437 -3438 -3439 -3440 -3441 -3442 -3443 -3444 -3445 -3446 -3447 -3448 -3449 -3450 -3451 -3452 -3453 -3454 -3455 -3456 -3457 -3458 -3459 -3460 -3461 -3462 -3463 -3464 -3465 -3466 -3467 -3468 -3469 -3470 -3471 -3472 -3473 -3474 -3475 -3476 -3477 -3478 -3479 -3480 -3481 -3482 -3483 -3484 -3485 -3486 -3487 -3488 -3489 -3490 -3491 -3492 -3493 -3494 -3495 -3496 -3497 -3498 -3499 -3500 -3501 -3502 -3503 -3504 -3505 -3506 -3507 -3508 -3509 -3510 -3511 -3512 -3513 -3514 -3515 -3516 -3517 -3518 -3519 -3520 -3521 -3522 -3523 -3524 -3525 -3526 -3527 -3528 -3529 -3530 -3531 -3532 -3533 -3534 -3535 -3536 -3537 -3538 -3539 -3540 -3541 -3542 -3543 -3544 -3545 -3546 -3547 -3548 -3549 -3550 -3551 -3552 -3553 -3554 -3555 -3556 -3557 -3558 -3559 -3560 -3561 -3562 -3563 -3564 -3565 -3566 -3567 -3568 -3569 -3570 -3571 -3572 -3573 -3574 -3575 -3576 -3577 -3578 -3579 -3580 -3581 -3582 -3583 -3584 -3585 -3586 -3587 -3588 -3589 -3590 -3591 -3592 -3593 -3594 -3595 -3596 -3597 -3598 -3599 -3600 -3601 -3602 -3603 -3604 -3605 -3606 -3607 -3608 -3609 -3610 -3611 -3612 -3613 -3614 -3615 -3616 -3617 -3618 -3619 -3620 -3621 -3622 -3623 -3624 -3625 -3626 -3627 -3628 -3629 -3630 -3631 -3632 -3633 -3634 -3635 -3636 -3637 -3638 -3639 -3640 -3641 -3642 -3643 -3644 -3645 -3646 -3647 -3648 -3649 -3650 -3651 -3652 -3653 -3654 -3655 -3656 -3657 -3658 -3659 -3660 -3661 -3662 -3663 -3664 -3665 -3666 -3667 -3668 -3669 -3670 -3671 -3672 -3673 -3674 -3675 -3676 -3677 -3678 -3679 -3680 -3681 -3682 -3683 -3684 -3685 -3686 -3687 -3688 -3689 -3690 -3691 -3692 -3693 -3694 -3695 -3696 -3697 -3698 -3699 -3700 -3701 -3702 -3703 -3704 -3705 -3706 -3707 -3708 -3709 -3710 -3711 -3712 -3713 -3714 -3715 -3716 -3717 -3718 -3719 -3720 -3721 -3722 -3723 -3724 -3725 -3726 -3727 -3728 -3729 -3730 -3731 -3732 -3733 -3734 -3735 -3736 -3737 -3738 -3739 -3740 -3741 -3742 -3743 -3744 -3745 -3746 -3747 -3748 -3749 -3750 -3751 -3752 -3753 -3754 -3755 -3756 -3757 -3758 -3759 -3760 -3761 -3762 -3763 -3764 -3765 -3766 -3767 -3768 -3769 -3770 -3771 -3772 -3773 -3774 -3775 -3776 -3777 -3778 -3779 -3780 -3781 -3782 -3783 -3784 -3785 -3786 -3787 -3788 -3789 -3790 -3791 -3792 -3793 -3794 -3795 -3796 -3797 -3798 -3799 -3800 -3801 -3802 -3803 -3804 -3805 -3806 -3807 -3808 -3809 -3810 -3811 -3812 -3813 -3814 -3815 -3816 -3817 -3818 -3819 -3820 -3821 -3822 -3823 -3824 -3825 -3826 -3827 -3828 -3829 -3830 -3831 -3832 -3833 -3834 -3835 -3836 -3837 -3838 -3839 -3840 -3841 -3842 -3843 -3844 -3845 -3846 -3847 -3848 -3849 -3850 -3851 -3852 -3853 -3854 -3855 -3856 -3857 -3858 -3859 -3860 -3861 -3862 -3863 -3864 -3865 -3866 -3867 -3868 -3869 -3870 -3871 -3872 -3873 -3874 -3875 -3876 -3877 -3878 -3879 -3880 -3881 -3882 -3883 -3884 -3885 -3886 -3887 -3888 -3889 -3890 -3891 -3892 -3893 -3894 -3895 -3896 -3897 -3898 -3899 -3900 -3901 -3902 -3903 -3904 -3905 -3906 -3907 -3908 -3909 -3910 -3911 -3912 -3913 -3914 -3915 -3916 -3917 -3918 -3919 -3920 -3921 -3922 -3923 -3924 -3925 -3926 -3927 -3928 -3929 -3930 -3931 -3932 -3933 -3934 -3935 -3936 -3937 -3938 -3939 -3940 -3941 -3942 -3943 -3944 -3945 -3946 -3947 -3948 -3949 -3950 -3951 -3952 -3953 -3954 -3955 -3956 -3957 -3958 -3959 -3960 -3961 -3962 -3963 -3964 -3965 -3966 -3967 -3968 -3969 -3970 -3971 -3972 -3973 -3974 -3975 -3976 -3977 -3978 -3979 -3980 -3981 -3982 -3983 -3984 -3985 -3986 -3987 -3988 -3989 -3990 -3991 -3992 -3993 -3994 -3995 -3996 -3997 -3998 -3999 -4000 -4001 -4002 -4003 -4004 -4005 -4006 -4007 -4008 -4009 -4010 -4011 -4012 -4013 -4014 -4015 -4016 -4017 -4018 -4019 -4020 -4021 -4022 -4023 -4024 -4025 -4026 -4027 -4028 -4029 -4030 -4031 -4032 -4033 -4034 -4035 -4036 -4037 -4038 -4039 -4040 -4041 -4042 -4043 -4044 -4045 -4046 -4047 -4048 -4049 -4050 -4051 -4052 -4053 -4054 -4055 -4056 -4057 -4058 -4059 -4060 -4061 -4062 -4063 -4064 -4065 -4066 -4067 -4068 -4069 -4070 -4071 -4072 -4073 -4074 -4075 -4076 -4077 -4078 -4079 -4080 -4081 -4082 -4083 -4084 -4085 -4086 -4087 -4088 -4089 -4090 -4091 -4092 -4093 -4094 -4095 - - Written Files(4096) +int(2048) diff --git a/ext/phar/tests/phar_buildfromiterator10.phpt b/ext/phar/tests/phar_buildfromiterator10.phpt index 495f177fabb2..5a4dbdd8dcbd 100644 --- a/ext/phar/tests/phar_buildfromiterator10.phpt +++ b/ext/phar/tests/phar_buildfromiterator10.phpt @@ -2,19 +2,26 @@ Phar::buildFromIterator() RegexIterator(RecursiveIteratorIterator), SplFileInfo as current --EXTENSIONS-- phar ---CONFLICTS-- -all --INI-- phar.require_hash=0 phar.readonly=0 --FILE-- buildFromIterator(new RegexIterator($iter, '/_\d{3}\.phpt$/'), __DIR__ . DIRECTORY_SEPARATOR); + $a = $phar->buildFromIterator( + new RegexIterator($iter, '/_\d{3}\.phpt$/'), + $input . DIRECTORY_SEPARATOR, + ); asort($a); var_dump($a); } catch (Exception $e) { @@ -25,76 +32,17 @@ try { --CLEAN-- --EXPECTF-- -array(34) { - ["phar_ctx_001.phpt"]=> - string(%d) "%sphar_ctx_001.phpt" - ["phar_get_supported_signatures_002.phpt"]=> - string(%d) "%sphar_get_supported_signatures_002.phpt" - ["phar_oo_001.phpt"]=> - string(%d) "%sphar_oo_001.phpt" - ["phar_oo_002.phpt"]=> - string(%d) "%sphar_oo_002.phpt" - ["phar_oo_003.phpt"]=> - string(%d) "%sphar_oo_003.phpt" - ["phar_oo_004.phpt"]=> - string(%d) "%sphar_oo_004.phpt" - ["phar_oo_005.phpt"]=> - string(%d) "%sphar_oo_005.phpt" - ["phar_oo_006.phpt"]=> - string(%d) "%sphar_oo_006.phpt" - ["phar_oo_007.phpt"]=> - string(%d) "%sphar_oo_007.phpt" - ["phar_oo_008.phpt"]=> - string(%d) "%sphar_oo_008.phpt" - ["phar_oo_009.phpt"]=> - string(%d) "%sphar_oo_009.phpt" - ["phar_oo_010.phpt"]=> - string(%d) "%sphar_oo_010.phpt" - ["phar_oo_011.phpt"]=> - string(%d) "%sphar_oo_011.phpt" - ["phar_oo_012.phpt"]=> - string(%d) "%sphar_oo_012.phpt" - ["phar_oo_compressed_001.phpt"]=> - string(%d) "%sphar_oo_compressed_001.phpt" - ["phar_oo_compressed_002.phpt"]=> - string(%d) "%sphar_oo_compressed_002.phpt" - ["phpinfo_001.phpt"]=> - string(%d) "%sphpinfo_001.phpt" - ["phpinfo_002.phpt"]=> - string(%d) "%sphpinfo_002.phpt" - ["phpinfo_003.phpt"]=> - string(%d) "%sphpinfo_003.phpt" - ["phpinfo_004.phpt"]=> - string(%d) "%sphpinfo_004.phpt" - ["tar/tar_001.phpt"]=> - string(%d) "%star%ctar_001.phpt" - ["tar/tar_002.phpt"]=> - string(%d) "%star%ctar_002.phpt" - ["tar/tar_003.phpt"]=> - string(%d) "%star%ctar_003.phpt" - ["tar/tar_004.phpt"]=> - string(%d) "%star%ctar_004.phpt" - ["zip/corrupt_001.phpt"]=> - string(%d) "%szip%ccorrupt_001.phpt" - ["zip/corrupt_002.phpt"]=> - string(%d) "%szip%ccorrupt_002.phpt" - ["zip/corrupt_003.phpt"]=> - string(%d) "%szip%ccorrupt_003.phpt" - ["zip/corrupt_004.phpt"]=> - string(%d) "%szip%ccorrupt_004.phpt" - ["zip/corrupt_005.phpt"]=> - string(%d) "%szip%ccorrupt_005.phpt" - ["zip/corrupt_006.phpt"]=> - string(%d) "%szip%ccorrupt_006.phpt" - ["zip/corrupt_007.phpt"]=> - string(%d) "%szip%ccorrupt_007.phpt" - ["zip/corrupt_008.phpt"]=> - string(%d) "%szip%ccorrupt_008.phpt" - ["zip/corrupt_009.phpt"]=> - string(%d) "%szip%ccorrupt_009.phpt" - ["zip/corrupt_010.phpt"]=> - string(%d) "%szip%ccorrupt_010.phpt" +array(2) { + ["input_001.phpt"]=> + string(%d) "%sbuildfromiterator10%sinput_001.phpt" + ["nested/input_002.phpt"]=> + string(%d) "%sbuildfromiterator10%snested%sinput_002.phpt" } From 67b1626febc8706b2a6b64fdff157c2d702bb7b3 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:55 +0700 Subject: [PATCH 15/26] Avoid opcache revalidation delays --- ext/opcache/tests/issue0140.phpt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/opcache/tests/issue0140.phpt b/ext/opcache/tests/issue0140.phpt index f61b812fef48..de37d9f7d86a 100644 --- a/ext/opcache/tests/issue0140.phpt +++ b/ext/opcache/tests/issue0140.phpt @@ -16,16 +16,16 @@ file_put_contents(FILENAME, "1\n"); var_dump(is_readable(FILENAME)); include(FILENAME); -var_dump(filemtime(FILENAME)); +$mtime = filemtime(FILENAME); +var_dump($mtime); -sleep(2); file_put_contents(FILENAME, "2\n"); +touch(FILENAME, $mtime + 2); var_dump(is_readable(FILENAME)); include(FILENAME); var_dump(filemtime(FILENAME)); -sleep(2); unlink(FILENAME); var_dump(is_readable(FILENAME)); From d0955fd9afed11cdee82aa7c89dd8834119b536c Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:29:55 +0700 Subject: [PATCH 16/26] Synchronize proc_open tests without fixed delays --- ext/standard/tests/general_functions/bug39322.phpt | 11 +++++++++-- .../tests/general_functions/proc_open_pipes1.phpt | 2 +- .../tests/general_functions/proc_open_pipes2.phpt | 2 +- .../tests/general_functions/proc_open_pipes3.phpt | 2 +- .../tests/general_functions/proc_open_pipes_exit.inc | 2 ++ .../tests/general_functions/proc_open_pipes_sleep.inc | 2 -- .../tests/general_functions/proc_open_sockets1.inc | 5 +++-- .../tests/general_functions/proc_open_sockets1.phpt | 8 +++++++- .../tests/general_functions/proc_open_sockets2.inc | 3 ++- .../tests/general_functions/proc_open_sockets2.phpt | 1 + .../tests/general_functions/proc_open_sockets3.phpt | 1 + 11 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 ext/standard/tests/general_functions/proc_open_pipes_exit.inc delete mode 100644 ext/standard/tests/general_functions/proc_open_pipes_sleep.inc diff --git a/ext/standard/tests/general_functions/bug39322.phpt b/ext/standard/tests/general_functions/bug39322.phpt index 162f2babdaac..80a13df4235d 100644 --- a/ext/standard/tests/general_functions/bug39322.phpt +++ b/ext/standard/tests/general_functions/bug39322.phpt @@ -17,8 +17,15 @@ $pipes = array(); $process = proc_open('/bin/sleep 120', $descriptors, $pipes); proc_terminate($process, 9); -sleep(1); // wait a bit to let the process finish -var_dump(proc_get_status($process)); +$deadline = microtime(true) + 5; +do { + $status = proc_get_status($process); + if (!$status['running']) { + break; + } + usleep(1000); +} while (microtime(true) < $deadline); +var_dump($status); echo "Done!\n"; diff --git a/ext/standard/tests/general_functions/proc_open_pipes1.phpt b/ext/standard/tests/general_functions/proc_open_pipes1.phpt index 9822607c6f7e..16db9b34272d 100644 --- a/ext/standard/tests/general_functions/proc_open_pipes1.phpt +++ b/ext/standard/tests/general_functions/proc_open_pipes1.phpt @@ -8,7 +8,7 @@ for ($i = 3; $i<= 30; $i++) { } $php = getenv("TEST_PHP_EXECUTABLE_ESCAPED"); -$callee = escapeshellarg(__DIR__ . "/proc_open_pipes_sleep.inc"); +$callee = escapeshellarg(__DIR__ . "/proc_open_pipes_exit.inc"); proc_open("$php -n $callee", $spec, $pipes); var_dump(count($spec)); diff --git a/ext/standard/tests/general_functions/proc_open_pipes2.phpt b/ext/standard/tests/general_functions/proc_open_pipes2.phpt index c147a2f37646..140ff699ca9d 100644 --- a/ext/standard/tests/general_functions/proc_open_pipes2.phpt +++ b/ext/standard/tests/general_functions/proc_open_pipes2.phpt @@ -6,7 +6,7 @@ proc_open() with no pipes $spec = array(); $php = getenv("TEST_PHP_EXECUTABLE_ESCAPED"); -$callee = escapeshellarg(__DIR__ . "/proc_open_pipes_sleep.inc"); +$callee = escapeshellarg(__DIR__ . "/proc_open_pipes_exit.inc"); proc_open("$php -n $callee", $spec, $pipes); var_dump(count($spec)); diff --git a/ext/standard/tests/general_functions/proc_open_pipes3.phpt b/ext/standard/tests/general_functions/proc_open_pipes3.phpt index 8f065401eb14..58be70d631c7 100644 --- a/ext/standard/tests/general_functions/proc_open_pipes3.phpt +++ b/ext/standard/tests/general_functions/proc_open_pipes3.phpt @@ -8,7 +8,7 @@ for ($i = 3; $i<= 5; $i++) { } $php = getenv("TEST_PHP_EXECUTABLE_ESCAPED"); -$callee = __DIR__ . "/proc_open_pipes_sleep.inc"; +$callee = __DIR__ . "/proc_open_pipes_exit.inc"; $callee_escaped = escapeshellarg($callee); $spec[$i] = array('pi'); diff --git a/ext/standard/tests/general_functions/proc_open_pipes_exit.inc b/ext/standard/tests/general_functions/proc_open_pipes_exit.inc new file mode 100644 index 000000000000..c79471759288 --- /dev/null +++ b/ext/standard/tests/general_functions/proc_open_pipes_exit.inc @@ -0,0 +1,2 @@ + --EXPECT-- bool(true) diff --git a/ext/standard/tests/general_functions/proc_open_sockets2.inc b/ext/standard/tests/general_functions/proc_open_sockets2.inc index e9a6c9e33dc4..d92310bb8791 100644 --- a/ext/standard/tests/general_functions/proc_open_sockets2.inc +++ b/ext/standard/tests/general_functions/proc_open_sockets2.inc @@ -1,7 +1,8 @@ Date: Tue, 28 Jul 2026 20:30:11 +0700 Subject: [PATCH 17/26] Use a bounded passthru binary fixture --- ext/standard/tests/file/bug22414.phpt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ext/standard/tests/file/bug22414.phpt b/ext/standard/tests/file/bug22414.phpt index c0369eef17ee..a5e119d9b75b 100644 --- a/ext/standard/tests/file/bug22414.phpt +++ b/ext/standard/tests/file/bug22414.phpt @@ -9,8 +9,8 @@ output_handler= --FILE-- '.escapeshellarg($tmpfile); } else { @@ -28,12 +35,13 @@ output_handler= } exec($cmd); - if (md5_file($php) == md5_file($tmpfile)) { + if (md5_file($source) == md5_file($tmpfile)) { echo "Works\n"; } else { echo "Does not work\n"; } + @unlink($source); @unlink($tmpfile); ?> --EXPECT-- From 08998c665ba5e2736a6dd49c1c6ea3a710ff7560 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:30:11 +0700 Subject: [PATCH 18/26] Parallelize OpenSSL tests safely --- ext/openssl/tests/MAX_CONCURRENCY | 1 + ext/openssl/tests/{CONFLICTS => SCOPED_CONFLICTS} | 0 ext/openssl/tests/ServerClientTestCase.inc | 2 +- ext/openssl/tests/gh20802.phpt | 4 ++-- ext/openssl/tests/openssl_cms_verify_der.phpt | 4 ++-- .../tests/openssl_pkcs12_export_to_file_error.phpt | 4 ++-- .../tests/session_resumption_import_export_session.phpt | 4 ++-- .../tests/session_resumption_invalid_session_import.phpt | 4 ---- ..._resumption_server_external_with_context_id_tls12.phpt | 4 ++-- ext/openssl/tests/stream_server_reneg_limit.phpt | 8 ++++---- 10 files changed, 16 insertions(+), 19 deletions(-) create mode 100644 ext/openssl/tests/MAX_CONCURRENCY rename ext/openssl/tests/{CONFLICTS => SCOPED_CONFLICTS} (100%) diff --git a/ext/openssl/tests/MAX_CONCURRENCY b/ext/openssl/tests/MAX_CONCURRENCY new file mode 100644 index 000000000000..f599e28b8ab0 --- /dev/null +++ b/ext/openssl/tests/MAX_CONCURRENCY @@ -0,0 +1 @@ +10 diff --git a/ext/openssl/tests/CONFLICTS b/ext/openssl/tests/SCOPED_CONFLICTS similarity index 100% rename from ext/openssl/tests/CONFLICTS rename to ext/openssl/tests/SCOPED_CONFLICTS diff --git a/ext/openssl/tests/ServerClientTestCase.inc b/ext/openssl/tests/ServerClientTestCase.inc index b7e866954fcf..0c3c49db7259 100644 --- a/ext/openssl/tests/ServerClientTestCase.inc +++ b/ext/openssl/tests/ServerClientTestCase.inc @@ -24,7 +24,7 @@ function phpt_has_sslv3() { if (!is_null($result)) { return $result; } - $server = @stream_socket_server('sslv3://127.0.0.1:10013'); + $server = @stream_socket_server('sslv3://127.0.0.1:0'); if ($result = !!$server) { fclose($server); } diff --git a/ext/openssl/tests/gh20802.phpt b/ext/openssl/tests/gh20802.phpt index 786679c41c6a..e078f461fc44 100644 --- a/ext/openssl/tests/gh20802.phpt +++ b/ext/openssl/tests/gh20802.phpt @@ -26,7 +26,7 @@ $serverCode = <<<'CODE' ] ] ]); - $server = stream_socket_server('tls://127.0.0.1:12443', $errno, $errstr, $flags, $ctx); + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); phpt_notify_server_start($server); stream_socket_accept($server, 3); CODE; @@ -42,7 +42,7 @@ $ctx = stream_context_create([ 'verify_peer' => false ] ]); - @stream_socket_client("tls://127.0.0.1:12443", $errno, $errstr, 1, $flags, $ctx); + @stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 1, $flags, $ctx); CODE; include 'CertificateGenerator.inc'; diff --git a/ext/openssl/tests/openssl_cms_verify_der.phpt b/ext/openssl/tests/openssl_cms_verify_der.phpt index b7267cbdfa33..84e1b70c9f2b 100644 --- a/ext/openssl/tests/openssl_cms_verify_der.phpt +++ b/ext/openssl/tests/openssl_cms_verify_der.phpt @@ -14,7 +14,7 @@ if ($contentfile === false) { die("failed to get a temporary filename!"); } -$pkcsfile = __DIR__ . "/openssl_cms_verify__pkcsfile.tmp"; +$pkcsfile = __DIR__ . "/openssl_cms_verify_der__pkcsfile.tmp"; $eml = __DIR__ . "/signed.eml"; $wrong = "wrong"; $empty = ""; @@ -45,7 +45,7 @@ if (file_exists($contentfile)) { ?> --CLEAN-- --EXPECT-- bool(false) diff --git a/ext/openssl/tests/openssl_pkcs12_export_to_file_error.phpt b/ext/openssl/tests/openssl_pkcs12_export_to_file_error.phpt index d98f6d15fef1..d95f9a1b3cfd 100644 --- a/ext/openssl/tests/openssl_pkcs12_export_to_file_error.phpt +++ b/ext/openssl/tests/openssl_pkcs12_export_to_file_error.phpt @@ -4,7 +4,7 @@ openssl_pkcs12_export_to_file() error tests openssl --FILE-- --CLEAN-- --FILE-- run($clientCode, $serverCode); ?> --CLEAN-- --EXPECTF-- string(%d) "-----BEGIN SSL SESSION PARAMETERS----- diff --git a/ext/openssl/tests/session_resumption_invalid_session_import.phpt b/ext/openssl/tests/session_resumption_invalid_session_import.phpt index a9c5b65f2051..efd49e7c693b 100644 --- a/ext/openssl/tests/session_resumption_invalid_session_import.phpt +++ b/ext/openssl/tests/session_resumption_invalid_session_import.phpt @@ -15,9 +15,5 @@ try { echo $e->getMessage() . "\n"; } ?> ---CLEAN-- - --EXPECT-- Failed to import session data diff --git a/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt b/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt index bdc3d2ce1bf6..12d5a13c3755 100644 --- a/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt +++ b/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt @@ -8,7 +8,7 @@ if (!function_exists("proc_open")) die("skip no proc_open"); ?> --FILE-- run($clientCode, $serverCode); ?> --CLEAN-- --EXPECTF-- Client first connection resumed: no diff --git a/ext/openssl/tests/stream_server_reneg_limit.phpt b/ext/openssl/tests/stream_server_reneg_limit.phpt index d84906c81ca7..a9e147404690 100644 --- a/ext/openssl/tests/stream_server_reneg_limit.phpt +++ b/ext/openssl/tests/stream_server_reneg_limit.phpt @@ -25,7 +25,7 @@ $certFile = __DIR__ . DIRECTORY_SEPARATOR . 'stream_server_reneg_limit.pem.tmp'; $serverCode = <<<'CODE' $printed = false; - $serverUri = "ssl://127.0.0.1:64321"; + $serverUri = "ssl://127.0.0.1:0"; $serverFlags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; $serverCtx = stream_context_create(['ssl' => [ 'local_cert' => '%s', @@ -42,7 +42,7 @@ $serverCode = <<<'CODE' ]]); $server = stream_socket_server($serverUri, $errno, $errstr, $serverFlags, $serverCtx); - phpt_notify(); + phpt_notify(message: stream_socket_get_name($server, false)); $clients = []; while (1) { @@ -71,9 +71,9 @@ CODE; $serverCode = sprintf($serverCode, $certFile); $clientCode = <<<'CODE' - phpt_wait(); + $addr = trim(phpt_wait()); - $cmd = 'openssl s_client -connect 127.0.0.1:64321'; + $cmd = 'openssl s_client -connect ' . escapeshellarg($addr); $descriptorSpec = [["pipe", "r"], ["pipe", "w"], ["pipe", "w"]]; $process = proc_open($cmd, $descriptorSpec, $pipes); From 9df6d855cc6304f35d35b5fb78683f5df41e9219 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:30:32 +0700 Subject: [PATCH 19/26] Bypass the shell for test subprocesses --- run-tests.php | 162 +++++++++++++++++++++++--- tests/basic/req60524-win.phpt | 2 +- tests/run-test/clean_environment.phpt | 16 +++ 3 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 tests/run-test/clean_environment.phpt diff --git a/run-tests.php b/run-tests.php index fa3bb16588c6..8a0704610db2 100755 --- a/run-tests.php +++ b/run-tests.php @@ -149,7 +149,7 @@ function main(): void $ignored_by_ext, $ini_overwrites, $colorize, $cli_opcache_enabled, $log_format, $no_clean, $no_file_cache, - $pass_options, $php, $php_cgi, $preload, + $pass_options, $pass_options_args, $php, $php_cgi, $preload, $result_tests_file, $slow_min_ms, $start_time, $temp_source, $temp_target, $test_cnt, $test_files, $test_idx, $test_results, $testfile, @@ -343,6 +343,7 @@ function main(): void $failed_tests_file = false; $pass_option_n = false; $pass_options = ''; + $pass_options_args = []; $output_file = INIT_DIR . '/php_test_results_' . date('Ymd_Hi') . '.txt'; @@ -490,11 +491,13 @@ function main(): void case 'n': if (!$pass_option_n) { $pass_options .= ' -n'; + $pass_options_args[] = '-n'; } $pass_option_n = true; break; case 'e': $pass_options .= ' -e'; + $pass_options_args[] = '-e'; break; case '--preload': $preload = true; @@ -711,8 +714,13 @@ function main(): void if ($conf_passed !== null) { if (IS_WINDOWS) { $pass_options .= " -c " . escapeshellarg($conf_passed); + $pass_options_args[] = '-c'; + $pass_options_args[] = $conf_passed; } else { - $pass_options .= " -c '" . realpath($conf_passed) . "'"; + $configurationFile = realpath($conf_passed); + $pass_options .= " -c '" . $configurationFile . "'"; + $pass_options_args[] = '-c'; + $pass_options_args[] = (string) $configurationFile; } } @@ -1273,19 +1281,20 @@ function error_report(string $testname, string $logname, string $tested): void * @return false|string */ function system_with_timeout( - string $commandline, + string|array $commandline, ?array $env = null, ?string $stdin = null, bool $captureStdIn = true, bool $captureStdOut = true, - bool $captureStdErr = true + bool $captureStdErr = true, + bool $mergeStdErr = false ) { global $valgrind; // when proc_open cmd is passed as a string (without bypass_shell=true option) the cmd goes thru shell // and on Windows quotes are discarded, this is a fix to honor the quotes and allow values containing // spaces like '"C:\Program Files\PHP\php.exe"' to be passed as 1 argument correctly - if (IS_WINDOWS) { + if (IS_WINDOWS && is_string($commandline)) { $commandline = 'start "" /b /wait ' . $commandline . ' & exit'; } @@ -1304,7 +1313,9 @@ function system_with_timeout( $descriptorspec[1] = ['pipe', 'w']; } if ($captureStdErr) { - $descriptorspec[2] = ['pipe', 'w']; + $descriptorspec[2] = $mergeStdErr + ? ['redirect', 1] + : ['pipe', 'w']; } $proc = proc_open($commandline, $descriptorspec, $pipes, TEST_PHP_SRCDIR, $bin_env, ['suppress_errors' => true]); @@ -1652,6 +1663,49 @@ function has_only_fork_safe_ini_settings(TestFile $test): bool return true; } +function can_run_with_structured_test_command(TestFile $test): bool +{ + global $preload, $valgrind; + + return !$valgrind + && !$preload + && !$test->hasAnySections( + 'ARGS', + 'CAPTURE_STDIO', + 'DEFLATE_POST', + 'GZIP_POST', + 'POST', + 'POST_RAW', + 'PUT', + ); +} + +function create_structured_test_command( + string $php, + array $sapiOptionArgs, + array $passOptionArgs, + array $iniSettings, + string $testFile, + int $numRepeats +): array { + $command = [ + $php, + ...$sapiOptionArgs, + ...$passOptionArgs, + ]; + if ($numRepeats > 1) { + $command[] = '--repeat'; + $command[] = (string) $numRepeats; + } + + return [ + ...$command, + ...settings2arguments($iniSettings), + '-f', + $testFile, + ]; +} + function test_fork_server_cache_key( string|array $command, array $env, @@ -2391,7 +2445,7 @@ function skip_test(string $tested, string $tested_file, string $shortname, strin function run_test(string $php, $file, array $env): string { global $log_format, $ini_overwrites, $PHP_FAILED_TESTS; - global $pass_options, $DETAILED, $IN_REDIRECT, $test_cnt, $test_idx; + global $pass_options, $pass_options_args, $DETAILED, $IN_REDIRECT, $test_cnt, $test_idx; global $valgrind, $temp_source, $temp_target, $cfg, $environment; global $no_clean; global $SHOW_ONLY_GROUPS; @@ -2413,6 +2467,9 @@ function run_test(string $php, $file, array $env): string $skipCache = new SkipCache($enableSkipCache, $cfg['keep']['skip']); } + $originalPhpExecutable = $php; + $phpExecutable = $php; + $sapiOptionArgs = []; $php = escapeshellarg($php); $orig_php = $php; @@ -2484,6 +2541,8 @@ function run_test(string $php, $file, array $env): string if (!$php_cgi) { return skip_test($tested, $tested_file, $shortname, 'CGI not available'); } + $phpExecutable = $php_cgi; + $sapiOptionArgs[] = '-C'; $php = escapeshellarg($php_cgi) . ' -C '; $uses_cgi = true; if ($num_repeats > 1) { @@ -2493,13 +2552,17 @@ function run_test(string $php, $file, array $env): string /* For phpdbg tests, check if phpdbg sapi is available and if it is, use it. */ $extra_options = ''; + $extraOptionArgs = []; if ($test->hasSection('PHPDBG')) { if (isset($phpdbg)) { + $phpExecutable = $phpdbg; + $sapiOptionArgs[] = '-qIb'; $php = escapeshellarg($phpdbg) . ' -qIb'; // Additional phpdbg command line options for sections that need to // be run straight away. For example, EXTENSIONS, SKIPIF, CLEAN. $extra_options = '-rr'; + $extraOptionArgs[] = '-rr'; } else { return skip_test($tested, $tested_file, $shortname, 'phpdbg not available'); } @@ -2653,6 +2716,7 @@ function run_test(string $php, $file, array $env): string //$ini_overwrites[] = 'setting=value'; settings2array($ini_overwrites, $ini_settings); + $orig_ini_settings_args = settings2arguments($ini_settings); $orig_ini_settings = settings2params($ini_settings); if ($file_cache !== null) { @@ -2700,6 +2764,7 @@ function run_test(string $php, $file, array $env): string } } + $testIniSettings = $ini_settings; $ini_settings = settings2params($ini_settings); $env['TEST_PHP_EXTRA_ARGS'] = $pass_options . ' ' . $ini_settings; @@ -2710,8 +2775,6 @@ function run_test(string $php, $file, array $env): string if ($test->sectionNotEmpty('SKIPIF')) { show_file_block('skip', $test->getSection('SKIPIF')); - $extra = !IS_WINDOWS ? - "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : ""; if ($valgrind) { $env['USE_ZEND_ALLOC'] = '0'; @@ -2721,7 +2784,22 @@ function run_test(string $php, $file, array $env): string $junit->startTimer($shortname); $startTime = microtime(true); - $commandLine = "$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache -d display_errors=1 -d display_startup_errors=0"; + $commandLine = [ + $phpExecutable, + ...$sapiOptionArgs, + ...$pass_options_args, + ...$extraOptionArgs, + '-q', + ...$orig_ini_settings_args, + '-d', + 'opcache.file_cache=', + '-d', + 'opcache.file_cache_only=0', + '-d', + 'display_errors=1', + '-d', + 'display_startup_errors=0', + ]; $output = $skipCache->checkSkip( $commandLine, $test->getSection('SKIPIF'), @@ -3081,7 +3159,26 @@ function run_test(string $php, $file, array $env): string ); } if ($out === null) { - $out = system_with_timeout($cmd, $env, $stdin, $captureStdIn, $captureStdOut, $captureStdErr); + $useStructuredCommand = can_run_with_structured_test_command($test); + $testCommand = $useStructuredCommand + ? create_structured_test_command( + $phpExecutable, + $sapiOptionArgs, + $pass_options_args, + $testIniSettings, + $test_file, + $num_repeats, + ) + : $cmd; + $out = system_with_timeout( + $testCommand, + $env, + $stdin, + $captureStdIn, + $captureStdOut, + $captureStdErr, + $useStructuredCommand && $captureStdOut && $captureStdErr, + ); } $junit->stopTimer($shortname); @@ -3104,9 +3201,16 @@ function run_test(string $php, $file, array $env): string save_text($test_clean, trim($test->getSection('CLEAN')), $temp_clean); if (!$no_clean) { - $extra = !IS_WINDOWS ? - "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : ""; - $cleanCommand = "$extra $orig_php $pass_options -q $orig_ini_settings $no_file_cache"; + $cleanCommand = [ + $originalPhpExecutable, + ...$pass_options_args, + '-q', + ...$orig_ini_settings_args, + '-d', + 'opcache.file_cache=', + '-d', + 'opcache.file_cache_only=0', + ]; $cleanEnv = $env; if (!IS_WINDOWS) { unset( @@ -3129,7 +3233,8 @@ function run_test(string $php, $file, array $env): string ); } if ($clean_output === null) { - $clean_output = system_with_timeout("$cleanCommand \"$test_clean\"", $cleanEnv); + $cleanCommand[] = $test_clean; + $clean_output = system_with_timeout($cleanCommand, $cleanEnv); } } @@ -3641,6 +3746,20 @@ function settings2params(array $ini_settings): string return $settings; } +function settings2arguments(array $ini_settings): array +{ + $arguments = []; + + foreach ($ini_settings as $name => $value) { + foreach ((array) $value as $item) { + $arguments[] = '-d'; + $arguments[] = "$name=$item"; + } + } + + return $arguments; +} + function compute_summary(): void { global $n_total, $test_results, $ignored_by_ext, $sum_results, $percent_results; @@ -4250,7 +4369,7 @@ public function __construct(bool $enable, bool $keepFile) } public function checkSkip( - string $php, + string|array $command, string $code, string $checkFile, string $tempFile, @@ -4261,7 +4380,7 @@ public function checkSkip( // Extension tests frequently use something like $dir"; + $key = (is_array($command) ? implode("\0", $command) : $command) . " => $dir"; if (isset($this->skips[$key][$code])) { $this->hits++; @@ -4275,7 +4394,7 @@ public function checkSkip( $result = null; if ($useForkServer) { $result = system_with_test_fork_server( - $php, + $command, $checkFile, $env, channel: 'skip', @@ -4285,7 +4404,12 @@ public function checkSkip( ); } if ($result === null) { - $result = system_with_timeout("$php \"$checkFile\"", $env); + if (is_array($command)) { + $command[] = $checkFile; + } else { + $command .= " \"$checkFile\""; + } + $result = system_with_timeout($command, $env); } $result = trim($result); if (strpos($result, 'nocache') === 0) { diff --git a/tests/basic/req60524-win.phpt b/tests/basic/req60524-win.phpt index 26fa9d9c5c7b..d75eb8c7dcf0 100644 --- a/tests/basic/req60524-win.phpt +++ b/tests/basic/req60524-win.phpt @@ -10,4 +10,4 @@ if(PHP_OS_FAMILY !== "Windows") --FILE-- --EXPECT-- -C:\\Windows +C:\Windows diff --git a/tests/run-test/clean_environment.phpt b/tests/run-test/clean_environment.phpt new file mode 100644 index 000000000000..01d1cabff61b --- /dev/null +++ b/tests/run-test/clean_environment.phpt @@ -0,0 +1,16 @@ +--TEST-- +CLEAN does not inherit request environment variables on POSIX +--FILE-- + +--CLEAN-- + +--EXPECT-- From 85a4ec51fd42c26155d7337378fef36b2fe23917 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:30:46 +0700 Subject: [PATCH 20/26] Run Curl tests with bounded concurrency --- ext/curl/tests/CONFLICTS | 1 - ext/curl/tests/MAX_CONCURRENCY | 1 + ext/curl/tests/bug48203_multi.phpt | 4 ++-- ext/curl/tests/bug54798-unix.phpt | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 ext/curl/tests/CONFLICTS create mode 100644 ext/curl/tests/MAX_CONCURRENCY diff --git a/ext/curl/tests/CONFLICTS b/ext/curl/tests/CONFLICTS deleted file mode 100644 index 0702cb5bfbb0..000000000000 --- a/ext/curl/tests/CONFLICTS +++ /dev/null @@ -1 +0,0 @@ -all diff --git a/ext/curl/tests/MAX_CONCURRENCY b/ext/curl/tests/MAX_CONCURRENCY new file mode 100644 index 000000000000..b8626c4cff28 --- /dev/null +++ b/ext/curl/tests/MAX_CONCURRENCY @@ -0,0 +1 @@ +4 diff --git a/ext/curl/tests/bug48203_multi.phpt b/ext/curl/tests/bug48203_multi.phpt index ec9748517ab8..db8cfde6a548 100644 --- a/ext/curl/tests/bug48203_multi.phpt +++ b/ext/curl/tests/bug48203_multi.phpt @@ -13,7 +13,7 @@ if (curl_version()['version_number'] === 0x080a00) { --CLEAN-- - + --EXPECTF-- Warning: curl_multi_add_handle(): CURLOPT_STDERR resource has gone away, resetting to stderr in %s on line %d %A diff --git a/ext/curl/tests/bug54798-unix.phpt b/ext/curl/tests/bug54798-unix.phpt index 6bbca3375ed5..1634e39c8b30 100644 --- a/ext/curl/tests/bug54798-unix.phpt +++ b/ext/curl/tests/bug54798-unix.phpt @@ -12,7 +12,7 @@ if(substr(PHP_OS, 0, 3) == 'WIN' ) { --CLEAN-- - + --EXPECTF-- %a %aOk for CURLOPT_STDERR From 62e48712866b3099f1c320c3193904da7486fb09 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:30:46 +0700 Subject: [PATCH 21/26] Isolate open_basedir test fixtures --- tests/security/MAX_CONCURRENCY | 1 + .../security/{CONFLICTS => SCOPED_CONFLICTS} | 0 tests/security/open_basedir.inc | 29 +++++++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 tests/security/MAX_CONCURRENCY rename tests/security/{CONFLICTS => SCOPED_CONFLICTS} (100%) diff --git a/tests/security/MAX_CONCURRENCY b/tests/security/MAX_CONCURRENCY new file mode 100644 index 000000000000..f599e28b8ab0 --- /dev/null +++ b/tests/security/MAX_CONCURRENCY @@ -0,0 +1 @@ +10 diff --git a/tests/security/CONFLICTS b/tests/security/SCOPED_CONFLICTS similarity index 100% rename from tests/security/CONFLICTS rename to tests/security/SCOPED_CONFLICTS diff --git a/tests/security/open_basedir.inc b/tests/security/open_basedir.inc index 21338ef1cf55..abb2b3140812 100644 --- a/tests/security/open_basedir.inc +++ b/tests/security/open_basedir.inc @@ -40,7 +40,7 @@ function recursive_delete_directory($directory) { if ($item != '.') { if ($item != '..') { $path = ($directory.'/'.$item); - if (is_dir($path) == TRUE) { + if (is_dir($path) == TRUE && !is_link($path)) { recursive_delete_directory($path); } else { @chmod($path, 0777); @@ -57,9 +57,24 @@ function recursive_delete_directory($directory) { return TRUE; } +function open_basedir_test_directory() { + foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { + if (!isset($frame["file"]) || $frame["file"] === __FILE__) { + continue; + } + + $test = preg_replace('/(?:\.clean)?\.php$/', '', basename($frame["file"])); + return __DIR__."/open_basedir_test_".$test; + } + + return __DIR__."/open_basedir_test"; +} + function create_directories() { delete_directories(); - $directory = getcwd(); + $directory = open_basedir_test_directory(); + mkdir($directory); + chdir($directory); var_dump(mkdir($directory."/test")); var_dump(mkdir($directory."/test/ok")); @@ -69,8 +84,7 @@ function create_directories() { } function delete_directories() { - $directory = (getcwd()."/test"); - recursive_delete_directory($directory); + recursive_delete_directory(open_basedir_test_directory()); } function test_open_basedir_error($function) { @@ -87,12 +101,15 @@ function test_open_basedir_error($function) { } function test_open_basedir_before($function, $change = TRUE) { - global $savedDirectory; + global $initdir, $savedDirectory; echo "*** Testing open_basedir configuration [$function] ***\n"; + create_directories(); $directory = getcwd(); + if (isset($initdir)) { + $initdir = $directory; + } $savedDirectory = $directory; var_dump(chdir($directory)); - create_directories(); // Optionally change directory if ($change == TRUE) { From cc7a95ee12c47540ba1a8ef2a026614aed2c2ec4 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Tue, 28 Jul 2026 20:30:46 +0700 Subject: [PATCH 22/26] Skip unavailable Unix datagram socket tests --- ext/sockets/tests/bug76839.phpt | 5 +-- .../socket_sendmsg_scm_rights_object.phpt | 1 + ext/sockets/tests/unix_dgram_skipif.inc | 31 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 ext/sockets/tests/unix_dgram_skipif.inc diff --git a/ext/sockets/tests/bug76839.phpt b/ext/sockets/tests/bug76839.phpt index 0b681e791a3b..a34c68d2cda3 100644 --- a/ext/sockets/tests/bug76839.phpt +++ b/ext/sockets/tests/bug76839.phpt @@ -7,6 +7,7 @@ sockets if (strtolower(substr(PHP_OS, 0, 3)) === 'win') { die('skip not valid for Windows.'); } +require __DIR__ . '/unix_dgram_skipif.inc'; ?> --FILE-- --FILE-- $error !== null); + if (in_array($error, $unavailableErrors, true)) { + die('skip unable to bind an AF_UNIX datagram socket'); + } + die('unexpected AF_UNIX datagram bind failure: ' . socket_strerror($error)); +} +socket_close($socket); +unlink($socketPath); From 2b975e6e3c8e5f61f71b18c4ee84ffd30e6025a4 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 29 Jul 2026 00:57:06 +0700 Subject: [PATCH 23/26] Fix shmop test leaking private shared memory --- ext/shmop/tests/shmop_open_private.phpt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ext/shmop/tests/shmop_open_private.phpt b/ext/shmop/tests/shmop_open_private.phpt index 8757de0a6334..0ba74e5af65c 100644 --- a/ext/shmop/tests/shmop_open_private.phpt +++ b/ext/shmop/tests/shmop_open_private.phpt @@ -13,6 +13,8 @@ $shm2 = shmop_open(0, 'c', 0777, 1024); $read = shmop_read($shm2, 0, 4); var_dump(is_string($read) && $read !== $write); +shmop_delete($shm1); +shmop_delete($shm2); ?> --EXPECT-- bool(true) From 69fc4b2bcfbee3e05d717424665dc261001eee97 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 29 Jul 2026 00:58:08 +0700 Subject: [PATCH 24/26] Fix GH-16591 test leaking shared memory --- ext/sysvshm/tests/gh16591.phpt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ext/sysvshm/tests/gh16591.phpt b/ext/sysvshm/tests/gh16591.phpt index d3ece7cd796a..fb1c58168f57 100644 --- a/ext/sysvshm/tests/gh16591.phpt +++ b/ext/sysvshm/tests/gh16591.phpt @@ -13,11 +13,15 @@ class C { } } -$mem = shm_attach(1); +$key = ftok(__FILE__, 't'); +$mem = shm_attach($key); +$cleanup = shm_attach($key); try { shm_put_var($mem, 1, new C); } catch (Error $e) { echo $e->getMessage(), "\n"; +} finally { + shm_remove($cleanup); } ?> From a3df7ecd361a75b1b4c23c5578b5042aaaa5fc0e Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 29 Jul 2026 02:01:42 +0700 Subject: [PATCH 25/26] Clean up IPC objects in arginfo mismatch test --- Zend/tests/arginfo_zpp_mismatch.phpt | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Zend/tests/arginfo_zpp_mismatch.phpt b/Zend/tests/arginfo_zpp_mismatch.phpt index d7aefc6f374f..94f6b070900e 100644 --- a/Zend/tests/arginfo_zpp_mismatch.phpt +++ b/Zend/tests/arginfo_zpp_mismatch.phpt @@ -10,6 +10,36 @@ if (getenv('SKIP_MSAN')) die("skip msan misses interceptors for some functions") require __DIR__ . "/arginfo_zpp_mismatch.inc"; +function testWeakSysvIpcFunction($function): bool { + if (!in_array($function, ['msg_queue_exists', 'msg_get_queue', 'sem_get', 'shm_attach'], true)) { + return false; + } + + for ($argumentCount = 0; $argumentCount <= 8; $argumentCount++) { + $arguments = array_fill(0, $argumentCount, null); + if ($function === 'msg_queue_exists' && $argumentCount >= 1) { + $arguments[0] = 1; + } + if (($function === 'sem_get' || $function === 'shm_attach') && $argumentCount >= 3) { + $arguments[2] = 0600; + } + + try { + $result = @$function(...$arguments); + if ($result instanceof SysvMessageQueue) { + msg_remove_queue($result); + } elseif ($result instanceof SysvSemaphore) { + sem_remove($result); + } elseif ($result instanceof SysvSharedMemory) { + shm_remove($result); + } + } catch (Throwable) { + } + } + + return true; +} + function test($function) { if (skipFunction($function)) { return; @@ -21,6 +51,10 @@ function test($function) { } else { echo "Testing " . get_class($function[0]) . "::$function[1]\n"; } + if (testWeakSysvIpcFunction($function)) { + ob_end_clean(); + return; + } try { @$function(); } catch (Throwable) { From 8c3fbe48a941bf444e0a5a0ec93ebb1dad016ac6 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Wed, 29 Jul 2026 02:03:20 +0700 Subject: [PATCH 26/26] Fix GH-16592 test leaking a message queue --- ext/sysvmsg/tests/gh16592.phpt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ext/sysvmsg/tests/gh16592.phpt b/ext/sysvmsg/tests/gh16592.phpt index 8490d000f891..f5b8119ef8d8 100644 --- a/ext/sysvmsg/tests/gh16592.phpt +++ b/ext/sysvmsg/tests/gh16592.phpt @@ -8,12 +8,14 @@ class Test { function __serialize() {} } -$q = msg_get_queue(1); +// use a private queue, so we only remove our own. +$q = msg_get_queue(0, 0600); try { msg_send($q, 1, new Test, true); } catch (\TypeError $e) { echo $e->getMessage(); } +msg_remove_queue($q); ?> --EXPECT-- Test::__serialize() must return an array