diff --git a/src/Framework/Helpers.php b/src/Framework/Helpers.php index dc126ed7..00b60e0a 100644 --- a/src/Framework/Helpers.php +++ b/src/Framework/Helpers.php @@ -81,7 +81,7 @@ public static function errorTypeToString($type) */ public static function escapeArg($s) { - if (preg_match('#^[a-z0-9._-]+\z#i', $s)) { + if (preg_match('#^[a-z0-9._=/:-]+\z#i', $s)) { return $s; } diff --git a/src/Runner/CliTester.php b/src/Runner/CliTester.php index 2c95bfa0..9d62e2b9 100644 --- a/src/Runner/CliTester.php +++ b/src/Runner/CliTester.php @@ -9,7 +9,6 @@ use Tester\CodeCoverage; use Tester\Environment; -use Tester\Helpers; use Tester\Dumper; @@ -142,52 +141,25 @@ private function loadOptions() /** @return void */ private function createPhpInterpreter() { - $args = ''; + $args = []; if ($this->options['-c']) { - $args .= ' -c ' . Helpers::escapeArg($this->options['-c']); + array_push($args, '-c', $this->options['-c']); } elseif (!$this->options['--info']) { echo "Note: No php.ini is used.\n"; } if (in_array($this->options['-o'], ['tap', 'junit'])) { - $args .= ' -d html_errors=off'; + array_push($args, '-d', 'html_errors=off'); } foreach ($this->options['-d'] as $item) { - $args .= ' -d ' . Helpers::escapeArg($item); + array_push($args, '-d', $item); } - // Is the executable Zend PHP or HHVM? - $proc = @proc_open( // @ is escalated to exception - $this->options['-p'] . ' --version', - [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], - $pipes, - NULL, - NULL, - ['bypass_shell' => TRUE] - ); - if ($proc === FALSE) { - throw new \Exception('Cannot run PHP interpreter ' . $this->options['-p'] . '. Use -p option.'); - } - $output = stream_get_contents($pipes[1]); - $error = stream_get_contents($pipes[2]); - if (proc_close($proc)) { - throw new \Exception("Unable to run '{$this->options['-p']}': " . preg_replace('#[\r\n ]+#', ' ', $error)); - } - - if (preg_match('#HipHop VM#', $output)) { - $this->interpreter = new HhvmPhpInterpreter($this->options['-p'], $args); - } elseif (strpos($output, 'phpdbg') !== FALSE) { - $this->interpreter = new ZendPhpDbgInterpreter($this->options['-p'], $args); - } else { - $this->interpreter = new ZendPhpInterpreter($this->options['-p'], $args); - } + $this->interpreter = new PhpInterpreter($this->options['-p'], $args); - if ($this->interpreter->getErrorOutput()) { - echo Dumper::color('red', 'PHP startup error: ' . $this->interpreter->getErrorOutput()) . "\n"; - if ($this->interpreter->isCgi()) { - echo "(note that PHP CLI generates better error messages)\n"; - } + if ($error = $this->interpreter->getStartupError()) { + echo Dumper::color('red', "PHP startup error: $error") . "\n"; } } @@ -230,7 +202,7 @@ private function createRunner() /** @return string */ private function prepareCodeCoverage() { - if (!$this->interpreter->hasXdebug()) { + if (!$this->interpreter->canMeasureCodeCoverage()) { $alternative = PHP_VERSION_ID >= 70000 ? ' or phpdbg SAPI' : ''; throw new \Exception("Code coverage functionality requires Xdebug extension$alternative (used {$this->interpreter->getCommandLine()})"); } diff --git a/src/Runner/HhvmPhpInterpreter.php b/src/Runner/HhvmPhpInterpreter.php deleted file mode 100644 index 345d565b..00000000 --- a/src/Runner/HhvmPhpInterpreter.php +++ /dev/null @@ -1,106 +0,0 @@ -path = Helpers::escapeArg($path); - $proc = proc_open( - "$this->path --php $args -r " . Helpers::escapeArg('echo HHVM_VERSION . "|" . PHP_VERSION;'), - [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], - $pipes, - NULL, - NULL, - ['bypass_shell' => TRUE] - ); - $output = stream_get_contents($pipes[1]); - $this->error = trim(stream_get_contents($pipes[2])); - - if (proc_close($proc)) { - throw new \Exception("Unable to run '$path': " . preg_replace('#[\r\n ]+#', ' ', $this->error)); - } elseif (count($tmp = explode('|', $output)) !== 2) { - throw new \Exception("Unable to detect HHVM version (output: $output)."); - } - - list($this->version, $this->phpVersion) = $tmp; - if (version_compare($this->version, '3.3.0', '<')) { - throw new \Exception('HHVM below version 3.3.0 is not supported.'); - } - $this->arguments = ' --php -d hhvm.log.always_log_unhandled_exceptions=false' . ($args ? " $args" : ''); // HHVM issue #3019 - } - - - /** - * @return string - */ - public function getCommandLine() - { - return $this->path . $this->arguments; - } - - - /** - * @return string - */ - public function getVersion() - { - return $this->phpVersion; - } - - - /** - * @return bool - */ - public function hasXdebug() - { - return FALSE; - } - - - /** - * @return bool - */ - public function isCgi() - { - return FALSE; - } - - - /** - * @return string - */ - public function getErrorOutput() - { - return $this->error; - } - -} diff --git a/src/Runner/Job.php b/src/Runner/Job.php index e8a05d33..72b84e23 100644 --- a/src/Runner/Job.php +++ b/src/Runner/Job.php @@ -8,6 +8,7 @@ namespace Tester\Runner; use Tester\Environment; +use Tester\Helpers; /** @@ -83,7 +84,7 @@ public function run($flags = NULL) putenv(Environment::COLORS . '=' . (int) Environment::$useColors); $this->proc = proc_open( $this->interpreter->getCommandLine() - . ' -n -d register_argc_argv=on ' . \Tester\Helpers::escapeArg($this->file) . ' ' . implode(' ', $this->args), + . ' -d register_argc_argv=on ' . Helpers::escapeArg($this->file) . ' ' . implode(' ', $this->args), [ ['pipe', 'r'], ['pipe', 'w'], diff --git a/src/Runner/Output/ConsolePrinter.php b/src/Runner/Output/ConsolePrinter.php index 29281585..a286b7b4 100644 --- a/src/Runner/Output/ConsolePrinter.php +++ b/src/Runner/Output/ConsolePrinter.php @@ -40,7 +40,7 @@ public function __construct(Runner $runner, $displaySkipped = FALSE) public function begin() { $this->time = -microtime(TRUE); - echo 'PHP ' . $this->runner->getInterpreter()->getVersion() + echo $this->runner->getInterpreter()->getShortInfo() . ' | ' . $this->runner->getInterpreter()->getCommandLine() . " | {$this->runner->threadCount} thread" . ($this->runner->threadCount > 1 ? 's' : '') . "\n\n"; } diff --git a/src/Runner/PhpInterpreter.php b/src/Runner/PhpInterpreter.php index 401b5982..d8d3651f 100644 --- a/src/Runner/PhpInterpreter.php +++ b/src/Runner/PhpInterpreter.php @@ -7,33 +7,156 @@ namespace Tester\Runner; +use Tester\Helpers; -interface PhpInterpreter + +/** + * PHP command-line executable. + */ +class PhpInterpreter { + /** @var string */ + private $commandLine; + + /** @var bool is CGI? */ + private $cgi; + + /** @var \stdClass created by info.php */ + private $info; + + /** @var string */ + private $error; + + + public function __construct($path, array $args = []) + { + $this->commandLine = Helpers::escapeArg($path); + $proc = @proc_open( // @ is escalated to exception + $this->commandLine . ' --version', + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], + $pipes, + NULL, + NULL, + ['bypass_shell' => TRUE] + ); + if ($proc === FALSE) { + throw new \Exception("Cannot run PHP interpreter $path. Use -p option."); + } + $output = stream_get_contents($pipes[1]); + proc_close($proc); + + $args = ' -n ' . implode(' ', array_map(['Tester\Helpers', 'escapeArg'], $args)); + if (preg_match('#HipHop VM#', $output)) { + $args = ' --php' . $args . ' -d hhvm.log.always_log_unhandled_exceptions=false'; // HHVM issue #3019 + } elseif (strpos($output, 'phpdbg') !== FALSE) { + $args = ' -qrrb -S cli' . $args; + } + $this->commandLine .= $args; + + $proc = proc_open( + $this->commandLine . ' ' . Helpers::escapeArg(__DIR__ . '/info.php') . ' serialized', + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], + $pipes, + NULL, + NULL, + ['bypass_shell' => TRUE] + ); + $output = stream_get_contents($pipes[1]); + $this->error = trim(stream_get_contents($pipes[2])); + if (proc_close($proc)) { + throw new \Exception("Unable to run $path: " . preg_replace('#[\r\n ]+#', ' ', $this->error)); + } + + $parts = explode("\r\n\r\n", $output, 2); + $this->cgi = count($parts) === 2; + if (!($this->info = @unserialize($parts[$this->cgi]))) { + throw new \Exception("Unable to detect PHP version (output: $output)."); + + } elseif ($this->info->hhvmVersion && version_compare($this->info->hhvmVersion, '3.3.0', '<')) { + throw new \Exception('HHVM below version 3.3.0 is not supported.'); + + } elseif ($this->info->phpDbgVersion && version_compare($this->info->version, '7.0.0', '<')) { + throw new \Exception('Unable to use phpdbg on PHP < 7.0.0.'); + + } elseif ($this->cgi && $this->error) { + $this->error .= "\n(note that PHP CLI generates better error messages)"; + } + } + + + /** + * @param string + * @param string + */ + public function addPhpIniOption($name, $value = NULL) + { + $this->commandLine .= ' -d ' . Helpers::escapeArg($name . ($value === NULL ? '' : "=$value")); + } + /** * @return string */ - function getCommandLine(); + public function getCommandLine() + { + return $this->commandLine; + } + /** * @return string */ - function getVersion(); + public function getVersion() + { + return $this->info->version; + } + /** * @return bool */ - function hasXdebug(); + public function canMeasureCodeCoverage() + { + return $this->info->canMeasureCodeCoverage; + } + /** * @return bool */ - function isCgi(); + public function isCgi() + { + return $this->cgi; + } + + + /** + * @return string + */ + public function getStartupError() + { + return $this->error; + } + /** * @return string */ - function getErrorOutput(); + public function getShortInfo() + { + return "PHP {$this->info->version} ({$this->info->sapi})" + . ($this->info->phpDbgVersion ? "; PHPDBG {$this->info->phpDbgVersion}" : '') + . ($this->info->hhvmVersion ? "; HHVM {$this->info->hhvmVersion}" : ''); + } + + + /** + * @param string + * @return bool + */ + public function hasExtension($name) + { + return in_array(strtolower($name), array_map('strtolower', $this->info->extensions), TRUE); + } } diff --git a/src/Runner/TestHandler.php b/src/Runner/TestHandler.php index fabac6c5..d8e1c9ab 100644 --- a/src/Runner/TestHandler.php +++ b/src/Runner/TestHandler.php @@ -113,7 +113,8 @@ private function initiatePhpVersion($version, PhpInterpreter $interpreter) private function initiatePhpIni($value, PhpInterpreter $interpreter) { - $interpreter->arguments .= ' -d ' . Helpers::escapeArg($value); + list($name, $value) = explode('=', $value, 2) + [1 => NULL]; + $interpreter->addPhpIniOption($name, $value); } diff --git a/src/Runner/ZendPhpDbgInterpreter.php b/src/Runner/ZendPhpDbgInterpreter.php deleted file mode 100644 index cdc60b67..00000000 --- a/src/Runner/ZendPhpDbgInterpreter.php +++ /dev/null @@ -1,102 +0,0 @@ -path = Helpers::escapeArg($path); - $proc = proc_open( - "$this->path -n $args -V", - [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], - $pipes, - NULL, - NULL, - ['bypass_shell' => TRUE] - ); - $output = stream_get_contents($pipes[1]); - - $this->error = trim(stream_get_contents($pipes[2])); - if (proc_close($proc)) { - throw new \Exception("Unable to run '$path': " . preg_replace('#[\r\n ]+#', ' ', $this->error)); - } elseif (!preg_match('#^PHP ([\w.-]+)#im', $output, $matches)) { - throw new \Exception("Unable to detect PHP version (output: $output)."); - } elseif (version_compare($matches[1], '7.0.0', '<')) { - throw new \Exception('Unable to use phpdbg on PHP < 7.0.0.'); - } - - $this->version = $matches[1]; - $this->arguments = $args; - } - - - /** - * @return string - */ - public function getCommandLine() - { - return $this->path . ' -qrrb -S cli' . $this->arguments; - } - - - /** - * @return string - */ - public function getVersion() - { - return $this->version; - } - - - /** - * @return bool - */ - public function hasXdebug() - { - return TRUE; - } - - - /** - * @return bool - */ - public function isCgi() - { - return FALSE; - } - - - /** - * @return string - */ - public function getErrorOutput() - { - return $this->error; - } - -} diff --git a/src/Runner/ZendPhpInterpreter.php b/src/Runner/ZendPhpInterpreter.php deleted file mode 100644 index a00d8ceb..00000000 --- a/src/Runner/ZendPhpInterpreter.php +++ /dev/null @@ -1,110 +0,0 @@ -path = Helpers::escapeArg($path); - $proc = proc_open( - "$this->path -n $args -v", // -v must be the last - [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], - $pipes, - NULL, - NULL, - ['bypass_shell' => TRUE] - ); - $output = stream_get_contents($pipes[1]); - $this->error = trim(stream_get_contents($pipes[2])); - if (proc_close($proc)) { - throw new \Exception("Unable to run '$path': " . preg_replace('#[\r\n ]+#', ' ', $this->error)); - } elseif (!preg_match('#^PHP (\S+).*c(g|l)i#i', $output, $matches)) { - throw new \Exception("Unable to detect PHP version (output: $output)."); - } - - $this->version = $matches[1]; - $this->cgi = strcasecmp($matches[2], 'g') === 0; - $this->arguments = $args; - - $job = new Job(__DIR__ . '/info.php', $this, ['xdebug']); - $job->run(); - $this->xdebug = !$job->getExitCode(); - } - - - /** - * @return string - */ - public function getCommandLine() - { - return $this->path . $this->arguments; - } - - - /** - * @return string - */ - public function getVersion() - { - return $this->version; - } - - - /** - * @return bool - */ - public function hasXdebug() - { - return $this->xdebug; - } - - - /** - * @return bool - */ - public function isCgi() - { - return $this->cgi; - } - - - /** - * @return string - */ - public function getErrorOutput() - { - return $this->error; - } - -} diff --git a/src/Runner/info.php b/src/Runner/info.php index 13ddba8d..f28cbb44 100644 --- a/src/Runner/info.php +++ b/src/Runner/info.php @@ -4,32 +4,38 @@ * @internal */ -if (isset($_SERVER['argv'][1])) { - die(extension_loaded($_SERVER['argv'][1]) ? 0 : 1); -} - -$iniFiles = array_merge( - ($tmp = php_ini_loaded_file()) === FALSE ? [] : [$tmp], - (function_exists('php_ini_scanned_files') && strlen($tmp = php_ini_scanned_files())) ? explode(",\n", trim($tmp)) : [] -); - +$isPhpDbg = defined('PHPDBG_VERSION'); +$isHhvm = defined('HHVM_VERSION'); $extensions = get_loaded_extensions(); natcasesort($extensions); -$isHhvm = defined('HHVM_VERSION'); - -$values = [ - 'PHP binary' => defined('PHP_BINARY') ? PHP_BINARY : '(not available)', - - 'PHP version' . ($isHhvm ? '; HHVM version' : '') => PHP_VERSION . ' (' . PHP_SAPI . ')' . ($isHhvm ? '; ' . HHVM_VERSION : ''), - - 'Loaded php.ini files' => count($iniFiles) ? implode(', ', $iniFiles) : ($isHhvm ? '(unable to detect under HHVM)' : '(none)'), - - 'Loaded extensions' => count($extensions) ? implode(', ', $extensions) : '(none)', +$info = (object) [ + 'binary' => defined('PHP_BINARY') ? PHP_BINARY : NULL, + 'version' => PHP_VERSION, + 'phpDbgVersion' => $isPhpDbg ? PHPDBG_VERSION : NULL, + 'sapi' => PHP_SAPI, + 'hhvmVersion' => $isHhvm ? HHVM_VERSION : NULL, + 'iniFiles' => array_merge( + ($tmp = php_ini_loaded_file()) === FALSE ? [] : [$tmp], + (function_exists('php_ini_scanned_files') && strlen($tmp = php_ini_scanned_files())) ? explode(",\n", trim($tmp)) : [] + ), + 'extensions' => $extensions, + 'canMeasureCodeCoverage' => $isPhpDbg || (!$isHhvm && in_array('xdebug', $extensions, TRUE)), ]; -foreach ($values as $title => $value) { +if (isset($_SERVER['argv'][1])) { + echo serialize($info); + die(); +} + +foreach ([ + 'PHP binary' => $info->binary ?: '(not available)', + 'PHP version' . ($isPhpDbg ? '; PHPDBG version' : '') . ($isHhvm ? '; HHVM version' : '') + => "$info->version ($info->sapi)" . ($isPhpDbg ? "; $info->phpDbgVersion" : '') . ($isHhvm ? "; $info->hhvmVersion" : ''), + 'Loaded php.ini files' => count($info->iniFiles) ? implode(', ', $info->iniFiles) : ($isHhvm ? '(unable to detect under HHVM)' : '(none)'), + 'Loaded extensions' => count($info->extensions) ? implode(', ', $info->extensions) : '(none)', +] as $title => $value) { echo "\033[1;32m$title\033[0m:\n$value\n\n"; } -echo "\n\n"; +echo "\n"; diff --git a/src/tester.php b/src/tester.php index fc1e5a98..552cca4f 100644 --- a/src/tester.php +++ b/src/tester.php @@ -6,9 +6,6 @@ */ require __DIR__ . '/Runner/PhpInterpreter.php'; -require __DIR__ . '/Runner/ZendPhpInterpreter.php'; -require __DIR__ . '/Runner/ZendPhpDbgInterpreter.php'; -require __DIR__ . '/Runner/HhvmPhpInterpreter.php'; require __DIR__ . '/Runner/Runner.php'; require __DIR__ . '/Runner/CliTester.php'; require __DIR__ . '/Runner/Job.php'; diff --git a/tests/Runner/HhvmPhpInterpreter.phpt b/tests/Runner/PhpInterpreter.HHVM.phpt similarity index 86% rename from tests/Runner/HhvmPhpInterpreter.phpt rename to tests/Runner/PhpInterpreter.HHVM.phpt index a682b14a..10deb60d 100644 --- a/tests/Runner/HhvmPhpInterpreter.phpt +++ b/tests/Runner/PhpInterpreter.HHVM.phpt @@ -13,5 +13,5 @@ $executable = createInterpreter(); Assert::contains(PHP_BINARY, $executable->getCommandLine()); Assert::same(PHP_VERSION, $executable->getVersion()); -Assert::same(FALSE, $executable->hasXdebug()); +Assert::same(FALSE, $executable->canMeasureCodeCoverage()); Assert::same(FALSE, $executable->isCgi()); diff --git a/tests/Runner/ZendPhpExecutable.phpt b/tests/Runner/PhpInterpreter.Zend.phpt similarity index 90% rename from tests/Runner/ZendPhpExecutable.phpt rename to tests/Runner/PhpInterpreter.Zend.phpt index 20944e5f..9e9a8d1e 100644 --- a/tests/Runner/ZendPhpExecutable.phpt +++ b/tests/Runner/PhpInterpreter.Zend.phpt @@ -13,5 +13,5 @@ $interpreter = createInterpreter(); Assert::contains(PHP_BINARY, $interpreter->getCommandLine()); Assert::same(PHP_VERSION, $interpreter->getVersion()); -Assert::same(extension_loaded('xdebug') || defined('PHPDBG_VERSION'), $interpreter->hasXdebug()); +Assert::same(extension_loaded('xdebug') || defined('PHPDBG_VERSION'), $interpreter->canMeasureCodeCoverage()); Assert::same(strpos(PHP_SAPI, 'cgi') !== FALSE, $interpreter->isCgi()); diff --git a/tests/Runner/PhpInterpreter.phpt b/tests/Runner/PhpInterpreter.phpt new file mode 100644 index 00000000..a5993231 --- /dev/null +++ b/tests/Runner/PhpInterpreter.phpt @@ -0,0 +1,10 @@ +hasExtension('DaTe')); +Assert::false($interpreter->hasExtension('foo-bar')); diff --git a/tests/Runner/Runner.multiple-fails.phpt b/tests/Runner/Runner.multiple-fails.phpt index 89d512af..f31cd7c6 100644 --- a/tests/Runner/Runner.multiple-fails.phpt +++ b/tests/Runner/Runner.multiple-fails.phpt @@ -24,7 +24,8 @@ class Logger implements Tester\Runner\OutputHandler } $interpreter = createInterpreter(); -$interpreter->arguments .= ' -d display_errors=On -d html_errors=Off'; +$interpreter->addPhpIniOption('display_errors', 'on'); +$interpreter->addPhpIniOption('html_errors', 'off'); $runner = new Runner($interpreter); $runner->paths[] = __DIR__ . '/multiple-fails/*.phptx'; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 5a42c42c..f87d48ca 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,10 +1,10 @@