diff --git a/src/Framework/TestCase.php b/src/Framework/TestCase.php index 497f117f870..25235a8a04f 100644 --- a/src/Framework/TestCase.php +++ b/src/Framework/TestCase.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\Constraint\ExceptionCode; use PHPUnit\Framework\Constraint\ExceptionMessage; use PHPUnit\Framework\Constraint\ExceptionMessageRegularExpression; +use PHPUnit\Util\PHP; use PHPUnit_Framework_MockObject_Generator; use PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount; use PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex; @@ -34,7 +35,6 @@ use PHPUnit\Runner\PhptTestCase; use PHPUnit\Util\GlobalState; use PHPUnit\Util\InvalidArgumentHelper; -use PHPUnit\Util\PHP\AbstractPhpProcess; use Prophecy; use ReflectionClass; use ReflectionException; @@ -861,7 +861,7 @@ public function run(TestResult $result = null) $this->prepareTemplate($template); - $php = AbstractPhpProcess::factory(); + $php = new PHP(); $php->runTestJob($template->render(), $this, $result); } else { $result->run($this); diff --git a/src/Runner/PhptTestCase.php b/src/Runner/PhptTestCase.php index 7fcb5497c9c..bcb403fe7fb 100644 --- a/src/Runner/PhptTestCase.php +++ b/src/Runner/PhptTestCase.php @@ -18,7 +18,7 @@ use PHPUnit\Framework\SkippedTestError; use PHPUnit\Framework\SelfDescribing; use PHPUnit\Util\InvalidArgumentHelper; -use PHPUnit\Util\PHP\AbstractPhpProcess; +use PHPUnit\Util\PHP; use Throwable; /** @@ -32,7 +32,7 @@ class PhptTestCase implements Test, SelfDescribing private $filename; /** - * @var AbstractPhpProcess + * @var \PHPUnit\Util\PHP */ private $phpUtil; @@ -66,7 +66,7 @@ class PhptTestCase implements Test, SelfDescribing * Constructs a test case with the given filename. * * @param string $filename - * @param AbstractPhpProcess $phpUtil + * @param \PHPUnit\Util\PHP $phpUtil * * @throws Exception */ @@ -86,7 +86,7 @@ public function __construct($filename, $phpUtil = null) } $this->filename = $filename; - $this->phpUtil = $phpUtil ?: AbstractPhpProcess::factory(); + $this->phpUtil = $phpUtil ?: new PHP(); } /** diff --git a/src/Util/PHP/AbstractPhpProcess.php b/src/Util/PHP.php similarity index 65% rename from src/Util/PHP/AbstractPhpProcess.php rename to src/Util/PHP.php index 913e7a75d67..1eccd207933 100644 --- a/src/Util/PHP/AbstractPhpProcess.php +++ b/src/Util/PHP.php @@ -7,24 +7,27 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ - -namespace PHPUnit\Util\PHP; +namespace PHPUnit\Util; use __PHP_Incomplete_Class; use ErrorException; +use SebastianBergmann\Environment\Runtime; use PHPUnit\Framework\Exception; use PHPUnit\Framework\TestResult; use PHPUnit\Framework\TestFailure; use PHPUnit\Framework\Test; use PHPUnit\Framework\SyntheticError; -use PHPUnit\Util\InvalidArgumentHelper; -use SebastianBergmann\Environment\Runtime; /** - * Utility methods for PHP sub-processes. + * Default utility for PHP sub-processes. */ -abstract class AbstractPhpProcess +class PHP { + /** + * @var string + */ + protected $tempFile; + /** * @var Runtime */ @@ -171,18 +174,6 @@ public function getTimeout() return $this->timeout; } - /** - * @return AbstractPhpProcess - */ - public static function factory() - { - if (DIRECTORY_SEPARATOR == '\\') { - return new WindowsPhpProcess; - } - - return new DefaultPhpProcess; - } - /** * Runs a single test in a separate PHP process. * @@ -209,12 +200,12 @@ public function runTestJob($job, Test $test, TestResult $result) /** * Returns the command based into the configurations. * - * @param array $settings - * @param string|null $file + * @param array $settings + * @param string $file * * @return string */ - public function getCommand(array $settings, $file = null) + public function getCommand(array $settings, $file) { $command = $this->runtime->getBinary(); $command .= $this->settingsToParameters($settings); @@ -222,12 +213,8 @@ public function getCommand(array $settings, $file = null) if ('phpdbg' === PHP_SAPI) { $command .= ' -qrr '; - if ($file) { - $command .= '-e ' . escapeshellarg($file); - } else { - $command .= escapeshellarg(__DIR__ . '/PHP/eval-stdin.php'); - } - } elseif ($file) { + $command .= '-e ' . escapeshellarg($file); + } else { $command .= ' -f ' . escapeshellarg($file); } @@ -239,21 +226,14 @@ public function getCommand(array $settings, $file = null) $command .= ' 2>&1'; } + // Special case windows. + if (DIRECTORY_SEPARATOR == '\\') { + $command = '"' . $command . '"'; + } + return $command; } - /** - * Runs a single job (PHP code) using a separate PHP process. - * - * @param string $job - * @param array $settings - * - * @return array - * - * @throws Exception - */ - abstract public function runJob($job, array $settings = []); - /** * @param array $settings * @@ -417,4 +397,190 @@ private function getException(TestFailure $error) return $exception; } + + /** + * Runs a single job (PHP code) using a separate PHP process. + * + * @param string $job + * @param array $settings + * + * @return array + * + * @throws Exception + */ + public function runJob($job, array $settings = []) + { + if (!($this->tempFile = tempnam(sys_get_temp_dir(), 'PHPUnit')) || + file_put_contents($this->tempFile, $job) === false + ) { + throw new Exception( + 'Unable to write temporary file' + ); + } + + if ($this->stdin) { + $job = $this->stdin; + } + + return $this->runProcess($job, $settings); + } + + /** + * Returns an array of file handles to be used in place of pipes + * + * @return array + */ + protected function getHandles() + { + return []; + } + + /** + * Handles creating the child process and returning the STDOUT and STDERR + * + * @param string $job + * @param array $settings + * + * @return array + * + * @throws Exception + */ + protected function runProcess($job, $settings) + { + $handles = $this->getHandles(); + + $env = null; + if ($this->env) { + $env = isset($_SERVER) ? $_SERVER : []; + unset($env['argv'], $env['argc']); + $env = array_merge($env, $this->env); + + foreach ($env as $envKey => $envVar) { + if (is_array($envVar)) { + unset($env[$envKey]); + } + } + } + + $pipeSpec = [ + 0 => isset($handles[0]) ? $handles[0] : ['pipe', 'r'], + 1 => isset($handles[1]) ? $handles[1] : ['pipe', 'w'], + 2 => isset($handles[2]) ? $handles[2] : ['pipe', 'w'], + ]; + $process = proc_open( + $this->getCommand($settings, $this->tempFile), + $pipeSpec, + $pipes, + null, + $env + ); + + if (!is_resource($process)) { + throw new Exception( + 'Unable to spawn worker process' + ); + } + + if ($job) { + $this->process($pipes[0], $job); + } + fclose($pipes[0]); + + if ($this->timeout) { + $stderr = $stdout = ''; + unset($pipes[0]); + + while (true) { + $r = $pipes; + $w = null; + $e = null; + + $n = @stream_select($r, $w, $e, $this->timeout); + + if ($n === false) { + break; + } elseif ($n === 0) { + proc_terminate($process, 9); + throw new Exception(sprintf('Job execution aborted after %d seconds', $this->timeout)); + } elseif ($n > 0) { + foreach ($r as $pipe) { + $pipeOffset = 0; + foreach ($pipes as $i => $origPipe) { + if ($pipe == $origPipe) { + $pipeOffset = $i; + break; + } + } + + if (!$pipeOffset) { + break; + } + + $line = fread($pipe, 8192); + if (strlen($line) == 0) { + fclose($pipes[$pipeOffset]); + unset($pipes[$pipeOffset]); + } else { + if ($pipeOffset == 1) { + $stdout .= $line; + } else { + $stderr .= $line; + } + } + } + + if (empty($pipes)) { + break; + } + } + } + } else { + if (isset($pipes[1])) { + $stdout = stream_get_contents($pipes[1]); + fclose($pipes[1]); + } + + if (isset($pipes[2])) { + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[2]); + } + } + + if (isset($handles[1])) { + rewind($handles[1]); + $stdout = stream_get_contents($handles[1]); + fclose($handles[1]); + } + + if (isset($handles[2])) { + rewind($handles[2]); + $stderr = stream_get_contents($handles[2]); + fclose($handles[2]); + } + + proc_close($process); + $this->cleanup(); + + return ['stdout' => $stdout, 'stderr' => $stderr]; + } + + /** + * @param resource $pipe + * @param string $job + * + * @throws Exception + */ + protected function process($pipe, $job) + { + fwrite($pipe, $job); + } + + /** + */ + protected function cleanup() + { + if ($this->tempFile) { + unlink($this->tempFile); + } + } } diff --git a/src/Util/PHP/DefaultPhpProcess.php b/src/Util/PHP/DefaultPhpProcess.php deleted file mode 100644 index f6485eccbaf..00000000000 --- a/src/Util/PHP/DefaultPhpProcess.php +++ /dev/null @@ -1,214 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace PHPUnit\Util\PHP; - -use PHPUnit\Framework\Exception; - -/** - * Default utility for PHP sub-processes. - */ -class DefaultPhpProcess extends AbstractPhpProcess -{ - /** - * @var string - */ - protected $tempFile; - - /** - * @var bool - */ - protected $useTempFile = false; - - /** - * Runs a single job (PHP code) using a separate PHP process. - * - * @param string $job - * @param array $settings - * - * @return array - * - * @throws Exception - */ - public function runJob($job, array $settings = []) - { - if ($this->useTempFile || $this->stdin) { - if (!($this->tempFile = tempnam(sys_get_temp_dir(), 'PHPUnit')) || - file_put_contents($this->tempFile, $job) === false - ) { - throw new Exception( - 'Unable to write temporary file' - ); - } - - $job = $this->stdin; - } - - return $this->runProcess($job, $settings); - } - - /** - * Returns an array of file handles to be used in place of pipes - * - * @return array - */ - protected function getHandles() - { - return []; - } - - /** - * Handles creating the child process and returning the STDOUT and STDERR - * - * @param string $job - * @param array $settings - * - * @return array - * - * @throws Exception - */ - protected function runProcess($job, $settings) - { - $handles = $this->getHandles(); - - $env = null; - if ($this->env) { - $env = isset($_SERVER) ? $_SERVER : []; - unset($env['argv'], $env['argc']); - $env = array_merge($env, $this->env); - - foreach ($env as $envKey => $envVar) { - if (is_array($envVar)) { - unset($env[$envKey]); - } - } - } - - $pipeSpec = [ - 0 => isset($handles[0]) ? $handles[0] : ['pipe', 'r'], - 1 => isset($handles[1]) ? $handles[1] : ['pipe', 'w'], - 2 => isset($handles[2]) ? $handles[2] : ['pipe', 'w'], - ]; - $process = proc_open( - $this->getCommand($settings, $this->tempFile), - $pipeSpec, - $pipes, - null, - $env - ); - - if (!is_resource($process)) { - throw new Exception( - 'Unable to spawn worker process' - ); - } - - if ($job) { - $this->process($pipes[0], $job); - } - fclose($pipes[0]); - - if ($this->timeout) { - $stderr = $stdout = ''; - unset($pipes[0]); - - while (true) { - $r = $pipes; - $w = null; - $e = null; - - $n = @stream_select($r, $w, $e, $this->timeout); - - if ($n === false) { - break; - } elseif ($n === 0) { - proc_terminate($process, 9); - throw new Exception(sprintf('Job execution aborted after %d seconds', $this->timeout)); - } elseif ($n > 0) { - foreach ($r as $pipe) { - $pipeOffset = 0; - foreach ($pipes as $i => $origPipe) { - if ($pipe == $origPipe) { - $pipeOffset = $i; - break; - } - } - - if (!$pipeOffset) { - break; - } - - $line = fread($pipe, 8192); - if (strlen($line) == 0) { - fclose($pipes[$pipeOffset]); - unset($pipes[$pipeOffset]); - } else { - if ($pipeOffset == 1) { - $stdout .= $line; - } else { - $stderr .= $line; - } - } - } - - if (empty($pipes)) { - break; - } - } - } - } else { - if (isset($pipes[1])) { - $stdout = stream_get_contents($pipes[1]); - fclose($pipes[1]); - } - - if (isset($pipes[2])) { - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[2]); - } - } - - if (isset($handles[1])) { - rewind($handles[1]); - $stdout = stream_get_contents($handles[1]); - fclose($handles[1]); - } - - if (isset($handles[2])) { - rewind($handles[2]); - $stderr = stream_get_contents($handles[2]); - fclose($handles[2]); - } - - proc_close($process); - $this->cleanup(); - - return ['stdout' => $stdout, 'stderr' => $stderr]; - } - - /** - * @param resource $pipe - * @param string $job - * - * @throws Exception - */ - protected function process($pipe, $job) - { - fwrite($pipe, $job); - } - - /** - */ - protected function cleanup() - { - if ($this->tempFile) { - unlink($this->tempFile); - } - } -} diff --git a/src/Util/PHP/WindowsPhpProcess.php b/src/Util/PHP/WindowsPhpProcess.php deleted file mode 100644 index 988dfe769df..00000000000 --- a/src/Util/PHP/WindowsPhpProcess.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace PHPUnit\Util\PHP; - -use PHPUnit\Framework\Exception; - -/** - * Windows utility for PHP sub-processes. - * - * Reading from STDOUT or STDERR hangs forever on Windows if the output is - * too large. - * - * @see https://bugs.php.net/bug.php?id=51800 - */ -class WindowsPhpProcess extends DefaultPhpProcess -{ - protected $useTempFile = true; - - protected function getHandles() - { - if (false === $stdout_handle = tmpfile()) { - throw new Exception( - 'A temporary file could not be created; verify that your TEMP environment variable is writable' - ); - } - - return [ - 1 => $stdout_handle - ]; - } - - public function getCommand(array $settings, $file = null) - { - return '"' . parent::getCommand($settings, $file) . '"'; - } -} diff --git a/src/Util/PHP/eval-stdin.php b/src/Util/PHP/eval-stdin.php deleted file mode 100644 index ccf82714538..00000000000 --- a/src/Util/PHP/eval-stdin.php +++ /dev/null @@ -1,10 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -eval('?>' . file_get_contents('php://stdin')); diff --git a/tests/Runner/PhptTestCaseTest.php b/tests/Runner/PhptTestCaseTest.php index 16c069c220b..82b7f104e1f 100644 --- a/tests/Runner/PhptTestCaseTest.php +++ b/tests/Runner/PhptTestCaseTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\TestCase; use PHPUnit\Runner\PhptTestCase; -use PHPUnit\Util\PHP\AbstractPhpProcess; +use PHPUnit\Util\PHP; class Runner_PhptTestCaseTest extends TestCase { @@ -55,7 +55,7 @@ protected function setUp() $this->filename = sys_get_temp_dir() . '/phpunit.phpt'; touch($this->filename); - $this->phpUtil = $this->getMockForAbstractClass(AbstractPhpProcess::class, [], '', false); + $this->phpUtil = $this->createMock(PHP::class); $this->testCase = new PhptTestCase($this->filename, $this->phpUtil); } diff --git a/tests/Util/PHPTest.php b/tests/Util/PHPTest.php index a5ab24bf8b0..a0327ccc425 100644 --- a/tests/Util/PHPTest.php +++ b/tests/Util/PHPTest.php @@ -8,7 +8,7 @@ * file that was distributed with this source code. */ use PHPUnit\Framework\TestCase; -use PHPUnit\Util\PHP\AbstractPhpProcess; +use PHPUnit\Util\PHP; /** * @author Henrique Moody @@ -21,14 +21,14 @@ class PHPUnit_Util_PHPTest extends TestCase { public function testShouldNotUseStderrRedirectionByDefault() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $this->assertFalse($phpMock->useStderrRedirection()); } public function testShouldDefinedIfUseStderrRedirection() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $phpMock->setUseStderrRedirection(true); $this->assertTrue($phpMock->useStderrRedirection()); @@ -36,7 +36,7 @@ public function testShouldDefinedIfUseStderrRedirection() public function testShouldDefinedIfDoNotUseStderrRedirection() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $phpMock->setUseStderrRedirection(false); $this->assertFalse($phpMock->useStderrRedirection()); @@ -44,7 +44,7 @@ public function testShouldDefinedIfDoNotUseStderrRedirection() public function testShouldThrowsExceptionWhenStderrRedirectionVariableIsNotABoolean() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $this->expectException(PHPUnit\Framework\Exception::class); @@ -53,7 +53,7 @@ public function testShouldThrowsExceptionWhenStderrRedirectionVariableIsNotABool public function testShouldUseGivenSettingsToCreateCommand() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $settings = [ 'allow_url_fopen=1', @@ -61,37 +61,37 @@ public function testShouldUseGivenSettingsToCreateCommand() 'display_errors=1', ]; - $expectedCommandFormat = '%s -d allow_url_fopen=1 -d auto_append_file= -d display_errors=1'; - $actualCommand = $phpMock->getCommand($settings); + $expectedCommandFormat = '%s -d allow_url_fopen=1 -d auto_append_file= -d display_errors=1 -f file.php'; + $actualCommand = $phpMock->getCommand($settings, 'file.php'); $this->assertStringMatchesFormat($expectedCommandFormat, $actualCommand); } public function testShouldRedirectStderrToStdoutWhenDefined() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $phpMock->setUseStderrRedirection(true); $expectedCommandFormat = '%s 2>&1'; - $actualCommand = $phpMock->getCommand([]); + $actualCommand = $phpMock->getCommand([], 'file.php'); $this->assertStringMatchesFormat($expectedCommandFormat, $actualCommand); } public function testShouldUseArgsToCreateCommand() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $phpMock->setArgs('foo=bar'); $expectedCommandFormat = '%s -- foo=bar'; - $actualCommand = $phpMock->getCommand([]); + $actualCommand = $phpMock->getCommand([], 'file.php'); $this->assertStringMatchesFormat($expectedCommandFormat, $actualCommand); } public function testShouldHaveFileToCreateCommand() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $expectedCommandFormat = '%s -%c \'file.php\''; $actualCommand = $phpMock->getCommand([], 'file.php'); @@ -101,7 +101,7 @@ public function testShouldHaveFileToCreateCommand() public function testStdinGetterAndSetter() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $phpMock->setStdin('foo'); $this->assertEquals('foo', $phpMock->getStdin()); @@ -109,7 +109,7 @@ public function testStdinGetterAndSetter() public function testArgsGetterAndSetter() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $phpMock->setArgs('foo=bar'); $this->assertEquals('foo=bar', $phpMock->getArgs()); @@ -117,7 +117,7 @@ public function testArgsGetterAndSetter() public function testEnvGetterAndSetter() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $phpMock->setEnv(['foo' => 'bar']); $this->assertEquals(['foo' => 'bar'], $phpMock->getEnv()); @@ -125,7 +125,7 @@ public function testEnvGetterAndSetter() public function testTimeoutGetterAndSetter() { - $phpMock = $this->getMockForAbstractClass(AbstractPhpProcess::class); + $phpMock = new PHP(); $phpMock->setTimeout(30); $this->assertEquals(30, $phpMock->getTimeout());