From 0eb29beec93f1052e2393caca6a11a969618673f Mon Sep 17 00:00:00 2001 From: Doug Wright Date: Wed, 29 Apr 2020 09:25:38 +0100 Subject: [PATCH 01/24] Remove some ancient workarounds for very old Xdebug versions --- src/Driver/Xdebug.php | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/src/Driver/Xdebug.php b/src/Driver/Xdebug.php index 401d82b3c..971fe2618 100644 --- a/src/Driver/Xdebug.php +++ b/src/Driver/Xdebug.php @@ -17,11 +17,6 @@ */ final class Xdebug implements Driver { - /** - * @var array - */ - private $cacheNumLines = []; - /** * @var Filter */ @@ -68,43 +63,6 @@ public function stop(): array \xdebug_stop_code_coverage(); - return $this->cleanup($data); - } - - private function cleanup(array $data): array - { - foreach (\array_keys($data) as $file) { - unset($data[$file][0]); - - if (!$this->filter->isFile($file)) { - continue; - } - - $numLines = $this->getNumberOfLinesInFile($file); - - foreach (\array_keys($data[$file]) as $line) { - if ($line > $numLines) { - unset($data[$file][$line]); - } - } - } - return $data; } - - private function getNumberOfLinesInFile(string $fileName): int - { - if (!isset($this->cacheNumLines[$fileName])) { - $buffer = \file_get_contents($fileName); - $lines = \substr_count($buffer, "\n"); - - if (\substr($buffer, -1) !== "\n") { - $lines++; - } - - $this->cacheNumLines[$fileName] = $lines; - } - - return $this->cacheNumLines[$fileName]; - } } From 48b9ad2e97a06786248191957b9c138d4109e570 Mon Sep 17 00:00:00 2001 From: Doug Wright Date: Wed, 13 May 2020 13:52:50 +0100 Subject: [PATCH 02/24] Use Xdebug's built-in filter --- src/Driver/Xdebug.php | 7 ++---- tests/tests/Driver/XdebugTest.php | 39 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 tests/tests/Driver/XdebugTest.php diff --git a/src/Driver/Xdebug.php b/src/Driver/Xdebug.php index 971fe2618..dfd46abce 100644 --- a/src/Driver/Xdebug.php +++ b/src/Driver/Xdebug.php @@ -25,7 +25,7 @@ final class Xdebug implements Driver /** * @throws RuntimeException */ - public function __construct(Filter $filter = null) + public function __construct(Filter $filter) { if (!\extension_loaded('xdebug')) { throw new RuntimeException('This driver requires Xdebug'); @@ -35,11 +35,8 @@ public function __construct(Filter $filter = null) throw new RuntimeException('xdebug.coverage_enable=On has to be set in php.ini'); } - if ($filter === null) { - $filter = new Filter; - } - $this->filter = $filter; + \xdebug_set_filter(XDEBUG_FILTER_CODE_COVERAGE, XDEBUG_PATH_WHITELIST, $this->filter->getWhitelist()); } /** diff --git a/tests/tests/Driver/XdebugTest.php b/tests/tests/Driver/XdebugTest.php new file mode 100644 index 000000000..d21953524 --- /dev/null +++ b/tests/tests/Driver/XdebugTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use SebastianBergmann\Environment\Runtime; + +/** + * @covers SebastianBergmann\CodeCoverage\Driver\Xdebug + */ +class XdebugTest extends TestCase +{ + protected function setUp(): void + { + $runtime = new Runtime; + + if (!$runtime->hasXdebug()) { + $this->markTestSkipped('This test is only applicable to Xdebug'); + } + + if (!xdebug_code_coverage_started()) { + $this->markTestSkipped('This test requires code coverage to be running'); + } + } + + public function testFilterWorks(): void + { + $bankAccount = TEST_FILES_PATH . 'BankAccount.php'; + + require $bankAccount; + $this->assertArrayNotHasKey($bankAccount, \xdebug_get_code_coverage()); + } +} From c0fb0885bc2182669811971a91f791dfcfd26aec Mon Sep 17 00:00:00 2001 From: Doug Wright Date: Wed, 13 May 2020 13:53:29 +0100 Subject: [PATCH 03/24] Use PCOV's built-in filter --- src/CodeCoverage.php | 2 +- src/Driver/PCOV.php | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index 765629d33..9e7396214 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -897,7 +897,7 @@ private function selectDriver(Filter $filter): Driver } if ($runtime->hasPCOV()) { - return new PCOV; + return new PCOV($filter); } if ($runtime->hasXdebug()) { diff --git a/src/Driver/PCOV.php b/src/Driver/PCOV.php index d3d218993..1ee2e9b3d 100644 --- a/src/Driver/PCOV.php +++ b/src/Driver/PCOV.php @@ -9,11 +9,23 @@ */ namespace SebastianBergmann\CodeCoverage\Driver; +use SebastianBergmann\CodeCoverage\Filter; + /** * Driver for PCOV code coverage functionality. */ final class PCOV implements Driver { + /** + * @var Filter + */ + private $filter; + + public function __construct(Filter $filter) + { + $this->filter = $filter; + } + /** * Start collection of code coverage information. */ @@ -29,14 +41,9 @@ public function stop(): array { \pcov\stop(); - $waiting = \pcov\waiting(); - $collect = []; - - if ($waiting) { - $collect = \pcov\collect(\pcov\inclusive, $waiting); + $collect = \pcov\collect(\pcov\inclusive, $this->filter->getWhitelist()); - \pcov\clear(); - } + \pcov\clear(); return $collect; } From add204de79cfcd67e808fd7d4e8188b3136875d2 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Wed, 13 May 2020 15:56:04 +0200 Subject: [PATCH 04/24] Update Psalm baseline --- .psalm/baseline.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml index 646ce3a36..b4d7707bc 100644 --- a/.psalm/baseline.xml +++ b/.psalm/baseline.xml @@ -18,7 +18,9 @@ - + + XDEBUG_FILTER_CODE_COVERAGE + XDEBUG_PATH_WHITELIST XDEBUG_CC_UNUSED XDEBUG_CC_DEAD_CODE From 0069dfb5f26a1abcca8bb822918c0c2ccf070d32 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Wed, 13 May 2020 15:59:13 +0200 Subject: [PATCH 05/24] We do not need to keep a reference to the Filter object --- src/Driver/Xdebug.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Driver/Xdebug.php b/src/Driver/Xdebug.php index dfd46abce..f4e107c67 100644 --- a/src/Driver/Xdebug.php +++ b/src/Driver/Xdebug.php @@ -17,11 +17,6 @@ */ final class Xdebug implements Driver { - /** - * @var Filter - */ - private $filter; - /** * @throws RuntimeException */ @@ -35,8 +30,7 @@ public function __construct(Filter $filter) throw new RuntimeException('xdebug.coverage_enable=On has to be set in php.ini'); } - $this->filter = $filter; - \xdebug_set_filter(XDEBUG_FILTER_CODE_COVERAGE, XDEBUG_PATH_WHITELIST, $this->filter->getWhitelist()); + \xdebug_set_filter(XDEBUG_FILTER_CODE_COVERAGE, XDEBUG_PATH_WHITELIST, $filter->getWhitelist()); } /** From a511164777214c49195dc75145e58d61e788cdb2 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Wed, 13 May 2020 15:59:49 +0200 Subject: [PATCH 06/24] Fix CS/WS issues --- src/Driver/Xdebug.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Driver/Xdebug.php b/src/Driver/Xdebug.php index f4e107c67..793bb9972 100644 --- a/src/Driver/Xdebug.php +++ b/src/Driver/Xdebug.php @@ -30,7 +30,7 @@ public function __construct(Filter $filter) throw new RuntimeException('xdebug.coverage_enable=On has to be set in php.ini'); } - \xdebug_set_filter(XDEBUG_FILTER_CODE_COVERAGE, XDEBUG_PATH_WHITELIST, $filter->getWhitelist()); + \xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_WHITELIST, $filter->getWhitelist()); } /** @@ -39,7 +39,7 @@ public function __construct(Filter $filter) public function start(bool $determineUnusedAndDead = true): void { if ($determineUnusedAndDead) { - \xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); + \xdebug_start_code_coverage(\XDEBUG_CC_UNUSED | \XDEBUG_CC_DEAD_CODE); } else { \xdebug_start_code_coverage(); } From 0be1afd0d3d353fdcc2e8a41f8241f81accd367e Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Wed, 13 May 2020 16:07:21 +0200 Subject: [PATCH 07/24] Update Psalm baseline --- .psalm/baseline.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml index b4d7707bc..751b1bba6 100644 --- a/.psalm/baseline.xml +++ b/.psalm/baseline.xml @@ -19,10 +19,10 @@ - XDEBUG_FILTER_CODE_COVERAGE - XDEBUG_PATH_WHITELIST - XDEBUG_CC_UNUSED - XDEBUG_CC_DEAD_CODE + \XDEBUG_FILTER_CODE_COVERAGE + \XDEBUG_PATH_WHITELIST + \XDEBUG_CC_UNUSED + \XDEBUG_CC_DEAD_CODE From 3203df5982abe22ec19b9c685cc78b4676f0f2b5 Mon Sep 17 00:00:00 2001 From: Doug Wright Date: Wed, 13 May 2020 15:11:00 +0100 Subject: [PATCH 08/24] Encapsulate raw coverage data from drivers --- src/CodeCoverage.php | 78 +++---- src/Driver/Driver.php | 4 +- src/Driver/PCOV.php | 5 +- src/Driver/PHPDBG.php | 5 +- src/Driver/Xdebug.php | 5 +- .../UnknownCoverageDataFormatException.php | 26 +++ src/RawCodeCoverageData.php | 71 ++++++ tests/TestCase.php | 64 ++--- tests/tests/CodeCoverageTest.php | 10 +- tests/tests/RawCodeCoverageDataTest.php | 218 ++++++++++++++++++ 10 files changed, 405 insertions(+), 81 deletions(-) create mode 100644 src/Exception/UnknownCoverageDataFormatException.php create mode 100644 src/RawCodeCoverageData.php create mode 100644 tests/tests/RawCodeCoverageDataTest.php diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index 9e7396214..757a293c6 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -255,7 +255,7 @@ public function start($id, bool $clear = false): void * @throws InvalidArgumentException * @throws \ReflectionException */ - public function stop(bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], bool $ignoreForceCoversAnnotation = false): array + public function stop(bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], bool $ignoreForceCoversAnnotation = false): RawCodeCoverageData { if (!\is_array($linesToBeCovered) && $linesToBeCovered !== false) { throw InvalidArgumentException::create( @@ -285,7 +285,7 @@ public function stop(bool $append = true, $linesToBeCovered = [], array $linesTo * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException * @throws RuntimeException */ - public function append(array $data, $id = null, bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], bool $ignoreForceCoversAnnotation = false): void + public function append(RawCodeCoverageData $rawData, $id = null, bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], bool $ignoreForceCoversAnnotation = false): void { if ($id === null) { $id = $this->currentId; @@ -295,9 +295,9 @@ public function append(array $data, $id = null, bool $append = true, $linesToBeC throw new RuntimeException; } - $this->applyWhitelistFilter($data); - $this->applyIgnoredLinesFilter($data); - $this->initializeFilesThatAreSeenTheFirstTime($data); + $this->applyWhitelistFilter($rawData); + $this->applyIgnoredLinesFilter($rawData); + $this->initializeFilesThatAreSeenTheFirstTime($rawData); if (!$append) { return; @@ -305,14 +305,14 @@ public function append(array $data, $id = null, bool $append = true, $linesToBeC if ($id !== 'UNCOVERED_FILES_FROM_WHITELIST') { $this->applyCoversAnnotationFilter( - $data, + $rawData, $linesToBeCovered, $linesToBeUsed, $ignoreForceCoversAnnotation ); } - if (empty($data)) { + if (empty($rawData->getLineData())) { return; } @@ -339,7 +339,7 @@ public function append(array $data, $id = null, bool $append = true, $linesToBeC $this->tests[$id] = ['size' => $size, 'status' => $status]; - foreach ($data as $file => $lines) { + foreach ($rawData->getLineData() as $file => $lines) { if (!$this->filter->isFile($file)) { continue; } @@ -499,7 +499,7 @@ private function getLinePriority($data, $line) * @throws MissingCoversAnnotationException * @throws UnintentionallyCoveredCodeException */ - private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed, bool $ignoreForceCoversAnnotation): void + private function applyCoversAnnotationFilter(RawCodeCoverageData $rawData, $linesToBeCovered, array $linesToBeUsed, bool $ignoreForceCoversAnnotation): void { if ($linesToBeCovered === false || ($this->forceCoversAnnotation && empty($linesToBeCovered) && !$ignoreForceCoversAnnotation)) { @@ -507,7 +507,7 @@ private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, ar throw new MissingCoversAnnotationException; } - $data = []; + $rawData->clear(); return; } @@ -519,26 +519,32 @@ private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, ar if ($this->checkForUnintentionallyCoveredCode && (!$this->currentId instanceof TestCase || (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) { - $this->performUnintentionallyCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed); + $this->performUnintentionallyCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed); } if ($this->checkForUnexecutedCoveredCode) { - $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed); + $this->performUnexecutedCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed); } - $data = \array_intersect_key($data, $linesToBeCovered); + $rawLineData = $rawData->getLineData(); + $filesWithNoCoverage = \array_diff_key($rawLineData, $linesToBeCovered); - foreach (\array_keys($data) as $filename) { - $_linesToBeCovered = \array_flip($linesToBeCovered[$filename]); - $data[$filename] = \array_intersect_key($data[$filename], $_linesToBeCovered); + foreach (\array_keys($filesWithNoCoverage) as $fileWithNoCoverage) { + $rawData->removeCoverageDataForFile($fileWithNoCoverage); + } + + if (\is_array($linesToBeCovered)) { + foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) { + $rawData->keepCoverageDataOnlyForLines($fileToBeCovered, $includedLines); + } } } - private function applyWhitelistFilter(array &$data): void + private function applyWhitelistFilter(RawCodeCoverageData $data): void { - foreach (\array_keys($data) as $filename) { + foreach (\array_keys($data->getLineData()) as $filename) { if ($this->filter->isFiltered($filename)) { - unset($data[$filename]); + $data->removeCoverageDataForFile($filename); } } } @@ -546,22 +552,16 @@ private function applyWhitelistFilter(array &$data): void /** * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException */ - private function applyIgnoredLinesFilter(array &$data): void + private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void { - foreach (\array_keys($data) as $filename) { - if (!$this->filter->isFile($filename)) { - continue; - } - - foreach ($this->getLinesToBeIgnored($filename) as $line) { - unset($data[$filename][$line]); - } + foreach (\array_keys($data->getLineData()) as $filename) { + $data->removeCoverageDataForLines($filename, $this->getLinesToBeIgnored($filename)); } } - private function initializeFilesThatAreSeenTheFirstTime(array $data): void + private function initializeFilesThatAreSeenTheFirstTime(RawCodeCoverageData $data): void { - foreach ($data as $file => $lines) { + foreach ($data->getLineData() as $file => $lines) { if (!isset($this->data[$file]) && $this->filter->isFile($file)) { $this->data[$file] = []; @@ -602,7 +602,7 @@ private function addUncoveredFilesFromWhitelist(): void } } - $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); + $this->append(new RawCodeCoverageData($data), 'UNCOVERED_FILES_FROM_WHITELIST'); } private function getLinesToBeIgnored(string $fileName): array @@ -793,7 +793,7 @@ private function getLinesToBeIgnoredInner(string $fileName): array * @throws \ReflectionException * @throws UnintentionallyCoveredCodeException */ - private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed): void + private function performUnintentionallyCoveredCodeCheck(RawCodeCoverageData $data, array $linesToBeCovered, array $linesToBeUsed): void { $allowedLines = $this->getAllowedLines( $linesToBeCovered, @@ -802,7 +802,7 @@ private function performUnintentionallyCoveredCodeCheck(array &$data, array $lin $unintentionallyCoveredUnits = []; - foreach ($data as $file => $_data) { + foreach ($data->getLineData() as $file => $_data) { foreach ($_data as $line => $flag) { if ($flag === 1 && !isset($allowedLines[$file][$line])) { $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line); @@ -822,9 +822,9 @@ private function performUnintentionallyCoveredCodeCheck(array &$data, array $lin /** * @throws CoveredCodeNotExecutedException */ - private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed): void + private function performUnexecutedCoveredCodeCheck(RawCodeCoverageData $rawData, array $linesToBeCovered, array $linesToBeUsed): void { - $executedCodeUnits = $this->coverageToCodeUnits($data); + $executedCodeUnits = $this->coverageToCodeUnits($rawData); $message = ''; foreach ($this->linesToCodeUnits($linesToBeCovered) as $codeUnit) { @@ -958,7 +958,7 @@ private function initializeData(): void $data = []; - foreach ($this->driver->stop() as $file => $fileCoverage) { + foreach ($this->driver->stop()->getLineData() as $file => $fileCoverage) { if ($this->filter->isFiltered($file)) { continue; } @@ -972,15 +972,15 @@ private function initializeData(): void $data[$file] = $fileCoverage; } - $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); + $this->append(new RawCodeCoverageData($data), 'UNCOVERED_FILES_FROM_WHITELIST'); } } - private function coverageToCodeUnits(array $data): array + private function coverageToCodeUnits(RawCodeCoverageData $rawData): array { $codeUnits = []; - foreach ($data as $filename => $lines) { + foreach ($rawData->getLineData() as $filename => $lines) { foreach ($lines as $line => $flag) { if ($flag === 1) { $codeUnits[] = $this->wizard->lookup($filename, $line); diff --git a/src/Driver/Driver.php b/src/Driver/Driver.php index 8a5f4bf08..aba17b011 100644 --- a/src/Driver/Driver.php +++ b/src/Driver/Driver.php @@ -9,6 +9,8 @@ */ namespace SebastianBergmann\CodeCoverage\Driver; +use SebastianBergmann\CodeCoverage\RawCodeCoverageData; + /** * Interface for code coverage drivers. */ @@ -43,5 +45,5 @@ public function start(bool $determineUnusedAndDead = true): void; /** * Stop collection of code coverage information. */ - public function stop(): array; + public function stop(): RawCodeCoverageData; } diff --git a/src/Driver/PCOV.php b/src/Driver/PCOV.php index 1ee2e9b3d..af830c34c 100644 --- a/src/Driver/PCOV.php +++ b/src/Driver/PCOV.php @@ -10,6 +10,7 @@ namespace SebastianBergmann\CodeCoverage\Driver; use SebastianBergmann\CodeCoverage\Filter; +use SebastianBergmann\CodeCoverage\RawCodeCoverageData; /** * Driver for PCOV code coverage functionality. @@ -37,7 +38,7 @@ public function start(bool $determineUnusedAndDead = true): void /** * Stop collection of code coverage information. */ - public function stop(): array + public function stop(): RawCodeCoverageData { \pcov\stop(); @@ -45,6 +46,6 @@ public function stop(): array \pcov\clear(); - return $collect; + return new RawCodeCoverageData($collect); } } diff --git a/src/Driver/PHPDBG.php b/src/Driver/PHPDBG.php index 4739ae41f..57871d6cb 100644 --- a/src/Driver/PHPDBG.php +++ b/src/Driver/PHPDBG.php @@ -9,6 +9,7 @@ */ namespace SebastianBergmann\CodeCoverage\Driver; +use SebastianBergmann\CodeCoverage\RawCodeCoverageData; use SebastianBergmann\CodeCoverage\RuntimeException; /** @@ -45,7 +46,7 @@ public function start(bool $determineUnusedAndDead = true): void /** * Stop collection of code coverage information. */ - public function stop(): array + public function stop(): RawCodeCoverageData { static $fetchedLines = []; @@ -71,7 +72,7 @@ public function stop(): array $fetchedLines = \array_merge($fetchedLines, $sourceLines); - return $this->detectExecutedLines($fetchedLines, $dbgData); + return new RawCodeCoverageData($this->detectExecutedLines($fetchedLines, $dbgData)); } /** diff --git a/src/Driver/Xdebug.php b/src/Driver/Xdebug.php index 793bb9972..1264b21eb 100644 --- a/src/Driver/Xdebug.php +++ b/src/Driver/Xdebug.php @@ -10,6 +10,7 @@ namespace SebastianBergmann\CodeCoverage\Driver; use SebastianBergmann\CodeCoverage\Filter; +use SebastianBergmann\CodeCoverage\RawCodeCoverageData; use SebastianBergmann\CodeCoverage\RuntimeException; /** @@ -48,12 +49,12 @@ public function start(bool $determineUnusedAndDead = true): void /** * Stop collection of code coverage information. */ - public function stop(): array + public function stop(): RawCodeCoverageData { $data = \xdebug_get_code_coverage(); \xdebug_stop_code_coverage(); - return $data; + return new RawCodeCoverageData($data); } } diff --git a/src/Exception/UnknownCoverageDataFormatException.php b/src/Exception/UnknownCoverageDataFormatException.php new file mode 100644 index 000000000..f8cd7dc5a --- /dev/null +++ b/src/Exception/UnknownCoverageDataFormatException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +/** + * Exception that is raised when a driver supplies coverage data in a format that cannot be handled. + */ +final class UnknownCoverageDataFormatException extends RuntimeException +{ + public static function create(string $filename): self + { + return new self( + \sprintf( + 'Coverage data for file "%s" must be in Xdebug-compatible format, see https://xdebug.org/docs/code_coverage', + $filename + ) + ); + } +} diff --git a/src/RawCodeCoverageData.php b/src/RawCodeCoverageData.php new file mode 100644 index 000000000..f56312ee0 --- /dev/null +++ b/src/RawCodeCoverageData.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +/** + * Raw context-less code coverage data for SUT. + */ +final class RawCodeCoverageData +{ + /** + * Line coverage data. + * + * @see https://xdebug.org/docs/code_coverage for format + * + * @var array + */ + private $lineData = []; + + public function __construct(array $rawCoverage = []) + { + foreach ($rawCoverage as $file => $fileCoverageData) { + $hasOnlyIntegerKeys = \count(\array_filter(\array_keys($fileCoverageData), 'is_int')) === \count($fileCoverageData); + + if ($hasOnlyIntegerKeys) { + $this->lineData[$file] = $fileCoverageData; + } elseif (\count($fileCoverageData) === 2 && isset($fileCoverageData['lines'], $fileCoverageData['functions'])) { + $this->lineData[$file] = $fileCoverageData['lines']; + } else { + throw UnknownCoverageDataFormatException::create($file); + } + } + } + + public function clear(): void + { + $this->lineData = []; + } + + public function getLineData(): array + { + return $this->lineData; + } + + public function removeCoverageDataForFile(string $filename): void + { + unset($this->lineData[$filename]); + } + + /** + * @param int[] $lines + */ + public function keepCoverageDataOnlyForLines(string $filename, array $lines): void + { + $this->lineData[$filename] = \array_intersect_key($this->lineData[$filename], \array_flip($lines)); + } + + /** + * @param int[] $lines + */ + public function removeCoverageDataForLines(string $filename, array $lines): void + { + $this->lineData[$filename] = \array_diff_key($this->lineData[$filename], \array_flip($lines)); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index a1c9efb00..45e12dd2a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -23,7 +23,7 @@ public static function setUpBeforeClass(): void protected function getXdebugDataForBankAccount() { return [ - [ + new RawCodeCoverageData([ TEST_FILES_PATH . 'BankAccount.php' => [ 8 => 1, 9 => -2, @@ -39,24 +39,24 @@ protected function getXdebugDataForBankAccount() 31 => -1, 32 => -2, ], - ], - [ + ]), + new RawCodeCoverageData([ TEST_FILES_PATH . 'BankAccount.php' => [ 8 => 1, 13 => 1, 16 => 1, 29 => 1, ], - ], - [ + ]), + new RawCodeCoverageData([ TEST_FILES_PATH . 'BankAccount.php' => [ 8 => 1, 13 => 1, 16 => 1, 22 => 1, ], - ], - [ + ]), + new RawCodeCoverageData([ TEST_FILES_PATH . 'BankAccount.php' => [ 8 => 1, 13 => 1, @@ -68,7 +68,7 @@ protected function getXdebugDataForBankAccount() 29 => 1, 31 => 1, ], - ], + ]), ]; } @@ -314,14 +314,16 @@ protected function setUpXdebugStubForFileWithIgnoredLines(): Driver $stub->expects($this->any()) ->method('stop') ->will($this->returnValue( - [ - TEST_FILES_PATH . 'source_with_ignore.php' => [ - 2 => 1, - 4 => -1, - 6 => -1, - 7 => 1, - ], - ] + new RawCodeCoverageData( + [ + TEST_FILES_PATH . 'source_with_ignore.php' => [ + 2 => 1, + 4 => -1, + 6 => -1, + 7 => 1, + ], + ] + ) )); return $stub; @@ -350,19 +352,21 @@ protected function setUpXdebugStubForClassWithAnonymousFunction(): Driver $stub->expects($this->any()) ->method('stop') ->will($this->returnValue( - [ - TEST_FILES_PATH . 'source_with_class_and_anonymous_function.php' => [ - 7 => 1, - 9 => 1, - 10 => -1, - 11 => 1, - 12 => 1, - 13 => 1, - 14 => 1, - 17 => 1, - 18 => 1, - ], - ] + new RawCodeCoverageData( + [ + TEST_FILES_PATH . 'source_with_class_and_anonymous_function.php' => [ + 7 => 1, + 9 => 1, + 10 => -1, + 11 => 1, + 12 => 1, + 13 => 1, + 14 => 1, + 17 => 1, + 18 => 1, + ], + ] + ) )); return $stub; @@ -386,7 +390,7 @@ protected function setUpXdebugStubForCrashParsing(): Driver $stub->expects($this->any()) ->method('stop') - ->will($this->returnValue([])); + ->will($this->returnValue(new RawCodeCoverageData([]))); return $stub; } diff --git a/tests/tests/CodeCoverageTest.php b/tests/tests/CodeCoverageTest.php index b76fa43d9..3b7c1ae0d 100644 --- a/tests/tests/CodeCoverageTest.php +++ b/tests/tests/CodeCoverageTest.php @@ -44,7 +44,7 @@ public function testCannotAppendWithInvalidArgument(): void { $this->expectException(Exception::class); - $this->coverage->append([], null); + $this->coverage->append(new RawCodeCoverageData([]), null); } public function testCollect(): void @@ -290,12 +290,12 @@ public function testAppendThrowsExceptionIfCoveredCodeWasNotExecuted(): void $this->coverage->filter()->addDirectoryToWhitelist(TEST_FILES_PATH); $this->coverage->setCheckForUnexecutedCoveredCode(true); - $data = [ + $data = new RawCodeCoverageData([ TEST_FILES_PATH . 'BankAccount.php' => [ 29 => -1, 31 => -1, ], - ]; + ]); $linesToBeCovered = [ TEST_FILES_PATH . 'BankAccount.php' => [ @@ -316,12 +316,12 @@ public function testAppendThrowsExceptionIfUsedCodeWasNotExecuted(): void $this->coverage->filter()->addDirectoryToWhitelist(TEST_FILES_PATH); $this->coverage->setCheckForUnexecutedCoveredCode(true); - $data = [ + $data = new RawCodeCoverageData([ TEST_FILES_PATH . 'BankAccount.php' => [ 29 => -1, 31 => -1, ], - ]; + ]); $linesToBeCovered = [ TEST_FILES_PATH . 'BankAccount.php' => [ diff --git a/tests/tests/RawCodeCoverageDataTest.php b/tests/tests/RawCodeCoverageDataTest.php new file mode 100644 index 000000000..7d7a4984b --- /dev/null +++ b/tests/tests/RawCodeCoverageDataTest.php @@ -0,0 +1,218 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +class RawCodeCoverageDataTest extends TestCase +{ + /** + * In the standard XDebug format, there is only line data. Therefore output should match input. + */ + public function testLineDataFromStandardXDebugFormat(): void + { + $lineDataFromDriver = [ + '/some/path/SomeClass.php' => [ + 8 => 1, + 9 => -2, + 13 => -1, + ], + ]; + + $dataObject = new RawCodeCoverageData($lineDataFromDriver); + $this->assertEquals($lineDataFromDriver, $dataObject->getLineData()); + } + + /** + * In the branch-check XDebug format, the line data exists inside a "lines" array key. + */ + public function testLineDataFromBranchCheckXDebugFormat(): void + { + $rawDataFromDriver = [ + '/some/path/SomeClass.php' => [ + 'lines' => [ + 8 => 1, + 9 => -2, + 13 => -1, + ], + 'functions' => [ + + ], + ], + ]; + + $lineData = [ + '/some/path/SomeClass.php' => [ + 8 => 1, + 9 => -2, + 13 => -1, + ], + ]; + + $dataObject = new RawCodeCoverageData($rawDataFromDriver); + $this->assertEquals($lineData, $dataObject->getLineData()); + } + + /** + * Coverage data that does not match a known format should throw an exception. + */ + public function testDataFromUnknownFormat(): void + { + $this->expectException(UnknownCoverageDataFormatException::class); + + $lineDataFromDriver = [ + '/some/path/SomeClass.php' => [ + 'executedLines' => [ + 8, + ], + 'unExecutedLines' => [ + 13, + ], + 'nonExecutableLines' => [ + 9, + ], + ], + ]; + + $dataObject = new RawCodeCoverageData($lineDataFromDriver); + } + + public function testClear(): void + { + $lineDataFromDriver = [ + '/some/path/SomeClass.php' => [ + 8 => 1, + 9 => -2, + 13 => -1, + ], + ]; + + $dataObject = new RawCodeCoverageData($lineDataFromDriver); + $dataObject->clear(); + $this->assertEmpty($dataObject->getLineData()); + } + + public function testRemoveCoverageDataForFile(): void + { + $lineDataFromDriver = [ + '/some/path/SomeClass.php' => [ + 8 => 1, + 9 => -2, + 13 => -1, + ], + '/some/path/SomeOtherClass.php' => [ + 18 => 1, + 19 => -2, + 113 => -1, + ], + '/some/path/AnotherClass.php' => [ + 28 => 1, + 29 => -2, + 213 => -1, + ], + ]; + + $expectedFilterResult = [ + '/some/path/SomeClass.php' => [ + 8 => 1, + 9 => -2, + 13 => -1, + ], + '/some/path/AnotherClass.php' => [ + 28 => 1, + 29 => -2, + 213 => -1, + ], + ]; + + $dataObject = new RawCodeCoverageData($lineDataFromDriver); + $dataObject->removeCoverageDataForFile('/some/path/SomeOtherClass.php'); + $this->assertEquals($expectedFilterResult, $dataObject->getLineData()); + } + + public function testKeepCoverageDataOnlyForLines(): void + { + $lineDataFromDriver = [ + '/some/path/SomeClass.php' => [ + 8 => 1, + 9 => -2, + 13 => -1, + ], + '/some/path/SomeOtherClass.php' => [ + 18 => 1, + 19 => -2, + 113 => -1, + ], + '/some/path/AnotherClass.php' => [ + 28 => 1, + 29 => -2, + 213 => -1, + ], + ]; + + $expectedFilterResult = [ + '/some/path/SomeClass.php' => [ + 9 => -2, + 13 => -1, + ], + '/some/path/SomeOtherClass.php' => [ + ], + '/some/path/AnotherClass.php' => [ + 28 => 1, + ], + ]; + + $dataObject = new RawCodeCoverageData($lineDataFromDriver); + $dataObject->keepCoverageDataOnlyForLines('/some/path/SomeClass.php', [9, 13]); + $dataObject->keepCoverageDataOnlyForLines('/some/path/SomeOtherClass.php', [999]); + $dataObject->keepCoverageDataOnlyForLines('/some/path/AnotherClass.php', [28]); + $this->assertEquals($expectedFilterResult, $dataObject->getLineData()); + } + + public function testRemoveCoverageDataForLines(): void + { + $lineDataFromDriver = [ + '/some/path/SomeClass.php' => [ + 8 => 1, + 9 => -2, + 13 => -1, + ], + '/some/path/SomeOtherClass.php' => [ + 18 => 1, + 19 => -2, + 113 => -1, + ], + '/some/path/AnotherClass.php' => [ + 28 => 1, + 29 => -2, + 213 => -1, + ], + ]; + + $expectedFilterResult = [ + '/some/path/SomeClass.php' => [ + 8 => 1, + ], + '/some/path/SomeOtherClass.php' => [ + 18 => 1, + 19 => -2, + 113 => -1, + ], + '/some/path/AnotherClass.php' => [ + 29 => -2, + 213 => -1, + ], + ]; + + $dataObject = new RawCodeCoverageData($lineDataFromDriver); + $dataObject->removeCoverageDataForLines('/some/path/SomeClass.php', [9, 13]); + $dataObject->removeCoverageDataForLines('/some/path/SomeOtherClass.php', [999]); + $dataObject->removeCoverageDataForLines('/some/path/AnotherClass.php', [28]); + $this->assertEquals($expectedFilterResult, $dataObject->getLineData()); + } +} From 9cc38380372be22201860b366c8b58b68634aa63 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Thu, 14 May 2020 07:22:02 +0200 Subject: [PATCH 09/24] Fix issues identified by Psalm --- .psalm/baseline.xml | 10 ---------- src/Node/AbstractNode.php | 14 +++++++------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml index 751b1bba6..b43b7c5da 100644 --- a/.psalm/baseline.xml +++ b/.psalm/baseline.xml @@ -26,16 +26,6 @@ - - - int|string - int|string - int|string - int|string - int|string - int|string - int|string - $parent diff --git a/src/Node/AbstractNode.php b/src/Node/AbstractNode.php index 411c04e4c..b107c2b20 100644 --- a/src/Node/AbstractNode.php +++ b/src/Node/AbstractNode.php @@ -113,7 +113,7 @@ public function getParent(): ?self /** * Returns the percentage of classes that has been tested. * - * @return int|string + * @return float|int|string */ public function getTestedClassesPercent(bool $asString = true) { @@ -127,7 +127,7 @@ public function getTestedClassesPercent(bool $asString = true) /** * Returns the percentage of traits that has been tested. * - * @return int|string + * @return float|int|string */ public function getTestedTraitsPercent(bool $asString = true) { @@ -141,7 +141,7 @@ public function getTestedTraitsPercent(bool $asString = true) /** * Returns the percentage of classes and traits that has been tested. * - * @return int|string + * @return float|int|string */ public function getTestedClassesAndTraitsPercent(bool $asString = true) { @@ -155,7 +155,7 @@ public function getTestedClassesAndTraitsPercent(bool $asString = true) /** * Returns the percentage of functions that has been tested. * - * @return int|string + * @return float|int|string */ public function getTestedFunctionsPercent(bool $asString = true) { @@ -169,7 +169,7 @@ public function getTestedFunctionsPercent(bool $asString = true) /** * Returns the percentage of methods that has been tested. * - * @return int|string + * @return float|int|string */ public function getTestedMethodsPercent(bool $asString = true) { @@ -183,7 +183,7 @@ public function getTestedMethodsPercent(bool $asString = true) /** * Returns the percentage of functions and methods that has been tested. * - * @return int|string + * @return float|int|string */ public function getTestedFunctionsAndMethodsPercent(bool $asString = true) { @@ -197,7 +197,7 @@ public function getTestedFunctionsAndMethodsPercent(bool $asString = true) /** * Returns the percentage of executed lines. * - * @return int|string + * @return float|int|string */ public function getLineExecutedPercent(bool $asString = true) { From 07b8108677e75213a49410b41d3d16b4c7146a7b Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Thu, 14 May 2020 07:34:24 +0200 Subject: [PATCH 10/24] Remove superfluous code --- src/Exception/UnintentionallyCoveredCodeException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Exception/UnintentionallyCoveredCodeException.php b/src/Exception/UnintentionallyCoveredCodeException.php index efeec8d9f..12c9ede72 100644 --- a/src/Exception/UnintentionallyCoveredCodeException.php +++ b/src/Exception/UnintentionallyCoveredCodeException.php @@ -17,7 +17,7 @@ final class UnintentionallyCoveredCodeException extends RuntimeException /** * @var array */ - private $unintentionallyCoveredUnits = []; + private $unintentionallyCoveredUnits; public function __construct(array $unintentionallyCoveredUnits) { From a01b163f5ba648516542574b72875988730a8422 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Thu, 14 May 2020 07:34:41 +0200 Subject: [PATCH 11/24] Use strict comparison --- src/Node/AbstractNode.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Node/AbstractNode.php b/src/Node/AbstractNode.php index b107c2b20..3c61ca7ca 100644 --- a/src/Node/AbstractNode.php +++ b/src/Node/AbstractNode.php @@ -43,7 +43,7 @@ abstract class AbstractNode implements \Countable public function __construct(string $name, self $parent = null) { - if (\substr($name, -1) == \DIRECTORY_SEPARATOR) { + if (\substr($name, -1) === \DIRECTORY_SEPARATOR) { $name = \substr($name, 0, -1); } From 06bfa0c7237ca605dea11c7ccfe94a3ceebe9b03 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Thu, 14 May 2020 07:39:23 +0200 Subject: [PATCH 12/24] Rename lineData to lineCoverage --- src/CodeCoverage.php | 18 ++++++++-------- src/RawCodeCoverageData.php | 28 ++++++++++++++----------- tests/tests/RawCodeCoverageDataTest.php | 12 +++++------ 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index 757a293c6..8736f51e1 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -312,7 +312,7 @@ public function append(RawCodeCoverageData $rawData, $id = null, bool $append = ); } - if (empty($rawData->getLineData())) { + if (empty($rawData->getLineCoverage())) { return; } @@ -339,7 +339,7 @@ public function append(RawCodeCoverageData $rawData, $id = null, bool $append = $this->tests[$id] = ['size' => $size, 'status' => $status]; - foreach ($rawData->getLineData() as $file => $lines) { + foreach ($rawData->getLineCoverage() as $file => $lines) { if (!$this->filter->isFile($file)) { continue; } @@ -526,7 +526,7 @@ private function applyCoversAnnotationFilter(RawCodeCoverageData $rawData, $line $this->performUnexecutedCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed); } - $rawLineData = $rawData->getLineData(); + $rawLineData = $rawData->getLineCoverage(); $filesWithNoCoverage = \array_diff_key($rawLineData, $linesToBeCovered); foreach (\array_keys($filesWithNoCoverage) as $fileWithNoCoverage) { @@ -542,7 +542,7 @@ private function applyCoversAnnotationFilter(RawCodeCoverageData $rawData, $line private function applyWhitelistFilter(RawCodeCoverageData $data): void { - foreach (\array_keys($data->getLineData()) as $filename) { + foreach (\array_keys($data->getLineCoverage()) as $filename) { if ($this->filter->isFiltered($filename)) { $data->removeCoverageDataForFile($filename); } @@ -554,14 +554,14 @@ private function applyWhitelistFilter(RawCodeCoverageData $data): void */ private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void { - foreach (\array_keys($data->getLineData()) as $filename) { + foreach (\array_keys($data->getLineCoverage()) as $filename) { $data->removeCoverageDataForLines($filename, $this->getLinesToBeIgnored($filename)); } } private function initializeFilesThatAreSeenTheFirstTime(RawCodeCoverageData $data): void { - foreach ($data->getLineData() as $file => $lines) { + foreach ($data->getLineCoverage() as $file => $lines) { if (!isset($this->data[$file]) && $this->filter->isFile($file)) { $this->data[$file] = []; @@ -802,7 +802,7 @@ private function performUnintentionallyCoveredCodeCheck(RawCodeCoverageData $dat $unintentionallyCoveredUnits = []; - foreach ($data->getLineData() as $file => $_data) { + foreach ($data->getLineCoverage() as $file => $_data) { foreach ($_data as $line => $flag) { if ($flag === 1 && !isset($allowedLines[$file][$line])) { $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line); @@ -958,7 +958,7 @@ private function initializeData(): void $data = []; - foreach ($this->driver->stop()->getLineData() as $file => $fileCoverage) { + foreach ($this->driver->stop()->getLineCoverage() as $file => $fileCoverage) { if ($this->filter->isFiltered($file)) { continue; } @@ -980,7 +980,7 @@ private function coverageToCodeUnits(RawCodeCoverageData $rawData): array { $codeUnits = []; - foreach ($rawData->getLineData() as $filename => $lines) { + foreach ($rawData->getLineCoverage() as $filename => $lines) { foreach ($lines as $line => $flag) { if ($flag === 1) { $codeUnits[] = $this->wizard->lookup($filename, $line); diff --git a/src/RawCodeCoverageData.php b/src/RawCodeCoverageData.php index f56312ee0..6f3af62a8 100644 --- a/src/RawCodeCoverageData.php +++ b/src/RawCodeCoverageData.php @@ -15,13 +15,11 @@ final class RawCodeCoverageData { /** - * Line coverage data. + * @var array * * @see https://xdebug.org/docs/code_coverage for format - * - * @var array */ - private $lineData = []; + private $lineCoverage = []; public function __construct(array $rawCoverage = []) { @@ -29,9 +27,9 @@ public function __construct(array $rawCoverage = []) $hasOnlyIntegerKeys = \count(\array_filter(\array_keys($fileCoverageData), 'is_int')) === \count($fileCoverageData); if ($hasOnlyIntegerKeys) { - $this->lineData[$file] = $fileCoverageData; + $this->lineCoverage[$file] = $fileCoverageData; } elseif (\count($fileCoverageData) === 2 && isset($fileCoverageData['lines'], $fileCoverageData['functions'])) { - $this->lineData[$file] = $fileCoverageData['lines']; + $this->lineCoverage[$file] = $fileCoverageData['lines']; } else { throw UnknownCoverageDataFormatException::create($file); } @@ -40,17 +38,17 @@ public function __construct(array $rawCoverage = []) public function clear(): void { - $this->lineData = []; + $this->lineCoverage = []; } - public function getLineData(): array + public function getLineCoverage(): array { - return $this->lineData; + return $this->lineCoverage; } public function removeCoverageDataForFile(string $filename): void { - unset($this->lineData[$filename]); + unset($this->lineCoverage[$filename]); } /** @@ -58,7 +56,10 @@ public function removeCoverageDataForFile(string $filename): void */ public function keepCoverageDataOnlyForLines(string $filename, array $lines): void { - $this->lineData[$filename] = \array_intersect_key($this->lineData[$filename], \array_flip($lines)); + $this->lineCoverage[$filename] = \array_intersect_key( + $this->lineCoverage[$filename], + \array_flip($lines) + ); } /** @@ -66,6 +67,9 @@ public function keepCoverageDataOnlyForLines(string $filename, array $lines): vo */ public function removeCoverageDataForLines(string $filename, array $lines): void { - $this->lineData[$filename] = \array_diff_key($this->lineData[$filename], \array_flip($lines)); + $this->lineCoverage[$filename] = \array_diff_key( + $this->lineCoverage[$filename], + \array_flip($lines) + ); } } diff --git a/tests/tests/RawCodeCoverageDataTest.php b/tests/tests/RawCodeCoverageDataTest.php index 7d7a4984b..d21d52e50 100644 --- a/tests/tests/RawCodeCoverageDataTest.php +++ b/tests/tests/RawCodeCoverageDataTest.php @@ -25,7 +25,7 @@ public function testLineDataFromStandardXDebugFormat(): void ]; $dataObject = new RawCodeCoverageData($lineDataFromDriver); - $this->assertEquals($lineDataFromDriver, $dataObject->getLineData()); + $this->assertEquals($lineDataFromDriver, $dataObject->getLineCoverage()); } /** @@ -55,7 +55,7 @@ public function testLineDataFromBranchCheckXDebugFormat(): void ]; $dataObject = new RawCodeCoverageData($rawDataFromDriver); - $this->assertEquals($lineData, $dataObject->getLineData()); + $this->assertEquals($lineData, $dataObject->getLineCoverage()); } /** @@ -94,7 +94,7 @@ public function testClear(): void $dataObject = new RawCodeCoverageData($lineDataFromDriver); $dataObject->clear(); - $this->assertEmpty($dataObject->getLineData()); + $this->assertEmpty($dataObject->getLineCoverage()); } public function testRemoveCoverageDataForFile(): void @@ -132,7 +132,7 @@ public function testRemoveCoverageDataForFile(): void $dataObject = new RawCodeCoverageData($lineDataFromDriver); $dataObject->removeCoverageDataForFile('/some/path/SomeOtherClass.php'); - $this->assertEquals($expectedFilterResult, $dataObject->getLineData()); + $this->assertEquals($expectedFilterResult, $dataObject->getLineCoverage()); } public function testKeepCoverageDataOnlyForLines(): void @@ -171,7 +171,7 @@ public function testKeepCoverageDataOnlyForLines(): void $dataObject->keepCoverageDataOnlyForLines('/some/path/SomeClass.php', [9, 13]); $dataObject->keepCoverageDataOnlyForLines('/some/path/SomeOtherClass.php', [999]); $dataObject->keepCoverageDataOnlyForLines('/some/path/AnotherClass.php', [28]); - $this->assertEquals($expectedFilterResult, $dataObject->getLineData()); + $this->assertEquals($expectedFilterResult, $dataObject->getLineCoverage()); } public function testRemoveCoverageDataForLines(): void @@ -213,6 +213,6 @@ public function testRemoveCoverageDataForLines(): void $dataObject->removeCoverageDataForLines('/some/path/SomeClass.php', [9, 13]); $dataObject->removeCoverageDataForLines('/some/path/SomeOtherClass.php', [999]); $dataObject->removeCoverageDataForLines('/some/path/AnotherClass.php', [28]); - $this->assertEquals($expectedFilterResult, $dataObject->getLineData()); + $this->assertEquals($expectedFilterResult, $dataObject->getLineCoverage()); } } From 3699d42bf648b05bc6770893b4dfba9826bb364a Mon Sep 17 00:00:00 2001 From: Doug Wright Date: Fri, 15 May 2020 19:33:24 +0100 Subject: [PATCH 13/24] Change PHP report to use serialize, rather than var_export for better compatibility with data structure changes --- src/Report/PHP.php | 15 ++------------- tests/tests/PHPTest.php | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 tests/tests/PHPTest.php diff --git a/src/Report/PHP.php b/src/Report/PHP.php index 92926e3cf..61f00a004 100644 --- a/src/Report/PHP.php +++ b/src/Report/PHP.php @@ -22,21 +22,10 @@ final class PHP */ public function process(CodeCoverage $coverage, ?string $target = null): string { - $filter = $coverage->filter(); - $buffer = \sprintf( 'setData(%s); -$coverage->setTests(%s); - -$filter = $coverage->filter(); -$filter->setWhitelistedFiles(%s); - -return $coverage;', - \var_export($coverage->getData(true), true), - \var_export($coverage->getTests(), true), - \var_export($filter->getWhitelistedFiles(), true) +return \unserialize(\'%s\');', + \serialize($coverage) ); if ($target !== null) { diff --git a/tests/tests/PHPTest.php b/tests/tests/PHPTest.php new file mode 100644 index 000000000..204cd1f27 --- /dev/null +++ b/tests/tests/PHPTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage\Report; + +use SebastianBergmann\CodeCoverage\TestCase; + +class PHPTest extends TestCase +{ + protected function tearDown(): void + { + parent::tearDown(); + + $this->removeTemporaryFiles(); + } + + public function testPHPSerialisationProducesValidCode(): void + { + $coverage = $this->getCoverageForBankAccount(); + + (new PHP())->process($coverage, self::$TEST_TMP_PATH . '/serialised.php'); + + $unserialised = require self::$TEST_TMP_PATH . '/serialised.php'; + + $this->assertEquals($coverage, $unserialised); + } +} From 659ef5cc3a22bd66f8775dcd27525ec3dde643b7 Mon Sep 17 00:00:00 2001 From: Doug Wright Date: Thu, 14 May 2020 13:31:13 +0100 Subject: [PATCH 14/24] Encapsulate processed coverage data --- src/CodeCoverage.php | 105 ++------------ src/Node/Builder.php | 29 ++-- src/ProcessedCodeCoverageData.php | 134 ++++++++++++++++++ tests/TestCase.php | 16 ++- tests/tests/BuilderTest.php | 35 +++-- tests/tests/CodeCoverageTest.php | 28 +++- tests/tests/HTMLTest.php | 11 +- tests/tests/ProcessedCodeCoverageDataTest.php | 41 ++++++ 8 files changed, 260 insertions(+), 139 deletions(-) create mode 100644 src/ProcessedCodeCoverageData.php create mode 100644 tests/tests/ProcessedCodeCoverageDataTest.php diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index 8736f51e1..e7dcaccef 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -89,9 +89,9 @@ final class CodeCoverage /** * Code coverage data. * - * @var array + * @var ProcessedCodeCoverageData */ - private $data = []; + private $data; /** * @var array @@ -151,6 +151,7 @@ public function __construct(Driver $driver = null, Filter $filter = null) $this->filter = $filter; $this->wizard = new Wizard; + $this->data = new ProcessedCodeCoverageData(); } /** @@ -172,7 +173,7 @@ public function clear(): void { $this->isInitialized = false; $this->currentId = null; - $this->data = []; + $this->data = new ProcessedCodeCoverageData(); $this->tests = []; $this->report = null; } @@ -188,7 +189,7 @@ public function filter(): Filter /** * Returns the collected code coverage data. */ - public function getData(bool $raw = false): array + public function getData(bool $raw = false): ProcessedCodeCoverageData { if (!$raw && $this->addUncoveredFilesFromWhitelist) { $this->addUncoveredFilesFromWhitelist(); @@ -200,7 +201,7 @@ public function getData(bool $raw = false): array /** * Sets the coverage data. */ - public function setData(array $data): void + public function setData(ProcessedCodeCoverageData $data): void { $this->data = $data; $this->report = null; @@ -297,7 +298,7 @@ public function append(RawCodeCoverageData $rawData, $id = null, bool $append = $this->applyWhitelistFilter($rawData); $this->applyIgnoredLinesFilter($rawData); - $this->initializeFilesThatAreSeenTheFirstTime($rawData); + $this->data->initializeFilesThatAreSeenTheFirstTime($rawData); if (!$append) { return; @@ -339,19 +340,7 @@ public function append(RawCodeCoverageData $rawData, $id = null, bool $append = $this->tests[$id] = ['size' => $size, 'status' => $status]; - foreach ($rawData->getLineCoverage() as $file => $lines) { - if (!$this->filter->isFile($file)) { - continue; - } - - foreach ($lines as $k => $v) { - if ($v === Driver::LINE_EXECUTED) { - if (empty($this->data[$file][$k]) || !\in_array($id, $this->data[$file][$k])) { - $this->data[$file][$k][] = $id; - } - } - } - } + $this->data->markCodeAsExecutedByTestCase($id, $rawData); $this->report = null; } @@ -367,36 +356,7 @@ public function merge(self $that): void \array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles()) ); - foreach ($that->data as $file => $lines) { - if (!isset($this->data[$file])) { - if (!$this->filter->isFiltered($file)) { - $this->data[$file] = $lines; - } - - continue; - } - - // we should compare the lines if any of two contains data - $compareLineNumbers = \array_unique( - \array_merge( - \array_keys($this->data[$file]), - \array_keys($that->data[$file]) - ) - ); - - foreach ($compareLineNumbers as $line) { - $thatPriority = $this->getLinePriority($that->data[$file], $line); - $thisPriority = $this->getLinePriority($this->data[$file], $line); - - if ($thatPriority > $thisPriority) { - $this->data[$file][$line] = $that->data[$file][$line]; - } elseif ($thatPriority === $thisPriority && \is_array($this->data[$file][$line])) { - $this->data[$file][$line] = \array_unique( - \array_merge($this->data[$file][$line], $that->data[$file][$line]) - ); - } - } - } + $this->data->merge($that->data); $this->tests = \array_merge($this->tests, $that->getTests()); $this->report = null; @@ -457,38 +417,6 @@ public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist): $this->unintentionallyCoveredSubclassesWhitelist = $whitelist; } - /** - * Determine the priority for a line - * - * 1 = the line is not set - * 2 = the line has not been tested - * 3 = the line is dead code - * 4 = the line has been tested - * - * During a merge, a higher number is better. - * - * @param array $data - * @param int $line - * - * @return int - */ - private function getLinePriority($data, $line) - { - if (!\array_key_exists($line, $data)) { - return 1; - } - - if (\is_array($data[$line]) && \count($data[$line]) === 0) { - return 2; - } - - if ($data[$line] === null) { - return 3; - } - - return 4; - } - /** * Applies the @covers annotation filtering. * @@ -559,19 +487,6 @@ private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void } } - private function initializeFilesThatAreSeenTheFirstTime(RawCodeCoverageData $data): void - { - foreach ($data->getLineCoverage() as $file => $lines) { - if (!isset($this->data[$file]) && $this->filter->isFile($file)) { - $this->data[$file] = []; - - foreach ($lines as $k => $v) { - $this->data[$file][$k] = $v === -2 ? null : []; - } - } - } - } - /** * @throws CoveredCodeNotExecutedException * @throws InvalidArgumentException @@ -585,7 +500,7 @@ private function addUncoveredFilesFromWhitelist(): void $data = []; $uncoveredFiles = \array_diff( $this->filter->getWhitelist(), - \array_keys($this->data) + \array_keys($this->data->getLineCoverage()) ); foreach ($uncoveredFiles as $uncoveredFile) { diff --git a/src/Node/Builder.php b/src/Node/Builder.php index 7c20ba752..bd75b4f6b 100644 --- a/src/Node/Builder.php +++ b/src/Node/Builder.php @@ -10,13 +10,14 @@ namespace SebastianBergmann\CodeCoverage\Node; use SebastianBergmann\CodeCoverage\CodeCoverage; +use SebastianBergmann\CodeCoverage\ProcessedCodeCoverageData; final class Builder { public function build(CodeCoverage $coverage): Directory { - $files = $coverage->getData(); - $commonPath = $this->reducePaths($files); + $data = clone $coverage->getData(); // clone because path munging is destructive to the original data + $commonPath = $this->reducePaths($data); $root = new Directory( $commonPath, null @@ -24,7 +25,7 @@ public function build(CodeCoverage $coverage): Directory $this->addItems( $root, - $this->buildDirectoryStructure($files), + $this->buildDirectoryStructure($data), $coverage->getTests(), $coverage->getCacheTokens() ); @@ -90,8 +91,9 @@ private function addItems(Directory $root, array $items, array $tests, bool $cac * ) * */ - private function buildDirectoryStructure(array $files): array + private function buildDirectoryStructure(ProcessedCodeCoverageData $data): array { + $files = $data->getLineCoverage(); $result = []; foreach ($files as $path => $file) { @@ -152,20 +154,18 @@ private function buildDirectoryStructure(array $files): array * ) * */ - private function reducePaths(array &$files): string + private function reducePaths(ProcessedCodeCoverageData $coverage): string { - if (empty($files)) { + if (empty($coverage->getCoveredFiles())) { return '.'; } $commonPath = ''; - $paths = \array_keys($files); + $paths = $coverage->getCoveredFiles(); - if (\count($files) === 1) { + if (\count($paths) === 1) { $commonPath = \dirname($paths[0]) . \DIRECTORY_SEPARATOR; - $files[\basename($paths[0])] = $files[$paths[0]]; - - unset($files[$paths[0]]); + $coverage->renameFile($paths[0], \basename($paths[0])); return $commonPath; } @@ -212,16 +212,13 @@ private function reducePaths(array &$files): string } } - $original = \array_keys($files); + $original = $coverage->getCoveredFiles(); $max = \count($original); for ($i = 0; $i < $max; $i++) { - $files[\implode(\DIRECTORY_SEPARATOR, $paths[$i])] = $files[$original[$i]]; - unset($files[$original[$i]]); + $coverage->renameFile($original[$i], \implode(\DIRECTORY_SEPARATOR, $paths[$i])); } - \ksort($files); - return \substr($commonPath, 0, -1); } } diff --git a/src/ProcessedCodeCoverageData.php b/src/ProcessedCodeCoverageData.php new file mode 100644 index 000000000..5564ef84b --- /dev/null +++ b/src/ProcessedCodeCoverageData.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use SebastianBergmann\CodeCoverage\Driver\Driver; + +/** + * Processed/context-added code coverage data for SUT. + */ +final class ProcessedCodeCoverageData +{ + /** + * Line coverage data. + * An array of filenames, each having an array of linenumbers, each executable line having an array of testcase ids. + * + * @var array + */ + private $lineCoverage = []; + + public function initializeFilesThatAreSeenTheFirstTime(RawCodeCoverageData $rawData): void + { + foreach ($rawData->getLineCoverage() as $file => $lines) { + if (!isset($this->lineCoverage[$file])) { + $this->lineCoverage[$file] = []; + + foreach ($lines as $k => $v) { + $this->lineCoverage[$file][$k] = $v === Driver::LINE_NOT_EXECUTABLE ? null : []; + } + } + } + } + + public function markCodeAsExecutedByTestCase(string $testCaseId, RawCodeCoverageData $executedCode): void + { + foreach ($executedCode->getLineCoverage() as $file => $lines) { + foreach ($lines as $k => $v) { + if ($v === Driver::LINE_EXECUTED) { + if (empty($this->lineCoverage[$file][$k]) || !\in_array($testCaseId, $this->lineCoverage[$file][$k], true)) { + $this->lineCoverage[$file][$k][] = $testCaseId; + } + } + } + } + } + + public function setLineCoverage(array $lineCoverage): void + { + $this->lineCoverage = $lineCoverage; + } + + public function getLineCoverage(): array + { + \ksort($this->lineCoverage); + + return $this->lineCoverage; + } + + public function getCoveredFiles(): array + { + return \array_keys($this->lineCoverage); + } + + public function renameFile(string $oldFile, string $newFile): void + { + $this->lineCoverage[$newFile] = $this->lineCoverage[$oldFile]; + unset($this->lineCoverage[$oldFile]); + } + + public function merge(self $newData): void + { + foreach ($newData->lineCoverage as $file => $lines) { + if (!isset($this->lineCoverage[$file])) { + $this->lineCoverage[$file] = $lines; + + continue; + } + + // we should compare the lines if any of two contains data + $compareLineNumbers = \array_unique( + \array_merge( + \array_keys($this->lineCoverage[$file]), + \array_keys($newData->lineCoverage[$file]) + ) + ); + + foreach ($compareLineNumbers as $line) { + $thatPriority = $this->getLinePriority($newData->lineCoverage[$file], $line); + $thisPriority = $this->getLinePriority($this->lineCoverage[$file], $line); + + if ($thatPriority > $thisPriority) { + $this->lineCoverage[$file][$line] = $newData->lineCoverage[$file][$line]; + } elseif ($thatPriority === $thisPriority && \is_array($this->lineCoverage[$file][$line])) { + $this->lineCoverage[$file][$line] = \array_unique( + \array_merge($this->lineCoverage[$file][$line], $newData->lineCoverage[$file][$line]) + ); + } + } + } + } + + /** + * Determine the priority for a line + * + * 1 = the line is not set + * 2 = the line has not been tested + * 3 = the line is dead code + * 4 = the line has been tested + * + * During a merge, a higher number is better. + */ + private function getLinePriority(array $data, int $line): int + { + if (!\array_key_exists($line, $data)) { + return 1; + } + + if (\is_array($data[$line]) && \count($data[$line]) === 0) { + return 2; + } + + if ($data[$line] === null) { + return 3; + } + + return 4; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 45e12dd2a..0120f2e71 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -178,7 +178,7 @@ protected function getCoverageForBankAccountForFirstTwoTests(): CodeCoverage return $coverage; } - protected function getCoverageForBankAccountForLastTwoTests() + protected function getCoverageForBankAccountForLastTwoTests(): CodeCoverage { $data = $this->getXdebugDataForBankAccount(); @@ -394,4 +394,18 @@ protected function setUpXdebugStubForCrashParsing(): Driver return $stub; } + + protected function removeTemporaryFiles(): void + { + $tmpFilesIterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator(self::$TEST_TMP_PATH, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($tmpFilesIterator as $path => $fileInfo) { + /* @var \SplFileInfo $fileInfo */ + $pathname = $fileInfo->getPathname(); + $fileInfo->isDir() ? \rmdir($pathname) : \unlink($pathname); + } + } } diff --git a/tests/tests/BuilderTest.php b/tests/tests/BuilderTest.php index 11d6210af..4c3bb4e02 100644 --- a/tests/tests/BuilderTest.php +++ b/tests/tests/BuilderTest.php @@ -10,6 +10,7 @@ namespace SebastianBergmann\CodeCoverage\Report; use SebastianBergmann\CodeCoverage\Node\Builder; +use SebastianBergmann\CodeCoverage\ProcessedCodeCoverageData; use SebastianBergmann\CodeCoverage\TestCase; class BuilderTest extends TestCase @@ -134,7 +135,7 @@ public function testNotCrashParsing(): void $this->assertEquals($expectedPath, $root->getPath()); $this->assertEquals(2, $root->getNumExecutableLines()); $this->assertEquals(0, $root->getNumExecutedLines()); - $data = $coverage->getData(); + $data = $coverage->getData()->getLineCoverage(); $expectedFile = $expectedPath . \DIRECTORY_SEPARATOR . 'Crash.php'; $this->assertSame([$expectedFile => [1 => [], 2 => []]], $data); } @@ -166,11 +167,11 @@ public function testBuildDirectoryStructure(): void ], $method->invoke( $this->factory, - [ + $this->pathsToProcessedDataObjectHelper([ "src{$s}Money.php" => [], "src{$s}MoneyBag.php" => [], "src{$s}Foo{$s}Bar{$s}Baz{$s}Foo.php" => [], - ] + ]) ) ); } @@ -178,7 +179,7 @@ public function testBuildDirectoryStructure(): void /** * @dataProvider reducePathsProvider */ - public function testReducePaths($reducedPaths, $commonPath, $paths): void + public function testReducePaths(array $reducedPaths, string $commonPath, ProcessedCodeCoverageData $paths): void { $method = new \ReflectionMethod( Builder::class, @@ -187,9 +188,9 @@ public function testReducePaths($reducedPaths, $commonPath, $paths): void $method->setAccessible(true); - $_commonPath = $method->invokeArgs($this->factory, [&$paths]); + $_commonPath = $method->invokeArgs($this->factory, [$paths]); - $this->assertEquals($reducedPaths, $paths); + $this->assertEquals($reducedPaths, $paths->getLineCoverage()); $this->assertEquals($commonPath, $_commonPath); } @@ -200,7 +201,7 @@ public function reducePathsProvider() yield [ [], '.', - [], + $this->pathsToProcessedDataObjectHelper([]), ]; $prefixes = ["C:$s", "$s"]; @@ -211,9 +212,9 @@ public function reducePathsProvider() 'Money.php' => [], ], "{$p}home{$s}sb{$s}Money{$s}", - [ + $this->pathsToProcessedDataObjectHelper([ "{$p}home{$s}sb{$s}Money{$s}Money.php" => [], - ], + ]), ]; yield [ @@ -222,10 +223,10 @@ public function reducePathsProvider() 'MoneyBag.php' => [], ], "{$p}home{$s}sb{$s}Money", - [ + $this->pathsToProcessedDataObjectHelper([ "{$p}home{$s}sb{$s}Money{$s}Money.php" => [], "{$p}home{$s}sb{$s}Money{$s}MoneyBag.php" => [], - ], + ]), ]; yield [ @@ -235,12 +236,20 @@ public function reducePathsProvider() "Cash.phar{$s}Cash.php" => [], ], "{$p}home{$s}sb{$s}Money", - [ + $this->pathsToProcessedDataObjectHelper([ "{$p}home{$s}sb{$s}Money{$s}Money.php" => [], "{$p}home{$s}sb{$s}Money{$s}MoneyBag.php" => [], "phar://{$p}home{$s}sb{$s}Money{$s}Cash.phar{$s}Cash.php" => [], - ], + ]), ]; } } + + private function pathsToProcessedDataObjectHelper(array $paths): ProcessedCodeCoverageData + { + $coverage = new ProcessedCodeCoverageData(); + $coverage->setLineCoverage($paths); + + return $coverage; + } } diff --git a/tests/tests/CodeCoverageTest.php b/tests/tests/CodeCoverageTest.php index 3b7c1ae0d..da5944cb3 100644 --- a/tests/tests/CodeCoverageTest.php +++ b/tests/tests/CodeCoverageTest.php @@ -53,7 +53,7 @@ public function testCollect(): void $this->assertEquals( $this->getExpectedDataArrayForBankAccount(), - $coverage->getData() + $coverage->getData()->getLineCoverage() ); $this->assertEquals( @@ -67,6 +67,26 @@ public function testCollect(): void ); } + public function testWhitelistFiltering(): void + { + $this->coverage->filter()->addFileToWhitelist(TEST_FILES_PATH . 'BankAccount.php'); + + $data = new RawCodeCoverageData([ + TEST_FILES_PATH . 'BankAccount.php' => [ + 29 => -1, + 31 => -1, + ], + TEST_FILES_PATH . 'CoverageClassTest.php' => [ + 29 => -1, + 31 => -1, + ], + ]); + + $this->coverage->append($data, 'A test', true); + $this->assertContains(TEST_FILES_PATH . 'BankAccount.php', $this->coverage->getData()->getCoveredFiles()); + $this->assertNotContains(TEST_FILES_PATH . 'CoverageClassTest.php', $this->coverage->getData()->getCoveredFiles()); + } + public function testMerge(): void { $coverage = $this->getCoverageForBankAccountForFirstTwoTests(); @@ -74,7 +94,7 @@ public function testMerge(): void $this->assertEquals( $this->getExpectedDataArrayForBankAccount(), - $coverage->getData() + $coverage->getData()->getLineCoverage() ); } @@ -85,7 +105,7 @@ public function testMergeReverseOrder(): void $this->assertEquals( $this->getExpectedDataArrayForBankAccountInReverseOrder(), - $coverage->getData() + $coverage->getData()->getLineCoverage() ); } @@ -100,7 +120,7 @@ public function testMerge2(): void $this->assertEquals( $this->getExpectedDataArrayForBankAccount(), - $coverage->getData() + $coverage->getData()->getLineCoverage() ); } diff --git a/tests/tests/HTMLTest.php b/tests/tests/HTMLTest.php index f93cf8867..0c7e91ce6 100644 --- a/tests/tests/HTMLTest.php +++ b/tests/tests/HTMLTest.php @@ -26,16 +26,7 @@ protected function tearDown(): void { parent::tearDown(); - $tmpFilesIterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator(self::$TEST_TMP_PATH, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($tmpFilesIterator as $path => $fileInfo) { - /* @var \SplFileInfo $fileInfo */ - $pathname = $fileInfo->getPathname(); - $fileInfo->isDir() ? \rmdir($pathname) : \unlink($pathname); - } + $this->removeTemporaryFiles(); } public function testForBankAccountTest(): void diff --git a/tests/tests/ProcessedCodeCoverageDataTest.php b/tests/tests/ProcessedCodeCoverageDataTest.php new file mode 100644 index 000000000..f383fb922 --- /dev/null +++ b/tests/tests/ProcessedCodeCoverageDataTest.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +class ProcessedCodeCoverageDataTest extends TestCase +{ + public function testMerge(): void + { + $coverage = $this->getCoverageForBankAccountForFirstTwoTests()->getData(); + $coverage->merge($this->getCoverageForBankAccountForLastTwoTests()->getData()); + + $this->assertEquals( + $this->getExpectedDataArrayForBankAccount(), + $coverage->getLineCoverage() + ); + } + + public function testMergeOfAPreviouslyUnseenLine(): void + { + $newCoverage = new ProcessedCodeCoverageData(); + $newCoverage->setLineCoverage( + [ + '/some/path/SomeClass.php' => [ + 12 => [], + 34 => null, + ], + ] + ); + + $existingCoverage = new ProcessedCodeCoverageData(); + $existingCoverage->merge($newCoverage); + $this->assertArrayHasKey(12, $existingCoverage->getLineCoverage()['/some/path/SomeClass.php']); + } +} From 4dcffe30bad02859ef284ea627ebb59c2bb90895 Mon Sep 17 00:00:00 2001 From: Doug Wright Date: Thu, 14 May 2020 20:56:19 +0100 Subject: [PATCH 15/24] Use named constructors as requested --- src/CodeCoverage.php | 4 +- src/Driver/PCOV.php | 2 +- src/Driver/PHPDBG.php | 2 +- src/Driver/Xdebug.php | 2 +- .../UnknownCoverageDataFormatException.php | 26 ------------ src/RawCodeCoverageData.php | 26 +++++++----- tests/TestCase.php | 14 +++---- tests/tests/CodeCoverageTest.php | 8 ++-- tests/tests/RawCodeCoverageDataTest.php | 40 ++++--------------- 9 files changed, 40 insertions(+), 84 deletions(-) delete mode 100644 src/Exception/UnknownCoverageDataFormatException.php diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index e7dcaccef..9c26e729b 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -517,7 +517,7 @@ private function addUncoveredFilesFromWhitelist(): void } } - $this->append(new RawCodeCoverageData($data), 'UNCOVERED_FILES_FROM_WHITELIST'); + $this->append(RawCodeCoverageData::fromXdebugWithoutPathCoverage($data), 'UNCOVERED_FILES_FROM_WHITELIST'); } private function getLinesToBeIgnored(string $fileName): array @@ -887,7 +887,7 @@ private function initializeData(): void $data[$file] = $fileCoverage; } - $this->append(new RawCodeCoverageData($data), 'UNCOVERED_FILES_FROM_WHITELIST'); + $this->append(RawCodeCoverageData::fromXdebugWithoutPathCoverage($data), 'UNCOVERED_FILES_FROM_WHITELIST'); } } diff --git a/src/Driver/PCOV.php b/src/Driver/PCOV.php index af830c34c..379625494 100644 --- a/src/Driver/PCOV.php +++ b/src/Driver/PCOV.php @@ -46,6 +46,6 @@ public function stop(): RawCodeCoverageData \pcov\clear(); - return new RawCodeCoverageData($collect); + return RawCodeCoverageData::fromXdebugWithoutPathCoverage($collect); } } diff --git a/src/Driver/PHPDBG.php b/src/Driver/PHPDBG.php index 57871d6cb..126e846e7 100644 --- a/src/Driver/PHPDBG.php +++ b/src/Driver/PHPDBG.php @@ -72,7 +72,7 @@ public function stop(): RawCodeCoverageData $fetchedLines = \array_merge($fetchedLines, $sourceLines); - return new RawCodeCoverageData($this->detectExecutedLines($fetchedLines, $dbgData)); + return RawCodeCoverageData::fromXdebugWithoutPathCoverage($this->detectExecutedLines($fetchedLines, $dbgData)); } /** diff --git a/src/Driver/Xdebug.php b/src/Driver/Xdebug.php index 1264b21eb..672ce0e70 100644 --- a/src/Driver/Xdebug.php +++ b/src/Driver/Xdebug.php @@ -55,6 +55,6 @@ public function stop(): RawCodeCoverageData \xdebug_stop_code_coverage(); - return new RawCodeCoverageData($data); + return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data); } } diff --git a/src/Exception/UnknownCoverageDataFormatException.php b/src/Exception/UnknownCoverageDataFormatException.php deleted file mode 100644 index f8cd7dc5a..000000000 --- a/src/Exception/UnknownCoverageDataFormatException.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace SebastianBergmann\CodeCoverage; - -/** - * Exception that is raised when a driver supplies coverage data in a format that cannot be handled. - */ -final class UnknownCoverageDataFormatException extends RuntimeException -{ - public static function create(string $filename): self - { - return new self( - \sprintf( - 'Coverage data for file "%s" must be in Xdebug-compatible format, see https://xdebug.org/docs/code_coverage', - $filename - ) - ); - } -} diff --git a/src/RawCodeCoverageData.php b/src/RawCodeCoverageData.php index 6f3af62a8..bd2ab653d 100644 --- a/src/RawCodeCoverageData.php +++ b/src/RawCodeCoverageData.php @@ -21,19 +21,25 @@ final class RawCodeCoverageData */ private $lineCoverage = []; - public function __construct(array $rawCoverage = []) + public static function fromXdebugWithoutPathCoverage(array $rawCoverage): self { - foreach ($rawCoverage as $file => $fileCoverageData) { - $hasOnlyIntegerKeys = \count(\array_filter(\array_keys($fileCoverageData), 'is_int')) === \count($fileCoverageData); + return new self($rawCoverage); + } - if ($hasOnlyIntegerKeys) { - $this->lineCoverage[$file] = $fileCoverageData; - } elseif (\count($fileCoverageData) === 2 && isset($fileCoverageData['lines'], $fileCoverageData['functions'])) { - $this->lineCoverage[$file] = $fileCoverageData['lines']; - } else { - throw UnknownCoverageDataFormatException::create($file); - } + public static function fromXdebugWithPathCoverage(array $rawCoverage): self + { + $lineCoverage = []; + + foreach ($rawCoverage as $file => $fileCoverageData) { + $lineCoverage[$file] = $fileCoverageData['lines']; } + + return new self($lineCoverage); + } + + private function __construct(array $lineCoverage) + { + $this->lineCoverage = $lineCoverage; } public function clear(): void diff --git a/tests/TestCase.php b/tests/TestCase.php index 0120f2e71..c6c68df59 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -23,7 +23,7 @@ public static function setUpBeforeClass(): void protected function getXdebugDataForBankAccount() { return [ - new RawCodeCoverageData([ + RawCodeCoverageData::fromXdebugWithoutPathCoverage([ TEST_FILES_PATH . 'BankAccount.php' => [ 8 => 1, 9 => -2, @@ -40,7 +40,7 @@ protected function getXdebugDataForBankAccount() 32 => -2, ], ]), - new RawCodeCoverageData([ + RawCodeCoverageData::fromXdebugWithoutPathCoverage([ TEST_FILES_PATH . 'BankAccount.php' => [ 8 => 1, 13 => 1, @@ -48,7 +48,7 @@ protected function getXdebugDataForBankAccount() 29 => 1, ], ]), - new RawCodeCoverageData([ + RawCodeCoverageData::fromXdebugWithoutPathCoverage([ TEST_FILES_PATH . 'BankAccount.php' => [ 8 => 1, 13 => 1, @@ -56,7 +56,7 @@ protected function getXdebugDataForBankAccount() 22 => 1, ], ]), - new RawCodeCoverageData([ + RawCodeCoverageData::fromXdebugWithoutPathCoverage([ TEST_FILES_PATH . 'BankAccount.php' => [ 8 => 1, 13 => 1, @@ -314,7 +314,7 @@ protected function setUpXdebugStubForFileWithIgnoredLines(): Driver $stub->expects($this->any()) ->method('stop') ->will($this->returnValue( - new RawCodeCoverageData( + RawCodeCoverageData::fromXdebugWithoutPathCoverage( [ TEST_FILES_PATH . 'source_with_ignore.php' => [ 2 => 1, @@ -352,7 +352,7 @@ protected function setUpXdebugStubForClassWithAnonymousFunction(): Driver $stub->expects($this->any()) ->method('stop') ->will($this->returnValue( - new RawCodeCoverageData( + RawCodeCoverageData::fromXdebugWithoutPathCoverage( [ TEST_FILES_PATH . 'source_with_class_and_anonymous_function.php' => [ 7 => 1, @@ -390,7 +390,7 @@ protected function setUpXdebugStubForCrashParsing(): Driver $stub->expects($this->any()) ->method('stop') - ->will($this->returnValue(new RawCodeCoverageData([]))); + ->will($this->returnValue(RawCodeCoverageData::fromXdebugWithoutPathCoverage([]))); return $stub; } diff --git a/tests/tests/CodeCoverageTest.php b/tests/tests/CodeCoverageTest.php index da5944cb3..5574d360d 100644 --- a/tests/tests/CodeCoverageTest.php +++ b/tests/tests/CodeCoverageTest.php @@ -44,7 +44,7 @@ public function testCannotAppendWithInvalidArgument(): void { $this->expectException(Exception::class); - $this->coverage->append(new RawCodeCoverageData([]), null); + $this->coverage->append(RawCodeCoverageData::fromXdebugWithoutPathCoverage([]), null); } public function testCollect(): void @@ -71,7 +71,7 @@ public function testWhitelistFiltering(): void { $this->coverage->filter()->addFileToWhitelist(TEST_FILES_PATH . 'BankAccount.php'); - $data = new RawCodeCoverageData([ + $data = RawCodeCoverageData::fromXdebugWithoutPathCoverage([ TEST_FILES_PATH . 'BankAccount.php' => [ 29 => -1, 31 => -1, @@ -310,7 +310,7 @@ public function testAppendThrowsExceptionIfCoveredCodeWasNotExecuted(): void $this->coverage->filter()->addDirectoryToWhitelist(TEST_FILES_PATH); $this->coverage->setCheckForUnexecutedCoveredCode(true); - $data = new RawCodeCoverageData([ + $data = RawCodeCoverageData::fromXdebugWithoutPathCoverage([ TEST_FILES_PATH . 'BankAccount.php' => [ 29 => -1, 31 => -1, @@ -336,7 +336,7 @@ public function testAppendThrowsExceptionIfUsedCodeWasNotExecuted(): void $this->coverage->filter()->addDirectoryToWhitelist(TEST_FILES_PATH); $this->coverage->setCheckForUnexecutedCoveredCode(true); - $data = new RawCodeCoverageData([ + $data = RawCodeCoverageData::fromXdebugWithoutPathCoverage([ TEST_FILES_PATH . 'BankAccount.php' => [ 29 => -1, 31 => -1, diff --git a/tests/tests/RawCodeCoverageDataTest.php b/tests/tests/RawCodeCoverageDataTest.php index d21d52e50..8f7dfef5e 100644 --- a/tests/tests/RawCodeCoverageDataTest.php +++ b/tests/tests/RawCodeCoverageDataTest.php @@ -24,14 +24,14 @@ public function testLineDataFromStandardXDebugFormat(): void ], ]; - $dataObject = new RawCodeCoverageData($lineDataFromDriver); + $dataObject = RawCodeCoverageData::fromXdebugWithoutPathCoverage($lineDataFromDriver); $this->assertEquals($lineDataFromDriver, $dataObject->getLineCoverage()); } /** - * In the branch-check XDebug format, the line data exists inside a "lines" array key. + * In the path-coverage XDebug format, the line data exists inside a "lines" array key. */ - public function testLineDataFromBranchCheckXDebugFormat(): void + public function testLineDataFromPathCoverageXDebugFormat(): void { $rawDataFromDriver = [ '/some/path/SomeClass.php' => [ @@ -54,34 +54,10 @@ public function testLineDataFromBranchCheckXDebugFormat(): void ], ]; - $dataObject = new RawCodeCoverageData($rawDataFromDriver); + $dataObject = RawCodeCoverageData::fromXdebugWithPathCoverage($rawDataFromDriver); $this->assertEquals($lineData, $dataObject->getLineCoverage()); } - /** - * Coverage data that does not match a known format should throw an exception. - */ - public function testDataFromUnknownFormat(): void - { - $this->expectException(UnknownCoverageDataFormatException::class); - - $lineDataFromDriver = [ - '/some/path/SomeClass.php' => [ - 'executedLines' => [ - 8, - ], - 'unExecutedLines' => [ - 13, - ], - 'nonExecutableLines' => [ - 9, - ], - ], - ]; - - $dataObject = new RawCodeCoverageData($lineDataFromDriver); - } - public function testClear(): void { $lineDataFromDriver = [ @@ -92,7 +68,7 @@ public function testClear(): void ], ]; - $dataObject = new RawCodeCoverageData($lineDataFromDriver); + $dataObject = RawCodeCoverageData::fromXdebugWithoutPathCoverage($lineDataFromDriver); $dataObject->clear(); $this->assertEmpty($dataObject->getLineCoverage()); } @@ -130,7 +106,7 @@ public function testRemoveCoverageDataForFile(): void ], ]; - $dataObject = new RawCodeCoverageData($lineDataFromDriver); + $dataObject = RawCodeCoverageData::fromXdebugWithoutPathCoverage($lineDataFromDriver); $dataObject->removeCoverageDataForFile('/some/path/SomeOtherClass.php'); $this->assertEquals($expectedFilterResult, $dataObject->getLineCoverage()); } @@ -167,7 +143,7 @@ public function testKeepCoverageDataOnlyForLines(): void ], ]; - $dataObject = new RawCodeCoverageData($lineDataFromDriver); + $dataObject = RawCodeCoverageData::fromXdebugWithoutPathCoverage($lineDataFromDriver); $dataObject->keepCoverageDataOnlyForLines('/some/path/SomeClass.php', [9, 13]); $dataObject->keepCoverageDataOnlyForLines('/some/path/SomeOtherClass.php', [999]); $dataObject->keepCoverageDataOnlyForLines('/some/path/AnotherClass.php', [28]); @@ -209,7 +185,7 @@ public function testRemoveCoverageDataForLines(): void ], ]; - $dataObject = new RawCodeCoverageData($lineDataFromDriver); + $dataObject = RawCodeCoverageData::fromXdebugWithoutPathCoverage($lineDataFromDriver); $dataObject->removeCoverageDataForLines('/some/path/SomeClass.php', [9, 13]); $dataObject->removeCoverageDataForLines('/some/path/SomeOtherClass.php', [999]); $dataObject->removeCoverageDataForLines('/some/path/AnotherClass.php', [28]); From 68764da0529274f0d0e879cab1841d2bc78e1bca Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 16 May 2020 07:14:56 +0200 Subject: [PATCH 16/24] Make test class final --- tests/tests/PHPTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests/PHPTest.php b/tests/tests/PHPTest.php index 204cd1f27..c11342069 100644 --- a/tests/tests/PHPTest.php +++ b/tests/tests/PHPTest.php @@ -11,7 +11,7 @@ use SebastianBergmann\CodeCoverage\TestCase; -class PHPTest extends TestCase +final class PHPTest extends TestCase { protected function tearDown(): void { From 20d002150e674de4a6698a4abf3c31256ba6b2a9 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 16 May 2020 07:15:22 +0200 Subject: [PATCH 17/24] Fix typo --- tests/tests/PHPTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/tests/PHPTest.php b/tests/tests/PHPTest.php index c11342069..4501593a8 100644 --- a/tests/tests/PHPTest.php +++ b/tests/tests/PHPTest.php @@ -24,10 +24,10 @@ public function testPHPSerialisationProducesValidCode(): void { $coverage = $this->getCoverageForBankAccount(); - (new PHP())->process($coverage, self::$TEST_TMP_PATH . '/serialised.php'); + (new PHP())->process($coverage, self::$TEST_TMP_PATH . '/serialized.php'); - $unserialised = require self::$TEST_TMP_PATH . '/serialised.php'; + $unserialized = require self::$TEST_TMP_PATH . '/serialized.php'; - $this->assertEquals($coverage, $unserialised); + $this->assertEquals($coverage, $unserialized); } } From e2d8b8527cff94e72faee8d344c69fe7bd4bce44 Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 16 May 2020 07:15:51 +0200 Subject: [PATCH 18/24] Suppress PhpStorm's unused function result inspection --- tests/tests/PHPTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/tests/PHPTest.php b/tests/tests/PHPTest.php index 4501593a8..2f05e7110 100644 --- a/tests/tests/PHPTest.php +++ b/tests/tests/PHPTest.php @@ -24,6 +24,7 @@ public function testPHPSerialisationProducesValidCode(): void { $coverage = $this->getCoverageForBankAccount(); + /** @noinspection UnusedFunctionResultInspection */ (new PHP())->process($coverage, self::$TEST_TMP_PATH . '/serialized.php'); $unserialized = require self::$TEST_TMP_PATH . '/serialized.php'; From 0e37923c41017f5bcd207012c8eb109874a1647f Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 16 May 2020 07:16:35 +0200 Subject: [PATCH 19/24] Remove superfluous braces --- src/CodeCoverage.php | 2 +- tests/tests/BuilderTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index 9c26e729b..93de90665 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -151,7 +151,7 @@ public function __construct(Driver $driver = null, Filter $filter = null) $this->filter = $filter; $this->wizard = new Wizard; - $this->data = new ProcessedCodeCoverageData(); + $this->data = new ProcessedCodeCoverageData; } /** diff --git a/tests/tests/BuilderTest.php b/tests/tests/BuilderTest.php index 4c3bb4e02..0f7130f16 100644 --- a/tests/tests/BuilderTest.php +++ b/tests/tests/BuilderTest.php @@ -247,7 +247,7 @@ public function reducePathsProvider() private function pathsToProcessedDataObjectHelper(array $paths): ProcessedCodeCoverageData { - $coverage = new ProcessedCodeCoverageData(); + $coverage = new ProcessedCodeCoverageData; $coverage->setLineCoverage($paths); return $coverage; From 76e16f33168f5b721652f848b67310bb5fe6d73c Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Sat, 16 May 2020 07:20:15 +0200 Subject: [PATCH 20/24] Fix CS/WS issue introduced in e2d8b8527cff94e72faee8d344c69fe7bd4bce44 --- tests/tests/PHPTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests/PHPTest.php b/tests/tests/PHPTest.php index 2f05e7110..14ce3a9f9 100644 --- a/tests/tests/PHPTest.php +++ b/tests/tests/PHPTest.php @@ -24,7 +24,7 @@ public function testPHPSerialisationProducesValidCode(): void { $coverage = $this->getCoverageForBankAccount(); - /** @noinspection UnusedFunctionResultInspection */ + /* @noinspection UnusedFunctionResultInspection */ (new PHP())->process($coverage, self::$TEST_TMP_PATH . '/serialized.php'); $unserialized = require self::$TEST_TMP_PATH . '/serialized.php'; From a707e742879609e10fb4f0a4cf6374d01368f3e1 Mon Sep 17 00:00:00 2001 From: Doug Wright Date: Tue, 12 May 2020 02:05:51 +0100 Subject: [PATCH 21/24] Improve consistency in handling of namespaces --- src/Node/File.php | 4 ++++ src/Report/Text.php | 12 +++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Node/File.php b/src/Node/File.php index 5268edc5a..e6e87ba53 100644 --- a/src/Node/File.php +++ b/src/Node/File.php @@ -518,6 +518,10 @@ private function processTraits(\PHP_Token_Stream $tokens): void $link = $this->getId() . '.html#'; foreach ($traits as $traitName => $trait) { + if (!empty($trait['package']['namespace'])) { + $traitName = $trait['package']['namespace'] . '\\' . $traitName; + } + $this->traits[$traitName] = [ 'traitName' => $traitName, 'methods' => [], diff --git a/src/Report/Text.php b/src/Report/Text.php index 1cbbe2ab5..413a3f32d 100644 --- a/src/Report/Text.php +++ b/src/Report/Text.php @@ -200,16 +200,14 @@ public function process(CodeCoverage $coverage, bool $showColors = false): strin } } - $namespace = ''; + $package = ''; - if (!empty($class['package']['namespace'])) { - $namespace = '\\' . $class['package']['namespace'] . '::'; - } elseif (!empty($class['package']['fullPackage'])) { - $namespace = '@' . $class['package']['fullPackage'] . '::'; + if (!empty($class['package']['fullPackage'])) { + $package = '@' . $class['package']['fullPackage'] . '::'; } - $classCoverage[$namespace . $className] = [ - 'namespace' => $namespace, + $classCoverage[$package . $className] = [ + 'namespace' => $class['package']['namespace'], 'className ' => $className, 'methodsCovered' => $coveredMethods, 'methodCount' => $classMethods, From 227fa31f662b800e8b19440cd48e58d2459ec5ab Mon Sep 17 00:00:00 2001 From: Doug Wright Date: Mon, 18 May 2020 20:49:24 +0100 Subject: [PATCH 22/24] Add additional tests for text report --- tests/TestCase.php | 118 ++++++++++++++++++++ tests/_files/BankAccount-text-summary.txt | 7 ++ tests/_files/NamespacedBankAccount-text.txt | 14 +++ tests/_files/NamespacedBankAccount.php | 39 +++++++ tests/tests/FilterTest.php | 1 + tests/tests/TextTest.php | 20 ++++ 6 files changed, 199 insertions(+) create mode 100644 tests/_files/BankAccount-text-summary.txt create mode 100644 tests/_files/NamespacedBankAccount-text.txt create mode 100644 tests/_files/NamespacedBankAccount.php diff --git a/tests/TestCase.php b/tests/TestCase.php index c6c68df59..34300abea 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -138,6 +138,124 @@ protected function getCoverageForBankAccount(): CodeCoverage return $coverage; } + protected function getXdebugDataForNamespacedBankAccount() + { + return [ + RawCodeCoverageData::fromXdebugWithoutPathCoverage([ + TEST_FILES_PATH . 'NamespacedBankAccount.php' => [ + 14 => 1, + 15 => -2, + 19 => -1, + 20 => -1, + 21 => -1, + 22 => -1, + 24 => -1, + 28 => -1, + 30 => -1, + 31 => -2, + 35 => -1, + 37 => -1, + 38 => -2, + ], + ]), + RawCodeCoverageData::fromXdebugWithoutPathCoverage([ + TEST_FILES_PATH . 'NamespacedBankAccount.php' => [ + 14 => 1, + 19 => 1, + 22 => 1, + 35 => 1, + ], + ]), + RawCodeCoverageData::fromXdebugWithoutPathCoverage([ + TEST_FILES_PATH . 'NamespacedBankAccount.php' => [ + 14 => 1, + 19 => 1, + 22 => 1, + 28 => 1, + ], + ]), + RawCodeCoverageData::fromXdebugWithoutPathCoverage([ + TEST_FILES_PATH . 'NamespacedBankAccount.php' => [ + 14 => 1, + 19 => 1, + 20 => 1, + 21 => 1, + 24 => 1, + 28 => 1, + 30 => 1, + 35 => 1, + 37 => 1, + ], + ]), + ]; + } + + protected function getCoverageForNamespacedBankAccount(): CodeCoverage + { + $data = $this->getXdebugDataForNamespacedBankAccount(); + + $stub = $this->createMock(Driver::class); + + $stub->expects($this->any()) + ->method('stop') + ->will($this->onConsecutiveCalls( + $data[0], + $data[1], + $data[2], + $data[3] + )); + + $filter = new Filter; + $filter->addFileToWhitelist(TEST_FILES_PATH . 'NamespacedBankAccount.php'); + + $coverage = new CodeCoverage($stub, $filter); + + $coverage->start( + new \BankAccountTest('testBalanceIsInitiallyZero'), + true + ); + + $coverage->stop( + true, + [TEST_FILES_PATH . 'NamespacedBankAccount.php' => \range(12, 15)] + ); + + $coverage->start( + new \BankAccountTest('testBalanceCannotBecomeNegative') + ); + + $coverage->stop( + true, + [TEST_FILES_PATH . 'NamespacedBankAccount.php' => \range(33, 38)] + ); + + $coverage->start( + new \BankAccountTest('testBalanceCannotBecomeNegative2') + ); + + $coverage->stop( + true, + [TEST_FILES_PATH . 'NamespacedBankAccount.php' => \range(26, 31)] + ); + + $coverage->start( + new \BankAccountTest('testDepositWithdrawMoney') + ); + + $coverage->stop( + true, + [ + TEST_FILES_PATH . 'NamespacedBankAccount.php' => \array_merge( + \range(12, 15), + \range(26, 31), + \range(33, 38) + ), + ] + ); + + return $coverage; + } + protected function getCoverageForBankAccountForFirstTwoTests(): CodeCoverage { $data = $this->getXdebugDataForBankAccount(); diff --git a/tests/_files/BankAccount-text-summary.txt b/tests/_files/BankAccount-text-summary.txt new file mode 100644 index 000000000..c0fb9cc7d --- /dev/null +++ b/tests/_files/BankAccount-text-summary.txt @@ -0,0 +1,7 @@ + + +Code Coverage Report Summary: + Classes: 0.00% (0/1) + Methods: 75.00% (3/4) + Lines: 50.00% (5/10) + diff --git a/tests/_files/NamespacedBankAccount-text.txt b/tests/_files/NamespacedBankAccount-text.txt new file mode 100644 index 000000000..fa153bd8f --- /dev/null +++ b/tests/_files/NamespacedBankAccount-text.txt @@ -0,0 +1,14 @@ + + +Code Coverage Report: + %s + + Summary: + Classes: 0.00% (0/1) + Methods: 75.00% (3/4) + Lines: 50.00% (5/10) + +@OldStylePackageName::SomeNamespace\BankAccount + Methods: ( 0/ 0) Lines: ( 0/ 0) +SomeNamespace\BankAccountTrait + Methods: 75.00% ( 3/ 4) Lines: 50.00% ( 5/ 10) diff --git a/tests/_files/NamespacedBankAccount.php b/tests/_files/NamespacedBankAccount.php new file mode 100644 index 000000000..9a14e2421 --- /dev/null +++ b/tests/_files/NamespacedBankAccount.php @@ -0,0 +1,39 @@ +balance; + } + + protected function setBalance($balance) + { + if ($balance >= 0) { + $this->balance = $balance; + } else { + throw new \RuntimeException; + } + } + + public function depositMoney($balance) + { + $this->setBalance($this->getBalance() + $balance); + + return $this->getBalance(); + } + + public function withdrawMoney($balance) + { + $this->setBalance($this->getBalance() - $balance); + + return $this->getBalance(); + } +} diff --git a/tests/tests/FilterTest.php b/tests/tests/FilterTest.php index 807adbb80..ed8e55af3 100644 --- a/tests/tests/FilterTest.php +++ b/tests/tests/FilterTest.php @@ -64,6 +64,7 @@ protected function setUp(): void TEST_FILES_PATH . 'NamespaceCoverageProtectedTest.php', TEST_FILES_PATH . 'NamespaceCoveragePublicTest.php', TEST_FILES_PATH . 'NamespaceCoveredClass.php', + TEST_FILES_PATH . 'NamespacedBankAccount.php', TEST_FILES_PATH . 'NotExistingCoveredElementTest.php', TEST_FILES_PATH . 'source_with_class_and_anonymous_function.php', TEST_FILES_PATH . 'source_with_ignore.php', diff --git a/tests/tests/TextTest.php b/tests/tests/TextTest.php index ce0846fa7..f3b8d5c2b 100644 --- a/tests/tests/TextTest.php +++ b/tests/tests/TextTest.php @@ -26,6 +26,26 @@ public function testTextForBankAccountTest(): void ); } + public function testTextOnlySummaryForBankAccountTest(): void + { + $text = new Text(50, 90, false, true); + + $this->assertStringMatchesFormatFile( + TEST_FILES_PATH . 'BankAccount-text-summary.txt', + \str_replace(\PHP_EOL, "\n", $text->process($this->getCoverageForBankAccount())) + ); + } + + public function testTextForNamespacedBankAccountTest(): void + { + $text = new Text(50, 90, true, false); + + $this->assertStringMatchesFormatFile( + TEST_FILES_PATH . 'NamespacedBankAccount-text.txt', + \str_replace(\PHP_EOL, "\n", $text->process($this->getCoverageForNamespacedBankAccount())) + ); + } + public function testTextForFileWithIgnoredLines(): void { $text = new Text(50, 90, false, false); From f0d58f70987542ac00cf974cef48ad7b46be932c Mon Sep 17 00:00:00 2001 From: Doug Wright Date: Mon, 18 May 2020 21:01:54 +0100 Subject: [PATCH 23/24] Correct missing highlighting of dead code --- src/Report/Html/Renderer/Template/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Report/Html/Renderer/Template/css/style.css b/src/Report/Html/Renderer/Template/css/style.css index 6d9c21e89..c47011d34 100644 --- a/src/Report/Html/Renderer/Template/css/style.css +++ b/src/Report/Html/Renderer/Template/css/style.css @@ -48,7 +48,7 @@ body { background-color: #f2dede; } -.table tbody td.warning, li.warning, span.warning { +.table tbody tr.warning, .table tbody td.warning, li.warning, span.warning { background-color: #fcf8e3; } From ed411cb43a8ca74ac84a12c19359e3ffbdf1eddb Mon Sep 17 00:00:00 2001 From: Doug Wright Date: Mon, 18 May 2020 21:13:51 +0100 Subject: [PATCH 24/24] Rework how code coverage settings are propagated to the driver --- .psalm/baseline.xml | 2 +- src/CodeCoverage.php | 25 ++++--- src/Driver/Driver.php | 62 ++++++++++++++++- src/Driver/PCOV.php | 20 +++++- src/Driver/PHPDBG.php | 20 +++++- src/Driver/Xdebug.php | 39 +++++++++-- ...chAndPathCoverageNotSupportedException.php | 17 +++++ ...DeadCodeDetectionNotSupportedException.php | 17 +++++ tests/tests/Driver/PCOVTest.php | 67 +++++++++++++++++++ tests/tests/Driver/PHPDBGTest.php | 67 +++++++++++++++++++ tests/tests/Driver/XdebugTest.php | 43 +++++++++++- 11 files changed, 353 insertions(+), 26 deletions(-) create mode 100644 src/Exception/BranchAndPathCoverageNotSupportedException.php create mode 100644 src/Exception/DeadCodeDetectionNotSupportedException.php create mode 100644 tests/tests/Driver/PCOVTest.php create mode 100644 tests/tests/Driver/PHPDBGTest.php diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml index b43b7c5da..68ea3d6e9 100644 --- a/.psalm/baseline.xml +++ b/.psalm/baseline.xml @@ -18,7 +18,7 @@ - + \XDEBUG_FILTER_CODE_COVERAGE \XDEBUG_PATH_WHITELIST \XDEBUG_CC_UNUSED diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index 93de90665..7884e931c 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -122,13 +122,6 @@ final class CodeCoverage */ private $isInitialized = false; - /** - * Determine whether we need to check for dead and unused code on each test - * - * @var bool - */ - private $shouldCheckForDeadAndUnused = true; - /** * @var Directory */ @@ -242,7 +235,7 @@ public function start($id, bool $clear = false): void $this->currentId = $id; - $this->driver->start($this->shouldCheckForDeadAndUnused); + $this->driver->start(); } /** @@ -417,6 +410,11 @@ public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist): $this->unintentionallyCoveredSubclassesWhitelist = $whitelist; } + public function setBranchAndPathCollection(bool $flag): void + { + $this->driver->collectBranchAndPathCoverage($flag); + } + /** * Applies the @covers annotation filtering. * @@ -861,7 +859,11 @@ private function initializeData(): void $this->isInitialized = true; if ($this->processUncoveredFilesFromWhitelist) { - $this->shouldCheckForDeadAndUnused = false; + + // by collecting dead code data here on an initial pass, future runs with test data do not need to + if ($this->driver->canDetectDeadCode()) { + $this->driver->detectDeadCode(true); + } $this->driver->start(); @@ -888,6 +890,11 @@ private function initializeData(): void } $this->append(RawCodeCoverageData::fromXdebugWithoutPathCoverage($data), 'UNCOVERED_FILES_FROM_WHITELIST'); + + // having now collected dead code for the entire whitelist, we can safely skip this data on subsequent runs + if ($this->driver->canDetectDeadCode()) { + $this->driver->detectDeadCode(false); + } } } diff --git a/src/Driver/Driver.php b/src/Driver/Driver.php index aba17b011..284118d59 100644 --- a/src/Driver/Driver.php +++ b/src/Driver/Driver.php @@ -9,12 +9,14 @@ */ namespace SebastianBergmann\CodeCoverage\Driver; +use SebastianBergmann\CodeCoverage\BranchAndPathCoverageNotSupportedException; +use SebastianBergmann\CodeCoverage\DeadCodeDetectionNotSupportedException; use SebastianBergmann\CodeCoverage\RawCodeCoverageData; /** * Interface for code coverage drivers. */ -interface Driver +abstract class Driver { /** * @var int @@ -37,13 +39,67 @@ interface Driver */ public const LINE_NOT_EXECUTABLE = -2; + protected $detectDeadCode = false; + + protected $collectBranchAndPathCoverage = false; + + /** + * Does this driver support detecting dead code? + */ + abstract public function canDetectDeadCode(): bool; + + /** + * Does this driver support collecting branch and path coverage? + */ + abstract public function canCollectBranchAndPathCoverage(): bool; + + /** + * Detect dead code + */ + public function detectDeadCode(bool $flag): void + { + if ($flag && !$this->canDetectDeadCode()) { + throw new DeadCodeDetectionNotSupportedException; + } + + $this->detectDeadCode = $flag; + } + + /** + * Collecting path coverage + */ + public function collectBranchAndPathCoverage(bool $flag): void + { + if ($flag && !$this->canCollectBranchAndPathCoverage()) { + throw new BranchAndPathCoverageNotSupportedException; + } + + $this->collectBranchAndPathCoverage = $flag; + } + + /** + * Is this driver detecting dead code? + */ + public function detectingDeadCode(): bool + { + return $this->detectDeadCode; + } + + /** + * Is this driver collecting branch and path coverage? + */ + public function collectingBranchAndPathCoverage(): bool + { + return $this->collectBranchAndPathCoverage; + } + /** * Start collection of code coverage information. */ - public function start(bool $determineUnusedAndDead = true): void; + abstract public function start(): void; /** * Stop collection of code coverage information. */ - public function stop(): RawCodeCoverageData; + abstract public function stop(): RawCodeCoverageData; } diff --git a/src/Driver/PCOV.php b/src/Driver/PCOV.php index 379625494..5e51e74c8 100644 --- a/src/Driver/PCOV.php +++ b/src/Driver/PCOV.php @@ -15,7 +15,7 @@ /** * Driver for PCOV code coverage functionality. */ -final class PCOV implements Driver +final class PCOV extends Driver { /** * @var Filter @@ -27,10 +27,26 @@ public function __construct(Filter $filter) $this->filter = $filter; } + /** + * Does this driver support detecting dead code? + */ + public function canDetectDeadCode(): bool + { + return false; + } + + /** + * Does this driver support collecting path coverage? + */ + public function canCollectBranchAndPathCoverage(): bool + { + return false; + } + /** * Start collection of code coverage information. */ - public function start(bool $determineUnusedAndDead = true): void + public function start(): void { \pcov\start(); } diff --git a/src/Driver/PHPDBG.php b/src/Driver/PHPDBG.php index 126e846e7..b3481b5e2 100644 --- a/src/Driver/PHPDBG.php +++ b/src/Driver/PHPDBG.php @@ -15,7 +15,7 @@ /** * Driver for PHPDBG's code coverage functionality. */ -final class PHPDBG implements Driver +final class PHPDBG extends Driver { /** * @throws RuntimeException @@ -35,10 +35,26 @@ public function __construct() } } + /** + * Does this driver support detecting dead code? + */ + public function canDetectDeadCode(): bool + { + return false; + } + + /** + * Does this driver support collecting path coverage? + */ + public function canCollectBranchAndPathCoverage(): bool + { + return false; + } + /** * Start collection of code coverage information. */ - public function start(bool $determineUnusedAndDead = true): void + public function start(): void { \phpdbg_start_oplog(); } diff --git a/src/Driver/Xdebug.php b/src/Driver/Xdebug.php index 672ce0e70..e43811cd8 100644 --- a/src/Driver/Xdebug.php +++ b/src/Driver/Xdebug.php @@ -16,7 +16,7 @@ /** * Driver for Xdebug's code coverage functionality. */ -final class Xdebug implements Driver +final class Xdebug extends Driver { /** * @throws RuntimeException @@ -32,18 +32,41 @@ public function __construct(Filter $filter) } \xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_WHITELIST, $filter->getWhitelist()); + $this->detectDeadCode = true; + } + + /** + * Does this driver support detecting dead code? + */ + public function canDetectDeadCode(): bool + { + return true; + } + + /** + * Does this driver support collecting path coverage? + */ + public function canCollectBranchAndPathCoverage(): bool + { + return true; } /** * Start collection of code coverage information. */ - public function start(bool $determineUnusedAndDead = true): void + public function start(): void { - if ($determineUnusedAndDead) { - \xdebug_start_code_coverage(\XDEBUG_CC_UNUSED | \XDEBUG_CC_DEAD_CODE); - } else { - \xdebug_start_code_coverage(); + $flags = \XDEBUG_CC_UNUSED; + + if ($this->detectDeadCode || $this->collectBranchAndPathCoverage) { // branch/path collection requires enabling dead code checks + $flags |= \XDEBUG_CC_DEAD_CODE; + } + + if ($this->collectBranchAndPathCoverage) { + $flags |= \XDEBUG_CC_BRANCH_CHECK; } + + \xdebug_start_code_coverage($flags); } /** @@ -55,6 +78,10 @@ public function stop(): RawCodeCoverageData \xdebug_stop_code_coverage(); + if ($this->collectBranchAndPathCoverage) { + return RawCodeCoverageData::fromXdebugWithPathCoverage($data); + } + return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data); } } diff --git a/src/Exception/BranchAndPathCoverageNotSupportedException.php b/src/Exception/BranchAndPathCoverageNotSupportedException.php new file mode 100644 index 000000000..115914490 --- /dev/null +++ b/src/Exception/BranchAndPathCoverageNotSupportedException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +/** + * Exception that is raised when branch and path coverage is not supported by the driver but is attempted to be used. + */ +final class BranchAndPathCoverageNotSupportedException extends RuntimeException +{ +} diff --git a/src/Exception/DeadCodeDetectionNotSupportedException.php b/src/Exception/DeadCodeDetectionNotSupportedException.php new file mode 100644 index 000000000..4c2462ed3 --- /dev/null +++ b/src/Exception/DeadCodeDetectionNotSupportedException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +/** + * Exception that is raised when dead code detection is not supported by the driver but is attempted to be used. + */ +final class DeadCodeDetectionNotSupportedException extends RuntimeException +{ +} diff --git a/tests/tests/Driver/PCOVTest.php b/tests/tests/Driver/PCOVTest.php new file mode 100644 index 000000000..6c8354dbb --- /dev/null +++ b/tests/tests/Driver/PCOVTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use SebastianBergmann\CodeCoverage\Driver\PCOV; +use SebastianBergmann\Environment\Runtime; + +class PCOVTest extends TestCase +{ + protected function setUp(): void + { + $runtime = new Runtime; + + if (!$runtime->hasPCOV()) { + $this->markTestSkipped('This test is only applicable to PCOV'); + } + } + + public function testDefaultValueOfDeadCodeDetection(): void + { + $driver = new PCOV(new Filter()); + + $this->assertFalse($driver->detectingDeadCode()); + } + + public function testEnablingDeadCodeDetection(): void + { + $this->expectException(DeadCodeDetectionNotSupportedException::class); + + $driver = new PCOV(new Filter()); + + $driver->detectDeadCode(true); + } + + public function testDisablingDeadCodeDetection(): void + { + $driver = new PCOV(new Filter()); + + $driver->detectDeadCode(false); + $this->assertFalse($driver->detectingDeadCode()); + } + + public function testEnablingBranchAndPathCoverage(): void + { + $this->expectException(BranchAndPathCoverageNotSupportedException::class); + + $driver = new PCOV(new Filter()); + + $driver->collectBranchAndPathCoverage(true); + $this->assertTrue($driver->collectingBranchAndPathCoverage()); + } + + public function testDisablingBranchAndPathCoverage(): void + { + $driver = new PCOV(new Filter()); + + $driver->collectBranchAndPathCoverage(false); + $this->assertFalse($driver->collectingBranchAndPathCoverage()); + } +} diff --git a/tests/tests/Driver/PHPDBGTest.php b/tests/tests/Driver/PHPDBGTest.php new file mode 100644 index 000000000..f76db0b38 --- /dev/null +++ b/tests/tests/Driver/PHPDBGTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace SebastianBergmann\CodeCoverage; + +use SebastianBergmann\CodeCoverage\Driver\PHPDBG; +use SebastianBergmann\Environment\Runtime; + +class PHPDBGTest extends TestCase +{ + protected function setUp(): void + { + $runtime = new Runtime; + + if (!$runtime->hasPHPDBGCodeCoverage()) { + $this->markTestSkipped('This test is only applicable to PHPDBG'); + } + } + + public function testDefaultValueOfDeadCodeDetection(): void + { + $driver = new PHPDBG(); + + $this->assertFalse($driver->detectingDeadCode()); + } + + public function testEnablingDeadCodeDetection(): void + { + $this->expectException(DeadCodeDetectionNotSupportedException::class); + + $driver = new PHPDBG(); + + $driver->detectDeadCode(true); + } + + public function testDisablingDeadCodeDetection(): void + { + $driver = new PHPDBG(); + + $driver->detectDeadCode(false); + $this->assertFalse($driver->detectingDeadCode()); + } + + public function testEnablingBranchAndPathCoverage(): void + { + $this->expectException(BranchAndPathCoverageNotSupportedException::class); + + $driver = new PHPDBG(); + + $driver->collectBranchAndPathCoverage(true); + $this->assertTrue($driver->collectingBranchAndPathCoverage()); + } + + public function testDisablingBranchAndPathCoverage(): void + { + $driver = new PHPDBG(); + + $driver->collectBranchAndPathCoverage(false); + $this->assertFalse($driver->collectingBranchAndPathCoverage()); + } +} diff --git a/tests/tests/Driver/XdebugTest.php b/tests/tests/Driver/XdebugTest.php index d21953524..c30d595ca 100644 --- a/tests/tests/Driver/XdebugTest.php +++ b/tests/tests/Driver/XdebugTest.php @@ -9,11 +9,9 @@ */ namespace SebastianBergmann\CodeCoverage; +use SebastianBergmann\CodeCoverage\Driver\Xdebug; use SebastianBergmann\Environment\Runtime; -/** - * @covers SebastianBergmann\CodeCoverage\Driver\Xdebug - */ class XdebugTest extends TestCase { protected function setUp(): void @@ -36,4 +34,43 @@ public function testFilterWorks(): void require $bankAccount; $this->assertArrayNotHasKey($bankAccount, \xdebug_get_code_coverage()); } + + public function testDefaultValueOfDeadCodeDetection(): void + { + $driver = new Xdebug(new Filter()); + + $this->assertTrue($driver->detectingDeadCode()); + } + + public function testEnablingDeadCodeDetection(): void + { + $driver = new Xdebug(new Filter()); + + $driver->detectDeadCode(true); + $this->assertTrue($driver->detectingDeadCode()); + } + + public function testDisablingDeadCodeDetection(): void + { + $driver = new Xdebug(new Filter()); + + $driver->detectDeadCode(false); + $this->assertFalse($driver->detectingDeadCode()); + } + + public function testEnablingBranchAndPathCoverage(): void + { + $driver = new Xdebug(new Filter()); + + $driver->collectBranchAndPathCoverage(true); + $this->assertTrue($driver->collectingBranchAndPathCoverage()); + } + + public function testDisablingBranchAndPathCoverage(): void + { + $driver = new Xdebug(new Filter()); + + $driver->collectBranchAndPathCoverage(false); + $this->assertFalse($driver->collectingBranchAndPathCoverage()); + } }