diff --git a/src/CodeCoverage.php b/src/CodeCoverage.php index f87a15aba..a6dd15e92 100644 --- a/src/CodeCoverage.php +++ b/src/CodeCoverage.php @@ -619,11 +619,13 @@ private function initializeFilesThatAreSeenTheFirstTime(array $data): void foreach ($fileData['lines'] as $lineNumber => $flag) { if ($flag === Driver::LINE_NOT_EXECUTABLE) { $this->data[$file]['lines'][$lineNumber] = null; + } else { + $this->addCoverageLinePathCovered($file, $lineNumber, false); } + } foreach ($fileData['functions'] as $functionName => $functionData) { - // @todo - should this have a helper to merge covered paths? $this->data[$file]['paths'][$functionName] = $functionData['paths']; foreach ($functionData['branches'] as $branchIndex => $branchData) { @@ -837,7 +839,6 @@ private function addUncoveredFilesFromWhitelist(): void for ($line = 1; $line <= $lines; $line++) { $data[$uncoveredFile]['lines'][$line] = Driver::LINE_NOT_EXECUTED; } - // @todo - do the same here with functions and paths } $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); @@ -1190,9 +1191,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/Node/File.php b/src/Node/File.php index 74087de78..7ff0fa9f2 100644 --- a/src/Node/File.php +++ b/src/Node/File.php @@ -377,16 +377,21 @@ private function calculateStatistics(): void unset($tokens); foreach (\range(1, $this->linesOfCode['loc']) as $lineNumber) { - if (isset($this->coverageData['lines'][$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['lines'][$lineNumber]) > 0) { + if ($this->coverageData['lines'][$lineNumber]['pathCovered'] === true) { foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) { $codeUnit['executedLines']++; } @@ -472,6 +477,19 @@ private function calcAndApplyClassAggregate( foreach ($classOrTrait['methods'] as &$method) { $methodName = $method['methodName']; + if ($method['executableLines'] > 0) { + $method['coverage'] = ($method['executedLines'] / $method['executableLines']) * 100; + } else { + $method['coverage'] = 100; + } + + $method['crap'] = $this->crap( + $method['ccn'], + $method['coverage'] + ); + + $classOrTrait['ccn'] += $method['ccn']; + if (isset($this->coverageData['paths'])) { $methodCoveragePath = $methodName; @@ -499,19 +517,6 @@ private function calcAndApplyClassAggregate( $this->numTestedPaths += $numExexutedPaths; } - if ($method['executableLines'] > 0) { - $method['coverage'] = ($method['executedLines'] / - $method['executableLines']) * 100; - } else { - $method['coverage'] = 100; - } - - $method['crap'] = $this->crap( - $method['ccn'], - $method['coverage'] - ); - - $classOrTrait['ccn'] += $method['ccn']; } if (isset($this->coverageData['branches'])) { diff --git a/src/Report/Text.php b/src/Report/Text.php index 82a98cf84..3dd639faf 100644 --- a/src/Report/Text.php +++ b/src/Report/Text.php @@ -70,12 +70,18 @@ final class Text */ private $showOnlySummary; - public function __construct(int $lowUpperBound = 50, int $highLowerBound = 90, bool $showUncoveredFiles = false, bool $showOnlySummary = false) + /** + * @var bool + */ + private $determineBranchCoverage; + + public function __construct(int $lowUpperBound = 50, int $highLowerBound = 90, bool $showUncoveredFiles = false, bool $showOnlySummary = false, bool $determineBranchCoverage = false) { - $this->lowUpperBound = $lowUpperBound; - $this->highLowerBound = $highLowerBound; - $this->showUncoveredFiles = $showUncoveredFiles; - $this->showOnlySummary = $showOnlySummary; + $this->lowUpperBound = $lowUpperBound; + $this->highLowerBound = $highLowerBound; + $this->showUncoveredFiles = $showUncoveredFiles; + $this->showOnlySummary = $showOnlySummary; + $this->determineBranchCoverage = $determineBranchCoverage; } public function process(CodeCoverage $coverage, bool $showColors = false): string @@ -84,12 +90,14 @@ public function process(CodeCoverage $coverage, bool $showColors = false): strin $report = $coverage->getReport(); $colors = [ - 'header' => '', - 'classes' => '', - 'methods' => '', - 'lines' => '', - 'reset' => '', - 'eol' => '', + 'header' => '', + 'classes' => '', + 'methods' => '', + 'lines' => '', + 'branches' => '', + 'paths' => '', + 'reset' => '', + 'eol' => '', ]; if ($showColors) { @@ -108,13 +116,25 @@ public function process(CodeCoverage $coverage, bool $showColors = false): strin $report->getNumExecutableLines() ); + if ($this->determineBranchCoverage) { + $colors['branches'] = $this->getCoverageColor( + $report->getNumTestedBranches(), + $report->getNumBranches() + ); + + $colors['paths'] = $this->getCoverageColor( + $report->getNumTestedPaths(), + $report->getNumPaths() + ); + } + $colors['reset'] = self::COLOR_RESET; $colors['header'] = self::COLOR_HEADER; $colors['eol'] = self::COLOR_EOL; } $classes = \sprintf( - ' Classes: %6s (%d/%d)', + ' Classes: %6s (%d/%d)', Util::percent( $report->getNumTestedClassesAndTraits(), $report->getNumClassesAndTraits(), @@ -125,7 +145,7 @@ public function process(CodeCoverage $coverage, bool $showColors = false): strin ); $methods = \sprintf( - ' Methods: %6s (%d/%d)', + ' Methods: %6s (%d/%d)', Util::percent( $report->getNumTestedMethods(), $report->getNumMethods(), @@ -136,7 +156,7 @@ public function process(CodeCoverage $coverage, bool $showColors = false): strin ); $lines = \sprintf( - ' Lines: %6s (%d/%d)', + ' Lines: %6s (%d/%d)', Util::percent( $report->getNumExecutedLines(), $report->getNumExecutableLines(), @@ -146,6 +166,33 @@ public function process(CodeCoverage $coverage, bool $showColors = false): strin $report->getNumExecutableLines() ); + $paths = ''; + $branches = ''; + + if ($this->determineBranchCoverage) { + $branches = \sprintf( + ' Branches: %6s (%d/%d)', + Util::percent( + $report->getNumTestedBranches(), + $report->getNumBranches(), + true + ), + $report->getNumTestedBranches(), + $report->getNumBranches() + ); + + $paths = \sprintf( + ' Paths: %6s (%d/%d)', + Util::percent( + $report->getNumTestedPaths(), + $report->getNumPaths(), + true + ), + $report->getNumTestedPaths(), + $report->getNumPaths() + ); + } + $padding = \max(\array_map('strlen', [$classes, $methods, $lines])); if ($this->showOnlySummary) { @@ -167,12 +214,22 @@ public function process(CodeCoverage $coverage, bool $showColors = false): strin $output .= $this->format($colors['methods'], $padding, $methods); $output .= $this->format($colors['lines'], $padding, $lines); + if ($this->determineBranchCoverage) { + $output .= $this->format($colors['branches'], $padding, $branches); + $output .= $this->format($colors['paths'], $padding, $paths); + } + if ($this->showOnlySummary) { return $output . \PHP_EOL; } $classCoverage = []; + $maxMethods = 0; + $maxLines = 0; + $maxBranches = 0; + $maxPaths = 0; + foreach ($report as $item) { if (!$item instanceof File) { continue; @@ -185,9 +242,13 @@ public function process(CodeCoverage $coverage, bool $showColors = false): strin $coveredClassStatements = 0; $coveredMethods = 0; $classMethods = 0; + $classPaths = 0; + $coveredClassPaths = 0; + $classBranches = 0; + $coveredClassBranches = 0; foreach ($class['methods'] as $method) { - if ($method['executableLines'] == 0) { + if ($method['executableLines'] === 0) { continue; } @@ -195,11 +256,42 @@ public function process(CodeCoverage $coverage, bool $showColors = false): strin $classStatements += $method['executableLines']; $coveredClassStatements += $method['executedLines']; - if ($method['coverage'] == 100) { + if ($this->determineBranchCoverage) { + $classPaths += $method['executablePaths']; + $coveredClassPaths += $method['executedPaths']; + $classBranches += $method['executableBranches']; + $coveredClassBranches += $method['executedBranches']; + } + + if ($method['coverage'] === 100) { $coveredMethods++; } } + $maxMethods = \max( + $maxMethods, + \strlen((string) $classMethods), + \strlen((string) $coveredMethods) + ); + $maxLines = \max( + $maxLines, + \strlen((string) $classStatements), + \strlen((string) $coveredClassStatements) + ); + + if ($this->determineBranchCoverage) { + $maxBranches = \max( + $maxBranches, + \strlen((string) $classBranches), + \strlen((string) $coveredClassBranches) + ); + $maxPaths = \max( + $maxPaths, + \strlen((string) $classPaths), + \strlen((string) $coveredClassPaths) + ); + } + $namespace = ''; if (!empty($class['package']['namespace'])) { @@ -215,27 +307,44 @@ public function process(CodeCoverage $coverage, bool $showColors = false): strin 'methodCount' => $classMethods, 'statementsCovered' => $coveredClassStatements, 'statementCount' => $classStatements, + 'pathsCovered' => $coveredClassPaths, + 'pathCount' => $classPaths, + 'branchesCovered' => $coveredClassBranches, + 'branchCount' => $classBranches, ]; } } \ksort($classCoverage); - $methodColor = ''; - $linesColor = ''; - $resetColor = ''; + $methodColor = ''; + $linesColor = ''; + $resetColor = ''; + $pathsColor = ''; + $branchesColor = ''; foreach ($classCoverage as $fullQualifiedPath => $classInfo) { - if ($this->showUncoveredFiles || $classInfo['statementsCovered'] != 0) { + if ($this->showUncoveredFiles || $classInfo['statementsCovered'] !== 0) { if ($showColors) { - $methodColor = $this->getCoverageColor($classInfo['methodsCovered'], $classInfo['methodCount']); - $linesColor = $this->getCoverageColor($classInfo['statementsCovered'], $classInfo['statementCount']); - $resetColor = $colors['reset']; + $methodColor = $this->getCoverageColor($classInfo['methodsCovered'], $classInfo['methodCount']); + $linesColor = $this->getCoverageColor($classInfo['statementsCovered'], $classInfo['statementCount']); + + if ($this->determineBranchCoverage) { + $branchesColor = $this->getCoverageColor($classInfo['branchesCovered'], $classInfo['branchCount']); + $pathsColor = $this->getCoverageColor($classInfo['pathsCovered'], $classInfo['pathCount']); + } + $resetColor = $colors['reset']; } $output .= \PHP_EOL . $fullQualifiedPath . \PHP_EOL - . ' ' . $methodColor . 'Methods: ' . $this->printCoverageCounts($classInfo['methodsCovered'], $classInfo['methodCount'], 2) . $resetColor . ' ' - . ' ' . $linesColor . 'Lines: ' . $this->printCoverageCounts($classInfo['statementsCovered'], $classInfo['statementCount'], 3) . $resetColor; + . ' ' . $methodColor . 'Methods: ' . $this->printCoverageCounts($classInfo['methodsCovered'], $classInfo['methodCount'], $maxMethods) . $resetColor . ' ' + . ' ' . $linesColor . 'Lines: ' . $this->printCoverageCounts($classInfo['statementsCovered'], $classInfo['statementCount'], $maxLines) . $resetColor; + + if ($this->determineBranchCoverage) { + $output .= '' + . ' ' . $branchesColor . 'Branches: ' . $this->printCoverageCounts($classInfo['branchesCovered'], $classInfo['branchCount'], $maxBranches) . $resetColor . ' ' + . ' ' . $pathsColor . 'Paths: ' . $this->printCoverageCounts($classInfo['pathsCovered'], $classInfo['pathCount'], $maxPaths) . $resetColor; + } } } diff --git a/src/Util.php b/src/Util.php index 71ec8e0bd..8e81c8c61 100644 --- a/src/Util.php +++ b/src/Util.php @@ -20,7 +20,7 @@ final class Util public static function percent(float $a, float $b, bool $asString = false, bool $fixedWidth = false) { if ($asString && $b == 0) { - return ''; + return $fixedWidth ? ' ' : ''; } $percent = 100;