Commit 1d2ea5ce22c for php.net
commit 1d2ea5ce22cd941d1bbf073d08af2b04be99ecf3
Author: NickSdot <32384907+NickSdot@users.noreply.github.com>
Date: Mon Aug 10 16:33:43 2026 +0700
Run tests in parallel by default (#22939)
Runs the test suite in parallel by default; automatically selects up to 10 workers. Explicit -j1 remains the opt-out to run tests serial. Smaller batches improve balancing.
diff --git a/.gitignore b/.gitignore
index b76b5a787ca..769ac62cc5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -252,7 +252,7 @@ php
# Test results generated by `./run-tests.php`
php_test_results_*.txt
-# Temporary test information generated by `./run-tests.php`
+# Temporary test information generated by `./run-tests.php` (kept for BC; now lives in tmp dir)
/run-test-info.php
# Temporary POST data placeholder files generated by `./run-tests.php`
diff --git a/NEWS b/NEWS
index 8ea376a2c24..ae9a19c0cc8 100644
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,9 @@ PHP NEWS
operation depth. (iliaal)
- Core:
+ . Changed run-tests.php to run in parallel by default, using up to 10
+ automatically detected workers. Pass -j1 for sequential execution.
+ (NickSdot)
. Changed run-tests.php to run test subprocesses without a shell where
possible. (NickSdot)
. Fixed GH-23083 (SEGV build_trace_args in zend_exceptions.c with
diff --git a/README.md b/README.md
index d83203d74c0..80618f2d6f9 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/UPGRADING.INTERNALS b/UPGRADING.INTERNALS
index 28cefb1e0af..3c0d3b4f80f 100644
--- a/UPGRADING.INTERNALS
+++ b/UPGRADING.INTERNALS
@@ -223,6 +223,9 @@ PHP 8.6 INTERNALS UPGRADE NOTES
========================
- Abstract:
+ . run-tests.php now runs in parallel by default, using up to 10 automatically
+ detected workers. Pass -j1 for sequential execution. --asan, --msan, and
+ Valgrind default to at most two workers.
. Minimum required PHP version found on the host system for running scripts
like build/gen_stub.php during development has been updated from 7.4 to 8.1.
. build/gen_stub.php may now generate a _decl.h file in addition to
diff --git a/docs/source/miscellaneous/writing-tests.rst b/docs/source/miscellaneous/writing-tests.rst
index 8e17674ae48..4339e042e6a 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 ``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
<https://wiki.php.net/doc/articles/writing-tests>`_.
diff --git a/ext/gd/tests/createfromwbmp2.phpt b/ext/gd/tests/createfromwbmp2.phpt
index 4608c861323..7007ff545d7 100644
--- a/ext/gd/tests/createfromwbmp2.phpt
+++ b/ext/gd/tests/createfromwbmp2.phpt
@@ -8,7 +8,7 @@
?>
--FILE--
<?php
-$filename = __DIR__ . '/_tmp.wbmp';
+$filename = __DIR__ . '/_tmp_createfromwbmp2.wbmp';
$fp = fopen($filename,"wb");
if (!$fp) {
exit("Failed to create <$filename>");
diff --git a/ext/gd/tests/createfromwbmp2_extern.phpt b/ext/gd/tests/createfromwbmp2_extern.phpt
index 68895f9a357..711f2e8ca3e 100644
--- a/ext/gd/tests/createfromwbmp2_extern.phpt
+++ b/ext/gd/tests/createfromwbmp2_extern.phpt
@@ -4,7 +4,7 @@
gd
--FILE--
<?php
-$filename = __DIR__ . '/_tmp.wbmp';
+$filename = __DIR__ . '/_tmp_createfromwbmp2_extern.wbmp';
$fp = fopen($filename,"wb");
if (!$fp) {
exit("Failed to create <$filename>");
@@ -41,4 +41,4 @@
--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 9040c5565f8..be7f92dccb9 100644
--- a/ext/zip/tests/oo_addglob_leak.phpt
+++ b/ext/zip/tests/oo_addglob_leak.phpt
@@ -12,7 +12,7 @@
$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 @@
<?php
$dirname = __DIR__ . '/';
include $dirname . 'utils.inc';
-rmdir_rf(__DIR__ . '/__tmp_oo_addglob2/');
+rmdir_rf(__DIR__ . '/__tmp_oo_addglob_leak/');
?>
--EXPECTF--
array(1) {
diff --git a/ext/zlib/tests/readgzfile_basic.phpt b/ext/zlib/tests/readgzfile_basic.phpt
index ae829406e0d..cf579879e2f 100644
--- a/ext/zlib/tests/readgzfile_basic.phpt
+++ b/ext/zlib/tests/readgzfile_basic.phpt
@@ -10,7 +10,7 @@
for all languages
EOT;
-$dirname = 'readgzfile_temp';
+$dirname = 'readgzfile_basic_temp';
$filename = $dirname.'/readgzfile_basic.txt.gz';
mkdir($dirname);
$h = gzopen($filename, 'w');
@@ -19,9 +19,11 @@
var_dump(readgzfile( $filename ) );
-
-unlink($filename);
-rmdir($dirname);
+?>
+--CLEAN--
+<?php
+@unlink('readgzfile_basic_temp/readgzfile_basic.txt.gz');
+@rmdir('readgzfile_basic_temp');
?>
--EXPECT--
hello world
diff --git a/ext/zlib/tests/readgzfile_basic2.phpt b/ext/zlib/tests/readgzfile_basic2.phpt
index ff19bcd35e9..6edb4c85784 100644
--- a/ext/zlib/tests/readgzfile_basic2.phpt
+++ b/ext/zlib/tests/readgzfile_basic2.phpt
@@ -10,7 +10,7 @@
for all languages
EOT;
-$dirname = 'readgzfile_temp';
+$dirname = 'readgzfile_basic2_temp';
$filename = $dirname.'/readgzfile_basic2.txt';
mkdir($dirname);
$h = fopen($filename, 'w');
@@ -19,9 +19,11 @@
var_dump(readgzfile( $filename ) );
-
-unlink($filename);
-rmdir($dirname);
+?>
+--CLEAN--
+<?php
+@unlink('readgzfile_basic2_temp/readgzfile_basic2.txt');
+@rmdir('readgzfile_basic2_temp');
?>
--EXPECT--
hello world
diff --git a/run-tests.php b/run-tests.php
index 6eed6649aec..ce6bd94db1e 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<workers> Run up to <workers> simultaneous testing processes in parallel for
- quicker testing on systems with multiple logical processors.
- Note that this is experimental feature.
+ -j<workers> Run up to <workers> simultaneous testing processes. By default,
+ the worker count is detected automatically. Use -j1 to run
+ tests sequentially.
-l <file> Read the testfiles to be executed from <file>. After the test
has finished all failed tests are written to the same <file>.
@@ -356,6 +356,7 @@ function main(): void
$shuffle = false;
$bless = false;
$workers = null;
+ $workersExplicit = false;
$context_line_count = 3;
$num_repeats = 1;
$show_progress = true;
@@ -417,6 +418,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");
@@ -646,6 +648,17 @@ function main(): void
}
}
+ if (!$workersExplicit && (!$selected_tests || count($test_files) > 1)) {
+ $workers = get_default_worker_count();
+ if ($workers !== null
+ && ($valgrind !== null || isset($environment['SKIP_ASAN']))) {
+ $workers = min($workers, 2);
+ }
+ if ($workers !== null && !can_create_parallel_worker_socket()) {
+ $workers = null;
+ }
+ }
+
if ($online === null && !isset($environment['SKIP_ONLINE_TESTS'])) {
$online = false;
}
@@ -808,6 +821,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 = trim($workerCount);
+ if (preg_match('/^[0-9]+$/D', $workerCount) !== 1) {
+ return null;
+ }
+
+ $workerCount = (int) $workerCount;
+ return $workerCount >= 2 ? 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)) {
@@ -830,7 +890,7 @@ function write_information(array $user_tests, $phpdbg): void
$escaped_no_file_cache = escaped_shell_string_from($no_file_cache);
// Get info from php
- $info_file = __DIR__ . '/run-test-info.php';
+ $info_file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'run-test-info-' . getmypid() . '.php';
@unlink($info_file);
$php_info = '<?php echo "
PHP_SAPI : " , PHP_SAPI , "
@@ -1676,10 +1736,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;
diff --git a/sapi/cli/tests/010-2.phpt b/sapi/cli/tests/010-2.phpt
index 88fe1c832a1..ddf7315c829 100644
--- a/sapi/cli/tests/010-2.phpt
+++ b/sapi/cli/tests/010-2.phpt
@@ -12,7 +12,7 @@
$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 356b69bebf9..80758f368ab 100644
--- a/sapi/cli/tests/010.phpt
+++ b/sapi/cli/tests/010.phpt
@@ -14,7 +14,7 @@
$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 @@
--CLEAN--
<?php
@unlink(__DIR__."/010.test.php");
-@unlink(__DIR__."/010.test.txt");
+@unlink(__DIR__."/010-F.test.txt");
?>
--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 00000000000..5bf6df07365
--- /dev/null
+++ b/tests/run-test/automatic_worker_limit.phpt
@@ -0,0 +1,107 @@
+--TEST--
+Automatic worker detection is capped for regular and instrumented runs
+--SKIPIF--
+<?php
+if (PHP_OS_FAMILY === 'Windows') {
+ die('skip requires a POSIX shell');
+}
+?>
+--ENV--
+TEST_PHP_FORK_SERVER=0
+--FILE--
+<?php
+$root = __DIR__ . '/automatic_worker_limit_' . getmypid();
+$bin = $root . '/bin';
+$tests = $root . '/tests';
+mkdir($bin, recursive: true);
+mkdir($tests);
+
+$nproc = $bin . '/nproc';
+file_put_contents($nproc, "#!/bin/sh\nprintf '64\\n'\n");
+chmod($nproc, 0755);
+
+$testFiles = [];
+for ($i = 0; $i < 11; $i++) {
+ $testFiles[] = $file = $tests . "/$i.phpt";
+ file_put_contents($file, <<<PHPT
+ --TEST--
+ worker cap $i
+ --FILE--
+ <?php echo "ok\\n"; ?>
+ --EXPECT--
+ ok
+ PHPT);
+}
+
+$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;
+ }
+}
+
+$runTests = static function (array $arguments) use ($environment, $testFiles): array {
+ $process = proc_open(
+ [
+ getenv('TEST_PHP_EXECUTABLE'),
+ dirname(__DIR__, 2) . '/run-tests.php',
+ '-q',
+ '--no-progress',
+ ...$arguments,
+ ...$testFiles,
+ ],
+ [
+ 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), $output];
+};
+
+[$exitCode, $output] = $runTests([]);
+var_dump($exitCode);
+var_dump(str_contains($output, 'Spawning 10 workers...'));
+var_dump(str_contains($output, 'Spawning 11 workers...'));
+
+[$exitCode, $output] = $runTests(['--asan']);
+var_dump($exitCode);
+var_dump(str_contains($output, 'Spawning 2 workers...'));
+var_dump(str_contains($output, 'Spawning 10 workers...'));
+
+[$exitCode, $output] = $runTests(['--asan', '-j3']);
+var_dump($exitCode);
+var_dump(str_contains($output, 'Spawning 3 workers...'));
+?>
+--CLEAN--
+<?php
+foreach (glob(__DIR__ . '/automatic_worker_limit_*') ?: [] as $root) {
+ foreach (glob($root . '/tests/*.phpt') ?: [] as $file) {
+ unlink($file);
+ }
+ @unlink($root . '/bin/nproc');
+ @rmdir($root . '/tests');
+ @rmdir($root . '/bin');
+ @rmdir($root);
+}
+?>
+--EXPECT--
+int(0)
+bool(true)
+bool(false)
+int(0)
+bool(true)
+bool(false)
+int(0)
+bool(true)