|
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