2727use function is_readable ;
2828use function is_string ;
2929use function ltrim ;
30+ use function ob_get_clean ;
31+ use function ob_start ;
3032use function preg_match ;
3133use function preg_replace ;
3234use function preg_split ;
6769use SebastianBergmann \CodeCoverage \TestIdMissingException ;
6870use SebastianBergmann \CodeCoverage \UnintentionallyCoveredCodeException ;
6971use SebastianBergmann \Template \Template ;
72+ use staabm \SideEffectsDetector \SideEffect ;
73+ use staabm \SideEffectsDetector \SideEffectsDetector ;
7074use Throwable ;
7175
7276/**
@@ -426,19 +430,26 @@ private function shouldTestBeSkipped(array $sections, array $settings): bool
426430 return false ;
427431 }
428432
429- $ jobResult = JobRunnerRegistry::run (
430- new Job (
431- $ this ->render ($ sections ['SKIPIF ' ]),
432- $ this ->stringifyIni ($ settings ),
433- ),
434- );
433+ $ skipIfCode = $ this ->render ($ sections ['SKIPIF ' ]);
435434
436- Facade::emitter ()->testRunnerFinishedChildProcess ($ jobResult ->stdout (), $ jobResult ->stderr ());
435+ if ($ this ->shouldRunSkipIfInSubprocess ($ sections , $ skipIfCode )) {
436+ $ jobResult = JobRunnerRegistry::run (
437+ new Job (
438+ $ skipIfCode ,
439+ $ this ->stringifyIni ($ settings ),
440+ ),
441+ );
442+ $ output = $ jobResult ->stdout ();
443+
444+ Facade::emitter ()->testRunnerFinishedChildProcess ($ output , $ jobResult ->stderr ());
445+ } else {
446+ $ output = $ this ->runCodeInLocalSandbox ($ skipIfCode );
447+ }
437448
438- if (!strncasecmp ('skip ' , ltrim ($ jobResult -> stdout () ), 4 )) {
449+ if (!strncasecmp ('skip ' , ltrim ($ output ), 4 )) {
439450 $ message = '' ;
440451
441- if (preg_match ('/^\s*skip\s*(.+)\s*/i ' , $ jobResult -> stdout () , $ skipMatch )) {
452+ if (preg_match ('/^\s*skip\s*(.+)\s*/i ' , $ output , $ skipMatch )) {
442453 $ message = substr ($ skipMatch [1 ], 2 );
443454 }
444455
@@ -455,6 +466,48 @@ private function shouldTestBeSkipped(array $sections, array $settings): bool
455466 return false ;
456467 }
457468
469+ /**
470+ * @param array<non-empty-string, non-empty-string> $sections
471+ */
472+ private function shouldRunSkipIfInSubprocess (array $ sections , string $ skipIfCode ): bool
473+ {
474+ if (isset ($ sections ['INI ' ])) {
475+ // to get per-test INI settings, we need a dedicated subprocess
476+ return true ;
477+ }
478+
479+ $ detector = new SideEffectsDetector ;
480+ $ sideEffects = $ detector ->getSideEffects ($ skipIfCode );
481+
482+ if ($ sideEffects === []) {
483+ return false ; // no side-effects
484+ }
485+
486+ foreach ($ sideEffects as $ sideEffect ) {
487+ if ($ sideEffect === SideEffect::STANDARD_OUTPUT ) {
488+ // stdout is fine, we will catch it using output-buffering
489+ continue ;
490+ }
491+
492+ return true ;
493+ }
494+
495+ return false ;
496+ }
497+
498+ private function runCodeInLocalSandbox (string $ code ): string
499+ {
500+ $ code = preg_replace ('/^<\?(php)?/ ' , '' , $ code );
501+ $ code = preg_replace ('/declare\S?\([^)]+\)\S?;/ ' , '' , $ code );
502+
503+ // wrap in immediately invoked function to isolate local-side-effects of $code from our own process
504+ $ code = '(function() { ' . $ code . '})(); ' ;
505+ ob_start ();
506+ @eval ($ code );
507+
508+ return ob_get_clean ();
509+ }
510+
458511 /**
459512 * @param array<non-empty-string, non-empty-string> $sections
460513 */
0 commit comments