Skip to content
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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`
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
72 changes: 66 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,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;
}
Expand Down Expand Up @@ -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 = 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 +880,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 = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'run-test-info-' . getmypid() . '.php';
@unlink($info_file);
$php_info = '<?php echo "
PHP_SAPI : " , PHP_SAPI , "
Expand Down Expand Up @@ -1571,10 +1631,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 thread
NickSdot marked this conversation as resolved.
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