|
10 | 10 | namespace PHPUnit\Framework;
|
11 | 11 |
|
12 | 12 | use PHPUnit\TestRunner\TestResult\PassedTests;
|
| 13 | +use PHPUnit\Util\PHP\PcntlFork; |
13 | 14 | use const PHP_EOL;
|
14 | 15 | use function assert;
|
15 | 16 | use function class_exists;
|
@@ -251,179 +252,18 @@ public function run(TestCase $test): void
|
251 | 252 | */
|
252 | 253 | public function runInSeparateProcess(TestCase $test, bool $runEntireClass, bool $preserveGlobalState): void
|
253 | 254 | {
|
254 |
| - if ($this->isPcntlForkAvailable()) { |
| 255 | + if (PcntlFork::isPcntlForkAvailable()) { |
255 | 256 | // forking the parent process is a more lightweight way to run a test in isolation.
|
256 | 257 | // it requires the pcntl extension though.
|
257 |
| - $this->runInFork($test); |
| 258 | + $fork = new PcntlFork(); |
| 259 | + $fork->runTest($test); |
258 | 260 | return;
|
259 | 261 | }
|
260 | 262 |
|
261 | 263 | // running in a separate process is slow, but works in most situations.
|
262 | 264 | $this->runInWorkerProcess($test, $runEntireClass, $preserveGlobalState);
|
263 | 265 | }
|
264 | 266 |
|
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 |
| - // IPC inspired from https://github.com/barracudanetworks/forkdaemon-php |
277 |
| - private const SOCKET_HEADER_SIZE = 4; |
278 |
| - |
279 |
| - private function ipc_init(): array |
280 |
| - { |
281 |
| - // windows needs AF_INET |
282 |
| - $domain = strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' ? AF_INET : AF_UNIX; |
283 |
| - |
284 |
| - // create a socket pair for IPC |
285 |
| - $sockets = array(); |
286 |
| - if (socket_create_pair($domain, SOCK_STREAM, 0, $sockets) === false) |
287 |
| - { |
288 |
| - throw new \RuntimeException('socket_create_pair failed: ' . socket_strerror(socket_last_error())); |
289 |
| - } |
290 |
| - |
291 |
| - return $sockets; |
292 |
| - } |
293 |
| - |
294 |
| - /** |
295 |
| - * @param resource $socket |
296 |
| - */ |
297 |
| - private function socket_receive($socket): mixed |
298 |
| - { |
299 |
| - // initially read to the length of the header size, then |
300 |
| - // expand to read more |
301 |
| - $bytes_total = self::SOCKET_HEADER_SIZE; |
302 |
| - $bytes_read = 0; |
303 |
| - $have_header = false; |
304 |
| - $socket_message = ''; |
305 |
| - while ($bytes_read < $bytes_total) |
306 |
| - { |
307 |
| - $read = @socket_read($socket, $bytes_total - $bytes_read); |
308 |
| - if ($read === false) |
309 |
| - { |
310 |
| - throw new \RuntimeException('socket_receive error: ' . socket_strerror(socket_last_error())); |
311 |
| - } |
312 |
| - |
313 |
| - // blank socket_read means done |
314 |
| - if ($read == '') |
315 |
| - { |
316 |
| - break; |
317 |
| - } |
318 |
| - |
319 |
| - $bytes_read += strlen($read); |
320 |
| - $socket_message .= $read; |
321 |
| - |
322 |
| - if (!$have_header && $bytes_read >= self::SOCKET_HEADER_SIZE) |
323 |
| - { |
324 |
| - $have_header = true; |
325 |
| - list($bytes_total) = array_values(unpack('N', $socket_message)); |
326 |
| - $bytes_read = 0; |
327 |
| - $socket_message = ''; |
328 |
| - } |
329 |
| - } |
330 |
| - |
331 |
| - return @unserialize($socket_message); |
332 |
| - } |
333 |
| - |
334 |
| - /** |
335 |
| - * @param resource $socket |
336 |
| - * @param mixed $message |
337 |
| - */ |
338 |
| - private function socket_send($socket, $message): void |
339 |
| - { |
340 |
| - $serialized_message = @serialize($message); |
341 |
| - if ($serialized_message == false) |
342 |
| - { |
343 |
| - throw new \RuntimeException('socket_send failed to serialize message'); |
344 |
| - } |
345 |
| - |
346 |
| - $header = pack('N', strlen($serialized_message)); |
347 |
| - $data = $header . $serialized_message; |
348 |
| - $bytes_left = strlen($data); |
349 |
| - while ($bytes_left > 0) |
350 |
| - { |
351 |
| - $bytes_sent = @socket_write($socket, $data); |
352 |
| - if ($bytes_sent === false) |
353 |
| - { |
354 |
| - throw new \RuntimeException('socket_send failed to write to socket'); |
355 |
| - } |
356 |
| - |
357 |
| - $bytes_left -= $bytes_sent; |
358 |
| - $data = substr($data, $bytes_sent); |
359 |
| - } |
360 |
| - } |
361 |
| - |
362 |
| - private function runInFork(TestCase $test): void |
363 |
| - { |
364 |
| - list($socket_child, $socket_parent) = $this->ipc_init(); |
365 |
| - |
366 |
| - $pid = pcntl_fork(); |
367 |
| - |
368 |
| - if ($pid === -1 ) { |
369 |
| - throw new \Exception('could not fork'); |
370 |
| - } else if ($pid) { |
371 |
| - // we are the parent |
372 |
| - |
373 |
| - socket_close($socket_parent); |
374 |
| - |
375 |
| - // read child stdout, stderr |
376 |
| - $result = $this->socket_receive($socket_child); |
377 |
| - |
378 |
| - $stderr = ''; |
379 |
| - $stdout = ''; |
380 |
| - if (is_array($result) && array_key_exists('error', $result)) { |
381 |
| - $stderr = $result['error']; |
382 |
| - } else { |
383 |
| - $stdout = $result; |
384 |
| - } |
385 |
| - |
386 |
| - $php = AbstractPhpProcess::factory(); |
387 |
| - $php->processChildResult($test, $stdout, $stderr); |
388 |
| - |
389 |
| - } else { |
390 |
| - // we are the child |
391 |
| - |
392 |
| - socket_close($socket_child); |
393 |
| - |
394 |
| - $offset = hrtime(); |
395 |
| - $dispatcher = Event\Facade::instance()->initForIsolation( |
396 |
| - \PHPUnit\Event\Telemetry\HRTime::fromSecondsAndNanoseconds( |
397 |
| - $offset[0], |
398 |
| - $offset[1] |
399 |
| - ) |
400 |
| - ); |
401 |
| - |
402 |
| - $test->setInIsolation(true); |
403 |
| - try { |
404 |
| - $test->run(); |
405 |
| - } catch (Throwable $e) { |
406 |
| - $this->socket_send($socket_parent, ['error' => $e->getMessage()]); |
407 |
| - exit(); |
408 |
| - } |
409 |
| - |
410 |
| - $result = serialize( |
411 |
| - [ |
412 |
| - 'testResult' => $test->result(), |
413 |
| - 'codeCoverage' => CodeCoverage::instance()->isActive() ? CodeCoverage::instance()->codeCoverage() : null, |
414 |
| - 'numAssertions' => $test->numberOfAssertionsPerformed(), |
415 |
| - 'output' => !$test->expectsOutput() ? $test->output() : '', |
416 |
| - 'events' => $dispatcher->flush(), |
417 |
| - 'passedTests' => PassedTests::instance() |
418 |
| - ] |
419 |
| - ); |
420 |
| - |
421 |
| - // send result into parent |
422 |
| - $this->socket_send($socket_parent, $result); |
423 |
| - exit(); |
424 |
| - } |
425 |
| - } |
426 |
| - |
427 | 267 | private function runInWorkerProcess(TestCase $test, bool $runEntireClass, bool $preserveGlobalState): void
|
428 | 268 | {
|
429 | 269 | $class = new ReflectionClass($test);
|
|
0 commit comments