| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- <?php
- declare(strict_types=1);
- namespace NunoMaduro\Collision;
- use SebastianBergmann\CodeCoverage\CodeCoverage;
- use SebastianBergmann\CodeCoverage\Node\Directory;
- use SebastianBergmann\CodeCoverage\Node\File;
- use SebastianBergmann\Environment\Runtime;
- use Symfony\Component\Console\Output\OutputInterface;
- use function Termwind\render;
- use function Termwind\renderUsing;
- use function Termwind\terminal;
- /**
- * @internal
- */
- final class Coverage
- {
- /**
- * Returns the coverage path.
- */
- public static function getPath(): string
- {
- return implode(DIRECTORY_SEPARATOR, [
- dirname(__DIR__),
- '.temp',
- 'coverage',
- ]);
- }
- /**
- * Runs true there is any code coverage driver available.
- */
- public static function isAvailable(): bool
- {
- $runtime = new Runtime;
- if (! $runtime->canCollectCodeCoverage()) {
- return false;
- }
- if ($runtime->hasPCOV() || $runtime->hasPHPDBGCodeCoverage()) {
- return true;
- }
- if (self::usingXdebug()) {
- $mode = getenv('XDEBUG_MODE') ?: ini_get('xdebug.mode');
- return $mode && in_array('coverage', explode(',', $mode), true);
- }
- return true;
- }
- /**
- * If the user is using Xdebug.
- */
- public static function usingXdebug(): bool
- {
- return (new Runtime)->hasXdebug();
- }
- /**
- * Reports the code coverage report to the
- * console and returns the result in float.
- */
- public static function report(OutputInterface $output, bool $hideFullCoverage = false): float
- {
- if (! file_exists($reportPath = self::getPath())) {
- if (self::usingXdebug()) {
- $output->writeln(
- " <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</>?</>",
- );
- return 0.0;
- }
- $output->writeln(
- ' <fg=black;bg=yellow;options=bold> WARN </> No coverage driver detected.</> Did you install <href=https://xdebug.org/>Xdebug</> or <href=https://github.com/krakjoe/pcov>PCOV</>?',
- );
- return 0.0;
- }
- /** @var CodeCoverage $codeCoverage */
- $codeCoverage = require $reportPath;
- unlink($reportPath);
- $totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines();
- /** @var Directory<File|Directory> $report */
- $report = $codeCoverage->getReport();
- foreach ($report->getIterator() as $file) {
- if (! $file instanceof File) {
- continue;
- }
- $dirname = dirname($file->id());
- $basename = basename($file->id(), '.php');
- $name = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [
- $dirname,
- $basename,
- ]);
- $percentage = $file->numberOfExecutableLines() === 0
- ? '100.0'
- : number_format($file->percentageOfExecutedLines()->asFloat(), 1, '.', '');
- if ($percentage === '100.0' && $hideFullCoverage) {
- continue;
- }
- $uncoveredLines = '';
- $percentageOfExecutedLinesAsString = $file->percentageOfExecutedLines()->asString();
- if (! in_array($percentageOfExecutedLinesAsString, ['0.00%', '100.00%', '100.0%', ''], true)) {
- $uncoveredLines = trim(implode(', ', self::getMissingCoverage($file)));
- $uncoveredLines = sprintf('<span>%s</span>', $uncoveredLines).' <span class="text-gray"> / </span>';
- }
- $color = $percentage === '100.0' ? 'green' : ($percentage === '0.0' ? 'red' : 'yellow');
- $truncateAt = max(1, terminal()->width() - 12);
- renderUsing($output);
- render(<<<HTML
- <div class="flex mx-2">
- <span class="truncate-{$truncateAt}">{$name}</span>
- <span class="flex-1 content-repeat-[.] text-gray mx-1"></span>
- <span class="text-{$color}">$uncoveredLines {$percentage}%</span>
- </div>
- HTML);
- }
- $totalCoverageAsString = $totalCoverage->asFloat() === 0.0
- ? '0.0'
- : number_format($totalCoverage->asFloat(), 1, '.', '');
- renderUsing($output);
- render(<<<HTML
- <div class="mx-2">
- <hr class="text-gray" />
- <div class="w-full text-right">
- <span class="ml-1 font-bold">Total: {$totalCoverageAsString} %</span>
- </div>
- </div>
- HTML);
- return $totalCoverage->asFloat();
- }
- /**
- * Generates an array of missing coverage on the following format:.
- *
- * ```
- * ['11', '20..25', '50', '60..80'];
- * ```
- *
- * @param File $file
- * @return array<int, string>
- */
- public static function getMissingCoverage($file): array
- {
- $shouldBeNewLine = true;
- $eachLine = function (array $array, array $tests, int $line) use (&$shouldBeNewLine): array {
- if ($tests !== []) {
- $shouldBeNewLine = true;
- return $array;
- }
- if ($shouldBeNewLine) {
- $array[] = (string) $line;
- $shouldBeNewLine = false;
- return $array;
- }
- $lastKey = count($array) - 1;
- if (array_key_exists($lastKey, $array) && str_contains((string) $array[$lastKey], '..')) {
- [$from] = explode('..', (string) $array[$lastKey]);
- $array[$lastKey] = $line > $from ? sprintf('%s..%s', $from, $line) : sprintf('%s..%s', $line, $from);
- return $array;
- }
- $array[$lastKey] = sprintf('%s..%s', $array[$lastKey], $line);
- return $array;
- };
- $array = [];
- foreach (array_filter($file->lineCoverageData(), 'is_array') as $line => $tests) {
- $array = $eachLine($array, $tests, $line);
- }
- return $array;
- }
- }
|