Skip to content

Commit 6ed83cb

Browse files
committed
Implement PcntlForkJobRunner
1 parent dc5b5be commit 6ed83cb

File tree

2 files changed

+156
-1
lines changed

2 files changed

+156
-1
lines changed

src/Framework/TestRunner/IsolatedTestRunnerRegistry.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,55 @@
99
*/
1010
namespace PHPUnit\Framework;
1111

12+
use PHPUnit\Event\Facade;
13+
use PHPUnit\Runner\CodeCoverage;
14+
use PHPUnit\TestRunner\TestResult\PassedTests;
15+
1216
/**
1317
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
1418
*
1519
* @internal This class is not covered by the backward compatibility promise for PHPUnit
1620
*/
1721
final class IsolatedTestRunnerRegistry
1822
{
19-
private static ?IsolatedTestRunner $runner = null;
23+
private static ?IsolatedTestRunner $runner = null;
24+
private static ?PcntlForkJobRunner $pcntlForkJobRunner = null;
2025

2126
public static function run(TestCase $test, bool $runEntireClass, bool $preserveGlobalState): void
2227
{
2328
if (self::$runner === null) {
2429
self::$runner = new SeparateProcessTestRunner;
2530
}
2631

32+
$pcntlForkJobRunner = self::pcntlForkRunner();
33+
34+
if ($pcntlForkJobRunner->canRun($test, $runEntireClass, $preserveGlobalState)) {
35+
$pcntlForkJobRunner->run($test, $runEntireClass, $preserveGlobalState);
36+
37+
return;
38+
}
39+
2740
self::$runner->run($test, $runEntireClass, $preserveGlobalState);
2841
}
2942

3043
public static function set(IsolatedTestRunner $runner): void
3144
{
3245
self::$runner = $runner;
3346
}
47+
48+
private static function pcntlForkRunner(): PcntlForkJobRunner
49+
{
50+
if (self::$pcntlForkJobRunner === null) {
51+
self::$pcntlForkJobRunner = new PcntlForkJobRunner(
52+
new ChildProcessResultProcessor(
53+
Facade::instance(),
54+
Facade::emitter(),
55+
PassedTests::instance(),
56+
CodeCoverage::instance(),
57+
),
58+
);
59+
}
60+
61+
return self::$pcntlForkJobRunner;
62+
}
3463
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <[email protected]>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace PHPUnit\Framework;
11+
12+
use function array_search;
13+
use function file_get_contents;
14+
use function file_put_contents;
15+
use function function_exists;
16+
use function get_included_files;
17+
use function hrtime;
18+
use function ini_get;
19+
use function pcntl_fork;
20+
use function pcntl_waitpid;
21+
use function serialize;
22+
use function str_contains;
23+
use function sys_get_temp_dir;
24+
use function tempnam;
25+
use Exception;
26+
use PHPUnit\Event\Facade;
27+
use PHPUnit\Event\Facade as EventFacade;
28+
use PHPUnit\Event\Telemetry\HRTime;
29+
use PHPUnit\Runner\CodeCoverage;
30+
use PHPUnit\TestRunner\TestResult\PassedTests;
31+
use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry;
32+
33+
/**
34+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
35+
*
36+
* @internal This class is not covered by the backward compatibility promise for PHPUnit
37+
*/
38+
final class PcntlForkJobRunner implements IsolatedTestRunner
39+
{
40+
public function __construct(private ChildProcessResultProcessor $processor)
41+
{
42+
}
43+
44+
public function run(TestCase $test, bool $runEntireClass, bool $preserveGlobalState): void
45+
{
46+
$processResultFile = tempnam(sys_get_temp_dir(), 'phpunit_');
47+
48+
$pid = pcntl_fork();
49+
50+
if ($pid === -1) {
51+
// @codeCoverageIgnoreStart
52+
throw new Exception('could not fork');
53+
// @codeCoverageIgnoreEnd
54+
}
55+
56+
if ($pid !== 0) {
57+
// we are the parent
58+
Facade::emitter()->childProcessStarted();
59+
60+
pcntl_waitpid($pid, $status);
61+
62+
$this->processor->process($test, file_get_contents($processResultFile), '');
63+
64+
EventFacade::emitter()->childProcessFinished('', '');
65+
66+
return;
67+
}
68+
69+
// we are the child, run the test
70+
71+
$offset = hrtime();
72+
$dispatcher = Facade::instance()->initForIsolation(
73+
HRTime::fromSecondsAndNanoseconds(
74+
$offset[0],
75+
$offset[1],
76+
),
77+
);
78+
79+
$test->setInIsolation(true);
80+
$test->run();
81+
82+
file_put_contents(
83+
$processResultFile,
84+
serialize(
85+
(object) [
86+
'testResult' => $test->result(),
87+
'codeCoverage' => CodeCoverage::instance()->isActive() ? CodeCoverage::instance()->codeCoverage() : null,
88+
'numAssertions' => $test->numberOfAssertionsPerformed(),
89+
'output' => !$test->expectsOutput() ? $test->output() : '',
90+
'events' => $dispatcher->flush(),
91+
'passedTests' => PassedTests::instance(),
92+
],
93+
),
94+
);
95+
96+
exit();
97+
}
98+
99+
public function canRun(TestCase $test, bool $runEntireClass, bool $preserveGlobalState): bool
100+
{
101+
if (!$this->isPcntlForkAvailable()) {
102+
return false;
103+
}
104+
105+
// we support bootstrap files only if they have been already included in the parent process
106+
// as we cannot require a file and load it into the global scope from within a forked process.
107+
if (ConfigurationRegistry::get()->hasBootstrap()) {
108+
if (!in_array(ConfigurationRegistry::get()->bootstrap(), get_included_files(), true)) {
109+
return false;
110+
}
111+
}
112+
113+
return !$runEntireClass &&
114+
!$preserveGlobalState;
115+
}
116+
117+
private function isPcntlForkAvailable(): bool
118+
{
119+
$disabledFunctions = ini_get('disable_functions');
120+
121+
return
122+
function_exists('pcntl_fork') &&
123+
function_exists('pcntl_waitpid') &&
124+
!str_contains($disabledFunctions, 'pcntl');
125+
}
126+
}

0 commit comments

Comments
 (0)