From 9ea7248e88af2225e2e1fd1099a4158c4180536e Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Thu, 23 Mar 2023 17:41:20 +0100 Subject: [PATCH] Use temporary file (instead of STDOUT) to communicate result from child process to parent process --- src/Framework/TestRunner.php | 16 +-- src/Util/PHP/AbstractPhpProcess.php | 118 ++++++++++------------- src/Util/PHP/Template/TestCaseClass.tpl | 33 +++---- src/Util/PHP/Template/TestCaseMethod.tpl | 33 +++---- 4 files changed, 86 insertions(+), 114 deletions(-) diff --git a/src/Framework/TestRunner.php b/src/Framework/TestRunner.php index ef1d8c1d563..998c94cda5c 100644 --- a/src/Framework/TestRunner.php +++ b/src/Framework/TestRunner.php @@ -300,12 +300,13 @@ public function runInSeparateProcess(TestCase $test, bool $runEntireClass, bool $includePath = var_export(get_include_path(), true); // must do these fixes because TestCaseMethod.tpl has unserialize('{data}') in it, and we can't break BC // the lines above used to use addcslashes() rather than var_export(), which breaks null byte escape sequences - $data = "'." . $data . ".'"; - $dataName = "'.(" . $dataName . ").'"; - $dependencyInput = "'." . $dependencyInput . ".'"; - $includePath = "'." . $includePath . ".'"; - $offset = hrtime(); - $serializedConfiguration = $this->saveConfigurationForChildProcess(); + $data = "'." . $data . ".'"; + $dataName = "'.(" . $dataName . ").'"; + $dependencyInput = "'." . $dependencyInput . ".'"; + $includePath = "'." . $includePath . ".'"; + $offset = hrtime(); + $serializedConfiguration = $this->saveConfigurationForChildProcess(); + $fileWithSerializedChildResult = tempnam(sys_get_temp_dir(), 'phpunit_'); $var = [ 'bootstrap' => $bootstrap, @@ -327,6 +328,7 @@ public function runInSeparateProcess(TestCase $test, bool $runEntireClass, bool 'offsetSeconds' => $offset[0], 'offsetNanoseconds' => $offset[1], 'serializedConfiguration' => $serializedConfiguration, + 'fileWithSerializedChildResult' => $fileWithSerializedChildResult, ]; if (!$runEntireClass) { @@ -336,7 +338,7 @@ public function runInSeparateProcess(TestCase $test, bool $runEntireClass, bool $template->setVar($var); $php = AbstractPhpProcess::factory(); - $php->runTestJob($template->render(), $test); + $php->runTestJob($template->render(), $test, $fileWithSerializedChildResult); @unlink($serializedConfiguration); } diff --git a/src/Util/PHP/AbstractPhpProcess.php b/src/Util/PHP/AbstractPhpProcess.php index fe817b53be0..b7dcc91f2c3 100644 --- a/src/Util/PHP/AbstractPhpProcess.php +++ b/src/Util/PHP/AbstractPhpProcess.php @@ -15,15 +15,12 @@ use function array_merge; use function assert; use function escapeshellarg; +use function file_get_contents; use function ini_get_all; -use function restore_error_handler; -use function set_error_handler; -use function str_replace; -use function str_starts_with; -use function substr; +use function is_array; use function trim; +use function unlink; use function unserialize; -use ErrorException; use PHPUnit\Event\Code\TestMethodBuilder; use PHPUnit\Event\Code\ThrowableBuilder; use PHPUnit\Event\Facade; @@ -159,14 +156,14 @@ public function getTimeout(): int * @throws MoreThanOneDataSetFromDataProviderException * @throws NoPreviousThrowableException */ - public function runTestJob(string $job, Test $test): void + public function runTestJob(string $job, Test $test, string $fileWithSerializedChildResult): void { - $_result = $this->runJob($job); + $result = $this->runJob($job); $this->processChildResult( $test, - $_result['stdout'], - $_result['stderr'] + $result['stderr'], + $fileWithSerializedChildResult ); } @@ -238,18 +235,17 @@ protected function settingsToParameters(array $settings): string } /** - * @throws \PHPUnit\Runner\Exception * @throws Exception * @throws MoreThanOneDataSetFromDataProviderException * @throws NoPreviousThrowableException */ - private function processChildResult(Test $test, string $stdout, string $stderr): void + private function processChildResult(Test $test, string $stderr, string $fileWithSerializedChildResult): void { + assert($test instanceof TestCase); + if (!empty($stderr)) { $exception = new Exception(trim($stderr)); - assert($test instanceof TestCase); - Facade::emitter()->testErrored( TestMethodBuilder::fromTestCase($test), ThrowableBuilder::from($exception) @@ -258,75 +254,59 @@ private function processChildResult(Test $test, string $stdout, string $stderr): return; } - set_error_handler( - /** - * @throws ErrorException - */ - static function (int $errno, string $errstr, string $errfile, int $errline): never - { - throw new ErrorException($errstr, $errno, $errno, $errfile, $errline); - } - ); + $serializedChildResult = @file_get_contents($fileWithSerializedChildResult); - try { - if (str_starts_with($stdout, "#!/usr/bin/env php\n")) { - $stdout = substr($stdout, 19); - } + if (!$serializedChildResult) { + $this->testErrored($test); - $childResult = unserialize(str_replace("#!/usr/bin/env php\n", '', $stdout)); - restore_error_handler(); + return; + } - if ($childResult === false) { - $exception = new AssertionFailedError('Test was run in child process and ended unexpectedly'); + @unlink($fileWithSerializedChildResult); - assert($test instanceof TestCase); + $childResult = @unserialize($serializedChildResult); - Facade::emitter()->testErrored( - TestMethodBuilder::fromTestCase($test), - ThrowableBuilder::from($exception) - ); + if (!is_array($childResult)) { + $this->testErrored($test); - Facade::emitter()->testFinished( - TestMethodBuilder::fromTestCase($test), - 0 - ); - } - } catch (ErrorException $e) { - restore_error_handler(); - $childResult = false; + return; + } - $exception = new Exception(trim($stdout), 0, $e); + Facade::instance()->forward($childResult['events']); + PassedTests::instance()->import($childResult['passedTests']); - assert($test instanceof TestCase); + $test->setResult($childResult['testResult']); + $test->addToAssertionCount($childResult['numAssertions']); - Facade::emitter()->testErrored( - TestMethodBuilder::fromTestCase($test), - ThrowableBuilder::from($exception) + if (CodeCoverage::instance()->isActive() && + $childResult['codeCoverage'] instanceof \SebastianBergmann\CodeCoverage\CodeCoverage) { + CodeCoverage::instance()->codeCoverage()->merge( + $childResult['codeCoverage'] ); } - if ($childResult !== false) { - if (!empty($childResult['output'])) { - $output = $childResult['output']; - } - - Facade::instance()->forward($childResult['events']); - PassedTests::instance()->import($childResult['passedTests']); - - assert($test instanceof TestCase); + if (!empty($childResult['output'])) { + print $childResult['output']; + } + } - $test->setResult($childResult['testResult']); - $test->addToAssertionCount($childResult['numAssertions']); + /** + * @throws Exception + * @throws MoreThanOneDataSetFromDataProviderException + * @throws NoPreviousThrowableException + */ + private function testErrored(TestCase $test): void + { + $exception = new AssertionFailedError('Test was run in child process and ended unexpectedly'); - if (CodeCoverage::instance()->isActive() && $childResult['codeCoverage'] instanceof \SebastianBergmann\CodeCoverage\CodeCoverage) { - CodeCoverage::instance()->codeCoverage()->merge( - $childResult['codeCoverage'] - ); - } - } + Facade::emitter()->testErrored( + TestMethodBuilder::fromTestCase($test), + ThrowableBuilder::from($exception) + ); - if (!empty($output)) { - print $output; - } + Facade::emitter()->testFinished( + TestMethodBuilder::fromTestCase($test), + 0 + ); } } diff --git a/src/Util/PHP/Template/TestCaseClass.tpl b/src/Util/PHP/Template/TestCaseClass.tpl index 370c8057295..2356b13f290 100644 --- a/src/Util/PHP/Template/TestCaseClass.tpl +++ b/src/Util/PHP/Template/TestCaseClass.tpl @@ -21,8 +21,6 @@ set_include_path('{include_path}'); $composerAutoload = {composerAutoload}; $phar = {phar}; -ob_start(); - if ($composerAutoload) { require_once $composerAutoload; @@ -52,18 +50,12 @@ function __phpunit_run_isolated_test() $test->setDependencyInput(unserialize('{dependencyInput}')); $test->setInIsolation(true); - ob_end_clean(); - $test->run(); - $output = ''; - - if (!$test->hasExpectationOnOutput()) { - $output = $test->output(); - } - ini_set('xdebug.scream', '0'); + $output = $test->hasUnexpectedOutput() ? $test->output() : ''; + // Not every STDOUT target stream is rewindable @rewind(STDOUT); @@ -77,15 +69,18 @@ function __phpunit_run_isolated_test() } } - print serialize( - [ - 'testResult' => $test->result(), - 'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null, - 'numAssertions' => $test->numberOfAssertionsPerformed(), - 'output' => $output, - 'events' => $dispatcher->flush(), - 'passedTests' => PassedTests::instance() - ] + file_put_contents( + '{fileWithSerializedChildResult}', + serialize( + [ + 'testResult' => $test->result(), + 'output' => $output, + 'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null, + 'numAssertions' => $test->numberOfAssertionsPerformed(), + 'events' => $dispatcher->flush(), + 'passedTests' => PassedTests::instance() + ] + ) ); } diff --git a/src/Util/PHP/Template/TestCaseMethod.tpl b/src/Util/PHP/Template/TestCaseMethod.tpl index 3e3f80e1751..6354c7efa41 100644 --- a/src/Util/PHP/Template/TestCaseMethod.tpl +++ b/src/Util/PHP/Template/TestCaseMethod.tpl @@ -21,8 +21,6 @@ set_include_path('{include_path}'); $composerAutoload = {composerAutoload}; $phar = {phar}; -ob_start(); - if ($composerAutoload) { require_once $composerAutoload; @@ -53,18 +51,12 @@ function __phpunit_run_isolated_test() $test->setDependencyInput(unserialize('{dependencyInput}')); $test->setInIsolation(true); - ob_end_clean(); - $test->run(); - $output = ''; - - if (!$test->hasExpectationOnOutput()) { - $output = $test->output(); - } - ini_set('xdebug.scream', '0'); + $output = $test->hasUnexpectedOutput() ? $test->output() : ''; + // Not every STDOUT target stream is rewindable @rewind(STDOUT); @@ -78,15 +70,18 @@ function __phpunit_run_isolated_test() } } - print serialize( - [ - 'testResult' => $test->result(), - 'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null, - 'numAssertions' => $test->numberOfAssertionsPerformed(), - 'output' => $output, - 'events' => $dispatcher->flush(), - 'passedTests' => PassedTests::instance() - ] + file_put_contents( + '{fileWithSerializedChildResult}', + serialize( + [ + 'testResult' => $test->result(), + 'output' => $output, + 'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null, + 'numAssertions' => $test->numberOfAssertionsPerformed(), + 'events' => $dispatcher->flush(), + 'passedTests' => PassedTests::instance() + ] + ) ); }