Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ php
php_test_results_*.txt

# Temporary test information generated by `./run-tests.php`
/run-test-info.php
/run-test-info-*.php

# Temporary POST data placeholder files generated by `./run-tests.php`
phpt.*
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
5 changes: 5 additions & 0 deletions docs/source/miscellaneous/writing-tests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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>`_.
Expand Down
2 changes: 1 addition & 1 deletion ext/gd/tests/createfromwbmp2.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ gd
?>
--FILE--
<?php
$filename = __DIR__ . '/_tmp.wbmp';
$filename = __DIR__ . '/_tmp_createfromwbmp2.wbmp';
$fp = fopen($filename,"wb");
if (!$fp) {
exit("Failed to create <$filename>");
Expand Down
4 changes: 2 additions & 2 deletions ext/gd/tests/createfromwbmp2_extern.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ imagecreatefromwbmp with invalid wbmp
gd
--FILE--
<?php
$filename = __DIR__ . '/_tmp.wbmp';
$filename = __DIR__ . '/_tmp_createfromwbmp2_extern.wbmp';
$fp = fopen($filename,"wb");
if (!$fp) {
exit("Failed to create <$filename>");
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions ext/zip/tests/oo_addglob_leak.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -38,7 +38,7 @@ var_dump($zip->addGlob($dirname . 'bar.*', GLOB_BRACE, $options));
<?php
$dirname = __DIR__ . '/';
include $dirname . 'utils.inc';
rmdir_rf(__DIR__ . '/__tmp_oo_addglob2/');
rmdir_rf(__DIR__ . '/__tmp_oo_addglob_leak/');
?>
--EXPECTF--
array(1) {
Expand Down
76 changes: 70 additions & 6 deletions run-tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -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>.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -641,6 +643,19 @@ 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;
}
Expand Down Expand Up @@ -800,6 +815,55 @@ 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)) {
Expand All @@ -820,7 +884,7 @@ function write_information(array $user_tests, $phpdbg): void
$php_escaped = escapeshellarg($php);

// Get info from php
$info_file = __DIR__ . '/run-test-info.php';
$info_file = __DIR__ . '/run-test-info-' . getmypid() . '.php';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very rare; but hit it once so it cannot hurt. Can, for instance, happen when testing tests.

Just highlighting it because I moved this here after the first approval was made.

@unlink($info_file);
$php_info = '<?php echo "
PHP_SAPI : " , PHP_SAPI , "
Expand Down Expand Up @@ -1571,10 +1635,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)) {
Comment on lines -1574 to +1641

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unrelated to this change :)

@NickSdot NickSdot Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they are unrelated. Two reasons:

  1. Each commit on it's own cannot be a separate PR; they depend or interact with each other (though, not the case here).
  2. You didn't want me the spam all as separate PRs.

My idea was to group everything in ~10 sensible, logically grouped PRs (down from 26 commits). Hence, I wanted to combine these two here. Tried to reach you on Discord. Not sure what's the right balance, and split -- open for input!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I'm not often on Discord. And no worries.

As long as you don't open too many PRs at the same time it's not spamming. Splitting independent changes is convenient for reviews and ensures that each change is reviewed with the same attention. It also avoids blocking an entire PR if one of the changes is blocking.

For this particular change I think that it would be worth it to explore other solutions. I believe that the long tail is caused by slow tests being grouped together in the same batches (because related tests tend to have similar durations), so some batches are longer than others. Distributing tests in a more round robin fashion (or shuffling) would be an alternative way to fix the long tail problem without increasing communication overhead by reducing batch size.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Distributing tests in a more round robin fashion

I don't object. But allow me to add that I was thinking about duration based sharding as a follow up. Personally think that's the cleanest and (likely) will have the best results.

For this particular change I think that it would be worth it to explore other solutions. I believe that the long tail is caused by slow tests being grouped together in the same batches

Agreed. Commented this elsewhere already, this can optimised further.

Though, the question is, must this happen now or can/should it be a follow up? No strong opinion from my side. I, however, think the 1.8x gain this brings now is merge-worthy on it's own (without blocking later changes). If we then would decide for either, round robin or duration based sharding, it likely takes some time to get them discussed and right. This one is ready, and could make the suite faster starting tomorrow until a follow up lands.

But again, I am good with either and I leave that decision to you!

As long as you don't open too many PRs at the same time it's not spamming. Splitting independent changes is...

Roger that. I am on it.

foreach ($fileConflictsWith[$file] as $conflictKey) {
if (isset($activeConflicts[$conflictKey])) {
$waitingTests[$conflictKey][] = $file;
Expand Down
2 changes: 1 addition & 1 deletion sapi/cli/tests/010-2.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '
Expand Down
4 changes: 2 additions & 2 deletions sapi/cli/tests/010.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '
Expand All @@ -37,7 +37,7 @@ var_dump(shell_exec("cat $filename_txt_escaped | $php -n -F $filename_escaped"))
--CLEAN--
<?php
@unlink(__DIR__."/010.test.php");
@unlink(__DIR__."/010.test.txt");
@unlink(__DIR__."/010-F.test.txt");
?>
--EXPECT--
string(25) "
Expand Down
107 changes: 107 additions & 0 deletions tests/run-test/automatic_worker_limit.phpt
Original file line number Diff line number Diff line change
@@ -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)
Loading