Skip to content

Commit

Permalink
Merge pull request #258 from JanTvrdik/phpdbg
Browse files Browse the repository at this point in the history
Support for phpdbg with fast code coverage
  • Loading branch information
milo committed Jan 30, 2016
2 parents cf10fe1 + 9b62462 commit e227a3f
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 28 deletions.
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))
|| 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()) {
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.');
}

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

0 comments on commit e227a3f

Please sign in to comment.