<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <[email protected]>
*
* 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_pop;
use function count;
use function sprintf;
use function str_repeat;
use function substr_count;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\Version;
use SebastianBergmann\Environment\Runtime;
use SebastianBergmann\Template\Template;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
abstract class Renderer
{
/**
* @var string
*/
protected $templatePath;
/**
* @var string
*/
protected $generator;
/**
* @var string
*/
protected $date;
/**
* @var int
*/
protected $lowUpperBound;
/**
* @var int
*/
protected $highLowerBound;
/**
* @var bool
*/
protected $hasBranchCoverage;
/**
* @var string
*/
protected $version;
public function __construct(string $templatePath, string $generator, string $date, int $lowUpperBound, int $highLowerBound, bool $hasBranchCoverage)
{
$this->templatePath = $templatePath;
$this->generator = $generator;
$this->date = $date;
$this->lowUpperBound = $lowUpperBound;
$this->highLowerBound = $highLowerBound;
$this->version = Version::id();
$this->hasBranchCoverage = $hasBranchCoverage;
}
protected function renderItemTemplate(Template $template, array $data): string
{
$numSeparator = ' / ';
if (isset($data['numClasses']) && $data['numClasses'] > 0) {
$classesLevel = $this->colorLevel($data['testedClassesPercent']);
$classesNumber = $data['numTestedClasses'] . $numSeparator .
$data['numClasses'];
$classesBar = $this->coverageBar(
$data['testedClassesPercent']
);
} else {
$classesLevel = '';
$classesNumber = '0' . $numSeparator . '0';
$classesBar = '';
$data['testedClassesPercentAsString'] = 'n/a';
}
if ($data['numMethods'] > 0) {
$methodsLevel = $this->colorLevel($data['testedMethodsPercent']);
$methodsNumber = $data['numTestedMethods'] . $numSeparator .
$data['numMethods'];
$methodsBar = $this->coverageBar(
$data['testedMethodsPercent']
);
} else {
$methodsLevel = '';
$methodsNumber = '0' . $numSeparator . '0';
$methodsBar = '';
$data['testedMethodsPercentAsString'] = 'n/a';
}
if ($data['numExecutableLines'] > 0) {
$linesLevel = $this->colorLevel($data['linesExecutedPercent']);
$linesNumber = $data['numExecutedLines'] . $numSeparator .
$data['numExecutableLines'];
$linesBar = $this->coverageBar(
$data['linesExecutedPercent']
);
} else {
$linesLevel = '';
$linesNumber = '0' . $numSeparator . '0';
$linesBar = '';
$data['linesExecutedPercentAsString'] = 'n/a';
}
if ($data['numExecutablePaths'] > 0) {
$pathsLevel = $this->colorLevel($data['pathsExecutedPercent']);
$pathsNumber = $data['numExecutedPaths'] . $numSeparator .
$data['numExecutablePaths'];
$pathsBar = $this->coverageBar(
$data['pathsExecutedPercent']
);
} else {
$pathsLevel = '';
$pathsNumber = '0' . $numSeparator . '0';
$pathsBar = '';
$data['pathsExecutedPercentAsString'] = 'n/a';
}
if ($data['numExecutableBranches'] > 0) {
$branchesLevel = $this->colorLevel($data['branchesExecutedPercent']);
$branchesNumber = $data['numExecutedBranches'] . $numSeparator .
$data['numExecutableBranches'];
$branchesBar = $this->coverageBar(
$data['branchesExecutedPercent']
);
} else {
$branchesLevel = '';
$branchesNumber = '0' . $numSeparator . '0';
$branchesBar = '';
$data['branchesExecutedPercentAsString'] = 'n/a';
}
$template->setVar(
[
'icon' => $data['icon'] ?? '',
'crap' => $data['crap'] ?? '',
'name' => $data['name'],
'lines_bar' => $linesBar,
'lines_executed_percent' => $data['linesExecutedPercentAsString'],
'lines_level' => $linesLevel,
'lines_number' => $linesNumber,
'paths_bar' => $pathsBar,
'paths_executed_percent' => $data['pathsExecutedPercentAsString'],
'paths_level' => $pathsLevel,
'paths_number' => $pathsNumber,
'branches_bar' => $branchesBar,
'branches_executed_percent' => $data['branchesExecutedPercentAsString'],
'branches_level' => $branchesLevel,
'branches_number' => $branchesNumber,
'methods_bar' => $methodsBar,
'methods_tested_percent' => $data['testedMethodsPercentAsString'],
'methods_level' => $methodsLevel,
'methods_number' => $methodsNumber,
'classes_bar' => $classesBar,
'classes_tested_percent' => $data['testedClassesPercentAsString'] ?? '',
'classes_level' => $classesLevel,
'classes_number' => $classesNumber,
]
);
return $template->render();
}
protected function setCommonTemplateVariables(Template $template, AbstractNode $node): void
{
$template->setVar(
[
'id' => $node->id(),
'full_path' => $node->pathAsString(),
'path_to_root' => $this->pathToRoot($node),
'breadcrumbs' => $this->breadcrumbs($node),
'date' => $this->date,
'version' => $this->version,
'runtime' => $this->runtimeString(),
'generator' => $this->generator,
'low_upper_bound' => $this->lowUpperBound,
'high_lower_bound' => $this->highLowerBound,
]
);
}
protected function breadcrumbs(AbstractNode $node): string
{
$breadcrumbs = '';
$path = $node->pathAsArray();
$pathToRoot = [];
$max = count($path);
if ($node instanceof FileNode) {
$max--;
}
for ($i = 0; $i < $max; $i++) {
$pathToRoot[] = str_repeat('../', $i);
}
foreach ($path as $step) {
if ($step !== $node) {
$breadcrumbs .= $this->inactiveBreadcrumb(
$step,
array_pop($pathToRoot)
);
} else {
$breadcrumbs .= $this->activeBreadcrumb($step);
}
}
return $breadcrumbs;
}
protected function activeBreadcrumb(AbstractNode $node): string
{
$buffer = sprintf(
' <li class="breadcrumb-item active">%s</li>' . "\n",
$node->name()
);
if ($node instanceof DirectoryNode) {
$buffer .= ' <li class="breadcrumb-item">(<a href="dashboard.html">Dashboard</a>)</li>' . "\n";
}
return $buffer;
}
protected function inactiveBreadcrumb(AbstractNode $node, string $pathToRoot): string
{
return sprintf(
' <li class="breadcrumb-item"><a href="%sindex.html">%s</a></li>' . "\n",
$pathToRoot,
$node->name()
);
}
protected function pathToRoot(AbstractNode $node): string
{
$id = $node->id();
$depth = substr_count($id, '/');
if ($id !== 'index' &&
$node instanceof DirectoryNode) {
$depth++;
}
return str_repeat('../', $depth);
}
protected function coverageBar(float $percent): string
{
$level = $this->colorLevel($percent);
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'coverage_bar_branch.html' : 'coverage_bar.html');
$template = new Template(
$templateName,
'{{',
'}}'
);
$template->setVar(['level' => $level, 'percent' => sprintf('%.2F', $percent)]);
return $template->render();
}
protected function colorLevel(float $percent): string
{
if ($percent <= $this->lowUpperBound) {
return 'danger';
}
if ($percent > $this->lowUpperBound &&
$percent < $this->highLowerBound) {
return 'warning';
}
return 'success';
}
private function runtimeString(): string
{
$runtime = new Runtime;
return sprintf(
'<a href="%s" target="_top">%s %s</a>',
$runtime->getVendorUrl(),
$runtime->getName(),
$runtime->getVersion()
);
}
}