|
9 | 9 | */
|
10 | 10 | namespace PHPUnit\Framework;
|
11 | 11 |
|
| 12 | +use PHPUnit\TestRunner\TestResult\PassedTests; |
12 | 13 | use const PHP_EOL;
|
13 | 14 | use function assert;
|
14 | 15 | use function class_exists;
|
@@ -249,6 +250,86 @@ public function run(TestCase $test): void
|
249 | 250 | * @throws StaticAnalysisCacheNotConfiguredException
|
250 | 251 | */
|
251 | 252 | public function runInSeparateProcess(TestCase $test, bool $runEntireClass, bool $preserveGlobalState): void
|
| 253 | + { |
| 254 | + if ($runEntireClass && $this->isPcntlForkAvailable()) { |
| 255 | + // forking the parent process is a more lightweight way to run a test in isolation. |
| 256 | + // it requires the pcntl extension though. |
| 257 | + $this->runInFork($test); |
| 258 | + return; |
| 259 | + } |
| 260 | + |
| 261 | + // running in a separate process is slow, but works in most situations. |
| 262 | + $this->runInWorkerProcess($test, $runEntireClass, $preserveGlobalState); |
| 263 | + } |
| 264 | + |
| 265 | + private function isPcntlForkAvailable(): bool { |
| 266 | + $disabledFunctions = ini_get('disable_functions'); |
| 267 | + |
| 268 | + return |
| 269 | + function_exists('pcntl_fork') |
| 270 | + && !str_contains($disabledFunctions, 'pcntl') |
| 271 | + && function_exists('socket_create_pair') |
| 272 | + && !str_contains($disabledFunctions, 'socket') |
| 273 | + ; |
| 274 | + } |
| 275 | + |
| 276 | + private function runInFork(TestCase $test): void |
| 277 | + { |
| 278 | + if (socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets) === false) { |
| 279 | + throw new \Exception('could not create socket pair'); |
| 280 | + } |
| 281 | + |
| 282 | + $pid = pcntl_fork(); |
| 283 | + // pcntl_fork may return NULL if the function is disabled in php.ini. |
| 284 | + if ($pid === -1 || $pid === null) { |
| 285 | + throw new \Exception('could not fork'); |
| 286 | + } else if ($pid) { |
| 287 | + // we are the parent |
| 288 | + |
| 289 | + pcntl_waitpid($pid, $status); // protect against zombie children |
| 290 | + |
| 291 | + // read child output |
| 292 | + $output = ''; |
| 293 | + while(($read = socket_read($sockets[1], 2048, PHP_BINARY_READ)) !== false) { |
| 294 | + $output .= $read; |
| 295 | + } |
| 296 | + socket_close($sockets[1]); |
| 297 | + |
| 298 | + $php = AbstractPhpProcess::factory(); |
| 299 | + $php->processChildResult($test, $output, ''); // TODO stderr |
| 300 | + |
| 301 | + } else { |
| 302 | + // we are the child |
| 303 | + |
| 304 | + $offset = hrtime(); |
| 305 | + $dispatcher = Event\Facade::instance()->initForIsolation( |
| 306 | + \PHPUnit\Event\Telemetry\HRTime::fromSecondsAndNanoseconds( |
| 307 | + $offset[0], |
| 308 | + $offset[1] |
| 309 | + ) |
| 310 | + ); |
| 311 | + |
| 312 | + $test->setInIsolation(true); |
| 313 | + $test->runBare(); |
| 314 | + |
| 315 | + // send result into parent |
| 316 | + socket_write($sockets[0], |
| 317 | + serialize( |
| 318 | + [ |
| 319 | + 'testResult' => $test->result(), |
| 320 | + 'codeCoverage' => CodeCoverage::instance()->isActive() ? CodeCoverage::instance()->codeCoverage() : null, |
| 321 | + 'numAssertions' => $test->numberOfAssertionsPerformed(), |
| 322 | + 'output' => !$test->expectsOutput() ? $output : '', |
| 323 | + 'events' => $dispatcher->flush(), |
| 324 | + 'passedTests' => PassedTests::instance() |
| 325 | + ] |
| 326 | + ) |
| 327 | + ); |
| 328 | + socket_close($sockets[0]); |
| 329 | + } |
| 330 | + } |
| 331 | + |
| 332 | + private function runInWorkerProcess(TestCase $test, bool $runEntireClass, bool $preserveGlobalState): void |
252 | 333 | {
|
253 | 334 | $class = new ReflectionClass($test);
|
254 | 335 |
|
|
0 commit comments