From 1532eca72482895186fdedbef620e3dde2c792c6 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sun, 2 Mar 2014 18:02:08 +0100 Subject: [PATCH 01/31] Fix dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index cc00a4dbf..409ce8a38 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "sebastian/version": ">=1.0.0" }, "require-dev": { - "phpunit/phpunit": "4.0.*@dev", + "phpunit/phpunit": "4.1.*@dev", "ext-xdebug": ">=2.1.4" }, "suggest": { From 86cb041851e83b681ac1c44b83ba5b8931234f77 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sun, 16 Mar 2014 09:35:54 +0100 Subject: [PATCH 02/31] Eliminate unused local variable --- src/CodeCoverage/Report/HTML/Renderer/Dashboard.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php b/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php index dbf21252e..1c2f1fd55 100644 --- a/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php +++ b/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php @@ -175,7 +175,7 @@ protected function coverageDistribution(array $classes) ); foreach ($classes as $class) { - foreach ($class['methods'] as $methodName => $method) { + foreach ($class['methods'] as $method) { if ($method['coverage'] == 0) { $result['method']['0%']++; } elseif ($method['coverage'] == 100) { @@ -318,6 +318,10 @@ protected function projectRisks(array $classes) return $result; } + /** + * @param PHP_CodeCoverage_Report_Node $node + * @return string + */ protected function getActiveBreadcrumb(PHP_CodeCoverage_Report_Node $node) { return sprintf( From 1068cf760851468b95439ae6f1b789420e1b2e61 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sun, 16 Mar 2014 09:37:05 +0100 Subject: [PATCH 03/31] Eliminate unused local variable --- src/CodeCoverage/Report/HTML/Renderer/File.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CodeCoverage/Report/HTML/Renderer/File.php b/src/CodeCoverage/Report/HTML/Renderer/File.php index 8c73f8f63..7657e7af2 100644 --- a/src/CodeCoverage/Report/HTML/Renderer/File.php +++ b/src/CodeCoverage/Report/HTML/Renderer/File.php @@ -329,7 +329,6 @@ protected function renderSource(PHP_CodeCoverage_Report_Node_File $node) $i = 1; foreach ($codeLines as $line) { - $numTests = ''; $trClass = ''; $popoverContent = ''; $popoverTitle = ''; From 4876f10eb89914c2b17b178ecd5289954c180bcc Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sun, 16 Mar 2014 09:37:25 +0100 Subject: [PATCH 04/31] Add/Fix docblocks --- src/CodeCoverage/Filter.php | 6 +++--- src/CodeCoverage/Report/Factory.php | 3 ++- src/CodeCoverage/Report/HTML/Renderer.php | 21 +++++++++++++++++++ .../Report/HTML/Renderer/File.php | 2 ++ src/CodeCoverage/Util.php | 6 ++++-- .../Util/InvalidArgumentHelper.php | 7 ++++--- 6 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/CodeCoverage/Filter.php b/src/CodeCoverage/Filter.php index 361ca48a2..904651639 100644 --- a/src/CodeCoverage/Filter.php +++ b/src/CodeCoverage/Filter.php @@ -218,7 +218,8 @@ public function removeFileFromWhitelist($filename) /** * Checks whether a filename is a real filename. * - * @param string $filename + * @param string $filename + * @return boolean */ public function isFile($filename) { @@ -240,8 +241,7 @@ public function isFile($filename) * When the whitelist is empty (default), blacklisting is used. * When the whitelist is not empty, whitelisting is used. * - * @param string $filename - * @param boolean $ignoreWhitelist + * @param string $filename * @return boolean * @throws PHP_CodeCoverage_Exception */ diff --git a/src/CodeCoverage/Report/Factory.php b/src/CodeCoverage/Report/Factory.php index c8cb63b4b..72b974a84 100644 --- a/src/CodeCoverage/Report/Factory.php +++ b/src/CodeCoverage/Report/Factory.php @@ -57,7 +57,8 @@ class PHP_CodeCoverage_Report_Factory { /** - * @param PHP_CodeCoverage $coverage + * @param PHP_CodeCoverage $coverage + * @return PHP_CodeCoverage_Report_Node_Directory */ public function create(PHP_CodeCoverage $coverage) { diff --git a/src/CodeCoverage/Report/HTML/Renderer.php b/src/CodeCoverage/Report/HTML/Renderer.php index 6f0876c8a..598b10a7e 100644 --- a/src/CodeCoverage/Report/HTML/Renderer.php +++ b/src/CodeCoverage/Report/HTML/Renderer.php @@ -211,6 +211,10 @@ protected function setCommonTemplateVariables(Text_Template $template, PHP_CodeC ); } + /** + * @param PHP_CodeCoverage_Report_Node $node + * @return string + */ protected function getBreadcrumbs(PHP_CodeCoverage_Report_Node $node) { $breadcrumbs = ''; @@ -239,6 +243,10 @@ protected function getBreadcrumbs(PHP_CodeCoverage_Report_Node $node) return $breadcrumbs; } + /** + * @param PHP_CodeCoverage_Report_Node $node + * @return string + */ protected function getActiveBreadcrumb(PHP_CodeCoverage_Report_Node $node) { $buffer = sprintf( @@ -253,6 +261,11 @@ protected function getActiveBreadcrumb(PHP_CodeCoverage_Report_Node $node) return $buffer; } + /** + * @param PHP_CodeCoverage_Report_Node $node + * @param $pathToRoot + * @return string + */ protected function getInactiveBreadcrumb(PHP_CodeCoverage_Report_Node $node, $pathToRoot) { return sprintf( @@ -262,6 +275,10 @@ protected function getInactiveBreadcrumb(PHP_CodeCoverage_Report_Node $node, $pa ); } + /** + * @param PHP_CodeCoverage_Report_Node $node + * @return string + */ protected function getPathToRoot(PHP_CodeCoverage_Report_Node $node) { $id = $node->getId(); @@ -275,6 +292,10 @@ protected function getPathToRoot(PHP_CodeCoverage_Report_Node $node) return str_repeat('../', $depth); } + /** + * @param float $percent + * @return string + */ protected function getCoverageBar($percent) { $level = $this->getColorLevel($percent); diff --git a/src/CodeCoverage/Report/HTML/Renderer/File.php b/src/CodeCoverage/Report/HTML/Renderer/File.php index 7657e7af2..b8a119655 100644 --- a/src/CodeCoverage/Report/HTML/Renderer/File.php +++ b/src/CodeCoverage/Report/HTML/Renderer/File.php @@ -272,6 +272,8 @@ protected function renderFunctionItems(array $functions, Text_Template $template /** * @param Text_Template $template + * @param array $item + * @param string $indent * @return string */ protected function renderFunctionOrMethodItem(Text_Template $template, array $item, $indent = '') diff --git a/src/CodeCoverage/Util.php b/src/CodeCoverage/Util.php index f17904279..8cf922389 100644 --- a/src/CodeCoverage/Util.php +++ b/src/CodeCoverage/Util.php @@ -57,8 +57,10 @@ class PHP_CodeCoverage_Util { /** - * @param float $a - * @param float $b + * @param float $a + * @param float $b + * @param boolean $asString + * @param boolean $fixedWidth * @return float ($a / $b) * 100 */ public static function percent($a, $b, $asString = false, $fixedWidth = false) diff --git a/src/CodeCoverage/Util/InvalidArgumentHelper.php b/src/CodeCoverage/Util/InvalidArgumentHelper.php index 6637a978d..7cd5e712c 100644 --- a/src/CodeCoverage/Util/InvalidArgumentHelper.php +++ b/src/CodeCoverage/Util/InvalidArgumentHelper.php @@ -58,9 +58,10 @@ class PHP_CodeCoverage_Util_InvalidArgumentHelper { /** - * @param integer $argument - * @param string $type - * @param mixed $value + * @param integer $argument + * @param string $type + * @param mixed $value + * @return PHP_CodeCoverage_Exception */ public static function factory($argument, $type, $value = null) { From edd13bbf938b1fa6d41edf8e68d7d696e9a605b1 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Mon, 17 Mar 2014 09:42:12 +0100 Subject: [PATCH 05/31] Blacklist SebastianBergmann\Comparator\Comparator --- src/CodeCoverage/Filter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CodeCoverage/Filter.php b/src/CodeCoverage/Filter.php index 904651639..4f9f8055f 100644 --- a/src/CodeCoverage/Filter.php +++ b/src/CodeCoverage/Filter.php @@ -314,6 +314,7 @@ private function prefillBlacklist() $this->addDirectoryContainingClassToBlacklist('Symfony\Component\Yaml\Yaml'); $this->addDirectoryContainingClassToBlacklist('SebastianBergmann\Diff'); $this->addDirectoryContainingClassToBlacklist('SebastianBergmann\Environment\Runtime'); + $this->addDirectoryContainingClassToBlacklist('SebastianBergmann\Comparator\Comparator'); $this->addDirectoryContainingClassToBlacklist('SebastianBergmann\Exporter\Exporter'); $this->addDirectoryContainingClassToBlacklist('SebastianBergmann\Version'); $this->addDirectoryContainingClassToBlacklist('Composer\Autoload\ClassLoader'); From de3b3dded309fbefce73ce8f6ee81b811c73d857 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Wed, 19 Mar 2014 18:05:18 +0100 Subject: [PATCH 06/31] Move list of blacklisted classes to PHPUnit --- src/CodeCoverage/Filter.php | 72 ------------------------------------- 1 file changed, 72 deletions(-) diff --git a/src/CodeCoverage/Filter.php b/src/CodeCoverage/Filter.php index bfbd8644c..468622dd5 100644 --- a/src/CodeCoverage/Filter.php +++ b/src/CodeCoverage/Filter.php @@ -70,37 +70,6 @@ class PHP_CodeCoverage_Filter */ private $whitelistedFiles = array(); - /** - * @var boolean - */ - private $blacklistPrefilled = false; - - /** - * A list of classes which are always blacklisted - * - * @var array - */ - public static $blacklistClassNames = array( - 'File_Iterator' => 1, - 'PHP_CodeCoverage' => 1, - 'PHP_Invoker' => 1, - 'PHP_Timer' => 1, - 'PHP_Token' => 1, - 'PHPUnit_Framework_TestCase' => 2, - 'PHPUnit_Extensions_Database_TestCase' => 2, - 'PHPUnit_Framework_MockObject_Generator' => 2, - 'PHPUnit_Extensions_SeleniumTestCase' => 2, - 'PHPUnit_Extensions_Story_TestCase' => 2, - 'Text_Template' => 1, - 'Symfony\Component\Yaml\Yaml' => 1, - 'SebastianBergmann\Diff\Diff' => 1, - 'SebastianBergmann\Environment\Runtime' => 1, - 'SebastianBergmann\Comparator\Comparator' => 1, - 'SebastianBergmann\Exporter\Exporter' => 1, - 'SebastianBergmann\Version' => 1, - 'Composer\Autoload\ClassLoader' => 1 - ); - /** * Adds a directory to the blacklist (recursively). * @@ -279,10 +248,6 @@ public function isFiltered($filename) return !isset($this->whitelistedFiles[$filename]); } - if (!$this->blacklistPrefilled) { - $this->prefillBlacklist(); - } - return isset($this->blacklistedFiles[$filename]); } @@ -317,43 +282,6 @@ public function hasWhitelist() return !empty($this->whitelistedFiles); } - /** - * @since Method available since Release 1.2.3 - */ - private function prefillBlacklist() - { - if (defined('__PHPUNIT_PHAR__')) { - $this->addFileToBlacklist(__PHPUNIT_PHAR__); - } - - foreach (self::$blacklistClassNames as $className => $parent) { - $this->addDirectoryContainingClassToBlacklist($className, $parent); - } - - $this->blacklistPrefilled = true; - } - - /** - * @param string $className - * @param integer $parent - * @since Method available since Release 1.2.3 - */ - private function addDirectoryContainingClassToBlacklist($className, $parent = 1) - { - if (!class_exists($className)) { - return; - } - - $reflector = new ReflectionClass($className); - $directory = $reflector->getFileName(); - - for ($i = 0; $i < $parent; $i++) { - $directory = dirname($directory); - } - - $this->addDirectoryToBlacklist($directory); - } - /** * Returns the blacklisted files. * From f75186110012859f64ed143e426f02ce164f7b81 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Thu, 20 Mar 2014 09:53:42 +0100 Subject: [PATCH 07/31] Refactoring of data filtering and data cleanup * PHP_CodeCoverage_Driver is now an abstract class (was an interface before) * Move getAllowedLines and getLinesToBeIgnored() from PHP_CodeCoverage to new PHP_CodeCoverage_Parser class * Move the application of the blacklist/whitelist/"ignored lines" filtering from PHP_CodeCoverage to PHP_CodeCoverage_Driver * Driver-specific data cleanup is now performed after the blacklist/whitelist/"ignored lines" filtering --- src/CodeCoverage.php | 279 ++---------------------- src/CodeCoverage/Driver.php | 97 ++++++++- src/CodeCoverage/Driver/HHVM.php | 14 +- src/CodeCoverage/Driver/Xdebug.php | 24 +-- src/CodeCoverage/Parser.php | 297 ++++++++++++++++++++++++++ tests/PHP/CodeCoverage/ParserTest.php | 125 +++++++++++ tests/PHP/CodeCoverageTest.php | 159 +------------- tests/TestCase.php | 59 +++-- 8 files changed, 593 insertions(+), 461 deletions(-) create mode 100644 src/CodeCoverage/Parser.php create mode 100644 tests/PHP/CodeCoverage/ParserTest.php diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index 8b62fb8b1..36df7cec9 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -68,6 +68,11 @@ class PHP_CodeCoverage */ private $filter; + /** + * @var PHP_CodeCoverage_Parser + */ + private $parser; + /** * @var boolean */ @@ -110,11 +115,6 @@ class PHP_CodeCoverage */ private $data = array(); - /** - * @var array - */ - private $ignoredLines = array(); - /** * Test data. * @@ -131,24 +131,27 @@ class PHP_CodeCoverage */ public function __construct(PHP_CodeCoverage_Driver $driver = null, PHP_CodeCoverage_Filter $filter = null) { + if ($filter === null) { + $filter = new PHP_CodeCoverage_Filter; + } + + $parser = new PHP_CodeCoverage_Parser; + if ($driver === null) { $runtime = new Runtime; if ($runtime->isHHVM()) { - $driver = new PHP_CodeCoverage_Driver_HHVM; + $driver = new PHP_CodeCoverage_Driver_HHVM($filter, $parser); } elseif ($runtime->hasXdebug()) { - $driver = new PHP_CodeCoverage_Driver_Xdebug; + $driver = new PHP_CodeCoverage_Driver_Xdebug($filter, $parser); } else { throw new PHP_CodeCoverage_Exception('No code coverage driver available'); } } - if ($filter === null) { - $filter = new PHP_CodeCoverage_Filter; - } - $this->driver = $driver; $this->filter = $filter; + $this->parser = $parser; } /** @@ -197,12 +200,6 @@ public function getData() $this->addUncoveredFilesFromWhitelist(); } - // We need to apply the blacklist filter a second time - // when no whitelist is used. - if (!$this->filter->hasWhitelist()) { - $this->applyListsFilter($this->data); - } - return $this->data; } @@ -317,8 +314,6 @@ public function append(array $data, $id = null, $append = true, $linesToBeCovere throw new PHP_CodeCoverage_Exception; } - $this->applyListsFilter($data); - $this->applyIgnoredLinesFilter($data); $this->initializeFilesThatAreSeenTheFirstTime($data); if (!$append) { @@ -407,6 +402,8 @@ public function setCacheTokens($flag) ); } + $this->parser->setCacheTokens($flag); + $this->cacheTokens = $flag; } @@ -540,42 +537,6 @@ private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, ar } } - /** - * Applies the blacklist/whitelist filtering. - * - * @param array $data - */ - private function applyListsFilter(array &$data) - { - foreach (array_keys($data) as $filename) { - if ($this->filter->isFiltered($filename)) { - unset($data[$filename]); - } - } - } - - /** - * Applies the "ignored lines" filtering. - * - * @param array $data - */ - private function applyIgnoredLinesFilter(array &$data) - { - foreach (array_keys($data) as $filename) { - if (!$this->filter->isFile($filename)) { - continue; - } - - foreach ($this->getLinesToBeIgnored($filename) as $line) { - unset($data[$filename][$line]); - } - - if (empty($data[$filename])) { - unset($data[$filename]); - } - } - } - /** * @param array $data * @since Method available since Release 1.1.0 @@ -654,171 +615,6 @@ private function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, } } - /** - * Returns the lines of a source file that should be ignored. - * - * @param string $filename - * @return array - * @throws PHP_CodeCoverage_Exception - * @since Method available since Release 2.0.0 - */ - private function getLinesToBeIgnored($filename) - { - if (!is_string($filename)) { - throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( - 1, - 'string' - ); - } - - if (!isset($this->ignoredLines[$filename])) { - $this->ignoredLines[$filename] = array(); - $ignore = false; - $stop = false; - $lines = file($filename); - $numLines = count($lines); - - foreach ($lines as $index => $line) { - if (!trim($line)) { - $this->ignoredLines[$filename][] = $index + 1; - } - } - - if ($this->cacheTokens) { - $tokens = PHP_Token_Stream_CachingFactory::get($filename); - } else { - $tokens = new PHP_Token_Stream($filename); - } - - $classes = array_merge($tokens->getClasses(), $tokens->getTraits()); - $tokens = $tokens->tokens(); - - foreach ($tokens as $token) { - switch (get_class($token)) { - case 'PHP_Token_COMMENT': - case 'PHP_Token_DOC_COMMENT': - $_token = trim($token); - $_line = trim($lines[$token->getLine() - 1]); - - if ($_token == '// @codeCoverageIgnore' || - $_token == '//@codeCoverageIgnore') { - $ignore = true; - $stop = true; - } elseif ($_token == '// @codeCoverageIgnoreStart' || - $_token == '//@codeCoverageIgnoreStart') { - $ignore = true; - } elseif ($_token == '// @codeCoverageIgnoreEnd' || - $_token == '//@codeCoverageIgnoreEnd') { - $stop = true; - } - - // Do not ignore the whole line when there is a token - // before the comment on the same line - if (0 === strpos($_token, $_line)) { - $count = substr_count($token, "\n"); - $line = $token->getLine(); - - for ($i = $line; $i < $line + $count; $i++) { - $this->ignoredLines[$filename][] = $i; - } - - if ($token instanceof PHP_Token_DOC_COMMENT) { - // The DOC_COMMENT token does not contain the - // final \n character in its text - if (substr(trim($lines[$i-1]), -2) == '*/') { - $this->ignoredLines[$filename][] = $i; - } - } - } - break; - - case 'PHP_Token_INTERFACE': - case 'PHP_Token_TRAIT': - case 'PHP_Token_CLASS': - case 'PHP_Token_FUNCTION': - $docblock = $token->getDocblock(); - - $this->ignoredLines[$filename][] = $token->getLine(); - - if (strpos($docblock, '@codeCoverageIgnore')) { - $endLine = $token->getEndLine(); - - for ($i = $token->getLine(); $i <= $endLine; $i++) { - $this->ignoredLines[$filename][] = $i; - } - } elseif ($token instanceof PHP_Token_INTERFACE || - $token instanceof PHP_Token_TRAIT || - $token instanceof PHP_Token_CLASS) { - if (empty($classes[$token->getName()]['methods'])) { - for ($i = $token->getLine(); - $i <= $token->getEndLine(); - $i++) { - $this->ignoredLines[$filename][] = $i; - } - } else { - $firstMethod = array_shift( - $classes[$token->getName()]['methods'] - ); - - do { - $lastMethod = array_pop( - $classes[$token->getName()]['methods'] - ); - } while ($lastMethod !== null && - substr($lastMethod['signature'], 0, 18) == 'anonymous function'); - - if ($lastMethod === null) { - $lastMethod = $firstMethod; - } - - for ($i = $token->getLine(); - $i < $firstMethod['startLine']; - $i++) { - $this->ignoredLines[$filename][] = $i; - } - - for ($i = $token->getEndLine(); - $i > $lastMethod['endLine']; - $i--) { - $this->ignoredLines[$filename][] = $i; - } - } - } - break; - - case 'PHP_Token_NAMESPACE': - $this->ignoredLines[$filename][] = $token->getEndLine(); - - // Intentional fallthrough - case 'PHP_Token_OPEN_TAG': - case 'PHP_Token_CLOSE_TAG': - case 'PHP_Token_USE': - $this->ignoredLines[$filename][] = $token->getLine(); - break; - } - - if ($ignore) { - $this->ignoredLines[$filename][] = $token->getLine(); - - if ($stop) { - $ignore = false; - $stop = false; - } - } - } - - $this->ignoredLines[$filename][] = $numLines + 1; - - $this->ignoredLines[$filename] = array_unique( - $this->ignoredLines[$filename] - ); - - sort($this->ignoredLines[$filename]); - } - - return $this->ignoredLines[$filename]; - } - /** * @param array $data * @param array $linesToBeCovered @@ -828,7 +624,7 @@ private function getLinesToBeIgnored($filename) */ private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) { - $allowedLines = $this->getAllowedLines( + $allowedLines = $this->parser->getAllowedLines( $linesToBeCovered, $linesToBeUsed ); @@ -855,45 +651,4 @@ private function performUnintentionallyCoveredCodeCheck(array &$data, array $lin ); } } - - /** - * @param array $linesToBeCovered - * @param array $linesToBeUsed - * @return array - * @since Method available since Release 2.0.0 - */ - private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed) - { - $allowedLines = array(); - - foreach (array_keys($linesToBeCovered) as $file) { - if (!isset($allowedLines[$file])) { - $allowedLines[$file] = array(); - } - - $allowedLines[$file] = array_merge( - $allowedLines[$file], - $linesToBeCovered[$file] - ); - } - - foreach (array_keys($linesToBeUsed) as $file) { - if (!isset($allowedLines[$file])) { - $allowedLines[$file] = array(); - } - - $allowedLines[$file] = array_merge( - $allowedLines[$file], - $linesToBeUsed[$file] - ); - } - - foreach (array_keys($allowedLines) as $file) { - $allowedLines[$file] = array_flip( - array_unique($allowedLines[$file]) - ); - } - - return $allowedLines; - } } diff --git a/src/CodeCoverage/Driver.php b/src/CodeCoverage/Driver.php index e9bd69991..02afef163 100644 --- a/src/CodeCoverage/Driver.php +++ b/src/CodeCoverage/Driver.php @@ -40,11 +40,11 @@ * @copyright 2009-2014 Sebastian Bergmann * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License * @link http://github.com/sebastianbergmann/php-code-coverage - * @since File available since Release 1.0.0 + * @since File available since Release 2.1.0 */ /** - * Interface for code coverage drivers. + * Base class for code coverage drivers. * * @category PHP * @package CodeCoverage @@ -52,19 +52,104 @@ * @copyright 2009-2014 Sebastian Bergmann * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License * @link http://github.com/sebastianbergmann/php-code-coverage - * @since Class available since Release 1.0.0 + * @since Class available since Release 2.1.0 */ -interface PHP_CodeCoverage_Driver +abstract class PHP_CodeCoverage_Driver { + /** + * @var PHP_CodeCoverage_Filter + */ + private $filter; + + /** + * @var PHP_CodeCoverage_Parser + */ + private $parser; + + /** + * @var array + */ + private $ignoredLines = array(); + + /** + * @param PHP_CodeCoverage_Filter $filter + * @param PHP_CodeCoverage_Parser $parser + */ + public function __construct(PHP_CodeCoverage_Filter $filter, PHP_CodeCoverage_Parser $parser) + { + $this->filter = $filter; + $this->parser = $parser; + } + /** * Start collection of code coverage information. */ - public function start(); + public function start() + { + $this->doStart(); + } /** * Stop collection of code coverage information. * * @return array */ - public function stop(); + public function stop() + { + $data = $this->doStop(); + + $this->filter($data); + $this->cleanup($data); + + return $data; + } + + /** + * @throws PHP_CodeCoverage_Exception + */ + abstract protected function ensureDriverCanWork(); + + /** + * Start collection of code coverage information. + */ + abstract protected function doStart(); + + /** + * Stop collection of code coverage information. + * + * @return array + */ + abstract protected function doStop(); + + /** + * Template method to perform driver-specific data cleanup. + * + * @param array $data + */ + protected function cleanup(array &$data) + { + } + + /** + * Performs blacklist and whitelist as well as @codeCoverageIgnore* filtering. + * + * @param array $data + */ + private function filter(array &$data) + { + foreach (array_keys($data) as $filename) { + if ($this->filter->isFiltered($filename)) { + unset($data[$filename]); + continue; + } + + foreach ($this->parser->getLinesToBeIgnored($filename) as $line) { + unset($data[$filename][$line]); + } + + if (empty($data[$filename])) { + unset($data[$filename]); + } + } + } } diff --git a/src/CodeCoverage/Driver/HHVM.php b/src/CodeCoverage/Driver/HHVM.php index cc370e76e..554bb5578 100644 --- a/src/CodeCoverage/Driver/HHVM.php +++ b/src/CodeCoverage/Driver/HHVM.php @@ -55,12 +55,12 @@ * @since Class available since Release 1.3.0 * @codeCoverageIgnore */ -class PHP_CodeCoverage_Driver_HHVM implements PHP_CodeCoverage_Driver +class PHP_CodeCoverage_Driver_HHVM extends PHP_CodeCoverage_Driver { /** - * Constructor. + * @throws PHP_CodeCoverage_Exception */ - public function __construct() + protected function ensureDriverCanWork() { if (!defined('HHVM_VERSION')) { throw new PHP_CodeCoverage_Exception('This driver requires HHVM'); @@ -70,7 +70,7 @@ public function __construct() /** * Start collection of code coverage information. */ - public function start() + protected function doStart() { fb_enable_code_coverage(); } @@ -80,12 +80,12 @@ public function start() * * @return array */ - public function stop() + protected function doStop() { - $codeCoverage = fb_get_code_coverage(TRUE); + $data = fb_get_code_coverage(TRUE); fb_disable_code_coverage(); - return $codeCoverage; + return $data; } } diff --git a/src/CodeCoverage/Driver/Xdebug.php b/src/CodeCoverage/Driver/Xdebug.php index 3c2fb4c7b..b29c29470 100644 --- a/src/CodeCoverage/Driver/Xdebug.php +++ b/src/CodeCoverage/Driver/Xdebug.php @@ -55,12 +55,12 @@ * @since Class available since Release 1.0.0 * @codeCoverageIgnore */ -class PHP_CodeCoverage_Driver_Xdebug implements PHP_CodeCoverage_Driver +class PHP_CodeCoverage_Driver_Xdebug extends PHP_CodeCoverage_Driver { /** - * Constructor. + * @throws PHP_CodeCoverage_Exception */ - public function __construct() + protected function ensureDriverCanWork() { if (!extension_loaded('xdebug')) { throw new PHP_CodeCoverage_Exception('This driver requires Xdebug'); @@ -77,7 +77,7 @@ public function __construct() /** * Start collection of code coverage information. */ - public function start() + protected function doStart() { xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); } @@ -87,20 +87,19 @@ public function start() * * @return array */ - public function stop() + protected function doStop() { $data = xdebug_get_code_coverage(); + xdebug_stop_code_coverage(); - return $this->cleanup($data); + return $data; } /** - * @param array $data - * @return array - * @since Method available since Release 2.0.0 + * @param array $data */ - private function cleanup(array $data) + protected function cleanup(array &$data) { foreach (array_keys($data) as $file) { if (isset($data[$file][0])) { @@ -117,16 +116,13 @@ private function cleanup(array $data) } } } - - return $data; } /** * @param string $file * @return integer - * @since Method available since Release 2.0.0 */ - private function getNumberOfLinesInFile($file) + protected function getNumberOfLinesInFile($file) { $buffer = file_get_contents($file); $lines = substr_count($buffer, "\n"); diff --git a/src/CodeCoverage/Parser.php b/src/CodeCoverage/Parser.php new file mode 100644 index 000000000..aa7007a67 --- /dev/null +++ b/src/CodeCoverage/Parser.php @@ -0,0 +1,297 @@ +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @category PHP + * @package CodeCoverage + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @link http://github.com/sebastianbergmann/php-code-coverage + * @since File available since Release 2.1.0 + */ + +/** + * + * + * @category PHP + * @package CodeCoverage + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @link http://github.com/sebastianbergmann/php-code-coverage + * @since Class available since Release 2.1.0 + */ +class PHP_CodeCoverage_Parser +{ + /** + * @var boolean + */ + private $cacheTokens = false; + + /** + * @var array + */ + private $ignoredLines = array(); + + /** + * @param array $linesToBeCovered + * @param array $linesToBeUsed + * @return array + */ + public function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed) + { + $allowedLines = array(); + + foreach (array_keys($linesToBeCovered) as $file) { + if (!isset($allowedLines[$file])) { + $allowedLines[$file] = array(); + } + + $allowedLines[$file] = array_merge( + $allowedLines[$file], + $linesToBeCovered[$file] + ); + } + + foreach (array_keys($linesToBeUsed) as $file) { + if (!isset($allowedLines[$file])) { + $allowedLines[$file] = array(); + } + + $allowedLines[$file] = array_merge( + $allowedLines[$file], + $linesToBeUsed[$file] + ); + } + + foreach (array_keys($allowedLines) as $file) { + $allowedLines[$file] = array_flip( + array_unique($allowedLines[$file]) + ); + } + + return $allowedLines; + } + + /** + * Returns the lines of a source file that should be ignored. + * + * @param string $filename + * @return array + * @throws PHP_CodeCoverage_Exception + * @since Method available since Release 2.0.0 + */ + public function getLinesToBeIgnored($filename) + { + if (!is_string($filename)) { + throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + 1, + 'string' + ); + } + + if (!isset($this->ignoredLines[$filename])) { + $this->ignoredLines[$filename] = array(); + $ignore = false; + $stop = false; + $lines = file($filename); + $numLines = count($lines); + + foreach ($lines as $index => $line) { + if (!trim($line)) { + $this->ignoredLines[$filename][] = $index + 1; + } + } + + if ($this->cacheTokens) { + $tokens = PHP_Token_Stream_CachingFactory::get($filename); + } else { + $tokens = new PHP_Token_Stream($filename); + } + + $classes = array_merge($tokens->getClasses(), $tokens->getTraits()); + $tokens = $tokens->tokens(); + + foreach ($tokens as $token) { + switch (get_class($token)) { + case 'PHP_Token_COMMENT': + case 'PHP_Token_DOC_COMMENT': + $_token = trim($token); + $_line = trim($lines[$token->getLine() - 1]); + + if ($_token == '// @codeCoverageIgnore' || + $_token == '//@codeCoverageIgnore') { + $ignore = true; + $stop = true; + } elseif ($_token == '// @codeCoverageIgnoreStart' || + $_token == '//@codeCoverageIgnoreStart') { + $ignore = true; + } elseif ($_token == '// @codeCoverageIgnoreEnd' || + $_token == '//@codeCoverageIgnoreEnd') { + $stop = true; + } + + // Do not ignore the whole line when there is a token + // before the comment on the same line + if (0 === strpos($_token, $_line)) { + $count = substr_count($token, "\n"); + $line = $token->getLine(); + + for ($i = $line; $i < $line + $count; $i++) { + $this->ignoredLines[$filename][] = $i; + } + + if ($token instanceof PHP_Token_DOC_COMMENT) { + // The DOC_COMMENT token does not contain the + // final \n character in its text + if (substr(trim($lines[$i-1]), -2) == '*/') { + $this->ignoredLines[$filename][] = $i; + } + } + } + break; + + case 'PHP_Token_INTERFACE': + case 'PHP_Token_TRAIT': + case 'PHP_Token_CLASS': + case 'PHP_Token_FUNCTION': + $docblock = $token->getDocblock(); + + $this->ignoredLines[$filename][] = $token->getLine(); + + if (strpos($docblock, '@codeCoverageIgnore')) { + $endLine = $token->getEndLine(); + + for ($i = $token->getLine(); $i <= $endLine; $i++) { + $this->ignoredLines[$filename][] = $i; + } + } elseif ($token instanceof PHP_Token_INTERFACE || + $token instanceof PHP_Token_TRAIT || + $token instanceof PHP_Token_CLASS) { + if (empty($classes[$token->getName()]['methods'])) { + for ($i = $token->getLine(); + $i <= $token->getEndLine(); + $i++) { + $this->ignoredLines[$filename][] = $i; + } + } else { + $firstMethod = array_shift( + $classes[$token->getName()]['methods'] + ); + + do { + $lastMethod = array_pop( + $classes[$token->getName()]['methods'] + ); + } while ($lastMethod !== null && + substr($lastMethod['signature'], 0, 18) == 'anonymous function'); + + if ($lastMethod === null) { + $lastMethod = $firstMethod; + } + + for ($i = $token->getLine(); + $i < $firstMethod['startLine']; + $i++) { + $this->ignoredLines[$filename][] = $i; + } + + for ($i = $token->getEndLine(); + $i > $lastMethod['endLine']; + $i--) { + $this->ignoredLines[$filename][] = $i; + } + } + } + break; + + case 'PHP_Token_NAMESPACE': + $this->ignoredLines[$filename][] = $token->getEndLine(); + + // Intentional fallthrough + case 'PHP_Token_OPEN_TAG': + case 'PHP_Token_CLOSE_TAG': + case 'PHP_Token_USE': + $this->ignoredLines[$filename][] = $token->getLine(); + break; + } + + if ($ignore) { + $this->ignoredLines[$filename][] = $token->getLine(); + + if ($stop) { + $ignore = false; + $stop = false; + } + } + } + + $this->ignoredLines[$filename][] = $numLines + 1; + + $this->ignoredLines[$filename] = array_unique( + $this->ignoredLines[$filename] + ); + + sort($this->ignoredLines[$filename]); + } + + return $this->ignoredLines[$filename]; + } + + /** + * @param boolean $flag + * @throws PHP_CodeCoverage_Exception + */ + public function setCacheTokens($flag) + { + if (!is_bool($flag)) { + throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + 1, + 'boolean' + ); + } + + $this->cacheTokens = $flag; + } + + /** + * @return boolean + */ + public function getCacheTokens() + { + return $this->cacheTokens; + } +} diff --git a/tests/PHP/CodeCoverage/ParserTest.php b/tests/PHP/CodeCoverage/ParserTest.php new file mode 100644 index 000000000..59f46ac40 --- /dev/null +++ b/tests/PHP/CodeCoverage/ParserTest.php @@ -0,0 +1,125 @@ +parser = new PHP_CodeCoverage_Parser; + } + + /** + * @param string $filename + * @param array $expectedResult + * @dataProvider ignoredLinesProvider + * @covers PHP_CodeCoverage_Parser::getLinesToBeIgnored + */ + public function testLinesToBeIgnoredAreParsedCorrectly($filename, array $expectedResult) + { + $this->assertEquals($expectedResult, $this->parser->getLinesToBeIgnored($filename)); + } + + /** + * @return array + */ + public function ignoredLinesProvider() + { + return array( + array( + TEST_FILES_PATH . 'source_with_ignore.php', + array( + 1, + 3, + 4, + 5, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 30, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ) + ), + array( + TEST_FILES_PATH . 'source_without_ignore.php', + array(1, 5) + ), + array( + TEST_FILES_PATH . 'source_with_class_and_anonymous_function.php', + array( + 1, + 2, + 3, + 4, + 5, + 8, + 11, + 15, + 16, + 19, + 20 + ) + ), + array( + TEST_FILES_PATH . 'source_with_oneline_annotations.php', + array( + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 18, + 20, + 23, + 24, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 37 + ) + ) + ); + } +} diff --git a/tests/PHP/CodeCoverageTest.php b/tests/PHP/CodeCoverageTest.php index 745657c1f..01c065df2 100644 --- a/tests/PHP/CodeCoverageTest.php +++ b/tests/PHP/CodeCoverageTest.php @@ -70,6 +70,9 @@ */ class PHP_CodeCoverageTest extends PHP_CodeCoverage_TestCase { + /** + * @var PHP_CodeCoverage + */ private $coverage; protected function setUp() @@ -278,7 +281,6 @@ public function testClear() * @covers PHP_CodeCoverage::start * @covers PHP_CodeCoverage::stop * @covers PHP_CodeCoverage::append - * @covers PHP_CodeCoverage::applyListsFilter * @covers PHP_CodeCoverage::initializeFilesThatAreSeenTheFirstTime * @covers PHP_CodeCoverage::applyCoversAnnotationFilter * @covers PHP_CodeCoverage::getTests @@ -322,10 +324,11 @@ public function testMerge() */ public function testMerge2() { - $coverage = new PHP_CodeCoverage( - $this->getMock('PHP_CodeCoverage_Driver_Xdebug'), - new PHP_CodeCoverage_Filter - ); + $driver = $this->getMockBuilder('PHP_CodeCoverage_Driver') + ->setConstructorArgs(array(new PHP_CodeCoverage_Filter, new PHP_CodeCoverage_Parser)) + ->getMockForAbstractClass(); + + $coverage = new PHP_CodeCoverage($driver, new PHP_CodeCoverage_Filter); $coverage->merge($this->getCoverageForBankAccount()); @@ -333,150 +336,4 @@ public function testMerge2() $this->getExpectedDataArrayForBankAccount(), $coverage->getData() ); } - - /** - * @covers PHP_CodeCoverage::getLinesToBeIgnored - */ - public function testGetLinesToBeIgnored() - { - $this->assertEquals( - array( - 1, - 3, - 4, - 5, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 30, - 32, - 33, - 34, - 35, - 36, - 37, - 38 - ), - $this->getLinesToBeIgnored()->invoke( - $this->coverage, - TEST_FILES_PATH . 'source_with_ignore.php' - ) - ); - } - - /** - * @covers PHP_CodeCoverage::getLinesToBeIgnored - */ - public function testGetLinesToBeIgnored2() - { - $this->assertEquals( - array(1, 5), - $this->getLinesToBeIgnored()->invoke( - $this->coverage, - TEST_FILES_PATH . 'source_without_ignore.php' - ) - ); - } - - /** - * @covers PHP_CodeCoverage::getLinesToBeIgnored - */ - public function testGetLinesToBeIgnored3() - { - $this->assertEquals( - array( - 1, - 2, - 3, - 4, - 5, - 8, - 11, - 15, - 16, - 19, - 20 - ), - $this->getLinesToBeIgnored()->invoke( - $this->coverage, - TEST_FILES_PATH . 'source_with_class_and_anonymous_function.php' - ) - ); - } - - /** - * @covers PHP_CodeCoverage::getLinesToBeIgnored - */ - public function testGetLinesToBeIgnoredOneLineAnnotations() - { - $this->assertEquals( - array( - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 18, - 20, - 23, - 24, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 37 - ), - $this->getLinesToBeIgnored()->invoke( - $this->coverage, - TEST_FILES_PATH . 'source_with_oneline_annotations.php' - ) - ); - } - - /** - * @return ReflectionMethod - */ - private function getLinesToBeIgnored() - { - $getLinesToBeIgnored = new ReflectionMethod( - 'PHP_CodeCoverage', 'getLinesToBeIgnored' - ); - - $getLinesToBeIgnored->setAccessible(true); - - return $getLinesToBeIgnored; - } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 73ce71b99..135eb9422 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -58,7 +58,7 @@ */ abstract class PHP_CodeCoverage_TestCase extends PHPUnit_Framework_TestCase { - protected function getXdebugDataForBankAccount() + protected function getDataForBankAccount() { return array( array( @@ -112,14 +112,19 @@ protected function getXdebugDataForBankAccount() protected function getCoverageForBankAccount() { - $data = $this->getXdebugDataForBankAccount(); + $data = $this->getDataForBankAccount(); + + $stub = $this->getMockBuilder('PHP_CodeCoverage_Driver') + ->setConstructorArgs(array(new PHP_CodeCoverage_Filter, new PHP_CodeCoverage_Parser)) + ->getMockForAbstractClass(); - $stub = $this->getMock('PHP_CodeCoverage_Driver_Xdebug'); $stub->expects($this->any()) - ->method('stop') - ->will($this->onConsecutiveCalls( - $data[0], $data[1], $data[2], $data[3] - )); + ->method('doStop') + ->will( + $this->onConsecutiveCalls( + $data[0], $data[1], $data[2], $data[3] + ) + ); $coverage = new PHP_CodeCoverage($stub, new PHP_CodeCoverage_Filter); @@ -168,11 +173,14 @@ protected function getCoverageForBankAccount() protected function getCoverageForBankAccountForFirstTwoTests() { - $data = $this->getXdebugDataForBankAccount(); + $data = $this->getDataForBankAccount(); + + $stub = $this->getMockBuilder('PHP_CodeCoverage_Driver') + ->setConstructorArgs(array(new PHP_CodeCoverage_Filter, new PHP_CodeCoverage_Parser)) + ->getMockForAbstractClass(); - $stub = $this->getMock('PHP_CodeCoverage_Driver_Xdebug'); $stub->expects($this->any()) - ->method('stop') + ->method('doStop') ->will($this->onConsecutiveCalls( $data[0], $data[1] )); @@ -202,11 +210,14 @@ protected function getCoverageForBankAccountForFirstTwoTests() protected function getCoverageForBankAccountForLastTwoTests() { - $data = $this->getXdebugDataForBankAccount(); + $data = $this->getDataForBankAccount(); + + $stub = $this->getMockBuilder('PHP_CodeCoverage_Driver') + ->setConstructorArgs(array(new PHP_CodeCoverage_Filter, new PHP_CodeCoverage_Parser)) + ->getMockForAbstractClass(); - $stub = $this->getMock('PHP_CodeCoverage_Driver_Xdebug'); $stub->expects($this->any()) - ->method('stop') + ->method('doStop') ->will($this->onConsecutiveCalls( $data[2], $data[3] )); @@ -275,7 +286,7 @@ protected function getExpectedDataArrayForBankAccount() protected function getCoverageForFileWithIgnoredLines() { $coverage = new PHP_CodeCoverage( - $this->setUpXdebugStubForFileWithIgnoredLines(), + $this->setUpStubForFileWithIgnoredLines(), new PHP_CodeCoverage_Filter ); @@ -285,11 +296,14 @@ protected function getCoverageForFileWithIgnoredLines() return $coverage; } - protected function setUpXdebugStubForFileWithIgnoredLines() + protected function setUpStubForFileWithIgnoredLines() { - $stub = $this->getMock('PHP_CodeCoverage_Driver_Xdebug'); + $stub = $this->getMockBuilder('PHP_CodeCoverage_Driver') + ->setConstructorArgs(array(new PHP_CodeCoverage_Filter, new PHP_CodeCoverage_Parser)) + ->getMockForAbstractClass(); + $stub->expects($this->any()) - ->method('stop') + ->method('doStop') ->will($this->returnValue( array( TEST_FILES_PATH . 'source_with_ignore.php' => array( @@ -307,7 +321,7 @@ protected function setUpXdebugStubForFileWithIgnoredLines() protected function getCoverageForClassWithAnonymousFunction() { $coverage = new PHP_CodeCoverage( - $this->setUpXdebugStubForClassWithAnonymousFunction(), + $this->setUpStubForClassWithAnonymousFunction(), new PHP_CodeCoverage_Filter ); @@ -317,11 +331,14 @@ protected function getCoverageForClassWithAnonymousFunction() return $coverage; } - protected function setUpXdebugStubForClassWithAnonymousFunction() + protected function setUpStubForClassWithAnonymousFunction() { - $stub = $this->getMock('PHP_CodeCoverage_Driver_Xdebug'); + $stub = $this->getMockBuilder('PHP_CodeCoverage_Driver') + ->setConstructorArgs(array(new PHP_CodeCoverage_Filter, new PHP_CodeCoverage_Parser)) + ->getMockForAbstractClass(); + $stub->expects($this->any()) - ->method('stop') + ->method('doStop') ->will($this->returnValue( array( TEST_FILES_PATH . 'source_with_class_and_anonymous_function.php' => array( From a5cf7ee22cfbef4027993365a4dad68268a6ca5d Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 4 Mar 2014 20:38:13 +0100 Subject: [PATCH 08/31] fixed Report\Node to handle root directory --- src/CodeCoverage/Report/Node.php | 11 +- tests/PHP/CodeCoverage/Report/NodeTest.php | 228 +++++++++++++++++++++ 2 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 tests/PHP/CodeCoverage/Report/NodeTest.php diff --git a/src/CodeCoverage/Report/Node.php b/src/CodeCoverage/Report/Node.php index d4084317b..ef76ef0c4 100644 --- a/src/CodeCoverage/Report/Node.php +++ b/src/CodeCoverage/Report/Node.php @@ -89,7 +89,7 @@ abstract class PHP_CodeCoverage_Report_Node implements Countable */ public function __construct($name, PHP_CodeCoverage_Report_Node $parent = null) { - if (substr($name, -1) == '/') { + if ($this->hasTailingSlashAndIsNotRoot($name)) { $name = substr($name, 0, -1); } @@ -377,4 +377,13 @@ abstract public function getNumFunctions(); * @return integer */ abstract public function getNumTestedFunctions(); + + /** + * @param $name + * @return bool + */ + private function hasTailingSlashAndIsNotRoot($name) + { + return substr($name, -1) == '/' && strlen($name) > 1; + } } diff --git a/tests/PHP/CodeCoverage/Report/NodeTest.php b/tests/PHP/CodeCoverage/Report/NodeTest.php new file mode 100644 index 000000000..ad8ee75a3 --- /dev/null +++ b/tests/PHP/CodeCoverage/Report/NodeTest.php @@ -0,0 +1,228 @@ +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @category PHP + * @package CodeCoverage + * @subpackage Tests + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @link http://github.com/sebastianbergmann/php-code-coverage + */ + +/** + * Tests for the PHP_CodeCoverage_Report_Node class. + * + * @category PHP + * @package CodeCoverage + * @subpackage Tests + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @link http://github.com/sebastianbergmann/php-code-coverage + */ +class PHP_CodeCoverage_Report_NodeTest extends PHPUnit_Framework_TestCase +{ + public function testTrimsTailingSlashes() + { + $node = new DefaultNode('/SomeName.php/'); + $this->assertEquals('/SomeName.php', $node->getPath()); + } + + public function testNodeAcceptsRootScope() + { + $node = new DefaultNode('/'); + $this->assertEquals('/', $node->getPath()); + } +} + +class DefaultNode extends PHP_CodeCoverage_Report_Node +{ + + /** + * Returns the classes of this node. + * + * @return array + */ + public function getClasses() + { + // TODO: Implement getClasses() method. + } + + /** + * Returns the traits of this node. + * + * @return array + */ + public function getTraits() + { + // TODO: Implement getTraits() method. + } + + /** + * Returns the functions of this node. + * + * @return array + */ + public function getFunctions() + { + // TODO: Implement getFunctions() method. + } + + /** + * Returns the LOC/CLOC/NCLOC of this node. + * + * @return array + */ + public function getLinesOfCode() + { + // TODO: Implement getLinesOfCode() method. + } + + /** + * Returns the number of executable lines. + * + * @return integer + */ + public function getNumExecutableLines() + { + // TODO: Implement getNumExecutableLines() method. + } + + /** + * Returns the number of executed lines. + * + * @return integer + */ + public function getNumExecutedLines() + { + // TODO: Implement getNumExecutedLines() method. + } + + /** + * Returns the number of classes. + * + * @return integer + */ + public function getNumClasses() + { + // TODO: Implement getNumClasses() method. + } + + /** + * Returns the number of tested classes. + * + * @return integer + */ + public function getNumTestedClasses() + { + // TODO: Implement getNumTestedClasses() method. + } + + /** + * Returns the number of traits. + * + * @return integer + */ + public function getNumTraits() + { + // TODO: Implement getNumTraits() method. + } + + /** + * Returns the number of tested traits. + * + * @return integer + */ + public function getNumTestedTraits() + { + // TODO: Implement getNumTestedTraits() method. + } + + /** + * Returns the number of methods. + * + * @return integer + */ + public function getNumMethods() + { + // TODO: Implement getNumMethods() method. + } + + /** + * Returns the number of tested methods. + * + * @return integer + */ + public function getNumTestedMethods() + { + // TODO: Implement getNumTestedMethods() method. + } + + /** + * Returns the number of functions. + * + * @return integer + */ + public function getNumFunctions() + { + // TODO: Implement getNumFunctions() method. + } + + /** + * Returns the number of tested functions. + * + * @return integer + */ + public function getNumTestedFunctions() + { + // TODO: Implement getNumTestedFunctions() method. + } + + /** + * (PHP 5 >= 5.1.0)
+ * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + *

+ *

+ * The return value is cast to an integer. + */ + public function count() + { + // TODO: Implement count() method. + } +} \ No newline at end of file From d83d41debde4a78e86cc5838d4699b5f2cfb19f2 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 4 Mar 2014 20:39:11 +0100 Subject: [PATCH 09/31] removed unneccessary todos --- tests/PHP/CodeCoverage/Report/NodeTest.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/PHP/CodeCoverage/Report/NodeTest.php b/tests/PHP/CodeCoverage/Report/NodeTest.php index ad8ee75a3..a74c29ff1 100644 --- a/tests/PHP/CodeCoverage/Report/NodeTest.php +++ b/tests/PHP/CodeCoverage/Report/NodeTest.php @@ -79,7 +79,6 @@ class DefaultNode extends PHP_CodeCoverage_Report_Node */ public function getClasses() { - // TODO: Implement getClasses() method. } /** @@ -89,7 +88,6 @@ public function getClasses() */ public function getTraits() { - // TODO: Implement getTraits() method. } /** @@ -99,7 +97,6 @@ public function getTraits() */ public function getFunctions() { - // TODO: Implement getFunctions() method. } /** @@ -109,7 +106,6 @@ public function getFunctions() */ public function getLinesOfCode() { - // TODO: Implement getLinesOfCode() method. } /** @@ -119,7 +115,6 @@ public function getLinesOfCode() */ public function getNumExecutableLines() { - // TODO: Implement getNumExecutableLines() method. } /** @@ -129,7 +124,6 @@ public function getNumExecutableLines() */ public function getNumExecutedLines() { - // TODO: Implement getNumExecutedLines() method. } /** @@ -139,7 +133,6 @@ public function getNumExecutedLines() */ public function getNumClasses() { - // TODO: Implement getNumClasses() method. } /** @@ -149,7 +142,6 @@ public function getNumClasses() */ public function getNumTestedClasses() { - // TODO: Implement getNumTestedClasses() method. } /** @@ -159,7 +151,6 @@ public function getNumTestedClasses() */ public function getNumTraits() { - // TODO: Implement getNumTraits() method. } /** @@ -169,7 +160,6 @@ public function getNumTraits() */ public function getNumTestedTraits() { - // TODO: Implement getNumTestedTraits() method. } /** @@ -179,7 +169,6 @@ public function getNumTestedTraits() */ public function getNumMethods() { - // TODO: Implement getNumMethods() method. } /** @@ -189,7 +178,6 @@ public function getNumMethods() */ public function getNumTestedMethods() { - // TODO: Implement getNumTestedMethods() method. } /** @@ -199,7 +187,6 @@ public function getNumTestedMethods() */ public function getNumFunctions() { - // TODO: Implement getNumFunctions() method. } /** @@ -209,7 +196,6 @@ public function getNumFunctions() */ public function getNumTestedFunctions() { - // TODO: Implement getNumTestedFunctions() method. } /** @@ -223,6 +209,5 @@ public function getNumTestedFunctions() */ public function count() { - // TODO: Implement count() method. } } \ No newline at end of file From ed2da23ff7e7c916aa6ea847e0cc6b36e116be69 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 4 Mar 2014 20:46:48 +0100 Subject: [PATCH 10/31] fixed Factory to support "/" as the only common path --- src/CodeCoverage/Report/Factory.php | 14 +++++++++++++- tests/PHP/CodeCoverage/Report/FactoryTest.php | 11 +++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/CodeCoverage/Report/Factory.php b/src/CodeCoverage/Report/Factory.php index 72b974a84..812f271c6 100644 --- a/src/CodeCoverage/Report/Factory.php +++ b/src/CodeCoverage/Report/Factory.php @@ -277,6 +277,18 @@ private function reducePaths(&$files) ksort($files); - return substr($commonPath, 0, -1); + if ($this->isNotRoot($commonPath)) { + $commonPath = substr($commonPath, 0, -1); + } + return $commonPath; + } + + /** + * @param $commonPath + * @return bool + */ + private function isNotRoot($commonPath) + { + return strlen($commonPath) > 1; } } diff --git a/tests/PHP/CodeCoverage/Report/FactoryTest.php b/tests/PHP/CodeCoverage/Report/FactoryTest.php index 12b4f67f1..e2e77b07c 100644 --- a/tests/PHP/CodeCoverage/Report/FactoryTest.php +++ b/tests/PHP/CodeCoverage/Report/FactoryTest.php @@ -231,6 +231,17 @@ public function reducePathsProvider() '/home/sb/Money/MoneyBag.php' => array() ) ), + array( + array( + 'driveA/Money.php' => array(), + 'driveB/MoneyBag.php' => array() + ), + '/', + array( + '/driveA/Money.php' => array(), + '/driveB/MoneyBag.php' => array() + ) + ), array( array( 'Money.php' => array() From 6ab6543f6b4d1dbf0991fa0abc2a327c6e64f900 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 4 Mar 2014 20:47:48 +0100 Subject: [PATCH 11/31] refactoring of support for "/" as the only common path --- src/CodeCoverage/Report/Factory.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/CodeCoverage/Report/Factory.php b/src/CodeCoverage/Report/Factory.php index 812f271c6..bcd043140 100644 --- a/src/CodeCoverage/Report/Factory.php +++ b/src/CodeCoverage/Report/Factory.php @@ -277,10 +277,7 @@ private function reducePaths(&$files) ksort($files); - if ($this->isNotRoot($commonPath)) { - $commonPath = substr($commonPath, 0, -1); - } - return $commonPath; + return $this->removeTailingDirectorySeparator($commonPath); } /** @@ -291,4 +288,17 @@ private function isNotRoot($commonPath) { return strlen($commonPath) > 1; } + + /** + * @param $commonPath + * @return string + */ + private function removeTailingDirectorySeparator($commonPath) + { + if ($this->isNotRoot($commonPath)) { + $commonPath = substr($commonPath, 0, -1); + return $commonPath; + } + return $commonPath; + } } From 3aedd79439df30d57f092779d8cbc4b1ab70a264 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 4 Mar 2014 22:08:39 +0100 Subject: [PATCH 12/31] NodeTest: replaced DefaultNode class by a dynamic stub --- tests/PHP/CodeCoverage/Report/NodeTest.php | 155 +++------------------ 1 file changed, 20 insertions(+), 135 deletions(-) diff --git a/tests/PHP/CodeCoverage/Report/NodeTest.php b/tests/PHP/CodeCoverage/Report/NodeTest.php index a74c29ff1..78c244e24 100644 --- a/tests/PHP/CodeCoverage/Report/NodeTest.php +++ b/tests/PHP/CodeCoverage/Report/NodeTest.php @@ -58,156 +58,41 @@ class PHP_CodeCoverage_Report_NodeTest extends PHPUnit_Framework_TestCase { public function testTrimsTailingSlashes() { - $node = new DefaultNode('/SomeName.php/'); + $node = $this->getInstance('/SomeName.php/'); $this->assertEquals('/SomeName.php', $node->getPath()); } public function testNodeAcceptsRootScope() { - $node = new DefaultNode('/'); + $node = $this->getInstance('/'); $this->assertEquals('/', $node->getPath()); } -} - -class DefaultNode extends PHP_CodeCoverage_Report_Node -{ - - /** - * Returns the classes of this node. - * - * @return array - */ - public function getClasses() - { - } - - /** - * Returns the traits of this node. - * - * @return array - */ - public function getTraits() - { - } - - /** - * Returns the functions of this node. - * - * @return array - */ - public function getFunctions() - { - } - - /** - * Returns the LOC/CLOC/NCLOC of this node. - * - * @return array - */ - public function getLinesOfCode() - { - } - - /** - * Returns the number of executable lines. - * - * @return integer - */ - public function getNumExecutableLines() - { - } - - /** - * Returns the number of executed lines. - * - * @return integer - */ - public function getNumExecutedLines() - { - } - - /** - * Returns the number of classes. - * - * @return integer - */ - public function getNumClasses() - { - } - - /** - * Returns the number of tested classes. - * - * @return integer - */ - public function getNumTestedClasses() - { - } - - /** - * Returns the number of traits. - * - * @return integer - */ - public function getNumTraits() - { - } - - /** - * Returns the number of tested traits. - * - * @return integer - */ - public function getNumTestedTraits() - { - } /** - * Returns the number of methods. - * - * @return integer + * @param $path + * @return PHP_CodeCoverage_Report_Node */ - public function getNumMethods() + private function getInstance($path) { - } + $builder = $this->getMockBuilder('PHP_CodeCoverage_Report_Node') + ->setConstructorArgs(array($path)); - /** - * Returns the number of tested methods. - * - * @return integer - */ - public function getNumTestedMethods() - { + $this->mockMethods($builder); + return $builder->getMock(); } /** - * Returns the number of functions. - * - * @return integer + * @param $builder */ - public function getNumFunctions() + private function mockMethods($builder) { + $reflectionClass = new \ReflectionClass('PHP_CodeCoverage_Report_Node'); + $methods = array(); + foreach ($reflectionClass->getMethods() as $method) { + if ($method->isAbstract()) { + $methods[] = $method->getName(); + } + } + $builder->setMethods($methods); } - - /** - * Returns the number of tested functions. - * - * @return integer - */ - public function getNumTestedFunctions() - { - } - - /** - * (PHP 5 >= 5.1.0)
- * Count elements of an object - * @link http://php.net/manual/en/countable.count.php - * @return int The custom count as an integer. - *

- *

- * The return value is cast to an integer. - */ - public function count() - { - } -} \ No newline at end of file +} From 1b86a83be9e1e80f2840c16bc46fb4e71271ef99 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Fri, 21 Mar 2014 08:17:09 +0100 Subject: [PATCH 13/31] Make tests pass on HHVM --- tests/PHP/CodeCoverageTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHP/CodeCoverageTest.php b/tests/PHP/CodeCoverageTest.php index 01c065df2..88e442bed 100644 --- a/tests/PHP/CodeCoverageTest.php +++ b/tests/PHP/CodeCoverageTest.php @@ -87,7 +87,7 @@ protected function setUp() public function testConstructor() { $this->assertAttributeInstanceOf( - 'PHP_CodeCoverage_Driver_Xdebug', 'driver', $this->coverage + 'PHP_CodeCoverage_Driver', 'driver', $this->coverage ); $this->assertAttributeInstanceOf( @@ -105,7 +105,7 @@ public function testConstructor2() $coverage = new PHP_CodeCoverage(null, $filter); $this->assertAttributeInstanceOf( - 'PHP_CodeCoverage_Driver_Xdebug', 'driver', $coverage + 'PHP_CodeCoverage_Driver', 'driver', $coverage ); $this->assertSame($filter, $coverage->filter()); From adacb53da04b9be10a0abb469f71f8cce2fe48f8 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Tue, 25 Mar 2014 16:35:28 +0100 Subject: [PATCH 14/31] Add missing call to ensureDriverCanWork() in constructor --- src/CodeCoverage/Driver.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CodeCoverage/Driver.php b/src/CodeCoverage/Driver.php index 02afef163..56d257f1f 100644 --- a/src/CodeCoverage/Driver.php +++ b/src/CodeCoverage/Driver.php @@ -77,6 +77,8 @@ abstract class PHP_CodeCoverage_Driver */ public function __construct(PHP_CodeCoverage_Filter $filter, PHP_CodeCoverage_Parser $parser) { + $this->ensureDriverCanWork(); + $this->filter = $filter; $this->parser = $parser; } From 2c17b8d364ad3bd4ef6a3c991ece668d57ed84f4 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Tue, 25 Mar 2014 16:36:45 +0100 Subject: [PATCH 15/31] Bump version --- composer.json | 2 +- src/CodeCoverage/Report/HTML/Renderer.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 409ce8a38..bb613ce48 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.1.x-dev" + "dev-master": "3.0.x-dev" } }, "include-path": [ diff --git a/src/CodeCoverage/Report/HTML/Renderer.php b/src/CodeCoverage/Report/HTML/Renderer.php index 598b10a7e..b0861e5ff 100644 --- a/src/CodeCoverage/Report/HTML/Renderer.php +++ b/src/CodeCoverage/Report/HTML/Renderer.php @@ -99,7 +99,7 @@ abstract class PHP_CodeCoverage_Report_HTML_Renderer */ public function __construct($templatePath, $generator, $date, $lowUpperBound, $highLowerBound) { - $version = new SebastianBergmann\Version('2.1', __DIR__); + $version = new SebastianBergmann\Version('3.0', __DIR__); $this->templatePath = $templatePath; $this->generator = $generator; From 0f87633dd5e3d3336606903e5d8b65a61771e989 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sun, 6 Apr 2014 10:13:01 +0200 Subject: [PATCH 16/31] Target PHPUnit 4.2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bb613ce48..c23b4d130 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "sebastian/version": ">=1.0.0" }, "require-dev": { - "phpunit/phpunit": "4.1.*@dev", + "phpunit/phpunit": "4.2.*@dev", "ext-xdebug": ">=2.1.4" }, "suggest": { From 8cdc401070391f5db534421e211bd26c9faac005 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 26 Apr 2014 11:21:20 +0200 Subject: [PATCH 17/31] Document dependency on ext/xmlwriter --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c23b4d130..c9cac91a2 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,8 @@ }, "suggest": { "ext-dom": "*", - "ext-xdebug": ">=2.2.1" + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" }, "autoload": { "classmap": [ From 34b9a4213ec149d0223275c6075a0e8cb59f87be Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Wed, 30 Apr 2014 10:56:02 +0200 Subject: [PATCH 18/31] Eliminate dead code --- src/CodeCoverage/Report/HTML/Renderer/Dashboard.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php b/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php index afc149fd8..9647f6910 100644 --- a/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php +++ b/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php @@ -83,8 +83,7 @@ public function render(PHP_CodeCoverage_Report_Node_Directory $node, $file) 'complexity_class' => $complexity['class'], 'complexity_method' => $complexity['method'], 'class_coverage_distribution' => $coverageDistribution['class'], - 'method_coverage_distribution' => $coverageDistribution['method'], - 'backlink' => basename(str_replace('.dashboard', '', $file)) + 'method_coverage_distribution' => $coverageDistribution['method'] ) ); From 8d0fee37275a84c96defb2af51541446df91e904 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Wed, 30 Apr 2014 10:57:22 +0200 Subject: [PATCH 19/31] Fix link from dashboard view to directory view --- src/CodeCoverage/Report/HTML/Renderer/Dashboard.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php b/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php index 9647f6910..341fd025a 100644 --- a/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php +++ b/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php @@ -320,9 +320,8 @@ protected function projectRisks(array $classes) protected function getActiveBreadcrumb(PHP_CodeCoverage_Report_Node $node) { return sprintf( - '

  • %s
  • ' . "\n" . + '
  • %s
  • ' . "\n" . '
  • (Dashboard)
  • ' . "\n", - $node->getId(), $node->getName() ); } From 3e37a7b59fdf02f2d9e86d2ccd4b4ac1e1ef5c41 Mon Sep 17 00:00:00 2001 From: Andrejs Stepanovs Date: Sat, 17 May 2014 15:02:46 +0200 Subject: [PATCH 20/31] hhvm code coverage fix. Lines to be ignored will process only existing files --- src/CodeCoverage/Parser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CodeCoverage/Parser.php b/src/CodeCoverage/Parser.php index aa7007a67..0fdda0684 100644 --- a/src/CodeCoverage/Parser.php +++ b/src/CodeCoverage/Parser.php @@ -127,7 +127,7 @@ public function getLinesToBeIgnored($filename) $this->ignoredLines[$filename] = array(); $ignore = false; $stop = false; - $lines = file($filename); + $lines = file_exists($filename) ? file($filename) : array(); $numLines = count($lines); foreach ($lines as $index => $line) { From 879cb66361af0c011f18ce9630e84792a91d0500 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 17 May 2014 17:46:27 +0200 Subject: [PATCH 21/31] Revert "hhvm code coverage fix. Lines to be ignored will process only existing files" This reverts commit 3e37a7b59fdf02f2d9e86d2ccd4b4ac1e1ef5c41. --- src/CodeCoverage/Parser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CodeCoverage/Parser.php b/src/CodeCoverage/Parser.php index 0fdda0684..aa7007a67 100644 --- a/src/CodeCoverage/Parser.php +++ b/src/CodeCoverage/Parser.php @@ -127,7 +127,7 @@ public function getLinesToBeIgnored($filename) $this->ignoredLines[$filename] = array(); $ignore = false; $stop = false; - $lines = file_exists($filename) ? file($filename) : array(); + $lines = file($filename); $numLines = count($lines); foreach ($lines as $index => $line) { From 2b7576920b70702fe0cda90ea02606b4bc8fbdd8 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 17 May 2014 17:47:16 +0200 Subject: [PATCH 22/31] Better fix for #244 --- src/CodeCoverage/Driver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CodeCoverage/Driver.php b/src/CodeCoverage/Driver.php index 56d257f1f..5d133d229 100644 --- a/src/CodeCoverage/Driver.php +++ b/src/CodeCoverage/Driver.php @@ -140,7 +140,7 @@ protected function cleanup(array &$data) private function filter(array &$data) { foreach (array_keys($data) as $filename) { - if ($this->filter->isFiltered($filename)) { + if (!file_exists($filename) || $this->filter->isFiltered($filename)) { unset($data[$filename]); continue; } From 496bb0aeb06401bd853c5eb6bf5bc2144d8e195c Mon Sep 17 00:00:00 2001 From: Eric GELOEN Date: Sun, 29 Jun 2014 17:50:38 +0200 Subject: [PATCH 23/31] Ignore comment starting by '/*' --- src/CodeCoverage/Parser.php | 28 ++++++++++++++------------- tests/PHP/CodeCoverage/ParserTest.php | 3 +++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/CodeCoverage/Parser.php b/src/CodeCoverage/Parser.php index aa7007a67..63c141ae5 100644 --- a/src/CodeCoverage/Parser.php +++ b/src/CodeCoverage/Parser.php @@ -44,7 +44,7 @@ */ /** - * + * * * @category PHP * @package CodeCoverage @@ -164,22 +164,24 @@ public function getLinesToBeIgnored($filename) $stop = true; } - // Do not ignore the whole line when there is a token - // before the comment on the same line - if (0 === strpos($_token, $_line)) { - $count = substr_count($token, "\n"); - $line = $token->getLine(); + if (!$ignore) { + $start = $token->getLine(); + $end = $start + substr_count($token, "\n"); + + // Do not ignore the first line when there is a token + // before the comment + if (0 !== strpos($_token, $_line)) { + $start++; + } - for ($i = $line; $i < $line + $count; $i++) { + for ($i = $start; $i < $end; $i++) { $this->ignoredLines[$filename][] = $i; } - if ($token instanceof PHP_Token_DOC_COMMENT) { - // The DOC_COMMENT token does not contain the - // final \n character in its text - if (substr(trim($lines[$i-1]), -2) == '*/') { - $this->ignoredLines[$filename][] = $i; - } + // A DOC_COMMENT token or a COMMENT token starting with "/*" + // does not contain the final \n character in its text + if (0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i-1]), -2)) { + $this->ignoredLines[$filename][] = $i; } } break; diff --git a/tests/PHP/CodeCoverage/ParserTest.php b/tests/PHP/CodeCoverage/ParserTest.php index 59f46ac40..7ce366676 100644 --- a/tests/PHP/CodeCoverage/ParserTest.php +++ b/tests/PHP/CodeCoverage/ParserTest.php @@ -108,8 +108,11 @@ public function ignoredLinesProvider() 16, 18, 20, + 21, 23, 24, + 25, + 27, 28, 29, 30, From 0de2671f5ece75c4c623172fba240775384e6eb9 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Mon, 30 Jun 2014 12:40:31 +0200 Subject: [PATCH 24/31] Update dependencies --- composer.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index c9cac91a2..b6a107d6c 100644 --- a/composer.json +++ b/composer.json @@ -23,14 +23,14 @@ "minimum-stability": "dev", "require": { "php": ">=5.3.3", - "phpunit/php-file-iterator": ">=1.3.0", - "phpunit/php-token-stream": ">=1.1.3", - "phpunit/php-text-template": ">=1.2.0", - "sebastian/environment": ">=1.0.0", - "sebastian/version": ">=1.0.0" + "phpunit/php-file-iterator": "~1.3.1", + "phpunit/php-token-stream": "~1.2.2", + "phpunit/php-text-template": "~1.2.0", + "sebastian/environment": "~1.0.0", + "sebastian/version": "~1.0.3" }, "require-dev": { - "phpunit/phpunit": "4.2.*@dev", + "phpunit/phpunit": "dev-master", "ext-xdebug": ">=2.1.4" }, "suggest": { From 7cc6ac2c4f9923b2ea0742319ed1c01a2a39d3cf Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Mon, 30 Jun 2014 12:43:13 +0200 Subject: [PATCH 25/31] Update --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f6ef3445..40dbc25e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -Pull Requests for bug fixes should be made against the current release branch (1.2). +Pull Requests for bug fixes should be made against the current release branch (2.0). Pull Requests for new features should be made against master. From b54a794f92e2c6a5369c7ac2d495adc3fdc429a7 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 9 Jul 2014 08:54:12 +0200 Subject: [PATCH 26/31] Added HHVM to travis.yml --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5c23715f5..bae863c3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,12 @@ php: - 5.4 - 5.5 - 5.6 - + - hhvm + +matrix: + allow_failures: + - php: hhvm + before_script: - COMPOSER_ROOT_VERSION=dev-master composer install --dev --prefer-source From e25b477f9e598b3b5119706563f6b496940270e8 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 6 Aug 2014 10:45:04 +0100 Subject: [PATCH 27/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 603d623a4..71cc52eed 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ To add PHP_CodeCoverage as a local, per-project dependency to your project, simp { "require": { - "phpunit/php-code-coverage": "2.1.*" + "phpunit/php-code-coverage": "3.0.*" } } From e9a5cd47dae6c15886d7dea231af2ad948ae2203 Mon Sep 17 00:00:00 2001 From: sun Date: Thu, 14 Aug 2014 16:21:04 +0200 Subject: [PATCH 28/31] Removed obsolete 'include-path' from composer.json. --- composer.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/composer.json b/composer.json index b6a107d6c..eec2900f9 100644 --- a/composer.json +++ b/composer.json @@ -47,8 +47,5 @@ "branch-alias": { "dev-master": "3.0.x-dev" } - }, - "include-path": [ - "" - ] + } } From ac6aa79eae5ea380ea60b2a3b43b721dd0447789 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 1 Sep 2014 22:59:23 +0100 Subject: [PATCH 29/31] Fixed a typo in the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71cc52eed..53c6addfc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ## Installation -To add PHP_CodeCoverage as a local, per-project dependency to your project, simply add a dependency on `phpunit/php-code-coverage` to your project's `composer.json` file. Here is a minimal example of a `composer.json` file that just defines a dependency on PHP_CodeCoverage 2.1: +To add PHP_CodeCoverage as a local, per-project dependency to your project, simply add a dependency on `phpunit/php-code-coverage` to your project's `composer.json` file. Here is a minimal example of a `composer.json` file that just defines a dependency on PHP_CodeCoverage 3.0: { "require": { From cb7d09fd16118c88b16ff8845e4040de47c204ea Mon Sep 17 00:00:00 2001 From: tarlabs Date: Fri, 12 Sep 2014 20:12:03 +0530 Subject: [PATCH 30/31] Updated getIgnoredLines for missing coverage Updated getIgnoredLines for missing coverages. Now there is flag to not ignore lines which shouldn't be ignored. Few edge cases may get covered because of this which were getting ignore earlier. --- src/CodeCoverage/Parser.php | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/CodeCoverage/Parser.php b/src/CodeCoverage/Parser.php index 63c141ae5..8175a7299 100644 --- a/src/CodeCoverage/Parser.php +++ b/src/CodeCoverage/Parser.php @@ -65,7 +65,8 @@ class PHP_CodeCoverage_Parser * @var array */ private $ignoredLines = array(); - + private $doNotIgnoreLines = array(); + /** * @param array $linesToBeCovered * @param array $linesToBeUsed @@ -125,6 +126,9 @@ public function getLinesToBeIgnored($filename) if (!isset($this->ignoredLines[$filename])) { $this->ignoredLines[$filename] = array(); + $this->doNotIgnoreLines[$filename] = array(); + $lastLineIgnoredForStartOrEndPhpTag = 0; + $lastLine = 0; $ignore = false; $stop = false; $lines = file($filename); @@ -244,11 +248,24 @@ public function getLinesToBeIgnored($filename) $this->ignoredLines[$filename][] = $token->getEndLine(); // Intentional fallthrough + case 'PHP_Token_USE': + $this->ignoredLines[$filename][] = $token->getLine(); + break; case 'PHP_Token_OPEN_TAG': case 'PHP_Token_CLOSE_TAG': - case 'PHP_Token_USE': + $lastLineIgnoredForStartOrEndPhpTag = $token->getLine(); $this->ignoredLines[$filename][] = $token->getLine(); break; + case 'PHP_Token_WHITESPACE': + break; + default: + if ($lastLineIgnoredForStartOrEndPhpTag == $token->getLine()) + { + //This line possibly has some code and cannot be ignored + $this->doNotIgnoreLines[$filename][] = $token->getLine(); + } + break; + } if ($ignore) { @@ -259,12 +276,16 @@ public function getLinesToBeIgnored($filename) $stop = false; } } + + $lastLine = $token->getLine(); } $this->ignoredLines[$filename][] = $numLines + 1; + $this->doNotIgnoreLines[$filename] = array_unique($this->doNotIgnoreLines[$filename]); + $this->ignoredLines[$filename] = array_unique( - $this->ignoredLines[$filename] + array_diff($this->ignoredLines[$filename], $this->doNotIgnoreLines[$filename]) ); sort($this->ignoredLines[$filename]); From f66181e58a02a0f2891367040b8b8886487613b0 Mon Sep 17 00:00:00 2001 From: Tarun Lalwani Date: Tue, 9 Dec 2014 17:19:56 +0530 Subject: [PATCH 31/31] Fixed Codecoverage not to ignore executable lines --- src/CodeCoverage.php | 302 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 285 insertions(+), 17 deletions(-) diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index ceda7729d..5a23ba2ba 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -68,11 +68,6 @@ class PHP_CodeCoverage */ private $filter; - /** - * @var PHP_CodeCoverage_Parser - */ - private $parser; - /** * @var boolean */ @@ -115,6 +110,16 @@ class PHP_CodeCoverage */ private $data = array(); + /** + * @var array + */ + private $ignoredLines = array(); + + /** To track lines which have open brackets/close brackets + code + * @var array + */ + private $doNotIgnoreLines = array(); + /** * Test data. * @@ -131,27 +136,24 @@ class PHP_CodeCoverage */ public function __construct(PHP_CodeCoverage_Driver $driver = null, PHP_CodeCoverage_Filter $filter = null) { - if ($filter === null) { - $filter = new PHP_CodeCoverage_Filter; - } - - $parser = new PHP_CodeCoverage_Parser; - if ($driver === null) { $runtime = new Runtime; if ($runtime->isHHVM()) { - $driver = new PHP_CodeCoverage_Driver_HHVM($filter, $parser); + $driver = new PHP_CodeCoverage_Driver_HHVM; } elseif ($runtime->hasXdebug()) { - $driver = new PHP_CodeCoverage_Driver_Xdebug($filter, $parser); + $driver = new PHP_CodeCoverage_Driver_Xdebug; } else { throw new PHP_CodeCoverage_Exception('No code coverage driver available'); } } + if ($filter === null) { + $filter = new PHP_CodeCoverage_Filter; + } + $this->driver = $driver; $this->filter = $filter; - $this->parser = $parser; } /** @@ -202,6 +204,12 @@ public function getData($raw = false) $this->addUncoveredFilesFromWhitelist(); } + // We need to apply the blacklist filter a second time + // when no whitelist is used. + if (!$raw && !$this->filter->hasWhitelist()) { + $this->applyListsFilter($this->data); + } + return $this->data; } @@ -316,6 +324,8 @@ public function append(array $data, $id = null, $append = true, $linesToBeCovere throw new PHP_CodeCoverage_Exception; } + $this->applyListsFilter($data); + $this->applyIgnoredLinesFilter($data); $this->initializeFilesThatAreSeenTheFirstTime($data); if (!$append) { @@ -404,8 +414,6 @@ public function setCacheTokens($flag) ); } - $this->parser->setCacheTokens($flag); - $this->cacheTokens = $flag; } @@ -539,6 +547,38 @@ private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, ar } } + /** + * Applies the blacklist/whitelist filtering. + * + * @param array $data + */ + private function applyListsFilter(array &$data) + { + foreach (array_keys($data) as $filename) { + if ($this->filter->isFiltered($filename)) { + unset($data[$filename]); + } + } + } + + /** + * Applies the "ignored lines" filtering. + * + * @param array $data + */ + private function applyIgnoredLinesFilter(array &$data) + { + foreach (array_keys($data) as $filename) { + if (!$this->filter->isFile($filename)) { + continue; + } + + foreach ($this->getLinesToBeIgnored($filename) as $line) { + unset($data[$filename][$line]); + } + } + } + /** * @param array $data * @since Method available since Release 1.1.0 @@ -617,6 +657,193 @@ private function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, } } + /** + * Returns the lines of a source file that should be ignored. + * + * @param string $filename + * @return array + * @throws PHP_CodeCoverage_Exception + * @since Method available since Release 2.0.0 + */ + private function getLinesToBeIgnored($filename) + { + if (!is_string($filename)) { + throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( + 1, + 'string' + ); + } + + if (!isset($this->ignoredLines[$filename])) { + $this->ignoredLines[$filename] = array(); + //[TARLABS] Added below 2 lines for + $this->doNotIgnoreLines[$filename] = array(); + $lastLineIgnoredForStartOrEndPhpTag = 0; + + $ignore = false; + $stop = false; + $lines = file($filename); + $numLines = count($lines); + + foreach ($lines as $index => $line) { + if (!trim($line)) { + $this->ignoredLines[$filename][] = $index + 1; + } + } + + if ($this->cacheTokens) { + $tokens = PHP_Token_Stream_CachingFactory::get($filename); + } else { + $tokens = new PHP_Token_Stream($filename); + } + + $classes = array_merge($tokens->getClasses(), $tokens->getTraits()); + $tokens = $tokens->tokens(); + + foreach ($tokens as $token) { + switch (get_class($token)) { + case 'PHP_Token_COMMENT': + case 'PHP_Token_DOC_COMMENT': + $_token = trim($token); + $_line = trim($lines[$token->getLine() - 1]); + + if ($_token == '// @codeCoverageIgnore' || + $_token == '//@codeCoverageIgnore') { + $ignore = true; + $stop = true; + } elseif ($_token == '// @codeCoverageIgnoreStart' || + $_token == '//@codeCoverageIgnoreStart') { + $ignore = true; + } elseif ($_token == '// @codeCoverageIgnoreEnd' || + $_token == '//@codeCoverageIgnoreEnd') { + $stop = true; + } + + if (!$ignore) { + $start = $token->getLine(); + $end = $start + substr_count($token, "\n"); + + // Do not ignore the first line when there is a token + // before the comment + if (0 !== strpos($_token, $_line)) { + $start++; + } + + for ($i = $start; $i < $end; $i++) { + $this->ignoredLines[$filename][] = $i; + } + + // A DOC_COMMENT token or a COMMENT token starting with "/*" + // does not contain the final \n character in its text + if (0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i-1]), -2)) { + $this->ignoredLines[$filename][] = $i; + } + } + break; + + case 'PHP_Token_INTERFACE': + case 'PHP_Token_TRAIT': + case 'PHP_Token_CLASS': + case 'PHP_Token_FUNCTION': + $docblock = $token->getDocblock(); + + $this->ignoredLines[$filename][] = $token->getLine(); + + if (strpos($docblock, '@codeCoverageIgnore')) { + $endLine = $token->getEndLine(); + + for ($i = $token->getLine(); $i <= $endLine; $i++) { + $this->ignoredLines[$filename][] = $i; + } + } elseif ($token instanceof PHP_Token_INTERFACE || + $token instanceof PHP_Token_TRAIT || + $token instanceof PHP_Token_CLASS) { + if (empty($classes[$token->getName()]['methods'])) { + for ($i = $token->getLine(); + $i <= $token->getEndLine(); + $i++) { + $this->ignoredLines[$filename][] = $i; + } + } else { + $firstMethod = array_shift( + $classes[$token->getName()]['methods'] + ); + + do { + $lastMethod = array_pop( + $classes[$token->getName()]['methods'] + ); + } while ($lastMethod !== null && + substr($lastMethod['signature'], 0, 18) == 'anonymous function'); + + if ($lastMethod === null) { + $lastMethod = $firstMethod; + } + + for ($i = $token->getLine(); + $i < $firstMethod['startLine']; + $i++) { + $this->ignoredLines[$filename][] = $i; + } + + for ($i = $token->getEndLine(); + $i > $lastMethod['endLine']; + $i--) { + $this->ignoredLines[$filename][] = $i; + } + } + } + break; + + case 'PHP_Token_NAMESPACE': + $this->ignoredLines[$filename][] = $token->getEndLine(); + + // Intentional fallthrough + case 'PHP_Token_NAMESPACE': + $this->ignoredLines[$filename][] = $token->getEndLine(); + // Intentional fallthrough + case 'PHP_Token_USE': + $this->ignoredLines[$filename][] = $token->getLine(); + break; + case 'PHP_Token_OPEN_TAG': + case 'PHP_Token_CLOSE_TAG': + $lastLineIgnoredForStartOrEndPhpTag = $token->getLine(); + $this->ignoredLines[$filename][] = $token->getLine(); + break; + case 'PHP_Token_WHITESPACE': + break; + default: + if ($lastLineIgnoredForStartOrEndPhpTag == $token->getLine()) + { + //This line possibly has some code and cannot be ignored + $this->doNotIgnoreLines[$filename][] = $token->getLine(); + } + break; + } + + if ($ignore) { + $this->ignoredLines[$filename][] = $token->getLine(); + + if ($stop) { + $ignore = false; + $stop = false; + } + } + } + + + $this->ignoredLines[$filename][] = $numLines + 1; + $this->doNotIgnoreLines[$filename] = array_unique($this->doNotIgnoreLines[$filename]); + + $this->ignoredLines[$filename] = array_unique( + array_diff($this->ignoredLines[$filename], $this->doNotIgnoreLines[$filename]) + ); + sort($this->ignoredLines[$filename]); + } + + return $this->ignoredLines[$filename]; + } + /** * @param array $data * @param array $linesToBeCovered @@ -626,7 +853,7 @@ private function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, */ private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) { - $allowedLines = $this->parser->getAllowedLines( + $allowedLines = $this->getAllowedLines( $linesToBeCovered, $linesToBeUsed ); @@ -653,4 +880,45 @@ private function performUnintentionallyCoveredCodeCheck(array &$data, array $lin ); } } + + /** + * @param array $linesToBeCovered + * @param array $linesToBeUsed + * @return array + * @since Method available since Release 2.0.0 + */ + private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed) + { + $allowedLines = array(); + + foreach (array_keys($linesToBeCovered) as $file) { + if (!isset($allowedLines[$file])) { + $allowedLines[$file] = array(); + } + + $allowedLines[$file] = array_merge( + $allowedLines[$file], + $linesToBeCovered[$file] + ); + } + + foreach (array_keys($linesToBeUsed) as $file) { + if (!isset($allowedLines[$file])) { + $allowedLines[$file] = array(); + } + + $allowedLines[$file] = array_merge( + $allowedLines[$file], + $linesToBeUsed[$file] + ); + } + + foreach (array_keys($allowedLines) as $file) { + $allowedLines[$file] = array_flip( + array_unique($allowedLines[$file]) + ); + } + + return $allowedLines; + } }