*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Html;
use function array_values;
use function asort;
use function assert;
use function count;
use function explode;
use function floor;
use function json_encode;
use function sprintf;
use function str_replace;
use function uasort;
use function usort;
use SebastianBergmann\CodeCoverage\Data\ProcessedClassType;
use SebastianBergmann\CodeCoverage\Data\ProcessedMethodType;
use SebastianBergmann\CodeCoverage\Data\ProcessedTraitType;
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\Template\Exception;
use SebastianBergmann\Template\Template;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Dashboard extends Renderer
{
public function render(DirectoryNode $node, string $file): void
{
$classes = $node->classesAndTraits();
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'dashboard_branch.html' : 'dashboard.html');
$template = new Template(
$templateName,
'{{',
'}}',
);
$this->setCommonTemplateVariables($template, $node);
$baseLink = $node->id() . '/';
$complexity = $this->complexity($classes, $baseLink);
$coverageDistribution = $this->coverageDistribution($classes);
$insufficientCoverage = $this->insufficientCoverage($classes, $baseLink);
$projectRisks = $this->projectRisks($classes, $baseLink);
$template->setVar(
[
'insufficient_coverage_classes' => $insufficientCoverage['class'],
'insufficient_coverage_methods' => $insufficientCoverage['method'],
'project_risks_classes' => $projectRisks['class'],
'project_risks_methods' => $projectRisks['method'],
'complexity_class' => $complexity['class'],
'complexity_method' => $complexity['method'],
'class_coverage_distribution' => $coverageDistribution['class'],
'method_coverage_distribution' => $coverageDistribution['method'],
],
);
try {
$template->renderTo($file);
} catch (Exception $e) {
throw new FileCouldNotBeWrittenException(
$e->getMessage(),
$e->getCode(),
$e,
);
}
}
protected function activeBreadcrumb(AbstractNode $node): string
{
return sprintf(
'
%s' . "\n" .
' (Dashboard)' . "\n",
$this->escapeHtml($node->name()),
);
}
/**
* @param array $classes
*
* @return array{class: non-empty-string, method: non-empty-string}
*/
private function complexity(array $classes, string $baseLink): array
{
$result = ['class' => [], 'method' => []];
foreach ($classes as $className => $class) {
foreach ($class->methods as $methodName => $method) {
if ($className !== '*') {
$methodName = $className . '::' . $methodName;
}
$result['method'][] = [
$method->coverage,
$method->ccn,
str_replace($baseLink, '', $method->link),
$methodName,
$method->crap,
];
}
$result['class'][] = [
$class->coverage,
$class->ccn,
str_replace($baseLink, '', $class->link),
$className,
$class->crap,
];
}
usort($result['class'], static fn (mixed $a, mixed $b) => ($a[0] <=> $b[0]));
usort($result['method'], static fn (mixed $a, mixed $b) => ($a[0] <=> $b[0]));
$class = json_encode($result['class']);
assert($class !== false);
$method = json_encode($result['method']);
assert($method !== false);
return ['class' => $class, 'method' => $method];
}
/**
* @param array $classes
*
* @return array{class: non-empty-string, method: non-empty-string}
*/
private function coverageDistribution(array $classes): array
{
$result = [
'class' => [
'0%' => 0,
'0-10%' => 0,
'10-20%' => 0,
'20-30%' => 0,
'30-40%' => 0,
'40-50%' => 0,
'50-60%' => 0,
'60-70%' => 0,
'70-80%' => 0,
'80-90%' => 0,
'90-100%' => 0,
'100%' => 0,
],
'method' => [
'0%' => 0,
'0-10%' => 0,
'10-20%' => 0,
'20-30%' => 0,
'30-40%' => 0,
'40-50%' => 0,
'50-60%' => 0,
'60-70%' => 0,
'70-80%' => 0,
'80-90%' => 0,
'90-100%' => 0,
'100%' => 0,
],
];
foreach ($classes as $class) {
foreach ($class->methods as $method) {
if ($method->coverage === 0) {
$result['method']['0%']++;
} elseif ($method->coverage === 100) {
$result['method']['100%']++;
} else {
$key = floor($method->coverage / 10) * 10;
$key = $key . '-' . ($key + 10) . '%';
$result['method'][$key]++;
}
}
if ($class->coverage === 0) {
$result['class']['0%']++;
} elseif ($class->coverage === 100) {
$result['class']['100%']++;
} else {
$key = floor($class->coverage / 10) * 10;
$key = $key . '-' . ($key + 10) . '%';
$result['class'][$key]++;
}
}
$class = json_encode(array_values($result['class']));
assert($class !== false);
$method = json_encode(array_values($result['method']));
assert($method !== false);
return ['class' => $class, 'method' => $method];
}
/**
* @param array $classes
*
* @return array{class: string, method: string}
*/
private function insufficientCoverage(array $classes, string $baseLink): array
{
$leastTestedClasses = [];
$leastTestedMethods = [];
$result = ['class' => '', 'method' => ''];
foreach ($classes as $className => $class) {
foreach ($class->methods as $methodName => $method) {
if ($method->coverage < $this->thresholds->highLowerBound()) {
$key = $methodName;
if ($className !== '*') {
$key = $className . '::' . $methodName;
}
$leastTestedMethods[$key] = $method->coverage;
}
}
if ($class->coverage < $this->thresholds->highLowerBound()) {
$leastTestedClasses[$className] = $class->coverage;
}
}
asort($leastTestedClasses);
asort($leastTestedMethods);
foreach ($leastTestedClasses as $className => $coverage) {
$result['class'] .= sprintf(
' | %s | %d%% |
' . "\n",
str_replace($baseLink, '', $classes[$className]->link),
$className,
$coverage,
);
}
foreach ($leastTestedMethods as $methodName => $coverage) {
[$class, $method] = explode('::', $methodName);
$result['method'] .= sprintf(
' | %s | %d%% |
' . "\n",
str_replace($baseLink, '', $classes[$class]->methods[$method]->link),
$methodName,
$method,
$coverage,
);
}
return $result;
}
/**
* @param array $classes
*
* @return array{class: string, method: string}
*/
private function projectRisks(array $classes, string $baseLink): array
{
$classRisks = [];
$methodRisks = [];
$result = ['class' => '', 'method' => ''];
foreach ($classes as $className => $class) {
foreach ($class->methods as $methodName => $method) {
if ($method->coverage < $this->thresholds->highLowerBound() && $method->ccn > 1) {
$key = $methodName;
if ($className !== '*') {
$key = $className . '::' . $methodName;
}
$methodRisks[$key] = $method;
}
}
if ($class->coverage < $this->thresholds->highLowerBound() &&
$class->ccn > count($class->methods)) {
$classRisks[$className] = $class;
}
}
uasort($classRisks, static function (ProcessedClassType|ProcessedTraitType $a, ProcessedClassType|ProcessedTraitType $b)
{
return ((int) ($a->crap) <=> (int) ($b->crap)) * -1;
});
uasort($methodRisks, static function (ProcessedMethodType $a, ProcessedMethodType $b)
{
return ((int) ($a->crap) <=> (int) ($b->crap)) * -1;
});
foreach ($classRisks as $className => $class) {
$result['class'] .= sprintf(
' | %s | %.1f%% | %d | %d |
' . "\n",
str_replace($baseLink, '', $classes[$className]->link),
$className,
$class->coverage,
$class->ccn,
$class->crap,
);
}
foreach ($methodRisks as $methodName => $methodVals) {
[$class, $method] = explode('::', $methodName);
$result['method'] .= sprintf(
' | %s | %.1f%% | %d | %d |
' . "\n",
str_replace($baseLink, '', $classes[$class]->methods[$method]->link),
$methodName,
$method,
$methodVals->coverage,
$methodVals->ccn,
$methodVals->crap,
);
}
return $result;
}
}