Chris@14: Chris@14: * Chris@14: * For the full copyright and license information, please view the LICENSE Chris@14: * file that was distributed with this source code. Chris@14: */ Chris@14: Chris@14: namespace SebastianBergmann\CodeCoverage\Report; Chris@14: Chris@14: use SebastianBergmann\CodeCoverage\CodeCoverage; Chris@14: use SebastianBergmann\CodeCoverage\Node\File; Chris@14: use SebastianBergmann\CodeCoverage\Util; Chris@14: Chris@14: /** Chris@14: * Generates human readable output from a code coverage object. Chris@14: * Chris@14: * The output gets put into a text file our written to the CLI. Chris@14: */ Chris@14: class Text Chris@14: { Chris@14: private $lowUpperBound; Chris@14: private $highLowerBound; Chris@14: private $showUncoveredFiles; Chris@14: private $showOnlySummary; Chris@14: Chris@14: private $colors = [ Chris@14: 'green' => "\x1b[30;42m", Chris@14: 'yellow' => "\x1b[30;43m", Chris@14: 'red' => "\x1b[37;41m", Chris@14: 'header' => "\x1b[1;37;40m", Chris@14: 'reset' => "\x1b[0m", Chris@14: 'eol' => "\x1b[2K", Chris@14: ]; Chris@14: Chris@14: /** Chris@14: * @param int $lowUpperBound Chris@14: * @param int $highLowerBound Chris@14: * @param bool $showUncoveredFiles Chris@14: * @param bool $showOnlySummary Chris@14: */ Chris@14: public function __construct($lowUpperBound = 50, $highLowerBound = 90, $showUncoveredFiles = false, $showOnlySummary = false) Chris@14: { Chris@14: $this->lowUpperBound = $lowUpperBound; Chris@14: $this->highLowerBound = $highLowerBound; Chris@14: $this->showUncoveredFiles = $showUncoveredFiles; Chris@14: $this->showOnlySummary = $showOnlySummary; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param CodeCoverage $coverage Chris@14: * @param bool $showColors Chris@14: * Chris@14: * @return string Chris@14: */ Chris@14: public function process(CodeCoverage $coverage, $showColors = false) Chris@14: { Chris@14: $output = PHP_EOL . PHP_EOL; Chris@14: $report = $coverage->getReport(); Chris@14: unset($coverage); Chris@14: Chris@14: $colors = [ Chris@14: 'header' => '', Chris@14: 'classes' => '', Chris@14: 'methods' => '', Chris@14: 'lines' => '', Chris@14: 'reset' => '', Chris@14: 'eol' => '' Chris@14: ]; Chris@14: Chris@14: if ($showColors) { Chris@14: $colors['classes'] = $this->getCoverageColor( Chris@14: $report->getNumTestedClassesAndTraits(), Chris@14: $report->getNumClassesAndTraits() Chris@14: ); Chris@14: $colors['methods'] = $this->getCoverageColor( Chris@14: $report->getNumTestedMethods(), Chris@14: $report->getNumMethods() Chris@14: ); Chris@14: $colors['lines'] = $this->getCoverageColor( Chris@14: $report->getNumExecutedLines(), Chris@14: $report->getNumExecutableLines() Chris@14: ); Chris@14: $colors['reset'] = $this->colors['reset']; Chris@14: $colors['header'] = $this->colors['header']; Chris@14: $colors['eol'] = $this->colors['eol']; Chris@14: } Chris@14: Chris@14: $classes = \sprintf( Chris@14: ' Classes: %6s (%d/%d)', Chris@14: Util::percent( Chris@14: $report->getNumTestedClassesAndTraits(), Chris@14: $report->getNumClassesAndTraits(), Chris@14: true Chris@14: ), Chris@14: $report->getNumTestedClassesAndTraits(), Chris@14: $report->getNumClassesAndTraits() Chris@14: ); Chris@14: Chris@14: $methods = \sprintf( Chris@14: ' Methods: %6s (%d/%d)', Chris@14: Util::percent( Chris@14: $report->getNumTestedMethods(), Chris@14: $report->getNumMethods(), Chris@14: true Chris@14: ), Chris@14: $report->getNumTestedMethods(), Chris@14: $report->getNumMethods() Chris@14: ); Chris@14: Chris@14: $lines = \sprintf( Chris@14: ' Lines: %6s (%d/%d)', Chris@14: Util::percent( Chris@14: $report->getNumExecutedLines(), Chris@14: $report->getNumExecutableLines(), Chris@14: true Chris@14: ), Chris@14: $report->getNumExecutedLines(), Chris@14: $report->getNumExecutableLines() Chris@14: ); Chris@14: Chris@14: $padding = \max(\array_map('strlen', [$classes, $methods, $lines])); Chris@14: Chris@14: if ($this->showOnlySummary) { Chris@14: $title = 'Code Coverage Report Summary:'; Chris@14: $padding = \max($padding, \strlen($title)); Chris@14: Chris@14: $output .= $this->format($colors['header'], $padding, $title); Chris@14: } else { Chris@14: $date = \date(' Y-m-d H:i:s', $_SERVER['REQUEST_TIME']); Chris@14: $title = 'Code Coverage Report:'; Chris@14: Chris@14: $output .= $this->format($colors['header'], $padding, $title); Chris@14: $output .= $this->format($colors['header'], $padding, $date); Chris@14: $output .= $this->format($colors['header'], $padding, ''); Chris@14: $output .= $this->format($colors['header'], $padding, ' Summary:'); Chris@14: } Chris@14: Chris@14: $output .= $this->format($colors['classes'], $padding, $classes); Chris@14: $output .= $this->format($colors['methods'], $padding, $methods); Chris@14: $output .= $this->format($colors['lines'], $padding, $lines); Chris@14: Chris@14: if ($this->showOnlySummary) { Chris@14: return $output . PHP_EOL; Chris@14: } Chris@14: Chris@14: $classCoverage = []; Chris@14: Chris@14: foreach ($report as $item) { Chris@14: if (!$item instanceof File) { Chris@14: continue; Chris@14: } Chris@14: Chris@14: $classes = $item->getClassesAndTraits(); Chris@14: Chris@14: foreach ($classes as $className => $class) { Chris@14: $classStatements = 0; Chris@14: $coveredClassStatements = 0; Chris@14: $coveredMethods = 0; Chris@14: $classMethods = 0; Chris@14: Chris@14: foreach ($class['methods'] as $method) { Chris@14: if ($method['executableLines'] == 0) { Chris@14: continue; Chris@14: } Chris@14: Chris@14: $classMethods++; Chris@14: $classStatements += $method['executableLines']; Chris@14: $coveredClassStatements += $method['executedLines']; Chris@14: if ($method['coverage'] == 100) { Chris@14: $coveredMethods++; Chris@14: } Chris@14: } Chris@14: Chris@14: if (!empty($class['package']['namespace'])) { Chris@14: $namespace = '\\' . $class['package']['namespace'] . '::'; Chris@14: } elseif (!empty($class['package']['fullPackage'])) { Chris@14: $namespace = '@' . $class['package']['fullPackage'] . '::'; Chris@14: } else { Chris@14: $namespace = ''; Chris@14: } Chris@14: Chris@14: $classCoverage[$namespace . $className] = [ Chris@14: 'namespace' => $namespace, Chris@14: 'className ' => $className, Chris@14: 'methodsCovered' => $coveredMethods, Chris@14: 'methodCount' => $classMethods, Chris@14: 'statementsCovered' => $coveredClassStatements, Chris@14: 'statementCount' => $classStatements, Chris@14: ]; Chris@14: } Chris@14: } Chris@14: Chris@14: \ksort($classCoverage); Chris@14: Chris@14: $methodColor = ''; Chris@14: $linesColor = ''; Chris@14: $resetColor = ''; Chris@14: Chris@14: foreach ($classCoverage as $fullQualifiedPath => $classInfo) { Chris@14: if ($classInfo['statementsCovered'] != 0 || Chris@14: $this->showUncoveredFiles) { Chris@14: if ($showColors) { Chris@14: $methodColor = $this->getCoverageColor($classInfo['methodsCovered'], $classInfo['methodCount']); Chris@14: $linesColor = $this->getCoverageColor($classInfo['statementsCovered'], $classInfo['statementCount']); Chris@14: $resetColor = $colors['reset']; Chris@14: } Chris@14: Chris@14: $output .= PHP_EOL . $fullQualifiedPath . PHP_EOL Chris@14: . ' ' . $methodColor . 'Methods: ' . $this->printCoverageCounts($classInfo['methodsCovered'], $classInfo['methodCount'], 2) . $resetColor . ' ' Chris@14: . ' ' . $linesColor . 'Lines: ' . $this->printCoverageCounts($classInfo['statementsCovered'], $classInfo['statementCount'], 3) . $resetColor Chris@14: ; Chris@14: } Chris@14: } Chris@14: Chris@14: return $output . PHP_EOL; Chris@14: } Chris@14: Chris@14: protected function getCoverageColor($numberOfCoveredElements, $totalNumberOfElements) Chris@14: { Chris@14: $coverage = Util::percent( Chris@14: $numberOfCoveredElements, Chris@14: $totalNumberOfElements Chris@14: ); Chris@14: Chris@14: if ($coverage >= $this->highLowerBound) { Chris@14: return $this->colors['green']; Chris@14: } elseif ($coverage > $this->lowUpperBound) { Chris@14: return $this->colors['yellow']; Chris@14: } Chris@14: Chris@14: return $this->colors['red']; Chris@14: } Chris@14: Chris@14: protected function printCoverageCounts($numberOfCoveredElements, $totalNumberOfElements, $precision) Chris@14: { Chris@14: $format = '%' . $precision . 's'; Chris@14: Chris@14: return Util::percent( Chris@14: $numberOfCoveredElements, Chris@14: $totalNumberOfElements, Chris@14: true, Chris@14: true Chris@14: ) . Chris@14: ' (' . \sprintf($format, $numberOfCoveredElements) . '/' . Chris@14: \sprintf($format, $totalNumberOfElements) . ')'; Chris@14: } Chris@14: Chris@14: private function format($color, $padding, $string) Chris@14: { Chris@14: $reset = $color ? $this->colors['reset'] : ''; Chris@14: Chris@14: return $color . \str_pad($string, $padding) . $reset . PHP_EOL; Chris@14: } Chris@14: }