1<?php
2/*
3 * This file is part of the php-code-coverage package.
4 *
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11namespace SebastianBergmann\CodeCoverage\Report;
12
13use SebastianBergmann\CodeCoverage\CodeCoverage;
14use SebastianBergmann\CodeCoverage\Node\File;
15use SebastianBergmann\CodeCoverage\Util;
16
17/**
18 * Generates human readable output from a code coverage object.
19 *
20 * The output gets put into a text file our written to the CLI.
21 */
22class Text
23{
24    private $lowUpperBound;
25    private $highLowerBound;
26    private $showUncoveredFiles;
27    private $showOnlySummary;
28
29    private $colors = [
30        'green'  => "\x1b[30;42m",
31        'yellow' => "\x1b[30;43m",
32        'red'    => "\x1b[37;41m",
33        'header' => "\x1b[1;37;40m",
34        'reset'  => "\x1b[0m",
35        'eol'    => "\x1b[2K",
36    ];
37
38    /**
39     * @param int  $lowUpperBound
40     * @param int  $highLowerBound
41     * @param bool $showUncoveredFiles
42     * @param bool $showOnlySummary
43     */
44    public function __construct($lowUpperBound = 50, $highLowerBound = 90, $showUncoveredFiles = false, $showOnlySummary = false)
45    {
46        $this->lowUpperBound      = $lowUpperBound;
47        $this->highLowerBound     = $highLowerBound;
48        $this->showUncoveredFiles = $showUncoveredFiles;
49        $this->showOnlySummary    = $showOnlySummary;
50    }
51
52    /**
53     * @param CodeCoverage $coverage
54     * @param bool         $showColors
55     *
56     * @return string
57     */
58    public function process(CodeCoverage $coverage, $showColors = false)
59    {
60        $output = PHP_EOL . PHP_EOL;
61        $report = $coverage->getReport();
62        unset($coverage);
63
64        $colors = [
65            'header'  => '',
66            'classes' => '',
67            'methods' => '',
68            'lines'   => '',
69            'reset'   => '',
70            'eol'     => ''
71        ];
72
73        if ($showColors) {
74            $colors['classes'] = $this->getCoverageColor(
75                $report->getNumTestedClassesAndTraits(),
76                $report->getNumClassesAndTraits()
77            );
78            $colors['methods'] = $this->getCoverageColor(
79                $report->getNumTestedMethods(),
80                $report->getNumMethods()
81            );
82            $colors['lines']   = $this->getCoverageColor(
83                $report->getNumExecutedLines(),
84                $report->getNumExecutableLines()
85            );
86            $colors['reset']   = $this->colors['reset'];
87            $colors['header']  = $this->colors['header'];
88            $colors['eol']     = $this->colors['eol'];
89        }
90
91        $classes = sprintf(
92            '  Classes: %6s (%d/%d)',
93            Util::percent(
94                $report->getNumTestedClassesAndTraits(),
95                $report->getNumClassesAndTraits(),
96                true
97            ),
98            $report->getNumTestedClassesAndTraits(),
99            $report->getNumClassesAndTraits()
100        );
101
102        $methods = sprintf(
103            '  Methods: %6s (%d/%d)',
104            Util::percent(
105                $report->getNumTestedMethods(),
106                $report->getNumMethods(),
107                true
108            ),
109            $report->getNumTestedMethods(),
110            $report->getNumMethods()
111        );
112
113        $lines = sprintf(
114            '  Lines:   %6s (%d/%d)',
115            Util::percent(
116                $report->getNumExecutedLines(),
117                $report->getNumExecutableLines(),
118                true
119            ),
120            $report->getNumExecutedLines(),
121            $report->getNumExecutableLines()
122        );
123
124        $padding = max(array_map('strlen', [$classes, $methods, $lines]));
125
126        if ($this->showOnlySummary) {
127            $title   = 'Code Coverage Report Summary:';
128            $padding = max($padding, strlen($title));
129
130            $output .= $this->format($colors['header'], $padding, $title);
131        } else {
132            $date  = date('  Y-m-d H:i:s', $_SERVER['REQUEST_TIME']);
133            $title = 'Code Coverage Report:';
134
135            $output .= $this->format($colors['header'], $padding, $title);
136            $output .= $this->format($colors['header'], $padding, $date);
137            $output .= $this->format($colors['header'], $padding, '');
138            $output .= $this->format($colors['header'], $padding, ' Summary:');
139        }
140
141        $output .= $this->format($colors['classes'], $padding, $classes);
142        $output .= $this->format($colors['methods'], $padding, $methods);
143        $output .= $this->format($colors['lines'], $padding, $lines);
144
145        if ($this->showOnlySummary) {
146            return $output . PHP_EOL;
147        }
148
149        $classCoverage = [];
150
151        foreach ($report as $item) {
152            if (!$item instanceof File) {
153                continue;
154            }
155
156            $classes = $item->getClassesAndTraits();
157
158            foreach ($classes as $className => $class) {
159                $classStatements        = 0;
160                $coveredClassStatements = 0;
161                $coveredMethods         = 0;
162                $classMethods           = 0;
163
164                foreach ($class['methods'] as $method) {
165                    if ($method['executableLines'] == 0) {
166                        continue;
167                    }
168
169                    $classMethods++;
170                    $classStatements        += $method['executableLines'];
171                    $coveredClassStatements += $method['executedLines'];
172                    if ($method['coverage'] == 100) {
173                        $coveredMethods++;
174                    }
175                }
176
177                if (!empty($class['package']['namespace'])) {
178                    $namespace = '\\' . $class['package']['namespace'] . '::';
179                } elseif (!empty($class['package']['fullPackage'])) {
180                    $namespace = '@' . $class['package']['fullPackage'] . '::';
181                } else {
182                    $namespace = '';
183                }
184
185                $classCoverage[$namespace . $className] = [
186                    'namespace'         => $namespace,
187                    'className '        => $className,
188                    'methodsCovered'    => $coveredMethods,
189                    'methodCount'       => $classMethods,
190                    'statementsCovered' => $coveredClassStatements,
191                    'statementCount'    => $classStatements,
192                ];
193            }
194        }
195
196        ksort($classCoverage);
197
198        $methodColor = '';
199        $linesColor  = '';
200        $resetColor  = '';
201
202        foreach ($classCoverage as $fullQualifiedPath => $classInfo) {
203            if ($classInfo['statementsCovered'] != 0 ||
204                $this->showUncoveredFiles) {
205                if ($showColors) {
206                    $methodColor = $this->getCoverageColor($classInfo['methodsCovered'], $classInfo['methodCount']);
207                    $linesColor  = $this->getCoverageColor($classInfo['statementsCovered'], $classInfo['statementCount']);
208                    $resetColor  = $colors['reset'];
209                }
210
211                $output .= PHP_EOL . $fullQualifiedPath . PHP_EOL
212                    . '  ' . $methodColor . 'Methods: ' . $this->printCoverageCounts($classInfo['methodsCovered'], $classInfo['methodCount'], 2) . $resetColor . ' '
213                    . '  ' . $linesColor . 'Lines: ' . $this->printCoverageCounts($classInfo['statementsCovered'], $classInfo['statementCount'], 3) . $resetColor
214                ;
215            }
216        }
217
218        return $output . PHP_EOL;
219    }
220
221    protected function getCoverageColor($numberOfCoveredElements, $totalNumberOfElements)
222    {
223        $coverage = Util::percent(
224            $numberOfCoveredElements,
225            $totalNumberOfElements
226        );
227
228        if ($coverage >= $this->highLowerBound) {
229            return $this->colors['green'];
230        } elseif ($coverage > $this->lowUpperBound) {
231            return $this->colors['yellow'];
232        }
233
234        return $this->colors['red'];
235    }
236
237    protected function printCoverageCounts($numberOfCoveredElements, $totalNumberOfElements, $precision)
238    {
239        $format = '%' . $precision . 's';
240
241        return Util::percent(
242            $numberOfCoveredElements,
243            $totalNumberOfElements,
244            true,
245            true
246        ) .
247        ' (' . sprintf($format, $numberOfCoveredElements) . '/' .
248        sprintf($format, $totalNumberOfElements) . ')';
249    }
250
251    private function format($color, $padding, $string)
252    {
253        $reset = $color ? $this->colors['reset'] : '';
254
255        return $color . str_pad($string, $padding) . $reset . PHP_EOL;
256    }
257}
258