| 148 |
lars |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
declare(strict_types=1);
|
|
|
4 |
|
|
|
5 |
namespace NunoMaduro\Collision;
|
|
|
6 |
|
|
|
7 |
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
|
|
8 |
use SebastianBergmann\CodeCoverage\Node\Directory;
|
|
|
9 |
use SebastianBergmann\CodeCoverage\Node\File;
|
|
|
10 |
use SebastianBergmann\Environment\Runtime;
|
|
|
11 |
use Symfony\Component\Console\Output\OutputInterface;
|
|
|
12 |
use Symfony\Component\Console\Terminal;
|
|
|
13 |
|
|
|
14 |
/**
|
|
|
15 |
* @internal
|
|
|
16 |
*/
|
|
|
17 |
final class Coverage
|
|
|
18 |
{
|
|
|
19 |
/**
|
|
|
20 |
* Returns the coverage path.
|
|
|
21 |
*/
|
|
|
22 |
public static function getPath(): string
|
|
|
23 |
{
|
|
|
24 |
return implode(DIRECTORY_SEPARATOR, [
|
|
|
25 |
dirname(__DIR__),
|
|
|
26 |
'.temp',
|
|
|
27 |
'coverage',
|
|
|
28 |
]);
|
|
|
29 |
}
|
|
|
30 |
|
|
|
31 |
/**
|
|
|
32 |
* Runs true there is any code coverage driver available.
|
|
|
33 |
*/
|
|
|
34 |
public static function isAvailable(): bool
|
|
|
35 |
{
|
|
|
36 |
if (! (new Runtime())->canCollectCodeCoverage()) {
|
|
|
37 |
return false;
|
|
|
38 |
}
|
|
|
39 |
|
|
|
40 |
if (static::usingXdebug()) {
|
|
|
41 |
$mode = getenv('XDEBUG_MODE') ?: ini_get('xdebug.mode');
|
|
|
42 |
|
|
|
43 |
return $mode && in_array('coverage', explode(',', $mode), true);
|
|
|
44 |
}
|
|
|
45 |
|
|
|
46 |
return true;
|
|
|
47 |
}
|
|
|
48 |
|
|
|
49 |
/**
|
|
|
50 |
* If the user is using Xdebug.
|
|
|
51 |
*/
|
|
|
52 |
public static function usingXdebug(): bool
|
|
|
53 |
{
|
|
|
54 |
return (new Runtime())->hasXdebug();
|
|
|
55 |
}
|
|
|
56 |
|
|
|
57 |
/**
|
|
|
58 |
* Reports the code coverage report to the
|
|
|
59 |
* console and returns the result in float.
|
|
|
60 |
*/
|
|
|
61 |
public static function report(OutputInterface $output): float
|
|
|
62 |
{
|
|
|
63 |
if (! file_exists($reportPath = self::getPath())) {
|
|
|
64 |
if (self::usingXdebug()) {
|
|
|
65 |
$output->writeln(
|
|
|
66 |
" <fg=black;bg=yellow;options=bold> WARN </> Unable to get coverage using Xdebug. Did you set <href=https://xdebug.org/docs/code_coverage#mode>Xdebug's coverage mode</>?</>",
|
|
|
67 |
);
|
|
|
68 |
|
|
|
69 |
return 0.0;
|
|
|
70 |
}
|
|
|
71 |
|
|
|
72 |
$output->writeln(
|
|
|
73 |
' <fg=black;bg=yellow;options=bold> WARN </> No coverage driver detected.</>',
|
|
|
74 |
);
|
|
|
75 |
|
|
|
76 |
return 0.0;
|
|
|
77 |
}
|
|
|
78 |
|
|
|
79 |
/** @var CodeCoverage $codeCoverage */
|
|
|
80 |
$codeCoverage = require $reportPath;
|
|
|
81 |
unlink($reportPath);
|
|
|
82 |
|
|
|
83 |
$totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines();
|
|
|
84 |
|
|
|
85 |
$totalWidth = (new Terminal())->getWidth();
|
|
|
86 |
|
|
|
87 |
$dottedLineLength = $totalWidth;
|
|
|
88 |
|
|
|
89 |
/** @var Directory<File|Directory> $report */
|
|
|
90 |
$report = $codeCoverage->getReport();
|
|
|
91 |
|
|
|
92 |
foreach ($report->getIterator() as $file) {
|
|
|
93 |
if (! $file instanceof File) {
|
|
|
94 |
continue;
|
|
|
95 |
}
|
|
|
96 |
$dirname = dirname($file->id());
|
|
|
97 |
$basename = basename($file->id(), '.php');
|
|
|
98 |
|
|
|
99 |
$name = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [
|
|
|
100 |
$dirname,
|
|
|
101 |
$basename,
|
|
|
102 |
]);
|
|
|
103 |
$rawName = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [
|
|
|
104 |
$dirname,
|
|
|
105 |
$basename,
|
|
|
106 |
]);
|
|
|
107 |
|
|
|
108 |
$linesExecutedTakenSize = 0;
|
|
|
109 |
|
|
|
110 |
if ($file->percentageOfExecutedLines()->asString() != '0.00%') {
|
|
|
111 |
$linesExecutedTakenSize = strlen($uncoveredLines = trim(implode(', ', self::getMissingCoverage($file)))) + 1;
|
|
|
112 |
$name .= sprintf(' <fg=red>%s</>', $uncoveredLines);
|
|
|
113 |
}
|
|
|
114 |
|
|
|
115 |
$percentage = $file->numberOfExecutableLines() === 0
|
|
|
116 |
? '100.0'
|
|
|
117 |
: number_format($file->percentageOfExecutedLines()->asFloat(), 1, '.', '');
|
|
|
118 |
|
|
|
119 |
$takenSize = strlen($rawName.$percentage) + 8 + $linesExecutedTakenSize; // adding 3 space and percent sign
|
|
|
120 |
|
|
|
121 |
$percentage = sprintf(
|
|
|
122 |
'<fg=%s%s>%s</>',
|
|
|
123 |
$percentage === '100.0' ? 'green' : ($percentage === '0.0' ? 'red' : 'yellow'),
|
|
|
124 |
$percentage === '100.0' ? ';options=bold' : '',
|
|
|
125 |
$percentage
|
|
|
126 |
);
|
|
|
127 |
|
|
|
128 |
$output->writeln(sprintf(
|
|
|
129 |
' <fg=white>%s</> <fg=#6C7280>%s</> %s <fg=#6C7280>%%</>',
|
|
|
130 |
$name,
|
|
|
131 |
str_repeat('.', max($dottedLineLength - $takenSize, 1)),
|
|
|
132 |
$percentage
|
|
|
133 |
));
|
|
|
134 |
}
|
|
|
135 |
|
|
|
136 |
$output->writeln('');
|
|
|
137 |
|
|
|
138 |
$rawName = 'Total Coverage';
|
|
|
139 |
|
|
|
140 |
$takenSize = strlen($rawName.$totalCoverage->asString()) + 6;
|
|
|
141 |
|
|
|
142 |
$output->writeln(sprintf(
|
|
|
143 |
' <fg=white;options=bold>%s</> <fg=#6C7280>%s</> %s <fg=#6C7280>%%</>',
|
|
|
144 |
$rawName,
|
|
|
145 |
str_repeat('.', max($dottedLineLength - $takenSize, 1)),
|
|
|
146 |
number_format($totalCoverage->asFloat(), 1, '.', '')
|
|
|
147 |
));
|
|
|
148 |
|
|
|
149 |
return $totalCoverage->asFloat();
|
|
|
150 |
}
|
|
|
151 |
|
|
|
152 |
/**
|
|
|
153 |
* Generates an array of missing coverage on the following format:.
|
|
|
154 |
*
|
|
|
155 |
* ```
|
|
|
156 |
* ['11', '20..25', '50', '60..80'];
|
|
|
157 |
* ```
|
|
|
158 |
*
|
|
|
159 |
* @param File $file
|
|
|
160 |
* @return array<int, string>
|
|
|
161 |
*/
|
|
|
162 |
public static function getMissingCoverage($file): array
|
|
|
163 |
{
|
|
|
164 |
$shouldBeNewLine = true;
|
|
|
165 |
|
|
|
166 |
$eachLine = function (array $array, array $tests, int $line) use (&$shouldBeNewLine): array {
|
|
|
167 |
if (count($tests) > 0) {
|
|
|
168 |
$shouldBeNewLine = true;
|
|
|
169 |
|
|
|
170 |
return $array;
|
|
|
171 |
}
|
|
|
172 |
|
|
|
173 |
if ($shouldBeNewLine) {
|
|
|
174 |
$array[] = (string) $line;
|
|
|
175 |
$shouldBeNewLine = false;
|
|
|
176 |
|
|
|
177 |
return $array;
|
|
|
178 |
}
|
|
|
179 |
|
|
|
180 |
$lastKey = count($array) - 1;
|
|
|
181 |
|
|
|
182 |
if (array_key_exists($lastKey, $array) && str_contains($array[$lastKey], '..')) {
|
|
|
183 |
[$from] = explode('..', $array[$lastKey]);
|
|
|
184 |
$array[$lastKey] = $line > $from ? sprintf('%s..%s', $from, $line) : sprintf('%s..%s', $line, $from);
|
|
|
185 |
|
|
|
186 |
return $array;
|
|
|
187 |
}
|
|
|
188 |
|
|
|
189 |
$array[$lastKey] = sprintf('%s..%s', $array[$lastKey], $line);
|
|
|
190 |
|
|
|
191 |
return $array;
|
|
|
192 |
};
|
|
|
193 |
|
|
|
194 |
$array = [];
|
|
|
195 |
foreach (array_filter($file->lineCoverageData(), 'is_array') as $line => $tests) {
|
|
|
196 |
$array = $eachLine($array, $tests, $line);
|
|
|
197 |
}
|
|
|
198 |
|
|
|
199 |
return $array;
|
|
|
200 |
}
|
|
|
201 |
}
|