diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index 2c92ae2dc..e04946383 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -134,6 +134,13 @@ final class CodeCoverage */ private $report; + /** + * Determine whether to display branch coverage + * + * @var bool + */ + private $determineBranchCoverage = false; + /** * @throws RuntimeException */ @@ -241,6 +248,7 @@ public function start($id, bool $clear = false): void $this->currentId = $id; + $this->driver->setDetermineBranchCoverage($this->determineBranchCoverage); $this->driver->start($this->shouldCheckForDeadAndUnused); } @@ -339,15 +347,29 @@ public function append(array $data, $id = null, bool $append = true, $linesToBeC $this->tests[$id] = ['size' => $size, 'status' => $status]; - foreach ($data as $file => $lines) { + foreach ($data as $file => $fileData) { 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; + foreach ($fileData['lines'] as $line => $lineCoverage) { + if ($lineCoverage === Driver::LINE_EXECUTED) { + $this->addCoverageLinePathCovered($file, $line, true); + $this->addCoverageLineTest($file, $line, $id); + } + } + + if ($this->determineBranchCoverage) { + foreach ($fileData['functions'] as $function => $functionCoverage) { + foreach ($functionCoverage['branches'] as $branch => $branchCoverage) { + if (($branchCoverage['hit'] ?? 0) === 1) { + $this->addCoverageBranchHit($file, $function, $branch, $branchCoverage['hit'] ?? 0); + $this->addCoverageBranchTest($file, $function, $branch, $id); + } + } + + foreach ($functionCoverage['paths'] as $path => $pathCoverage) { + $this->addCoveragePathHit($file, $function, $path, $pathCoverage['hit'] ?? 0); } } } @@ -367,10 +389,13 @@ 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])) { + $thisData = $this->getData(); + $thatData = $that->getData(); + + foreach ($thatData as $file => $fileData) { + if (!isset($thisData[$file])) { if (!$this->filter->isFiltered($file)) { - $this->data[$file] = $lines; + $thisData[$file] = $fileData; } continue; @@ -379,27 +404,30 @@ public function merge(self $that): void // 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]) + \array_keys($thisData[$file]['lines']), + \array_keys($thatData[$file]['lines']) // can this be $fileData? ) ); foreach ($compareLineNumbers as $line) { - $thatPriority = $this->getLinePriority($that->data[$file], $line); - $thisPriority = $this->getLinePriority($this->data[$file], $line); + $thatPriority = $this->getLinePriority($thatData[$file]['lines'], $line); + $thisPriority = $this->getLinePriority($thisData[$file]['lines'], $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]) + $thisData[$file]['lines'][$line] = $thatData[$file]['lines'][$line]; + } elseif ($thatPriority === $thisPriority && \is_array($thisData[$file]['lines'][$line])) { + if ($line['pathCovered'] === true) { + $thisData[$file]['lines'][$line]['pathCovered'] = $line['pathCovered']; + } + $thisData[$file]['lines'][$line] = \array_unique( + \array_merge($thisData[$file]['lines'][$line], $thatData[$file]['lines'][$line]) ); } } } - $this->tests = \array_merge($this->tests, $that->getTests()); - $this->report = null; + $this->tests = \array_merge($this->tests, $that->getTests()); + $this->setData($thisData); } public function setCacheTokens(bool $flag): void @@ -457,6 +485,23 @@ public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist): $this->unintentionallyCoveredSubclassesWhitelist = $whitelist; } + /** + * Specify whether branch coverage should be processed, if the chosen driver supports branch coverage + * Branch coverage is only supported for the Xdebug driver, with an xdebug version of >= 2.3.2 + */ + public function setDetermineBranchCoverage(bool $flag): void + { + if ($flag) { + if ($this->driver instanceof Xdebug && \version_compare(\phpversion('xdebug'), '2.3.2', '>=')) { + $this->determineBranchCoverage = $flag; + } else { + throw new RuntimeException('Branch coverage requires Xdebug version 2.3.2 or newer'); + } + } else { + $this->determineBranchCoverage = false; + } + } + /** * Determine the priority for a line * @@ -467,12 +512,9 @@ public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist): * * During a merge, a higher number is better. * - * @param array $data - * @param int $line - * * @return int */ - private function getLinePriority($data, $line) + private function getLinePriority(array $data, int $line) { if (!\array_key_exists($line, $data)) { return 1; @@ -507,7 +549,10 @@ private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, ar throw new MissingCoversAnnotationException; } - $data = []; + $data = [ + 'lines' => [], + 'functions' => [], + ]; return; } @@ -518,7 +563,7 @@ private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, ar if ($this->checkForUnintentionallyCoveredCode && (!$this->currentId instanceof TestCase || - (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) { + (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) { $this->performUnintentionallyCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed); } @@ -530,7 +575,11 @@ private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, ar foreach (\array_keys($data) as $filename) { $_linesToBeCovered = \array_flip($linesToBeCovered[$filename]); - $data[$filename] = \array_intersect_key($data[$filename], $_linesToBeCovered); + + $data[$filename]['lines'] = \array_intersect_key( + $data[$filename], + $_linesToBeCovered + ); } } @@ -554,24 +603,238 @@ private function applyIgnoredLinesFilter(array &$data): void } foreach ($this->getLinesToBeIgnored($filename) as $line) { - unset($data[$filename][$line]); + unset($data[$filename]['lines'][$line]); } } } private function initializeFilesThatAreSeenTheFirstTime(array $data): void { - foreach ($data as $file => $lines) { - if (!isset($this->data[$file]) && $this->filter->isFile($file)) { - $this->data[$file] = []; + foreach ($data as $file => $fileData) { + if (isset($this->data[$file]) || !$this->filter->isFile($file)) { + continue; + } + $this->initializeFileCoverageData($file); + + // If this particular line is identified as not covered, mark it as null + foreach ($fileData['lines'] as $lineNumber => $flag) { + if ($flag === Driver::LINE_NOT_EXECUTABLE) { + $this->data[$file]['lines'][$lineNumber] = null; + } else { + $this->addCoverageLinePathCovered($file, $lineNumber, false); + } + } + + if ($this->determineBranchCoverage) { + foreach ($fileData['functions'] as $functionName => $functionData) { + $this->data[$file]['paths'][$functionName] = $functionData['paths']; - foreach ($lines as $k => $v) { - $this->data[$file][$k] = $v === -2 ? null : []; + foreach ($functionData['branches'] as $branchIndex => $branchData) { + $this->addCoverageBranchHit($file, $functionName, $branchIndex, $branchData['hit']); + $this->addCoverageBranchLineStart($file, $functionName, $branchIndex, $branchData['line_start']); + $this->addCoverageBranchLineEnd($file, $functionName, $branchIndex, $branchData['line_end']); + + for ($curLine = $branchData['line_start']; $curLine < $branchData['line_end']; $curLine++) { + if (isset($this->data[$file]['lines'][$curLine])) { + $this->addCoverageLinePathCovered($file, $curLine, (bool) $branchData['hit']); + } + } + } } } } } + private function initializeFileCoverageData(string $file): void + { + if (!isset($this->data[$file]) && $this->filter->isFile($file)) { + $default = ['lines' => []]; + + if ($this->determineBranchCoverage) { + $default = [ + 'lines' => [], + 'branches' => [], + 'paths' => [], + ]; + } + + $this->data[$file] = $default; + } + } + + private function addCoverageLinePathCovered(string $file, int $lineNumber, bool $isCovered): void + { + $this->initializeFileCoverageData($file); + + // Initialize the data coverage array for this line + if (!isset($this->data[$file]['lines'][$lineNumber])) { + $this->data[$file]['lines'][$lineNumber] = [ + 'pathCovered' => false, + 'tests' => [], + ]; + } + + $this->data[$file]['lines'][$lineNumber]['pathCovered'] = $isCovered; + } + + private function addCoverageLineTest(string $file, int $lineNumber, string $testId): void + { + $this->initializeFileCoverageData($file); + + // Initialize the data coverage array for this line + if (!isset($this->data[$file]['lines'][$lineNumber])) { + $this->data[$file]['lines'][$lineNumber] = [ + 'pathCovered' => false, + 'tests' => [], + ]; + } + + if (!\in_array($testId, $this->data[$file]['lines'][$lineNumber]['tests'], true)) { + $this->data[$file]['lines'][$lineNumber]['tests'][] = $testId; + } + } + + private function addCoverageBranchHit(string $file, string $functionName, int $branchIndex, int $hit): void + { + $this->initializeFileCoverageData($file); + if (!$this->determineBranchCoverage) { + return; + } + + if (!\array_key_exists($functionName, $this->data[$file]['branches'])) { + $this->data[$file]['branches'][$functionName] = []; + } + + if (!\array_key_exists($branchIndex, $this->data[$file]['branches'][$functionName])) { + $this->data[$file]['branches'][$functionName][$branchIndex] = [ + 'hit' => 0, + 'line_start' => 0, + 'line_end' => 0, + 'tests' => [], + ]; + } + + $this->data[$file]['branches'][$functionName][$branchIndex]['hit'] = \max( + $this->data[$file]['branches'][$functionName][$branchIndex]['hit'], + $hit + ); + } + + private function addCoverageBranchLineStart( + string $file, + string $functionName, + int $branchIndex, + int $lineStart + ): void { + $this->initializeFileCoverageData($file); + + if (!$this->determineBranchCoverage) { + return; + } + + if (!\array_key_exists($functionName, $this->data[$file]['branches'])) { + $this->data[$file]['branches'][$functionName] = []; + } + + if (!\array_key_exists($branchIndex, $this->data[$file]['branches'][$functionName])) { + $this->data[$file]['branches'][$functionName][$branchIndex] = [ + 'hit' => 0, + 'line_start' => 0, + 'line_end' => 0, + 'tests' => [], + ]; + } + + $this->data[$file]['branches'][$functionName][$branchIndex]['line_start'] = $lineStart; + } + + private function addCoverageBranchLineEnd( + string $file, + string $functionName, + int $branchIndex, + int $lineEnd + ): void { + $this->initializeFileCoverageData($file); + + if (!$this->determineBranchCoverage) { + return; + } + + if (!\array_key_exists($functionName, $this->data[$file]['branches'])) { + $this->data[$file]['branches'][$functionName] = []; + } + + if (!\array_key_exists($branchIndex, $this->data[$file]['branches'][$functionName])) { + $this->data[$file]['branches'][$functionName][$branchIndex] = [ + 'hit' => 0, + 'line_start' => 0, + 'line_end' => 0, + 'tests' => [], + ]; + } + + $this->data[$file]['branches'][$functionName][$branchIndex]['line_end'] = $lineEnd; + } + + private function addCoverageBranchTest( + string $file, + string $functionName, + int $branchIndex, + string $testId + ): void { + $this->initializeFileCoverageData($file); + + if (!$this->determineBranchCoverage) { + return; + } + + if (!\array_key_exists($functionName, $this->data[$file]['branches'])) { + $this->data[$file]['branches'][$functionName] = []; + } + + if (!\array_key_exists($branchIndex, $this->data[$file]['branches'][$functionName])) { + $this->data[$file]['branches'][$functionName][$branchIndex] = [ + 'hit' => 0, + 'line_start' => 0, + 'line_end' => 0, + 'tests' => [], + ]; + } + + if (!\in_array($testId, $this->data[$file]['branches'][$functionName][$branchIndex]['tests'], true)) { + $this->data[$file]['branches'][$functionName][$branchIndex]['tests'][] = $testId; + } + } + + private function addCoveragePathHit( + string $file, + string $functionName, + int $pathId, + int $hit + ): void { + $this->initializeFileCoverageData($file); + + if (!$this->determineBranchCoverage) { + return; + } + + if (!\array_key_exists($functionName, $this->data[$file]['paths'])) { + $this->data[$file]['paths'][$functionName] = []; + } + + if (!\array_key_exists($pathId, $this->data[$file]['paths'][$functionName])) { + $this->data[$file]['paths'][$functionName][$pathId] = [ + 'hit' => 0, + 'path' => [], + ]; + } + + $this->data[$file]['paths'][$functionName][$pathId]['hit'] = \max( + $this->data[$file]['paths'][$functionName][$pathId]['hit'], + $hit + ); + } + /** * @throws CoveredCodeNotExecutedException * @throws InvalidArgumentException @@ -593,12 +856,15 @@ private function addUncoveredFilesFromWhitelist(): void continue; } - $data[$uncoveredFile] = []; + $data[$uncoveredFile] = [ + 'lines' => [], + 'functions' => [], + ]; $lines = \count(\file($uncoveredFile)); - for ($i = 1; $i <= $lines; $i++) { - $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED; + for ($line = 1; $line <= $lines; $line++) { + $data[$uncoveredFile]['lines'][$line] = Driver::LINE_NOT_EXECUTED; } } @@ -802,10 +1068,10 @@ private function performUnintentionallyCoveredCodeCheck(array &$data, array $lin $unintentionallyCoveredUnits = []; - foreach ($data as $file => $_data) { - foreach ($_data as $line => $flag) { - if ($flag === 1 && !isset($allowedLines[$file][$line])) { - $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line); + foreach ($data as $file => $fileData) { + foreach ($fileData['lines'] as $lineNumber => $flag) { + if ($flag === 1 && !isset($allowedLines[$file][$lineNumber])) { + $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $lineNumber); } } } @@ -951,7 +1217,6 @@ private function initializeData(): void if ($this->processUncoveredFilesFromWhitelist) { $this->shouldCheckForDeadAndUnused = false; - $this->driver->start(); foreach ($this->filter->getWhitelist() as $file) { @@ -967,9 +1232,9 @@ private function initializeData(): void continue; } - foreach (\array_keys($fileCoverage) as $key) { - if ($fileCoverage[$key] === Driver::LINE_EXECUTED) { - $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED; + foreach (\array_keys($fileCoverage['lines']) as $key) { + if ($fileCoverage['lines'][$key] === Driver::LINE_EXECUTED) { + $fileCoverage['lines'][$key] = Driver::LINE_NOT_EXECUTED; } } diff --git a/src/Driver/Driver.php b/src/Driver/Driver.php index 17acbf62d..6154a08c0 100644 --- a/src/Driver/Driver.php +++ b/src/Driver/Driver.php @@ -44,4 +44,9 @@ public function start(bool $determineUnusedAndDead = true): void; * Stop collection of code coverage information. */ public function stop(): array; + + /** + * Specify that branch coverage should be included with collected code coverage information. + */ + public function setDetermineBranchCoverage(bool $flag): void; } diff --git a/src/Driver/PCOV.php b/src/Driver/PCOV.php index 7a6a3b6c8..87c6295c9 100644 --- a/src/Driver/PCOV.php +++ b/src/Driver/PCOV.php @@ -9,6 +9,8 @@ */ namespace SebastianBergmann\CodeCoverage\Driver; +use SebastianBergmann\CodeCoverage\RuntimeException; + /** * Driver for PCOV code coverage functionality. * @@ -16,6 +18,14 @@ */ final class PCOV implements Driver { + /** + * Specify that branch coverage should be included with collected code coverage information. + */ + public function setDetermineBranchCoverage(bool $flag): void + { + throw new RuntimeException('Branch coverage is not supported in PHPDBG'); + } + /** * Start collection of code coverage information. */ diff --git a/src/Driver/PHPDBG.php b/src/Driver/PHPDBG.php index e9f999a05..c3d820d9f 100644 --- a/src/Driver/PHPDBG.php +++ b/src/Driver/PHPDBG.php @@ -76,6 +76,14 @@ public function stop(): array return $this->detectExecutedLines($fetchedLines, $dbgData); } + /** + * Specify that branch coverage should be included with collected code coverage information. + */ + public function setDetermineBranchCoverage(bool $flag): void + { + throw new RuntimeException('Branch coverage is not supported in PHPDBG'); + } + /** * Convert phpdbg based data into the format CodeCoverage expects */ @@ -91,6 +99,13 @@ private function detectExecutedLines(array $sourceLines, array $dbgData): array } } + foreach ($sourceLines as $file => $lines) { + $sourceLines[$file] = [ + 'lines' => $lines, + 'functions' => [], + ]; + } + return $sourceLines; } } diff --git a/src/Driver/Xdebug.php b/src/Driver/Xdebug.php index 737949636..f89d25315 100644 --- a/src/Driver/Xdebug.php +++ b/src/Driver/Xdebug.php @@ -29,6 +29,11 @@ final class Xdebug implements Driver */ private $filter; + /** + * @var bool + */ + private $determineBranchCoverage = false; + /** * @throws RuntimeException */ @@ -54,11 +59,17 @@ public function __construct(Filter $filter = null) */ public function start(bool $determineUnusedAndDead = true): void { + $flag = 0; + if ($determineUnusedAndDead) { - \xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); - } else { - \xdebug_start_code_coverage(); + $flag = XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE; } + + if ($this->determineBranchCoverage) { + $flag = XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE | XDEBUG_CC_BRANCH_CHECK; + } + + \xdebug_start_code_coverage($flag); } /** @@ -73,10 +84,29 @@ public function stop(): array return $this->cleanup($data); } + /** + * Specify that branch coverage should be included with collected code coverage information. + */ + public function setDetermineBranchCoverage(bool $flag): void + { + if ($flag && \version_compare(\phpversion('xdebug'), '2.3.2', '<')) { + throw new RuntimeException('Branch coverage requires Xdebug 2.3.2 or newer'); + } + $this->determineBranchCoverage = $flag; + } + private function cleanup(array $data): array { foreach (\array_keys($data) as $file) { - unset($data[$file][0]); + if (!isset($data[$file]['lines'])) { + $data[$file] = ['lines' => $data[$file]]; + } + + if (!isset($data[$file]['functions'])) { + $data[$file]['functions'] = []; + } + + unset($data[$file]['lines'][0]); if (!$this->filter->isFile($file)) { continue; @@ -84,9 +114,9 @@ private function cleanup(array $data): array $numLines = $this->getNumberOfLinesInFile($file); - foreach (\array_keys($data[$file]) as $line) { + foreach (\array_keys($data[$file]['lines']) as $line) { if ($line > $numLines) { - unset($data[$file][$line]); + unset($data[$file]['lines'][$line]); } } } diff --git a/src/Node/AbstractNode.php b/src/Node/AbstractNode.php index 116a09f17..4f4f07881 100644 --- a/src/Node/AbstractNode.php +++ b/src/Node/AbstractNode.php @@ -180,6 +180,34 @@ public function getTestedMethodsPercent(bool $asString = true) ); } + /** + * Returns the percentage of paths that have been tested. + * + * @return int|string + */ + public function getTestedPathsPercent(bool $asString = true) + { + return Util::percent( + $this->getNumTestedPaths(), + $this->getNumPaths(), + $asString + ); + } + + /** + * Returns the percentage of branches that have been tested. + * + * @return int|string + */ + public function getTestedBranchesPercent(bool $asString = true) + { + return Util::percent( + $this->getNumTestedBranches(), + $this->getNumBranches(), + $asString + ); + } + /** * Returns the percentage of functions and methods that has been tested. * @@ -276,6 +304,16 @@ abstract public function getFunctions(): array; */ abstract public function getLinesOfCode(): array; + /** + * Returns the paths of this node. + */ + abstract public function getPaths(): array; + + /** + * Returns the branches of this node. + */ + abstract public function getBranches(): array; + /** * Returns the number of executable lines. */ @@ -325,4 +363,24 @@ abstract public function getNumFunctions(): int; * Returns the number of tested functions. */ abstract public function getNumTestedFunctions(): int; + + /** + * Returns the number of paths. + */ + abstract public function getNumPaths(): int; + + /** + * Returns the number of tested paths. + */ + abstract public function getNumTestedPaths(): int; + + /** + * Returns the number of branches. + */ + abstract public function getNumBranches(): int; + + /** + * Returns the number of tested branches. + */ + abstract public function getNumTestedBranches(): int; } diff --git a/src/Node/Directory.php b/src/Node/Directory.php index 7f1b5b228..bc9908ff9 100644 --- a/src/Node/Directory.php +++ b/src/Node/Directory.php @@ -51,6 +51,16 @@ final class Directory extends AbstractNode implements \IteratorAggregate */ private $linesOfCode; + /** + * @var array + */ + private $paths; + + /** + * @var array + */ + private $branches; + /** * @var int */ @@ -106,6 +116,26 @@ final class Directory extends AbstractNode implements \IteratorAggregate */ private $numTestedFunctions = -1; + /** + * @var int + */ + private $numPaths = -1; + + /** + * @var int + */ + private $numTestedPaths = -1; + + /** + * @var int + */ + private $numBranches = -1; + + /** + * @var int + */ + private $numTestedBranches = -1; + /** * Returns the number of files in/under this node. */ @@ -160,6 +190,8 @@ public function addFile(string $name, array $coverageData, array $testData, bool $this->numExecutableLines = -1; $this->numExecutedLines = -1; + $this->numPaths = -1; + $this->numTestedPaths = -1; return $file; } @@ -424,4 +456,106 @@ public function getNumTestedFunctions(): int return $this->numTestedFunctions; } + + /** + * Returns the paths of this node. + */ + public function getPaths(): array + { + if ($this->paths === null) { + $this->paths = []; + + foreach ($this->children as $child) { + $this->paths = \array_merge( + $this->paths, + $child->getPaths() + ); + } + } + + return $this->paths; + } + + /** + * Returns the branches of this node. + */ + public function getBranches(): array + { + if ($this->branches === null) { + $this->branches = []; + + foreach ($this->children as $child) { + $this->branches = \array_merge( + $this->branches, + $child->getBranches() + ); + } + } + + return $this->branches; + } + + /** + * Returns the number of paths. + */ + public function getNumPaths(): int + { + if ($this->numPaths === -1) { + $this->numPaths = 0; + + foreach ($this->children as $child) { + $this->numPaths += $child->getNumPaths(); + } + } + + return $this->numPaths; + } + + /** + * Returns the number of tested paths. + */ + public function getNumTestedPaths(): int + { + if ($this->numTestedPaths === -1) { + $this->numTestedPaths = 0; + + foreach ($this->children as $child) { + $this->numTestedPaths += $child->getNumTestedPaths(); + } + } + + return $this->numTestedPaths; + } + + /** + * Returns the number of branches. + */ + public function getNumBranches(): int + { + if ($this->numBranches === -1) { + $this->numBranches = 0; + + foreach ($this->children as $child) { + $this->numBranches += $child->getNumBranches(); + } + } + + return $this->numBranches; + } + + /** + * Returns the number of tested branches. + */ + public function getNumTestedBranches(): int + { + if ($this->numTestedBranches === -1) { + $this->numTestedBranches = 0; + + foreach ($this->children as $child) { + $this->numTestedBranches += $child->getNumTestedBranches(); + } + } + + return $this->numTestedBranches; + } } diff --git a/src/Node/File.php b/src/Node/File.php index d20c4dda8..c7886c29a 100644 --- a/src/Node/File.php +++ b/src/Node/File.php @@ -49,6 +49,16 @@ final class File extends AbstractNode */ private $functions = []; + /** + * @var array + */ + private $branches = []; + + /** + * @var array + */ + private $paths = []; + /** * @var array */ @@ -89,6 +99,26 @@ final class File extends AbstractNode */ private $numTestedFunctions; + /** + * @var int + */ + private $numPaths = 0; + + /** + * @var int + */ + private $numTestedPaths = 0; + + /** + * @var int + */ + private $numBranches = 0; + + /** + * @var int + */ + private $numTestedBranches = 0; + /** * @var bool */ @@ -352,16 +382,21 @@ private function calculateStatistics(): void unset($tokens); foreach (\range(1, $this->linesOfCode['loc']) as $lineNumber) { - if (isset($this->coverageData[$lineNumber])) { - foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) { - $codeUnit['executableLines']++; - } + // Check to see if we've identified this line as executed, not executed, or not executable + if (\array_key_exists($lineNumber, $this->coverageData['lines'])) { + // If the element is null, that indicates this line is not executable + if ($this->coverageData['lines'][$lineNumber] !== null) { + foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) { + $codeUnit['executableLines']++; + } - unset($codeUnit); + unset($codeUnit); + + $this->numExecutableLines++; + } - $this->numExecutableLines++; - if (\count($this->coverageData[$lineNumber]) > 0) { + if ($this->coverageData['lines'][$lineNumber]['pathCovered'] === true) { foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) { $codeUnit['executedLines']++; } @@ -374,98 +409,179 @@ private function calculateStatistics(): void } foreach ($this->traits as &$trait) { - foreach ($trait['methods'] as &$method) { - if ($method['executableLines'] > 0) { - $method['coverage'] = ($method['executedLines'] / - $method['executableLines']) * 100; - } else { - $method['coverage'] = 100; - } - - $method['crap'] = $this->crap( - $method['ccn'], - $method['coverage'] - ); + $this->calcAndApplyClassAggregate($trait, $trait['traitName'], $this->numTestedTraits); + } - $trait['ccn'] += $method['ccn']; - } + unset($trait); - unset($method); + foreach ($this->classes as &$class) { + $this->calcAndApplyClassAggregate($class, $class['className'], $this->numTestedClasses); + } - if ($trait['executableLines'] > 0) { - $trait['coverage'] = ($trait['executedLines'] / - $trait['executableLines']) * 100; + unset($class); - if ($trait['coverage'] === 100) { - $this->numTestedClasses++; - } + foreach ($this->functions as &$function) { + if ($function['executableLines'] > 0) { + $function['coverage'] = ($function['executedLines'] / + $function['executableLines']) * 100; } else { - $trait['coverage'] = 100; + $function['coverage'] = 100; } - $trait['crap'] = $this->crap( - $trait['ccn'], - $trait['coverage'] + if ($function['coverage'] === 100) { + $this->numTestedFunctions++; + } + + $function['crap'] = $this->crap( + $function['ccn'], + $function['coverage'] ); } - unset($trait); + unset($function); - foreach ($this->classes as &$class) { - foreach ($class['methods'] as &$method) { - if ($method['executableLines'] > 0) { - $method['coverage'] = ($method['executedLines'] / - $method['executableLines']) * 100; - } else { - $method['coverage'] = 100; - } + // Process Path Coverage for non-class functions + foreach ($this->functions as &$function) { + if (isset($this->coverageData['paths'][$function['functionName']])) { + $functionPaths = $this->coverageData['paths'][$function['functionName']]; - $method['crap'] = $this->crap( - $method['ccn'], - $method['coverage'] - ); + $this->calculatePathsAggregate($functionPaths, $numExecutablePaths, $numExecutedPaths); - $class['ccn'] += $method['ccn']; + $function['executablePaths'] = $numExecutablePaths; + $this->numPaths += $numExecutablePaths; + + $function['executedPaths'] = $numExecutedPaths; + $this->numTestedPaths += $numExecutablePaths; } - unset($method); + if (isset($this->coverageData['branches'][$function['functionName']])) { + $functionBranches = $this->coverageData['branches'][$function['functionName']]; - if ($class['executableLines'] > 0) { - $class['coverage'] = ($class['executedLines'] / - $class['executableLines']) * 100; + $this->calculatePathsAggregate($functionBranches, $numExecutableBranches, $numExecutedBranches); - if ($class['coverage'] === 100) { - $this->numTestedClasses++; - } + $function['executableBranches'] = $numExecutableBranches; + $this->numBranches += $numExecutableBranches; + + $function['executedBranches'] = $numExecutedBranches; + $this->numTestedBranches += $numExecutedBranches; + } + } + } + + + /** + * @param array $classOrTrait + * @param string $classOrTraitName + * @param int $numTestedClassOrTrait + */ + private function calcAndApplyClassAggregate( + array &$classOrTrait, + string $classOrTraitName, + int &$numTestedClassOrTrait + ): void { + foreach ($classOrTrait['methods'] as &$method) { + $methodName = $method['methodName']; + + if ($method['executableLines'] > 0) { + $method['coverage'] = ($method['executedLines'] / $method['executableLines']) * 100; } else { - $class['coverage'] = 100; + $method['coverage'] = 100; } - $class['crap'] = $this->crap( - $class['ccn'], - $class['coverage'] + $method['crap'] = $this->crap( + $method['ccn'], + $method['coverage'] ); - } - unset($class); + $classOrTrait['ccn'] += $method['ccn']; + + if (isset($this->coverageData['paths'])) { + $methodCoveragePath = $methodName; + + // @todo - Might not need this anonymous function handling... + if ($methodName === 'anonymous function') { + foreach ($this->coverageData['paths'] as $index => $path) { + if ($method['startLine'] === $path[0]['line_start']) { + $methodCoveragePath = $index; + } + } + } + + $methodCoveragePath = $classOrTraitName . '->' . $methodCoveragePath; + + if (isset($this->coverageData['paths'][$methodCoveragePath])) { + $methodPaths = $this->coverageData['paths'][$methodCoveragePath]; + $this->calculatePathsAggregate($methodPaths, $numExecutablePaths, $numExexutedPaths); + + $method['executablePaths'] = $numExecutablePaths; + $classOrTrait['executablePaths'] += $numExecutablePaths; + $this->numPaths += $numExecutablePaths; + + $method['executedPaths'] = $numExexutedPaths; + $classOrTrait['executedPaths'] += $numExexutedPaths; + $this->numTestedPaths += $numExexutedPaths; + } - foreach ($this->functions as &$function) { - if ($function['executableLines'] > 0) { - $function['coverage'] = ($function['executedLines'] / - $function['executableLines']) * 100; - } else { - $function['coverage'] = 100; } - if ($function['coverage'] === 100) { - $this->numTestedFunctions++; + if (isset($this->coverageData['branches'])) { + $methodCoverageBranch = $methodName; + + // @todo - Might not need this anonymous function handling... + if ($methodName === 'anonymous function') { + foreach ($this->coverageData['branches'] as $index => $branch) { + if ($method['startLine'] === $branch[0]['line_start']) { + $methodCoverageBranch = $index; + } + } + } + + $methodCoverageBranch = $classOrTraitName . '->' . $methodCoverageBranch; + + if (isset($this->coverageData['branches'][$methodCoverageBranch])) { + $methodPaths = $this->coverageData['branches'][$methodCoverageBranch]; + $this->calculatePathsAggregate($methodPaths, $numExecutableBranches, $numExexutedBranches); + + $method['executableBranches'] = $numExecutableBranches; + $classOrTrait['executableBranches'] += $numExecutableBranches; + $this->numBranches += $numExecutableBranches; + + $method['executedBranches'] = $numExexutedBranches; + $classOrTrait['executedBranches'] += $numExexutedBranches; + $this->numTestedBranches += $numExexutedBranches; + } } + } + unset($method); - $function['crap'] = $this->crap( - $function['ccn'], - $function['coverage'] - ); + if ($classOrTrait['executableLines'] > 0) { + $classOrTrait['coverage'] = ($classOrTrait['executedLines'] / + $classOrTrait['executableLines']) * 100; + + if ($classOrTrait['coverage'] === 100) { + $numTestedClassOrTrait++; + } + } else { + $classOrTrait['coverage'] = 100; } + + $classOrTrait['crap'] = $this->crap( + $classOrTrait['ccn'], + $classOrTrait['coverage'] + ); + } + + private function calculatePathsAggregate(array $paths, &$functionExecutablePaths, &$functionExecutedPaths): void + { + $functionExecutablePaths = \count($paths); + + $functionExecutedPaths = \array_reduce( + $paths, + static function ($carry, $value) { + return ($value['hit'] > 0) ? $carry + 1 : $carry; + }, + 0 + ); } private function processClasses(\PHP_Token_Stream $tokens): void @@ -483,16 +599,20 @@ private function processClasses(\PHP_Token_Stream $tokens): void } $this->classes[$className] = [ - 'className' => $className, - 'methods' => [], - 'startLine' => $class['startLine'], - 'executableLines' => 0, - 'executedLines' => 0, - 'ccn' => 0, - 'coverage' => 0, - 'crap' => 0, - 'package' => $class['package'], - 'link' => $link . $class['startLine'], + 'className' => $className, + 'methods' => [], + 'startLine' => $class['startLine'], + 'executableLines' => 0, + 'executedLines' => 0, + 'executablePaths' => 0, + 'executedPaths' => 0, + 'executableBranches' => 0, + 'executedBranches' => 0, + 'ccn' => 0, + 'coverage' => 0, + 'crap' => 0, + 'package' => $class['package'], + 'link' => $link . $class['startLine'], ]; foreach ($class['methods'] as $methodName => $method) { @@ -519,16 +639,20 @@ private function processTraits(\PHP_Token_Stream $tokens): void foreach ($traits as $traitName => $trait) { $this->traits[$traitName] = [ - 'traitName' => $traitName, - 'methods' => [], - 'startLine' => $trait['startLine'], - 'executableLines' => 0, - 'executedLines' => 0, - 'ccn' => 0, - 'coverage' => 0, - 'crap' => 0, - 'package' => $trait['package'], - 'link' => $link . $trait['startLine'], + 'traitName' => $traitName, + 'methods' => [], + 'startLine' => $trait['startLine'], + 'executableLines' => 0, + 'executedLines' => 0, + 'executablePaths' => 0, + 'executedPaths' => 0, + 'executableBranches' => 0, + 'executedBranches' => 0, + 'ccn' => 0, + 'coverage' => 0, + 'crap' => 0, + 'package' => $trait['package'], + 'link' => $link . $trait['startLine'], ]; foreach ($trait['methods'] as $methodName => $method) { @@ -559,15 +683,19 @@ private function processFunctions(\PHP_Token_Stream $tokens): void } $this->functions[$functionName] = [ - 'functionName' => $functionName, - 'signature' => $function['signature'], - 'startLine' => $function['startLine'], - 'executableLines' => 0, - 'executedLines' => 0, - 'ccn' => $function['ccn'], - 'coverage' => 0, - 'crap' => 0, - 'link' => $link . $function['startLine'], + 'functionName' => $functionName, + 'signature' => $function['signature'], + 'startLine' => $function['startLine'], + 'executableLines' => 0, + 'executedLines' => 0, + 'executablePaths' => 0, + 'executedPaths' => 0, + 'executableBranches' => 0, + 'executedBranches' => 0, + 'ccn' => $function['ccn'], + 'coverage' => 0, + 'crap' => 0, + 'link' => $link . $function['startLine'], ]; foreach (\range($function['startLine'], $function['endLine']) as $lineNumber) { @@ -595,17 +723,69 @@ private function crap(int $ccn, float $coverage): string private function newMethod(string $methodName, array $method, string $link): array { return [ - 'methodName' => $methodName, - 'visibility' => $method['visibility'], - 'signature' => $method['signature'], - 'startLine' => $method['startLine'], - 'endLine' => $method['endLine'], - 'executableLines' => 0, - 'executedLines' => 0, - 'ccn' => $method['ccn'], - 'coverage' => 0, - 'crap' => 0, - 'link' => $link . $method['startLine'], + 'methodName' => $methodName, + 'visibility' => $method['visibility'], + 'signature' => $method['signature'], + 'startLine' => $method['startLine'], + 'endLine' => $method['endLine'], + 'executableLines' => 0, + 'executedLines' => 0, + 'executablePaths' => 0, + 'executedPaths' => 0, + 'executableBranches' => 0, + 'executedBranches' => 0, + 'ccn' => $method['ccn'], + 'coverage' => 0, + 'crap' => 0, + 'link' => $link . $method['startLine'], ]; } + + /** + * Returns the paths of this node. + */ + public function getPaths(): array + { + return $this->paths; + } + + /** + * Returns the branches of this node. + */ + public function getBranches(): array + { + return $this->branches; + } + + /** + * Returns the number of paths. + */ + public function getNumPaths(): int + { + return $this->numPaths; + } + + /** + * Returns the number of tested paths. + */ + public function getNumTestedPaths(): int + { + return $this->numTestedPaths; + } + + /** + * Returns the number of branches. + */ + public function getNumBranches(): int + { + return $this->numBranches; + } + + /** + * Returns the number of tested branches. + */ + public function getNumTestedBranches(): int + { + return $this->numTestedBranches; + } } diff --git a/src/Report/Html/Facade.php b/src/Report/Html/Facade.php index 318b49a5a..5120fa72b 100644 --- a/src/Report/Html/Facade.php +++ b/src/Report/Html/Facade.php @@ -38,6 +38,11 @@ final class Facade */ private $highLowerBound; + /** + * @var bool + */ + private $determineBranchCoverage = false; + public function __construct(int $lowUpperBound = 50, int $highLowerBound = 90, string $generator = '') { $this->generator = $generator; @@ -69,6 +74,7 @@ public function process(CodeCoverage $coverage, string $target): void $this->lowUpperBound, $this->highLowerBound ); + $dashboard->setDetermineBranchCoverage($this->determineBranchCoverage); $directory = new Directory( $this->templatePath, @@ -77,6 +83,7 @@ public function process(CodeCoverage $coverage, string $target): void $this->lowUpperBound, $this->highLowerBound ); + $directory->setDetermineBranchCoverage($this->determineBranchCoverage); $file = new File( $this->templatePath, @@ -85,6 +92,7 @@ public function process(CodeCoverage $coverage, string $target): void $this->lowUpperBound, $this->highLowerBound ); + $file->setDetermineBranchCoverage($this->determineBranchCoverage); $directory->render($report, $target . 'index.html'); $dashboard->render($report, $target . 'dashboard.html'); @@ -113,6 +121,11 @@ public function process(CodeCoverage $coverage, string $target): void $this->copyFiles($target); } + public function setDetermineBranchCoverage(bool $determineBranchCoverage): void + { + $this->determineBranchCoverage = $determineBranchCoverage; + } + /** * @throws RuntimeException */ diff --git a/src/Report/Html/Renderer.php b/src/Report/Html/Renderer.php index 2a9024c0e..22678a8ef 100644 --- a/src/Report/Html/Renderer.php +++ b/src/Report/Html/Renderer.php @@ -50,6 +50,11 @@ abstract class Renderer */ protected $version; + /** + * @var bool + */ + protected $determineBranchCoverage = false; + public function __construct(string $templatePath, string $generator, string $date, int $lowUpperBound, int $highLowerBound) { $this->templatePath = $templatePath; @@ -60,6 +65,11 @@ public function __construct(string $templatePath, string $generator, string $dat $this->version = Version::id(); } + public function setDetermineBranchCoverage(bool $determineBranchCoverage): void + { + $this->determineBranchCoverage = $determineBranchCoverage; + } + protected function renderItemTemplate(\Text_Template $template, array $data): string { $numSeparator = ' / '; @@ -112,23 +122,40 @@ protected function renderItemTemplate(\Text_Template $template, array $data): st $data['linesExecutedPercentAsString'] = 'n/a'; } + $branchesLevel = ''; + $branchesNumber = '0' . $numSeparator . '0'; + $branchesBar = ''; + + // @todo - Remove the null coalesce + if (($data['numExecutableBranches'] ?? 0) > 0) { + $branchesLevel = $this->getColorLevel($data['testedBranchesPercent']); + $branchesBar = $this->getCoverageBar($data['testedBranchesPercent']); + $branchesNumber = $data['numExecutedBranches'] . $numSeparator . $data['numExecutableBranches']; + } else { + $data['testedBranchesPercentAsString'] = 'n/a'; + } + $template->setVar( [ - 'icon' => $data['icon'] ?? '', - 'crap' => $data['crap'] ?? '', - 'name' => $data['name'], - 'lines_bar' => $linesBar, - 'lines_executed_percent' => $data['linesExecutedPercentAsString'], - 'lines_level' => $linesLevel, - 'lines_number' => $linesNumber, - 'methods_bar' => $methodsBar, - 'methods_tested_percent' => $data['testedMethodsPercentAsString'], - 'methods_level' => $methodsLevel, - 'methods_number' => $methodsNumber, - 'classes_bar' => $classesBar, - 'classes_tested_percent' => $data['testedClassesPercentAsString'] ?? '', - 'classes_level' => $classesLevel, - 'classes_number' => $classesNumber, + 'icon' => $data['icon'] ?? '', + 'crap' => $data['crap'] ?? '', + 'name' => $data['name'], + 'lines_bar' => $linesBar, + 'lines_executed_percent' => $data['linesExecutedPercentAsString'], + 'lines_level' => $linesLevel, + 'lines_number' => $linesNumber, + 'methods_bar' => $methodsBar, + 'methods_tested_percent' => $data['testedMethodsPercentAsString'], + 'methods_level' => $methodsLevel, + 'methods_number' => $methodsNumber, + 'classes_bar' => $classesBar, + 'classes_tested_percent' => $data['testedClassesPercentAsString'] ?? '', + 'classes_level' => $classesLevel, + 'classes_number' => $classesNumber, + 'branches_bar' => $branchesBar, + 'branches_tested_percent' => $data['testedBranchesPercentAsString'] ?? '', + 'branches_level' => $branchesLevel, + 'branches_number' => $branchesNumber, ] ); diff --git a/src/Report/Html/Renderer/Directory.php b/src/Report/Html/Renderer/Directory.php index c2f086079..f31d894b3 100644 --- a/src/Report/Html/Renderer/Directory.php +++ b/src/Report/Html/Renderer/Directory.php @@ -23,7 +23,12 @@ final class Directory extends Renderer */ public function render(DirectoryNode $node, string $file): void { - $template = new \Text_Template($this->templatePath . 'directory.html', '{{', '}}'); + $templateName = $this->templatePath . 'directory.html'; + if ($this->determineBranchCoverage) { + $templateName = $this->templatePath . 'directory_branch.html'; + } + + $template = new \Text_Template($templateName, '{{', '}}'); $this->setCommonTemplateVariables($template, $node); @@ -47,21 +52,29 @@ public function render(DirectoryNode $node, string $file): void $template->renderTo($file); } - protected function renderItem(Node $node, bool $total = false): string + private function renderItem(Node $node, bool $total = false): string { $data = [ - 'numClasses' => $node->getNumClassesAndTraits(), - 'numTestedClasses' => $node->getNumTestedClassesAndTraits(), - 'numMethods' => $node->getNumFunctionsAndMethods(), - 'numTestedMethods' => $node->getNumTestedFunctionsAndMethods(), - 'linesExecutedPercent' => $node->getLineExecutedPercent(false), - 'linesExecutedPercentAsString' => $node->getLineExecutedPercent(), - 'numExecutedLines' => $node->getNumExecutedLines(), - 'numExecutableLines' => $node->getNumExecutableLines(), - 'testedMethodsPercent' => $node->getTestedFunctionsAndMethodsPercent(false), - 'testedMethodsPercentAsString' => $node->getTestedFunctionsAndMethodsPercent(), - 'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false), - 'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(), + 'numClasses' => $node->getNumClassesAndTraits(), + 'numTestedClasses' => $node->getNumTestedClassesAndTraits(), + 'numMethods' => $node->getNumFunctionsAndMethods(), + 'numTestedMethods' => $node->getNumTestedFunctionsAndMethods(), + 'linesExecutedPercent' => $node->getLineExecutedPercent(false), + 'linesExecutedPercentAsString' => $node->getLineExecutedPercent(), + 'numExecutedLines' => $node->getNumExecutedLines(), + 'numExecutableLines' => $node->getNumExecutableLines(), + 'testedMethodsPercent' => $node->getTestedFunctionsAndMethodsPercent(false), + 'testedMethodsPercentAsString' => $node->getTestedFunctionsAndMethodsPercent(), + 'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false), + 'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(), + 'testedBranchesPercent' => $node->getTestedBranchesPercent(false), + 'testedBranchesPercentAsString' => $node->getTestedBranchesPercent(), + 'testedPathsPercent' => $node->getTestedPathsPercent(false), + 'testedPathsPercentAsString' => $node->getTestedPathsPercent(), + 'numExecutablePaths' => $node->getNumPaths(), + 'numExecutedPaths' => $node->getNumTestedPaths(), + 'numExecutableBranches' => $node->getNumBranches(), + 'numExecutedBranches' => $node->getNumTestedBranches(), ]; if ($total) { @@ -90,8 +103,13 @@ protected function renderItem(Node $node, bool $total = false): string } } + $templateName = $this->templatePath . 'directory_item.html'; + if ($this->determineBranchCoverage) { + $templateName = $this->templatePath . 'directory_item_branch.html'; + } + return $this->renderItemTemplate( - new \Text_Template($this->templatePath . 'directory_item.html', '{{', '}}'), + new \Text_Template($templateName, '{{', '}}'), $data ); } diff --git a/src/Report/Html/Renderer/File.php b/src/Report/Html/Renderer/File.php index f0604bf21..e9a0b85b9 100644 --- a/src/Report/Html/Renderer/File.php +++ b/src/Report/Html/Renderer/File.php @@ -27,7 +27,12 @@ final class File extends Renderer */ public function render(FileNode $node, string $file): void { - $template = new \Text_Template($this->templatePath . 'file.html', '{{', '}}'); + $templateName = $this->templatePath . 'file.html'; + if ($this->determineBranchCoverage) { + $templateName = $this->templatePath . 'file_branch.html'; + } + + $template = new \Text_Template($templateName, '{{', '}}'); $template->setVar( [ @@ -43,31 +48,42 @@ public function render(FileNode $node, string $file): void protected function renderItems(FileNode $node): string { - $template = new \Text_Template($this->templatePath . 'file_item.html', '{{', '}}'); + $fileTemplateName = $this->templatePath . 'file_item.html'; + $methodItemTemplateName = $this->templatePath . 'method_item.html'; - $methodItemTemplate = new \Text_Template( - $this->templatePath . 'method_item.html', - '{{', - '}}' - ); + if ($this->determineBranchCoverage) { + $fileTemplateName = $this->templatePath . 'file_item_branch.html'; + $methodItemTemplateName = $this->templatePath . 'method_item_branch.html'; + } + + $template = new \Text_Template($fileTemplateName, '{{', '}}'); + $methodItemTemplate = new \Text_Template($methodItemTemplateName, '{{', '}}'); $items = $this->renderItemTemplate( $template, [ - 'name' => 'Total', - 'numClasses' => $node->getNumClassesAndTraits(), - 'numTestedClasses' => $node->getNumTestedClassesAndTraits(), - 'numMethods' => $node->getNumFunctionsAndMethods(), - 'numTestedMethods' => $node->getNumTestedFunctionsAndMethods(), - 'linesExecutedPercent' => $node->getLineExecutedPercent(false), - 'linesExecutedPercentAsString' => $node->getLineExecutedPercent(), - 'numExecutedLines' => $node->getNumExecutedLines(), - 'numExecutableLines' => $node->getNumExecutableLines(), - 'testedMethodsPercent' => $node->getTestedFunctionsAndMethodsPercent(false), - 'testedMethodsPercentAsString' => $node->getTestedFunctionsAndMethodsPercent(), - 'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false), - 'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(), - 'crap' => 'CRAP', + 'name' => 'Total', + 'numClasses' => $node->getNumClassesAndTraits(), + 'numTestedClasses' => $node->getNumTestedClassesAndTraits(), + 'numMethods' => $node->getNumFunctionsAndMethods(), + 'numTestedMethods' => $node->getNumTestedFunctionsAndMethods(), + 'linesExecutedPercent' => $node->getLineExecutedPercent(false), + 'linesExecutedPercentAsString' => $node->getLineExecutedPercent(), + 'numExecutedLines' => $node->getNumExecutedLines(), + 'numExecutableLines' => $node->getNumExecutableLines(), + 'testedMethodsPercent' => $node->getTestedFunctionsAndMethodsPercent(false), + 'testedMethodsPercentAsString' => $node->getTestedFunctionsAndMethodsPercent(), + 'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false), + 'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(), + 'testedBranchesPercent' => $node->getTestedBranchesPercent(false), + 'testedBranchesPercentAsString' => $node->getTestedBranchesPercent(), + 'testedPathsPercent' => $node->getTestedPathsPercent(false), + 'testedPathsPercentAsString' => $node->getTestedPathsPercent(), + 'numExecutablePaths' => $node->getNumPaths(), + 'numExecutedPaths' => $node->getNumTestedPaths(), + 'numExecutableBranches' => $node->getNumBranches(), + 'numExecutedBranches' => $node->getNumTestedBranches(), + 'crap' => 'CRAP', ] ); @@ -121,47 +137,73 @@ protected function renderTraitOrClassItems(array $items, \Text_Template $templat $item['executableLines'], true ); + $testedBranchesPercentAsString = Util::percent( + $item['executedBranches'], + $item['executableBranches'], + true + ); + $testedPathsPercentAsString = Util::percent( + $item['executedPaths'], + $item['executablePaths'], + true + ); } else { - $numClasses = 'n/a'; - $numTestedClasses = 'n/a'; - $linesExecutedPercentAsString = 'n/a'; + $numClasses = 'n/a'; + $numTestedClasses = 'n/a'; + $linesExecutedPercentAsString = 'n/a'; + $testedBranchesPercentAsString = 'n/a'; + $testedPathsPercentAsString = 'n/a'; } $buffer .= $this->renderItemTemplate( $template, [ - 'name' => $this->abbreviateClassName($name), - 'numClasses' => $numClasses, - 'numTestedClasses' => $numTestedClasses, - 'numMethods' => $numMethods, - 'numTestedMethods' => $numTestedMethods, - 'linesExecutedPercent' => Util::percent( + 'name' => $this->abbreviateClassName($name), + 'numClasses' => $numClasses, + 'numTestedClasses' => $numTestedClasses, + 'numMethods' => $numMethods, + 'numTestedMethods' => $numTestedMethods, + 'linesExecutedPercent' => Util::percent( $item['executedLines'], $item['executableLines'], false ), - 'linesExecutedPercentAsString' => $linesExecutedPercentAsString, - 'numExecutedLines' => $item['executedLines'], - 'numExecutableLines' => $item['executableLines'], - 'testedMethodsPercent' => Util::percent( + 'linesExecutedPercentAsString' => $linesExecutedPercentAsString, + 'numExecutedLines' => $item['executedLines'], + 'numExecutableLines' => $item['executableLines'], + 'testedMethodsPercent' => Util::percent( $numTestedMethods, $numMethods ), - 'testedMethodsPercentAsString' => Util::percent( + 'testedMethodsPercentAsString' => Util::percent( $numTestedMethods, $numMethods, true ), - 'testedClassesPercent' => Util::percent( - $numTestedMethods == $numMethods ? 1 : 0, + 'testedClassesPercent' => Util::percent( + $numTestedMethods === $numMethods ? 1 : 0, 1 ), 'testedClassesPercentAsString' => Util::percent( - $numTestedMethods == $numMethods ? 1 : 0, + $numTestedMethods === $numMethods ? 1 : 0, 1, true ), - 'crap' => $item['crap'], + 'crap' => $item['crap'], + 'testedBranchesPercent' => Util::percent( + $item['executedBranches'], + $item['executableBranches'] + ), + 'testedBranchesPercentAsString' => $testedBranchesPercentAsString, + 'testedPathsPercent' => Util::percent( + $item['executedPaths'], + $item['executablePaths'] + ), + 'testedPathsPercentAsString' => $testedPathsPercentAsString, + 'numExecutablePaths' => $item['executablePaths'], + 'numExecutedPaths' => $item['executedPaths'], + 'numExecutableBranches' => $item['executableBranches'], + 'numExecutedBranches' => $item['executedBranches'], ] ); @@ -211,36 +253,58 @@ protected function renderFunctionOrMethodItem(\Text_Template $template, array $i return $this->renderItemTemplate( $template, [ - 'name' => \sprintf( + 'name' => \sprintf( '%s%s', $indent, $item['startLine'], \htmlspecialchars($item['signature'], $this->htmlSpecialCharsFlags), $item['functionName'] ?? $item['methodName'] ), - 'numMethods' => $numMethods, - 'numTestedMethods' => $numTestedMethods, - 'linesExecutedPercent' => Util::percent( + 'numMethods' => $numMethods, + 'numTestedMethods' => $numTestedMethods, + 'linesExecutedPercent' => Util::percent( $item['executedLines'], $item['executableLines'] ), - 'linesExecutedPercentAsString' => Util::percent( + 'linesExecutedPercentAsString' => Util::percent( $item['executedLines'], $item['executableLines'], true ), - 'numExecutedLines' => $item['executedLines'], - 'numExecutableLines' => $item['executableLines'], - 'testedMethodsPercent' => Util::percent( + 'numExecutedLines' => $item['executedLines'], + 'numExecutableLines' => $item['executableLines'], + 'testedMethodsPercent' => Util::percent( $numTestedMethods, 1 ), - 'testedMethodsPercentAsString' => Util::percent( + 'testedMethodsPercentAsString' => Util::percent( $numTestedMethods, 1, true ), - 'crap' => $item['crap'], + 'crap' => $item['crap'], + 'testedBranchesPercent' => Util::percent( + $item['executedBranches'], + $item['executableBranches'] + ), + 'testedBranchesPercentAsString' => Util::percent( + $item['executedBranches'], + $item['executableBranches'], + true + ), + 'testedPathsPercent' => Util::percent( + $item['executedPaths'], + $item['executablePaths'] + ), + 'testedPathsPercentAsString' => Util::percent( + $item['executedPaths'], + $item['executablePaths'], + true + ), + 'numExecutablePaths' => $item['executablePaths'], + 'numExecutedPaths' => $item['executedPaths'], + 'numExecutableBranches' => $item['executableBranches'], + 'numExecutedBranches' => $item['executedBranches'], ] ); } @@ -258,12 +322,12 @@ protected function renderSource(FileNode $node): string $popoverContent = ''; $popoverTitle = ''; - if (\array_key_exists($i, $coverageData)) { - $numTests = ($coverageData[$i] ? \count($coverageData[$i]) : 0); + if (\array_key_exists($i, $coverageData['lines'])) { + $numTests = ($coverageData['lines'][$i] ? \count($coverageData['lines'][$i]['tests']) : 0); - if ($coverageData[$i] === null) { + if ($coverageData['lines'][$i]['tests'] === null) { $trClass = ' class="warning"'; - } elseif ($numTests == 0) { + } elseif ($numTests === 0) { $trClass = ' class="danger"'; } else { $lineCss = 'covered-by-large-tests'; @@ -275,10 +339,10 @@ protected function renderSource(FileNode $node): string $popoverTitle = '1 test covers line ' . $i; } - foreach ($coverageData[$i] as $test) { - if ($lineCss == 'covered-by-large-tests' && $testData[$test]['size'] == 'medium') { + foreach ($coverageData['lines'][$i]['tests'] as $test) { + if ($lineCss === 'covered-by-large-tests' && $testData[$test]['size'] === 'medium') { $lineCss = 'covered-by-medium-tests'; - } elseif ($testData[$test]['size'] == 'small') { + } elseif ($testData[$test]['size'] === 'small') { $lineCss = 'covered-by-small-tests'; } diff --git a/src/Report/Html/Renderer/Template/directory_branch.html.dist b/src/Report/Html/Renderer/Template/directory_branch.html.dist new file mode 100644 index 000000000..3d957580f --- /dev/null +++ b/src/Report/Html/Renderer/Template/directory_branch.html.dist @@ -0,0 +1,61 @@ + + +
+ +| + | Code Coverage |
+ |||||||||||
| + | Lines |
+ Branches |
+ Functions and Methods |
+ Classes and Traits |
+ ||||||||
| + | Code Coverage |
+ ||||||||||||
| + | Classes and Traits |
+ Functions and Methods |
+ Branches |
+ Lines |
+ |||||||||