Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for phpdbg with fast code coverage #258

Merged
merged 2 commits into from Jan 30, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .travis.yml
Expand Up @@ -23,6 +23,10 @@ matrix:
- php: hhvm
env: TESTER_PHP_BIN=php-cgi

include:
- php: 7.0
env: TESTER_PHP_BIN=phpdbg

script:
- src/tester -p $TESTER_PHP_BIN tests -s
- src/tester -p $TESTER_PHP_BIN -o none tests/fail.phptx; if [ $? -eq 0 ]; then echo "tests/fail.phptx SHOULD FAIL"; exit 1; else echo "fail checked"; fi;
Expand Down
85 changes: 67 additions & 18 deletions src/CodeCoverage/Collector.php
Expand Up @@ -24,42 +24,91 @@ class Collector
*/
public static function start($file)
{
if (!extension_loaded('xdebug')) {
throw new \Exception('Code coverage functionality requires Xdebug extension.');
} elseif (self::$file) {
if (self::$file) {
throw new \LogicException('Code coverage collector has been already started.');
}

self::$file = fopen($file, 'a+');
xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
register_shutdown_function(function () {
register_shutdown_function(array(__CLASS__, 'save'));

if (defined('PHPDBG_VERSION') && PHP_VERSION_ID >= 70000) {
phpdbg_start_oplog();
$collector = 'collectPhpDbg';

} elseif (extension_loaded('xdebug')) {
xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
$collector = 'collectXdebug';

} else {
$alternative = PHP_VERSION_ID >= 70000 ? ' or phpdbg SAPI' : '';
throw new \Exception("Code coverage functionality requires Xdebug extension$alternative.");
}

register_shutdown_function(function () use ($collector) {
register_shutdown_function(function () use ($collector) {
list($positive, $negative) = call_user_func(array(__CLASS__, $collector));
self::save($positive, $negative);
});
});
}


/**
* Saves information about code coverage. Do not call directly.
* @return void
* @internal
* Collects information about code coverage.
* @return array
*/
public static function save()
private static function collectXdebug()
{
flock(self::$file, LOCK_EX);
fseek(self::$file, 0);
$coverage = @unserialize(stream_get_contents(self::$file)); // @ file may be empty
$positive = $negative = array();

foreach (xdebug_get_code_coverage() as $filename => $lines) {
if (!file_exists($filename)) {
foreach (xdebug_get_code_coverage() as $file => $lines) {
if (!file_exists($file)) {
continue;
}

foreach ($lines as $num => $val) {
if (empty($coverage[$filename][$num]) || $val > 0) {
$coverage[$filename][$num] = $val; // -1 => untested; -2 => dead code
if ($val > 0) {
$positive[$file][$num] = $val;
} else {
$negative[$file][$num] = $val;
}
}
}

return array($positive, $negative);
}


/**
* Collects information about code coverage.
* @return array
*/
private static function collectPhpDbg()
{
$positive = phpdbg_end_oplog();
$negative = phpdbg_get_executable();

foreach ($positive as $file => & $lines) {
$lines = array_fill_keys(array_keys($lines), 1);
}

foreach ($negative as $file => & $lines) {
$lines = array_fill_keys(array_keys($lines), -1);
}

return array($positive, $negative);
}


/**
* Saves information about code coverage. Do not call directly.
* @return void
*/
private static function save(array $positive, array $negative)
{
flock(self::$file, LOCK_EX);
fseek(self::$file, 0);
$original = @unserialize(stream_get_contents(self::$file)) ?: array(); // @ file may be empty
$coverage = array_replace_recursive($negative, $original, $positive);

ftruncate(self::$file, 0);
fwrite(self::$file, serialize($coverage));
fclose(self::$file);
Expand Down
2 changes: 1 addition & 1 deletion src/Framework/Environment.php
Expand Up @@ -62,7 +62,7 @@ public static function setupColors()
{
self::$useColors = getenv(self::COLORS) !== FALSE
? (bool) getenv(self::COLORS)
: (PHP_SAPI === 'cli' && ((function_exists('posix_isatty') && posix_isatty(STDOUT))
: ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && ((function_exists('posix_isatty') && posix_isatty(STDOUT))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is PHP_SAPI === 'phpdbg' correct? PHP_SAPI is 'cli' when argument -qrrb -S cli is used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dg Handy when called phpdbg path/to/test.phpt manually.

|| getenv('ConEmuANSI') === 'ON' || getenv('ANSICON') !== FALSE));

$colors = & self::$useColors;
Expand Down
5 changes: 4 additions & 1 deletion src/Runner/CliTester.php
Expand Up @@ -177,6 +177,8 @@ private function createPhpInterpreter()

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);
}
Expand Down Expand Up @@ -229,7 +231,8 @@ private function createRunner()
private function prepareCodeCoverage()
{
if (!$this->interpreter->hasXdebug()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imho, this should stay there with modified message. And as a workaround, ZendPhpDbgInterpreter::hasXDebug() should return TRUE.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't make any sense. PHPDBG may or may not have XDebug loaded, just like normal CLI/CGI SAPI.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ZendPhpDbgInterpreter::hasXDebug() is used only for detection that Coverage is possible. I have a plan for refactoring.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JanTvrdik Or maybe better ... && !$this->interpreter instanceof ZendPhpDbgInterpreter.

throw new \Exception("Code coverage functionality requires Xdebug extension (used {$this->interpreter->getCommandLine()})");
$alternative = PHP_VERSION_ID >= 70000 ? ' or phpdbg SAPI' : '';
throw new \Exception("Code coverage functionality requires Xdebug extension$alternative (used {$this->interpreter->getCommandLine()})");
}
file_put_contents($this->options['--coverage'], '');
$file = realpath($this->options['--coverage']);
Expand Down
102 changes: 102 additions & 0 deletions src/Runner/ZendPhpDbgInterpreter.php
@@ -0,0 +1,102 @@
<?php

/**
* This file is part of the Nette Tester.
* Copyright (c) 2009 David Grudl (http://davidgrudl.com)
*/

namespace Tester\Runner;

use Tester\Helpers;


/**
* Zend phpdbg command-line executable.
*/
class ZendPhpDbgInterpreter implements PhpInterpreter
{
/** @var string PHP arguments */
public $arguments;

/** @var string PHP executable */
private $path;

/** @var string PHP version */
private $version;

/** @var string */
private $error;


public function __construct($path, $args = NULL)
{
$this->path = Helpers::escapeArg($path);
$proc = proc_open(
"$this->path -n $args -V",
array(array('pipe', 'r'), array('pipe', 'w'), array('pipe', 'w')),
$pipes,
NULL,
NULL,
array('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.');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen some really strange behavior when I ran phpdbg with XDebug loaded as a module. Maybe also check for XDebug not being loaded?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What? Phpdbg is only usable on php7 and xdebux is only usable on php5.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XDebug will be usable on PHP 7 as well, once it's ported. It is already working for debugging (not coverage though).


$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;
}

}
4 changes: 3 additions & 1 deletion src/Runner/ZendPhpInterpreter.php
Expand Up @@ -7,6 +7,8 @@

namespace Tester\Runner;

use Tester\Helpers;


/**
* Zend PHP command-line executable.
Expand Down Expand Up @@ -34,7 +36,7 @@ class ZendPhpInterpreter implements PhpInterpreter

public function __construct($path, $args = NULL)
{
$this->path = \Tester\Helpers::escapeArg($path);
$this->path = Helpers::escapeArg($path);
$proc = proc_open(
"$this->path -n $args -v", // -v must be the last
array(array('pipe', 'r'), array('pipe', 'w'), array('pipe', 'w')),
Expand Down
1 change: 1 addition & 0 deletions src/tester.php
Expand Up @@ -7,6 +7,7 @@

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';
Expand Down
11 changes: 9 additions & 2 deletions tests/Runner/Job.phpt
Expand Up @@ -18,8 +18,15 @@ test(function () {
Assert::same($file, $job->getFile());
Assert::same($args, $job->getArguments());
Assert::same(231, $job->getExitCode());
Assert::same('Args: one, two-output', $job->getOutput());
Assert::same('Error1Error2', $job->getErrorOutput());

if (defined('PHPDBG_VERSION') && PHP_VERSION_ID === 70000) { // bug #71056
Assert::same('Args: one, twoError1-outputError2', $job->getOutput());
Assert::same('', $job->getErrorOutput());
} else {
Assert::same('Args: one, two-output', $job->getOutput());
Assert::same('Error1Error2', $job->getErrorOutput());
}

if (PHP_SAPI !== 'cli') {
Assert::contains('Nette Tester', $job->getHeaders());
}
Expand Down
5 changes: 4 additions & 1 deletion tests/Runner/Runner.multiple-fails.phpt
Expand Up @@ -70,7 +70,10 @@ Assert::same(Runner::FAILED, $logger->results['testcase-pre-fail.phptx'][0]);
Assert::match(
defined('HHVM_VERSION')
? 'Fatal error: syntax error, unexpected $end in %a%testcase-syntax-error.phptx on line %d%'
: 'Parse error: syntax error, unexpected end of file in %a%testcase-syntax-error.phptx on line %d%',
: (defined('PHPDBG_VERSION')
? '%A%Parse error: syntax error, unexpected end of file in %a%testcase-syntax-error.phptx on line %d%'
: 'Parse error: syntax error, unexpected end of file in %a%testcase-syntax-error.phptx on line %d%'
),
trim($logger->results['testcase-syntax-error.phptx'][1])
);
Assert::same(Runner::FAILED, $logger->results['testcase-syntax-error.phptx'][0]);
Expand Down
2 changes: 1 addition & 1 deletion tests/Runner/ZendPhpExecutable.phpt
Expand Up @@ -17,5 +17,5 @@ $interpreter = createInterpreter();

Assert::contains(PHP_BINARY, $interpreter->getCommandLine());
Assert::same(PHP_VERSION, $interpreter->getVersion());
Assert::same(extension_loaded('xdebug'), $interpreter->hasXdebug());
Assert::same(extension_loaded('xdebug') || defined('PHPDBG_VERSION'), $interpreter->hasXdebug());
Assert::same(strpos(PHP_SAPI, 'cgi') !== FALSE, $interpreter->isCgi());
11 changes: 8 additions & 3 deletions tests/bootstrap.php
Expand Up @@ -3,6 +3,7 @@
require __DIR__ . '/../src/bootstrap.php';
require __DIR__ . '/../src/Runner/PhpInterpreter.php';
require __DIR__ . '/../src/Runner/ZendPhpInterpreter.php';
require __DIR__ . '/../src/Runner/ZendPhpDbgInterpreter.php';
require __DIR__ . '/../src/Runner/HhvmPhpInterpreter.php';


Expand All @@ -17,7 +18,11 @@ function test(\Closure $function)
/** @return Tester\Runner\PhpInterpreter */
function createInterpreter()
{
return defined('HHVM_VERSION')
? new Tester\Runner\HhvmPhpInterpreter(PHP_BINARY)
: new Tester\Runner\ZendPhpInterpreter(PHP_BINARY, ' -c ' . Tester\Helpers::escapeArg(php_ini_loaded_file()));
if (defined('HHVM_VERSION')) {
return new Tester\Runner\HhvmPhpInterpreter(PHP_BINARY);
} elseif (defined('PHPDBG_VERSION')) {
return new Tester\Runner\ZendPhpDbgInterpreter(PHP_BINARY, ' -c ' . Tester\Helpers::escapeArg(php_ini_loaded_file()));
} else {
return new Tester\Runner\ZendPhpInterpreter(PHP_BINARY, ' -c ' . Tester\Helpers::escapeArg(php_ini_loaded_file()));
}
}