Coverage.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <?php
  2. declare(strict_types=1);
  3. namespace NunoMaduro\Collision;
  4. use SebastianBergmann\CodeCoverage\CodeCoverage;
  5. use SebastianBergmann\CodeCoverage\Node\Directory;
  6. use SebastianBergmann\CodeCoverage\Node\File;
  7. use SebastianBergmann\Environment\Runtime;
  8. use Symfony\Component\Console\Output\OutputInterface;
  9. use function Termwind\render;
  10. use function Termwind\renderUsing;
  11. use function Termwind\terminal;
  12. /**
  13. * @internal
  14. */
  15. final class Coverage
  16. {
  17. /**
  18. * Returns the coverage path.
  19. */
  20. public static function getPath(): string
  21. {
  22. return implode(DIRECTORY_SEPARATOR, [
  23. dirname(__DIR__),
  24. '.temp',
  25. 'coverage',
  26. ]);
  27. }
  28. /**
  29. * Runs true there is any code coverage driver available.
  30. */
  31. public static function isAvailable(): bool
  32. {
  33. $runtime = new Runtime;
  34. if (! $runtime->canCollectCodeCoverage()) {
  35. return false;
  36. }
  37. if ($runtime->hasPCOV() || $runtime->hasPHPDBGCodeCoverage()) {
  38. return true;
  39. }
  40. if (self::usingXdebug()) {
  41. $mode = getenv('XDEBUG_MODE') ?: ini_get('xdebug.mode');
  42. return $mode && in_array('coverage', explode(',', $mode), true);
  43. }
  44. return true;
  45. }
  46. /**
  47. * If the user is using Xdebug.
  48. */
  49. public static function usingXdebug(): bool
  50. {
  51. return (new Runtime)->hasXdebug();
  52. }
  53. /**
  54. * Reports the code coverage report to the
  55. * console and returns the result in float.
  56. */
  57. public static function report(OutputInterface $output, bool $hideFullCoverage = false): float
  58. {
  59. if (! file_exists($reportPath = self::getPath())) {
  60. if (self::usingXdebug()) {
  61. $output->writeln(
  62. " <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</>?</>",
  63. );
  64. return 0.0;
  65. }
  66. $output->writeln(
  67. ' <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</>?',
  68. );
  69. return 0.0;
  70. }
  71. /** @var CodeCoverage $codeCoverage */
  72. $codeCoverage = require $reportPath;
  73. unlink($reportPath);
  74. $totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines();
  75. /** @var Directory<File|Directory> $report */
  76. $report = $codeCoverage->getReport();
  77. foreach ($report->getIterator() as $file) {
  78. if (! $file instanceof File) {
  79. continue;
  80. }
  81. $dirname = dirname($file->id());
  82. $basename = basename($file->id(), '.php');
  83. $name = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [
  84. $dirname,
  85. $basename,
  86. ]);
  87. $percentage = $file->numberOfExecutableLines() === 0
  88. ? '100.0'
  89. : number_format($file->percentageOfExecutedLines()->asFloat(), 1, '.', '');
  90. if ($percentage === '100.0' && $hideFullCoverage) {
  91. continue;
  92. }
  93. $uncoveredLines = '';
  94. $percentageOfExecutedLinesAsString = $file->percentageOfExecutedLines()->asString();
  95. if (! in_array($percentageOfExecutedLinesAsString, ['0.00%', '100.00%', '100.0%', ''], true)) {
  96. $uncoveredLines = trim(implode(', ', self::getMissingCoverage($file)));
  97. $uncoveredLines = sprintf('<span>%s</span>', $uncoveredLines).' <span class="text-gray"> / </span>';
  98. }
  99. $color = $percentage === '100.0' ? 'green' : ($percentage === '0.0' ? 'red' : 'yellow');
  100. $truncateAt = max(1, terminal()->width() - 12);
  101. renderUsing($output);
  102. render(<<<HTML
  103. <div class="flex mx-2">
  104. <span class="truncate-{$truncateAt}">{$name}</span>
  105. <span class="flex-1 content-repeat-[.] text-gray mx-1"></span>
  106. <span class="text-{$color}">$uncoveredLines {$percentage}%</span>
  107. </div>
  108. HTML);
  109. }
  110. $totalCoverageAsString = $totalCoverage->asFloat() === 0.0
  111. ? '0.0'
  112. : number_format($totalCoverage->asFloat(), 1, '.', '');
  113. renderUsing($output);
  114. render(<<<HTML
  115. <div class="mx-2">
  116. <hr class="text-gray" />
  117. <div class="w-full text-right">
  118. <span class="ml-1 font-bold">Total: {$totalCoverageAsString} %</span>
  119. </div>
  120. </div>
  121. HTML);
  122. return $totalCoverage->asFloat();
  123. }
  124. /**
  125. * Generates an array of missing coverage on the following format:.
  126. *
  127. * ```
  128. * ['11', '20..25', '50', '60..80'];
  129. * ```
  130. *
  131. * @param File $file
  132. * @return array<int, string>
  133. */
  134. public static function getMissingCoverage($file): array
  135. {
  136. $shouldBeNewLine = true;
  137. $eachLine = function (array $array, array $tests, int $line) use (&$shouldBeNewLine): array {
  138. if ($tests !== []) {
  139. $shouldBeNewLine = true;
  140. return $array;
  141. }
  142. if ($shouldBeNewLine) {
  143. $array[] = (string) $line;
  144. $shouldBeNewLine = false;
  145. return $array;
  146. }
  147. $lastKey = count($array) - 1;
  148. if (array_key_exists($lastKey, $array) && str_contains((string) $array[$lastKey], '..')) {
  149. [$from] = explode('..', (string) $array[$lastKey]);
  150. $array[$lastKey] = $line > $from ? sprintf('%s..%s', $from, $line) : sprintf('%s..%s', $line, $from);
  151. return $array;
  152. }
  153. $array[$lastKey] = sprintf('%s..%s', $array[$lastKey], $line);
  154. return $array;
  155. };
  156. $array = [];
  157. foreach (array_filter($file->lineCoverageData(), 'is_array') as $line => $tests) {
  158. $array = $eachLine($array, $tests, $line);
  159. }
  160. return $array;
  161. }
  162. }