@@ -273,34 +273,117 @@ function_exists('pcntl_fork')
273
273
;
274
274
}
275
275
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
+
276
355
private function runInFork (TestCase $ test ): void
277
356
{
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 ();
281
358
282
359
$ 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 ) {
285
362
throw new \Exception ('could not fork ' );
286
363
} else if ($ pid ) {
287
364
// we are the parent
288
365
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 );
290
370
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 ;
295
377
}
296
- socket_close ($ sockets [1 ]);
297
378
298
379
$ php = AbstractPhpProcess::factory ();
299
- $ php ->processChildResult ($ test , $ output , '' ); // TODO stderr
380
+ $ php ->processChildResult ($ test , $ stdout , $ stderr );
300
381
301
382
} else {
302
383
// we are the child
303
384
385
+ socket_close ($ socket_child );
386
+
304
387
$ offset = hrtime ();
305
388
$ dispatcher = Event \Facade::instance ()->initForIsolation (
306
389
\PHPUnit \Event \Telemetry \HRTime::fromSecondsAndNanoseconds (
@@ -310,22 +393,27 @@ private function runInFork(TestCase $test): void
310
393
);
311
394
312
395
$ 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
+ }
314
402
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
+ ]
327
412
);
328
- socket_close ($ sockets [0 ]);
413
+
414
+ // send result into parent
415
+ $ this ->socket_send ($ socket_parent , $ result );
416
+ exit ();
329
417
}
330
418
}
331
419
0 commit comments