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']); + } +}