Skip to content

Commit 1dc7b80

Browse files
committed
implement ipc
1 parent 9633614 commit 1dc7b80

File tree

1 file changed

+114
-26
lines changed

1 file changed

+114
-26
lines changed

src/Framework/TestRunner.php

+114-26
Original file line numberDiff line numberDiff line change
@@ -273,34 +273,117 @@ function_exists('pcntl_fork')
273273
;
274274
}
275275

276+
// IPC inspired from https://github.com/barracudanetworks/forkdaemon-php
277+
private const SOCKET_HEADER_SIZE = 4;
278+
279+
private function ipc_init()
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+
private function socket_receive($socket)
295+
{
296+
// initially read to the length of the header size, then
297+
// expand to read more
298+
$bytes_total = self::SOCKET_HEADER_SIZE;
299+
$bytes_read = 0;
300+
$have_header = false;
301+
$socket_message = '';
302+
while ($bytes_read < $bytes_total)
303+
{
304+
$read = @socket_read($socket, $bytes_total - $bytes_read);
305+
if ($read === false)
306+
{
307+
throw new \RuntimeException('socket_receive error: ' . socket_strerror(socket_last_error()));
308+
}
309+
310+
// blank socket_read means done
311+
if ($read == '')
312+
{
313+
break;
314+
}
315+
316+
$bytes_read += strlen($read);
317+
$socket_message .= $read;
318+
319+
if (!$have_header && $bytes_read >= self::SOCKET_HEADER_SIZE)
320+
{
321+
$have_header = true;
322+
list($bytes_total) = array_values(unpack('N', $socket_message));
323+
$bytes_read = 0;
324+
$socket_message = '';
325+
}
326+
}
327+
328+
return @unserialize($socket_message);
329+
}
330+
331+
private function socket_send($socket, $message)
332+
{
333+
$serialized_message = @serialize($message);
334+
if ($serialized_message == false)
335+
{
336+
throw new \RuntimeException('socket_send failed to serialize message');
337+
}
338+
339+
$header = pack('N', strlen($serialized_message));
340+
$data = $header . $serialized_message;
341+
$bytes_left = strlen($data);
342+
while ($bytes_left > 0)
343+
{
344+
$bytes_sent = @socket_write($socket, $data);
345+
if ($bytes_sent === false)
346+
{
347+
throw new \RuntimeException('socket_send failed to write to socket');
348+
}
349+
350+
$bytes_left -= $bytes_sent;
351+
$data = substr($data, $bytes_sent);
352+
}
353+
}
354+
276355
private function runInFork(TestCase $test): void
277356
{
278-
if (socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets) === false) {
279-
throw new \Exception('could not create socket pair');
280-
}
357+
list($socket_child, $socket_parent) = $this->ipc_init();
281358

282359
$pid = pcntl_fork();
283-
// pcntl_fork may return NULL if the function is disabled in php.ini.
284-
if ($pid === -1 || $pid === null) {
360+
361+
if ($pid === -1 ) {
285362
throw new \Exception('could not fork');
286363
} else if ($pid) {
287364
// we are the parent
288365

289-
pcntl_waitpid($pid, $status); // protect against zombie children
366+
socket_close($socket_parent);
367+
368+
// read child stdout, stderr
369+
$result = $this->socket_receive($socket_child);
290370

291-
// read child output
292-
$output = '';
293-
while(($read = socket_read($sockets[1], 2048, PHP_BINARY_READ)) !== false) {
294-
$output .= $read;
371+
$stderr = '';
372+
$stdout = '';
373+
if (is_array($result) && array_key_exists('error', $result)) {
374+
$stderr = $result['error'];
375+
} else {
376+
$stdout = $result;
295377
}
296-
socket_close($sockets[1]);
297378

298379
$php = AbstractPhpProcess::factory();
299-
$php->processChildResult($test, $output, ''); // TODO stderr
380+
$php->processChildResult($test, $stdout, $stderr);
300381

301382
} else {
302383
// we are the child
303384

385+
socket_close($socket_child);
386+
304387
$offset = hrtime();
305388
$dispatcher = Event\Facade::instance()->initForIsolation(
306389
\PHPUnit\Event\Telemetry\HRTime::fromSecondsAndNanoseconds(
@@ -310,22 +393,27 @@ private function runInFork(TestCase $test): void
310393
);
311394

312395
$test->setInIsolation(true);
313-
$test->runBare();
396+
try {
397+
$test->run();
398+
} catch (Throwable $e) {
399+
$this->socket_send($socket_parent, ['error' => $e->getMessage()]);
400+
exit();
401+
}
314402

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-
)
403+
$result = serialize(
404+
[
405+
'testResult' => $test->result(),
406+
'codeCoverage' => CodeCoverage::instance()->isActive() ? CodeCoverage::instance()->codeCoverage() : null,
407+
'numAssertions' => $test->numberOfAssertionsPerformed(),
408+
'output' => !$test->expectsOutput() ? $test->output() : '',
409+
'events' => $dispatcher->flush(),
410+
'passedTests' => PassedTests::instance()
411+
]
327412
);
328-
socket_close($sockets[0]);
413+
414+
// send result into parent
415+
$this->socket_send($socket_parent, $result);
416+
exit();
329417
}
330418
}
331419

0 commit comments

Comments
 (0)