glastree_on_gitea

This commit is contained in:
2026-05-26 08:14:29 +02:00
commit 0bed099d05
9556 changed files with 1186307 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use function count;
use function is_string;
use function ksort;
use function max;
use function range;
use function time;
use DOMDocument;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Util\Xml;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
final class Clover
{
/**
* @param null|non-empty-string $target
* @param null|non-empty-string $name
*
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
{
$time = (string) time();
$xmlDocument = new DOMDocument('1.0', 'UTF-8');
$xmlCoverage = $xmlDocument->createElement('coverage');
$xmlCoverage->setAttribute('generated', $time);
$xmlDocument->appendChild($xmlCoverage);
$xmlProject = $xmlDocument->createElement('project');
$xmlProject->setAttribute('timestamp', $time);
if (is_string($name)) {
$xmlProject->setAttribute('name', $name);
}
$xmlCoverage->appendChild($xmlProject);
$packages = [];
$report = $coverage->getReport();
foreach ($report as $item) {
if (!$item instanceof File) {
continue;
}
/* @var File $item */
$xmlFile = $xmlDocument->createElement('file');
$xmlFile->setAttribute('name', $item->pathAsString());
$classes = $item->classesAndTraits();
$coverageData = $item->lineCoverageData();
$lines = [];
$namespace = 'global';
foreach ($classes as $className => $class) {
$classStatements = 0;
$coveredClassStatements = 0;
$coveredMethods = 0;
$classMethods = 0;
// Assumption: one namespace per file
if ($class->namespace !== '') {
$namespace = $class->namespace;
}
foreach ($class->methods as $methodName => $method) {
/** @phpstan-ignore equal.notAllowed */
if ($method->executableLines == 0) {
continue;
}
$classMethods++;
$classStatements += $method->executableLines;
$coveredClassStatements += $method->executedLines;
/** @phpstan-ignore equal.notAllowed */
if ($method->coverage == 100) {
$coveredMethods++;
}
$methodCount = 0;
foreach (range($method->startLine, $method->endLine) as $line) {
if (isset($coverageData[$line])) {
$methodCount = max($methodCount, count($coverageData[$line]));
}
}
$lines[$method->startLine] = [
'ccn' => $method->ccn,
'count' => $methodCount,
'crap' => $method->crap,
'type' => 'method',
'visibility' => $method->visibility,
'name' => $methodName,
];
}
$xmlClass = $xmlDocument->createElement('class');
$xmlClass->setAttribute('name', $className);
$xmlClass->setAttribute('namespace', $namespace);
$xmlFile->appendChild($xmlClass);
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('complexity', (string) $class->ccn);
$xmlMetrics->setAttribute('methods', (string) $classMethods);
$xmlMetrics->setAttribute('coveredmethods', (string) $coveredMethods);
$xmlMetrics->setAttribute('conditionals', (string) $class->executableBranches);
$xmlMetrics->setAttribute('coveredconditionals', (string) $class->executedBranches);
$xmlMetrics->setAttribute('statements', (string) $classStatements);
$xmlMetrics->setAttribute('coveredstatements', (string) $coveredClassStatements);
$xmlMetrics->setAttribute('elements', (string) ($classMethods + $classStatements + $class->executableBranches));
$xmlMetrics->setAttribute('coveredelements', (string) ($coveredMethods + $coveredClassStatements + $class->executedBranches));
$xmlClass->appendChild($xmlMetrics);
}
foreach ($coverageData as $line => $data) {
if ($data === null || isset($lines[$line])) {
continue;
}
$lines[$line] = [
'count' => count($data), 'type' => 'stmt',
];
}
ksort($lines);
foreach ($lines as $line => $data) {
$xmlLine = $xmlDocument->createElement('line');
$xmlLine->setAttribute('num', (string) $line);
$xmlLine->setAttribute('type', $data['type']);
if (isset($data['name'])) {
$xmlLine->setAttribute('name', $data['name']);
}
if (isset($data['visibility'])) {
$xmlLine->setAttribute('visibility', $data['visibility']);
}
if (isset($data['ccn'])) {
$xmlLine->setAttribute('complexity', (string) $data['ccn']);
}
if (isset($data['crap'])) {
$xmlLine->setAttribute('crap', (string) $data['crap']);
}
$xmlLine->setAttribute('count', (string) $data['count']);
$xmlFile->appendChild($xmlLine);
}
$linesOfCode = $item->linesOfCode();
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode());
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode());
$xmlMetrics->setAttribute('classes', (string) $item->numberOfClassesAndTraits());
$xmlMetrics->setAttribute('methods', (string) $item->numberOfMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $item->numberOfTestedMethods());
$xmlMetrics->setAttribute('conditionals', (string) $item->numberOfExecutableBranches());
$xmlMetrics->setAttribute('coveredconditionals', (string) $item->numberOfExecutedBranches());
$xmlMetrics->setAttribute('statements', (string) $item->numberOfExecutableLines());
$xmlMetrics->setAttribute('coveredstatements', (string) $item->numberOfExecutedLines());
$xmlMetrics->setAttribute('elements', (string) ($item->numberOfMethods() + $item->numberOfExecutableLines() + $item->numberOfExecutableBranches()));
$xmlMetrics->setAttribute('coveredelements', (string) ($item->numberOfTestedMethods() + $item->numberOfExecutedLines() + $item->numberOfExecutedBranches()));
$xmlFile->appendChild($xmlMetrics);
if ($namespace === 'global') {
$xmlProject->appendChild($xmlFile);
} else {
if (!isset($packages[$namespace])) {
$packages[$namespace] = $xmlDocument->createElement(
'package',
);
$packages[$namespace]->setAttribute('name', $namespace);
$xmlProject->appendChild($packages[$namespace]);
}
$packages[$namespace]->appendChild($xmlFile);
}
}
$linesOfCode = $report->linesOfCode();
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('files', (string) count($report));
$xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode());
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode());
$xmlMetrics->setAttribute('classes', (string) $report->numberOfClassesAndTraits());
$xmlMetrics->setAttribute('methods', (string) $report->numberOfMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $report->numberOfTestedMethods());
$xmlMetrics->setAttribute('conditionals', (string) $report->numberOfExecutableBranches());
$xmlMetrics->setAttribute('coveredconditionals', (string) $report->numberOfExecutedBranches());
$xmlMetrics->setAttribute('statements', (string) $report->numberOfExecutableLines());
$xmlMetrics->setAttribute('coveredstatements', (string) $report->numberOfExecutedLines());
$xmlMetrics->setAttribute('elements', (string) ($report->numberOfMethods() + $report->numberOfExecutableLines() + $report->numberOfExecutableBranches()));
$xmlMetrics->setAttribute('coveredelements', (string) ($report->numberOfTestedMethods() + $report->numberOfExecutedLines() + $report->numberOfExecutedBranches()));
$xmlProject->appendChild($xmlMetrics);
$buffer = Xml::asString($xmlDocument);
if ($target !== null) {
Filesystem::write($target, $buffer);
}
return $buffer;
}
}
@@ -0,0 +1,299 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use const DIRECTORY_SEPARATOR;
use function basename;
use function count;
use function preg_match;
use function range;
use function str_replace;
use function time;
use DOMImplementation;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Util\Xml;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
final class Cobertura
{
/**
* @param null|non-empty-string $target
*
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null): string
{
$time = (string) time();
$report = $coverage->getReport();
$implementation = new DOMImplementation;
$documentType = $implementation->createDocumentType(
'coverage',
'',
'http://cobertura.sourceforge.net/xml/coverage-04.dtd',
);
$document = $implementation->createDocument('', '', $documentType);
$document->xmlVersion = '1.0';
$document->encoding = 'UTF-8';
$coverageElement = $document->createElement('coverage');
$linesValid = $report->numberOfExecutableLines();
$linesCovered = $report->numberOfExecutedLines();
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
$coverageElement->setAttribute('line-rate', (string) $lineRate);
$branchesValid = $report->numberOfExecutableBranches();
$branchesCovered = $report->numberOfExecutedBranches();
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
$coverageElement->setAttribute('branch-rate', (string) $branchRate);
$coverageElement->setAttribute('lines-covered', (string) $report->numberOfExecutedLines());
$coverageElement->setAttribute('lines-valid', (string) $report->numberOfExecutableLines());
$coverageElement->setAttribute('branches-covered', (string) $report->numberOfExecutedBranches());
$coverageElement->setAttribute('branches-valid', (string) $report->numberOfExecutableBranches());
$coverageElement->setAttribute('complexity', '');
$coverageElement->setAttribute('version', '0.4');
$coverageElement->setAttribute('timestamp', $time);
$document->appendChild($coverageElement);
$sourcesElement = $document->createElement('sources');
$coverageElement->appendChild($sourcesElement);
$sourceElement = $document->createElement('source', $report->pathAsString());
$sourcesElement->appendChild($sourceElement);
$packagesElement = $document->createElement('packages');
$coverageElement->appendChild($packagesElement);
$complexity = 0;
foreach ($report as $item) {
if (!$item instanceof File) {
continue;
}
$packageElement = $document->createElement('package');
$packageComplexity = 0;
$packageElement->setAttribute('name', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString()));
$linesValid = $item->numberOfExecutableLines();
$linesCovered = $item->numberOfExecutedLines();
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
$packageElement->setAttribute('line-rate', (string) $lineRate);
$branchesValid = $item->numberOfExecutableBranches();
$branchesCovered = $item->numberOfExecutedBranches();
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
$packageElement->setAttribute('branch-rate', (string) $branchRate);
$packageElement->setAttribute('complexity', '');
$packagesElement->appendChild($packageElement);
$classesElement = $document->createElement('classes');
$packageElement->appendChild($classesElement);
$classes = $item->classesAndTraits();
$coverageData = $item->lineCoverageData();
foreach ($classes as $className => $class) {
$complexity += $class->ccn;
$packageComplexity += $class->ccn;
$linesValid = $class->executableLines;
$linesCovered = $class->executedLines;
$lineRate = $linesValid === 0 ? 0 : ($linesCovered / $linesValid);
$branchesValid = $class->executableBranches;
$branchesCovered = $class->executedBranches;
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
$classElement = $document->createElement('class');
$classElement->setAttribute('name', $className);
$classElement->setAttribute('filename', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString()));
$classElement->setAttribute('line-rate', (string) $lineRate);
$classElement->setAttribute('branch-rate', (string) $branchRate);
$classElement->setAttribute('complexity', (string) $class->ccn);
$classesElement->appendChild($classElement);
$methodsElement = $document->createElement('methods');
$classElement->appendChild($methodsElement);
$classLinesElement = $document->createElement('lines');
$classElement->appendChild($classLinesElement);
foreach ($class->methods as $methodName => $method) {
if ($method->executableLines === 0) {
continue;
}
preg_match("/\((.*?)\)/", $method->signature, $signature);
$linesValid = $method->executableLines;
$linesCovered = $method->executedLines;
$lineRate = $linesCovered / $linesValid;
$branchesValid = $method->executableBranches;
$branchesCovered = $method->executedBranches;
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
$methodElement = $document->createElement('method');
$methodElement->setAttribute('name', $methodName);
$methodElement->setAttribute('signature', $signature[1]);
$methodElement->setAttribute('line-rate', (string) $lineRate);
$methodElement->setAttribute('branch-rate', (string) $branchRate);
$methodElement->setAttribute('complexity', (string) $method->ccn);
$methodLinesElement = $document->createElement('lines');
$methodElement->appendChild($methodLinesElement);
foreach (range($method->startLine, $method->endLine) as $line) {
if (!isset($coverageData[$line])) {
continue;
}
$methodLineElement = $document->createElement('line');
$methodLineElement->setAttribute('number', (string) $line);
$methodLineElement->setAttribute('hits', (string) count($coverageData[$line]));
$methodLinesElement->appendChild($methodLineElement);
$classLineElement = $methodLineElement->cloneNode();
$classLinesElement->appendChild($classLineElement);
}
$methodsElement->appendChild($methodElement);
}
}
if ($item->numberOfFunctions() === 0) {
$packageElement->setAttribute('complexity', (string) $packageComplexity);
continue;
}
$functionsComplexity = 0;
$functionsLinesValid = 0;
$functionsLinesCovered = 0;
$functionsBranchesValid = 0;
$functionsBranchesCovered = 0;
$classElement = $document->createElement('class');
$classElement->setAttribute('name', basename($item->pathAsString()));
$classElement->setAttribute('filename', str_replace($report->pathAsString() . DIRECTORY_SEPARATOR, '', $item->pathAsString()));
$methodsElement = $document->createElement('methods');
$classElement->appendChild($methodsElement);
$classLinesElement = $document->createElement('lines');
$classElement->appendChild($classLinesElement);
$functions = $item->functions();
foreach ($functions as $functionName => $function) {
if ($function->executableLines === 0) {
continue;
}
$complexity += $function->ccn;
$packageComplexity += $function->ccn;
$functionsComplexity += $function->ccn;
$linesValid = $function->executableLines;
$linesCovered = $function->executedLines;
$lineRate = $linesCovered / $linesValid;
$functionsLinesValid += $linesValid;
$functionsLinesCovered += $linesCovered;
$branchesValid = $function->executableBranches;
$branchesCovered = $function->executedBranches;
$branchRate = $branchesValid === 0 ? 0 : ($branchesCovered / $branchesValid);
$functionsBranchesValid += $branchesValid;
$functionsBranchesCovered += $branchesValid;
$methodElement = $document->createElement('method');
$methodElement->setAttribute('name', $functionName);
$methodElement->setAttribute('signature', $function->signature);
$methodElement->setAttribute('line-rate', (string) $lineRate);
$methodElement->setAttribute('branch-rate', (string) $branchRate);
$methodElement->setAttribute('complexity', (string) $function->ccn);
$methodLinesElement = $document->createElement('lines');
$methodElement->appendChild($methodLinesElement);
foreach (range($function->startLine, $function->endLine) as $line) {
if (!isset($coverageData[$line])) {
continue;
}
$methodLineElement = $document->createElement('line');
$methodLineElement->setAttribute('number', (string) $line);
$methodLineElement->setAttribute('hits', (string) count($coverageData[$line]));
$methodLinesElement->appendChild($methodLineElement);
$classLineElement = $methodLineElement->cloneNode();
$classLinesElement->appendChild($classLineElement);
}
$methodsElement->appendChild($methodElement);
}
$packageElement->setAttribute('complexity', (string) $packageComplexity);
if ($functionsLinesValid === 0) {
continue;
}
$lineRate = $functionsLinesCovered / $functionsLinesValid;
$branchRate = $functionsBranchesValid === 0 ? 0 : ($functionsBranchesCovered / $functionsBranchesValid);
$classElement->setAttribute('line-rate', (string) $lineRate);
$classElement->setAttribute('branch-rate', (string) $branchRate);
$classElement->setAttribute('complexity', (string) $functionsComplexity);
$classesElement->appendChild($classElement);
}
$coverageElement->setAttribute('complexity', (string) $complexity);
$buffer = Xml::asString($document);
if ($target !== null) {
Filesystem::write($target, $buffer);
}
return $buffer;
}
}
+147
View File
@@ -0,0 +1,147 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use function date;
use function htmlspecialchars;
use function is_string;
use function round;
use DOMDocument;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Util\Xml;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
final readonly class Crap4j
{
private int $threshold;
public function __construct(int $threshold = 30)
{
$this->threshold = $threshold;
}
/**
* @param null|non-empty-string $target
* @param null|non-empty-string $name
*
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
{
$document = new DOMDocument('1.0', 'UTF-8');
$root = $document->createElement('crap_result');
$document->appendChild($root);
$project = $document->createElement('project', is_string($name) ? $name : '');
$root->appendChild($project);
$root->appendChild($document->createElement('timestamp', date('Y-m-d H:i:s')));
$stats = $document->createElement('stats');
$methodsNode = $document->createElement('methods');
$report = $coverage->getReport();
unset($coverage);
$fullMethodCount = 0;
$fullCrapMethodCount = 0;
$fullCrapLoad = 0;
$fullCrap = 0;
foreach ($report as $item) {
$namespace = 'global';
if (!$item instanceof File) {
continue;
}
$file = $document->createElement('file');
$file->setAttribute('name', $item->pathAsString());
$classes = $item->classesAndTraits();
foreach ($classes as $className => $class) {
foreach ($class->methods as $methodName => $method) {
$crapLoad = $this->crapLoad((float) $method->crap, $method->ccn, $method->coverage);
$fullCrap += $method->crap;
$fullCrapLoad += $crapLoad;
$fullMethodCount++;
if ($method->crap >= $this->threshold) {
$fullCrapMethodCount++;
}
$methodNode = $document->createElement('method');
if ($class->namespace !== '') {
$namespace = $class->namespace;
}
$methodNode->appendChild($document->createElement('package', $namespace));
$methodNode->appendChild($document->createElement('className', $className));
$methodNode->appendChild($document->createElement('methodName', $methodName));
$methodNode->appendChild($document->createElement('methodSignature', htmlspecialchars($method->signature)));
$methodNode->appendChild($document->createElement('fullMethod', htmlspecialchars($method->signature)));
$methodNode->appendChild($document->createElement('crap', (string) $this->roundValue((float) $method->crap)));
$methodNode->appendChild($document->createElement('complexity', (string) $method->ccn));
$methodNode->appendChild($document->createElement('coverage', (string) $this->roundValue($method->coverage)));
$methodNode->appendChild($document->createElement('crapLoad', (string) round($crapLoad)));
$methodsNode->appendChild($methodNode);
}
}
}
$stats->appendChild($document->createElement('name', 'Method Crap Stats'));
$stats->appendChild($document->createElement('methodCount', (string) $fullMethodCount));
$stats->appendChild($document->createElement('crapMethodCount', (string) $fullCrapMethodCount));
$stats->appendChild($document->createElement('crapLoad', (string) round($fullCrapLoad)));
$stats->appendChild($document->createElement('totalCrap', (string) $fullCrap));
$crapMethodPercent = 0;
if ($fullMethodCount > 0) {
$crapMethodPercent = $this->roundValue((100 * $fullCrapMethodCount) / $fullMethodCount);
}
$stats->appendChild($document->createElement('crapMethodPercent', (string) $crapMethodPercent));
$root->appendChild($stats);
$root->appendChild($methodsNode);
$buffer = Xml::asString($document);
if ($target !== null) {
Filesystem::write($target, $buffer);
}
return $buffer;
}
private function crapLoad(float $crapValue, int $cyclomaticComplexity, float $coveragePercent): float
{
$crapLoad = 0;
if ($crapValue >= $this->threshold) {
$crapLoad += $cyclomaticComplexity * (1.0 - $coveragePercent / 100);
$crapLoad += $cyclomaticComplexity / $this->threshold;
}
return $crapLoad;
}
private function roundValue(float $value): float
{
return round($value, 2);
}
}
@@ -0,0 +1,66 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Html;
/**
* @immutable
*/
final readonly class Colors
{
private string $successLow;
private string $successMedium;
private string $successHigh;
private string $warning;
private string $danger;
public static function default(): self
{
return new self('#dff0d8', '#c3e3b5', '#99cb84', '#fcf8e3', '#f2dede');
}
public static function from(string $successLow, string $successMedium, string $successHigh, string $warning, string $danger): self
{
return new self($successLow, $successMedium, $successHigh, $warning, $danger);
}
private function __construct(string $successLow, string $successMedium, string $successHigh, string $warning, string $danger)
{
$this->successLow = $successLow;
$this->successMedium = $successMedium;
$this->successHigh = $successHigh;
$this->warning = $warning;
$this->danger = $danger;
}
public function successLow(): string
{
return $this->successLow;
}
public function successMedium(): string
{
return $this->successMedium;
}
public function successHigh(): string
{
return $this->successHigh;
}
public function warning(): string
{
return $this->warning;
}
public function danger(): string
{
return $this->danger;
}
}
@@ -0,0 +1,50 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* 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 is_file;
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
/**
* @immutable
*/
final readonly class CustomCssFile
{
private string $path;
public static function default(): self
{
return new self(__DIR__ . '/Renderer/Template/css/custom.css');
}
/**
* @throws InvalidArgumentException
*/
public static function from(string $path): self
{
if (!is_file($path)) {
throw new InvalidArgumentException(
'$path does not exist',
);
}
return new self($path);
}
private function __construct(string $path)
{
$this->path = $path;
}
public function path(): string
{
return $this->path;
}
}
@@ -0,0 +1,154 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* 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 const DIRECTORY_SEPARATOR;
use function copy;
use function date;
use function dirname;
use function str_ends_with;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Report\Thresholds;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\Template\Exception;
use SebastianBergmann\Template\Template;
final readonly class Facade
{
private string $templatePath;
private string $generator;
private Colors $colors;
private Thresholds $thresholds;
private CustomCssFile $customCssFile;
public function __construct(string $generator = '', ?Colors $colors = null, ?Thresholds $thresholds = null, ?CustomCssFile $customCssFile = null)
{
$this->generator = $generator;
$this->colors = $colors ?? Colors::default();
$this->thresholds = $thresholds ?? Thresholds::default();
$this->customCssFile = $customCssFile ?? CustomCssFile::default();
$this->templatePath = __DIR__ . '/Renderer/Template/';
}
public function process(CodeCoverage $coverage, string $target): void
{
$target = $this->directory($target);
$report = $coverage->getReport();
$date = date('D M j G:i:s T Y');
$hasBranchCoverage = $coverage->getData(true)->functionCoverage() !== [];
$dashboard = new Dashboard(
$this->templatePath,
$this->generator,
$date,
$this->thresholds,
$hasBranchCoverage,
);
$directory = new Directory(
$this->templatePath,
$this->generator,
$date,
$this->thresholds,
$hasBranchCoverage,
);
$file = new File(
$this->templatePath,
$this->generator,
$date,
$this->thresholds,
$hasBranchCoverage,
);
$directory->render($report, $target . 'index.html');
$dashboard->render($report, $target . 'dashboard.html');
foreach ($report as $node) {
$id = $node->id();
if ($node instanceof DirectoryNode) {
Filesystem::createDirectory($target . $id);
$directory->render($node, $target . $id . '/index.html');
$dashboard->render($node, $target . $id . '/dashboard.html');
} else {
$dir = dirname($target . $id);
Filesystem::createDirectory($dir);
$file->render($node, $target . $id);
}
}
$this->copyFiles($target);
$this->renderCss($target);
}
private function copyFiles(string $target): void
{
$dir = $this->directory($target . '_css');
copy($this->templatePath . 'css/billboard.min.css', $dir . 'billboard.min.css');
copy($this->templatePath . 'css/bootstrap.min.css', $dir . 'bootstrap.min.css');
copy($this->customCssFile->path(), $dir . 'custom.css');
copy($this->templatePath . 'css/octicons.css', $dir . 'octicons.css');
$dir = $this->directory($target . '_icons');
copy($this->templatePath . 'icons/file-code.svg', $dir . 'file-code.svg');
copy($this->templatePath . 'icons/file-directory.svg', $dir . 'file-directory.svg');
$dir = $this->directory($target . '_js');
copy($this->templatePath . 'js/billboard.pkgd.min.js', $dir . 'billboard.pkgd.min.js');
copy($this->templatePath . 'js/bootstrap.bundle.min.js', $dir . 'bootstrap.bundle.min.js');
copy($this->templatePath . 'js/jquery.min.js', $dir . 'jquery.min.js');
copy($this->templatePath . 'js/file.js', $dir . 'file.js');
}
private function renderCss(string $target): void
{
$template = new Template($this->templatePath . 'css/style.css', '{{', '}}');
$template->setVar(
[
'success-low' => $this->colors->successLow(),
'success-medium' => $this->colors->successMedium(),
'success-high' => $this->colors->successHigh(),
'warning' => $this->colors->warning(),
'danger' => $this->colors->danger(),
],
);
try {
$template->renderTo($this->directory($target . '_css') . 'style.css');
// @codeCoverageIgnoreStart
} catch (Exception $e) {
throw new FileCouldNotBeWrittenException(
$e->getMessage(),
$e->getCode(),
$e,
);
// @codeCoverageIgnoreEnd
}
}
private function directory(string $directory): string
{
if (!str_ends_with($directory, DIRECTORY_SEPARATOR)) {
$directory .= DIRECTORY_SEPARATOR;
}
Filesystem::createDirectory($directory);
return $directory;
}
}
@@ -0,0 +1,289 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* 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\Report\Thresholds;
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
{
protected string $templatePath;
protected string $generator;
protected string $date;
protected Thresholds $thresholds;
protected bool $hasBranchCoverage;
protected string $version;
public function __construct(string $templatePath, string $generator, string $date, Thresholds $thresholds, bool $hasBranchCoverage)
{
$this->templatePath = $templatePath;
$this->generator = $generator;
$this->date = $date;
$this->thresholds = $thresholds;
$this->version = Version::id();
$this->hasBranchCoverage = $hasBranchCoverage;
}
/**
* @param array<non-empty-string, float|int|string> $data
*/
protected function renderItemTemplate(Template $template, array $data): string
{
$numSeparator = '&nbsp;/&nbsp;';
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' => (string) $this->thresholds->lowUpperBound(),
'high_lower_bound' => (string) $this->thresholds->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->thresholds->lowUpperBound()) {
return 'danger';
}
if ($percent > $this->thresholds->lowUpperBound() &&
$percent < $this->thresholds->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(),
);
}
}
@@ -0,0 +1,330 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* 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(
' <li class="breadcrumb-item"><a href="index.html">%s</a></li>' . "\n" .
' <li class="breadcrumb-item active">(Dashboard)</li>' . "\n",
$node->name(),
);
}
/**
* @param array<string, ProcessedClassType|ProcessedTraitType> $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<string, ProcessedClassType|ProcessedTraitType> $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<string, ProcessedClassType|ProcessedTraitType> $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(
' <tr><td><a href="%s">%s</a></td><td class="text-right">%d%%</td></tr>' . "\n",
str_replace($baseLink, '', $classes[$className]->link),
$className,
$coverage,
);
}
foreach ($leastTestedMethods as $methodName => $coverage) {
[$class, $method] = explode('::', $methodName);
$result['method'] .= sprintf(
' <tr><td><a href="%s"><abbr title="%s">%s</abbr></a></td><td class="text-right">%d%%</td></tr>' . "\n",
str_replace($baseLink, '', $classes[$class]->methods[$method]->link),
$methodName,
$method,
$coverage,
);
}
return $result;
}
/**
* @param array<string, ProcessedClassType|ProcessedTraitType> $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(
' <tr><td><a href="%s">%s</a></td><td class="text-right">%.1f%%</td><td class="text-right">%d</td><td class="text-right">%d</td></tr>' . "\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(
' <tr><td><a href="%s"><abbr title="%s">%s</abbr></a></td><td class="text-right">%.1f%%</td><td class="text-right">%d</td><td class="text-right">%d</td></tr>' . "\n",
str_replace($baseLink, '', $classes[$class]->methods[$method]->link),
$methodName,
$method,
$methodVals->coverage,
$methodVals->ccn,
$methodVals->crap,
);
}
return $result;
}
}
@@ -0,0 +1,123 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* 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 count;
use function sprintf;
use function str_repeat;
use SebastianBergmann\CodeCoverage\FileCouldNotBeWrittenException;
use SebastianBergmann\CodeCoverage\Node\AbstractNode as Node;
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 Directory extends Renderer
{
public function render(DirectoryNode $node, string $file): void
{
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'directory_branch.html' : 'directory.html');
$template = new Template($templateName, '{{', '}}');
$this->setCommonTemplateVariables($template, $node);
$items = $this->renderItem($node, true);
foreach ($node->directories() as $item) {
$items .= $this->renderItem($item);
}
foreach ($node->files() as $item) {
$items .= $this->renderItem($item);
}
$template->setVar(
[
'id' => $node->id(),
'items' => $items,
],
);
try {
$template->renderTo($file);
} catch (Exception $e) {
throw new FileCouldNotBeWrittenException(
$e->getMessage(),
$e->getCode(),
$e,
);
}
}
private function renderItem(Node $node, bool $total = false): string
{
$data = [
'numClasses' => $node->numberOfClassesAndTraits(),
'numTestedClasses' => $node->numberOfTestedClassesAndTraits(),
'numMethods' => $node->numberOfFunctionsAndMethods(),
'numTestedMethods' => $node->numberOfTestedFunctionsAndMethods(),
'linesExecutedPercent' => $node->percentageOfExecutedLines()->asFloat(),
'linesExecutedPercentAsString' => $node->percentageOfExecutedLines()->asString(),
'numExecutedLines' => $node->numberOfExecutedLines(),
'numExecutableLines' => $node->numberOfExecutableLines(),
'branchesExecutedPercent' => $node->percentageOfExecutedBranches()->asFloat(),
'branchesExecutedPercentAsString' => $node->percentageOfExecutedBranches()->asString(),
'numExecutedBranches' => $node->numberOfExecutedBranches(),
'numExecutableBranches' => $node->numberOfExecutableBranches(),
'pathsExecutedPercent' => $node->percentageOfExecutedPaths()->asFloat(),
'pathsExecutedPercentAsString' => $node->percentageOfExecutedPaths()->asString(),
'numExecutedPaths' => $node->numberOfExecutedPaths(),
'numExecutablePaths' => $node->numberOfExecutablePaths(),
'testedMethodsPercent' => $node->percentageOfTestedFunctionsAndMethods()->asFloat(),
'testedMethodsPercentAsString' => $node->percentageOfTestedFunctionsAndMethods()->asString(),
'testedClassesPercent' => $node->percentageOfTestedClassesAndTraits()->asFloat(),
'testedClassesPercentAsString' => $node->percentageOfTestedClassesAndTraits()->asString(),
];
if ($total) {
$data['name'] = 'Total';
} else {
$up = str_repeat('../', count($node->pathAsArray()) - 2);
$data['icon'] = sprintf('<img src="%s_icons/file-code.svg" class="octicon" />', $up);
if ($node instanceof DirectoryNode) {
$data['name'] = sprintf(
'<a href="%s/index.html">%s</a>',
$node->name(),
$node->name(),
);
$data['icon'] = sprintf('<img src="%s_icons/file-directory.svg" class="octicon" />', $up);
} elseif ($this->hasBranchCoverage) {
$data['name'] = sprintf(
'%s <a class="small" href="%s.html">[line]</a> <a class="small" href="%s_branch.html">[branch]</a> <a class="small" href="%s_path.html">[path]</a>',
$node->name(),
$node->name(),
$node->name(),
$node->name(),
);
} else {
$data['name'] = sprintf(
'<a href="%s.html">%s</a>',
$node->name(),
$node->name(),
);
}
}
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'directory_item_branch.html' : 'directory_item.html');
return $this->renderItemTemplate(
new Template($templateName, '{{', '}}'),
$data,
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
<hr/>
<h4>Branches</h4>
<p>
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an <code>if</code> statement
<i>always</i> has an <code>else</code> as part of its logical flow even if you didn't write one.
</p>
{{branches}}
@@ -0,0 +1,5 @@
<div class="progress">
<div class="progress-bar bg-{{level}}" role="progressbar" aria-valuenow="{{percent}}" aria-valuemin="0" aria-valuemax="100" style="width: {{percent}}%">
<span class="visually-hidden">{{percent}}% covered ({{level}})</span>
</div>
</div>
@@ -0,0 +1,5 @@
<div class="progress">
<div class="progress-bar bg-{{level}}" role="progressbar" aria-valuenow="{{percent}}" aria-valuemin="0" aria-valuemax="100" style="width: {{percent}}%">
<span class="visually-hidden">{{percent}}% covered ({{level}})</span>
</div>
</div>
@@ -0,0 +1,10 @@
/*!
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*
* billboard.js, JavaScript chart library
* https://naver.github.io/billboard.js/
*
* @version 3.15.1
*/
.bb svg{font:10px sans-serif;-webkit-tap-highlight-color:rgba(0,0,0,0)}.bb path,.bb line{fill:none;stroke:#000}.bb text,.bb .bb-button{-webkit-user-select:none;-moz-user-select:none;user-select:none}.bb-legend-item-tile,.bb-xgrid-focus,.bb-ygrid-focus,.bb-ygrid{shape-rendering:crispEdges}.bb-chart-arcs .bb-needle,.bb-chart-arc .bb-gauge-value{fill:#000}.bb-chart-arc path{stroke:#fff}.bb-chart-arc rect{stroke:#fff;stroke-width:1}.bb-chart-arc text{fill:#fff;font-size:13px}.bb-chart-funnels path{stroke-width:0}.bb-chart-funnels+.bb-chart-texts text{font-size:13px;fill:#fff}.bb-axis{shape-rendering:crispEdges}.bb-axis .bb-axis-x-tooltip,.bb-axis .bb-axis-y-tooltip,.bb-axis .bb-axis-y2-tooltip{font-size:1em;fill:#fff;white-space:nowrap}.bb-grid{pointer-events:none}.bb-grid line{stroke:#aaa}.bb-grid text{fill:#aaa}.bb-xgrid,.bb-ygrid{stroke-dasharray:3 3}.bb-text.bb-empty{fill:gray;font-size:2em}.bb-line{stroke-width:1px}.bb-circle._expanded_{stroke-width:1px;stroke:#fff}.bb-selected-circle{fill:#fff;stroke-width:2px}.bb-bar{stroke-width:0}.bb-bar._expanded_{fill-opacity:.75}.bb-candlestick{stroke-width:1px}.bb-candlestick._expanded_{fill-opacity:.75}.bb-target.bb-focused,.bb-circles.bb-focused{opacity:1}.bb-target.bb-focused path.bb-line,.bb-target.bb-focused path.bb-step,.bb-circles.bb-focused path.bb-line,.bb-circles.bb-focused path.bb-step{stroke-width:2px}.bb-target.bb-defocused,.bb-circles.bb-defocused{opacity:.3!important}.bb-target.bb-defocused .text-overlapping,.bb-circles.bb-defocused .text-overlapping{opacity:.05!important}.bb-region{fill:#4682b4}.bb-region rect{fill-opacity:.1}.bb-zoom-brush,.bb-brush .extent{fill-opacity:.1}.bb-legend-item{font-size:12px;user-select:none}.bb-legend-item-hidden{opacity:.15}.bb-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.bb-title{font:14px sans-serif}.bb-chart-treemaps rect{stroke:#fff;stroke-width:1px}.bb-tooltip-container{z-index:10;user-select:none}.bb-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;opacity:.9;box-shadow:7px 7px 12px -9px #777;white-space:nowrap}.bb-tooltip tr{border:1px solid #CCC}.bb-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#fff}.bb-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.bb-tooltip td>span,.bb-tooltip td>svg{display:inline-block;width:10px;height:10px;margin-right:6px}.bb-tooltip.value{text-align:right}.bb-area{stroke-width:0;opacity:.2}.bb-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}text.bb-chart-arcs-gauge-title{dominant-baseline:middle;font-size:2.7em}.bb-chart-arcs .bb-chart-arcs-background{fill:#e0e0e0;stroke:#fff}.bb-chart-arcs .bb-chart-arcs-gauge-unit{fill:#000;font-size:16px}.bb-chart-arcs .bb-chart-arcs-gauge-max,.bb-chart-arcs .bb-chart-arcs-gauge-min{fill:#777}.bb-chart-arcs .bb-levels circle{fill:none;stroke:#848282;stroke-width:.5px}.bb-chart-arcs .bb-levels text{fill:#848282}.bb-chart-radars .bb-levels polygon{fill:none;stroke:#848282;stroke-width:.5px}.bb-chart-radars .bb-levels text{fill:#848282}.bb-chart-radars .bb-axis line{stroke:#848282;stroke-width:.5px}.bb-chart-radars .bb-axis text{font-size:1.15em;cursor:default}.bb-chart-radars .bb-shapes polygon{fill-opacity:.2;stroke-width:1px}.bb-button{position:absolute;top:10px;right:10px}.bb-button .bb-zoom-reset{font-size:11px;border:solid 1px #ccc;background-color:#fff;padding:5px;border-radius:5px;cursor:pointer}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
.octicon {
display: inline-block;
vertical-align: text-top;
fill: currentColor;
}
@@ -0,0 +1,274 @@
:root {
/* Implementing an auto-selection of dark/light theme via: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark */
color-scheme: light dark;
/* PHPUnit light/dark colors */
--phpunit-breadcrumbs: light-dark(var(--bs-gray-200), var(--bs-gray-800));
--phpunit-success-bar: light-dark(#28a745 ,#1f8135);
--phpunit-success-high: light-dark(#99cb84, #3d5c4e);
--phpunit-success-medium: light-dark(#c3e3b5,#3c6051);
--phpunit-success-low: light-dark(#dff0d8, #2d4431);
--phpunit-warning: light-dark(#fcf8e3, #3e3408);
--phpunit-warning-bar: light-dark(#ffc107 ,#c19406);
--phpunit-danger: light-dark(#f2dede, #42221e);
--phpunit-danger-bar: light-dark(#dc3545, #a62633);
/* Bootstrap v5.3 default colors (light, dark) */
--bs-body-bg-rgb: 255, 255, 255;
--bs-body-bg: light-dark(#fff, #212529);
--bs-body-color-rgb: light-dark(33, 37, 41, 222, 226, 230);
--bs-body-color: light-dark(#212529, #dee2e6);
--bs-border-color-translucent: light-dark(rgba(0, 0, 0, 0.175), rgba(255, 255, 255, 0.15));
--bs-border-color: light-dark(#dee2e6, #495057);
--bs-code-color: light-dark(#d63384, #e685b5);
--bs-danger-bg-subtle: light-dark(#f8d7da, #2c0b0e);
--bs-danger-border-subtle: light-dark(#f1aeb5, #842029);
--bs-danger-text-emphasis: light-dark(#58151c, #ea868f);
--bs-dark-bg-subtle: light-dark(#ced4da, #1a1d20);
--bs-dark-border-subtle: light-dark(#adb5bd, #343a40);
--bs-dark-text-emphasis: light-dark(#495057, #dee2e6);
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-emphasis-color: light-dark(#000, #fff);
--bs-form-invalid-border-color: light-dark(#dc3545, #ea868f);
--bs-form-invalid-color: light-dark(#dc3545, #ea868f);
--bs-form-valid-border-color: light-dark(#198754, #75b798);
--bs-form-valid-color: light-dark(#198754, #75b798);
--bs-highlight-bg: light-dark(#fff3cd, #664d03);
--bs-highlight-color: light-dark(#212529, #dee2e6);
--bs-info-bg-subtle: light-dark(#cff4fc, #032830);
--bs-info-border-subtle: light-dark(#9eeaf9, #087990);
--bs-info-text-emphasis: light-dark(#055160, #6edff6);
--bs-light-bg-subtle: light-dark(#fcfcfd, #343a40);
--bs-light-border-subtle: light-dark(#e9ecef, #495057);
--bs-light-text-emphasis: light-dark(#495057, #f8f9fa);
--bs-link-color-rgb: 13, 110, 253;
--bs-link-color: light-dark(#0d6efd, #6ea8fe);
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-link-hover-color: light-dark(#0a58ca, #8bb9fe);
--bs-primary-bg-subtle: light-dark(#cfe2ff, #031633);
--bs-primary-border-subtle: light-dark(#9ec5fe, #084298);
--bs-primary-text-emphasis: light-dark(#052c65, #6ea8fe);
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-secondary-bg-subtle: light-dark(#e2e3e5, #161719);
--bs-secondary-bg: light-dark(#e9ecef, #343a40);
--bs-secondary-border-subtle: light-dark(#c4c8cb, #41464b);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-color: light-dark(rgba(33, 37, 41, 0.75), rgba(222, 226, 230, 0.75));
--bs-secondary-text-emphasis: light-dark(#2b2f32, #a7acb1);
--bs-success-bg-subtle: light-dark(#d1e7dd, #051b11);
--bs-success-border-subtle: light-dark(#a3cfbb, #0f5132);
--bs-success-text-emphasis: light-dark(#0a3622, #75b798);
--bs-tertiary-bg-rgb: light-dark(248, 249, 250, 43, 48, 53);
--bs-tertiary-bg: light-dark(#f8f9fa, #2b3035);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-color: light-dark(rgba(33, 37, 41, 0.5), rgba(222, 226, 230, 0.5));
--bs-warning-bg-subtle: light-dark(#fff3cd, #332701);
--bs-warning-border-subtle: light-dark(#ffe69c, #997404);
--bs-warning-text-emphasis: light-dark(#664d03, #ffda6a);
}
@media (prefers-color-scheme: dark) {
:root {
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-secondary-color-rgb: 222, 226, 230;
--bs-tertiary-color-rgb: 222, 226, 230;
}
/* Invert icon's colors on dark mode to improve readability */
img.octicon { filter: invert(1); }
}
body {
font-family: sans-serif;
font-size: 1em;
font-kerning: normal;
text-rendering: optimizeLegibility;
padding-top: 10px;
}
nav .breadcrumb {
border-radius: var(--bs-border-radius);
background-color: var(--phpunit-breadcrumbs);
padding: .75rem 1rem;
}
.popover {
max-width: none;
}
.popover-body {
max-height: 90vh;
overflow-y: auto;
}
.octicon {
margin-right:.25em;
vertical-align: baseline;
width: 0.75em;
}
.table-bordered>thead>tr>td {
border-bottom-width: 1px;
}
.table tbody>tr>td, .table thead>tr>td {
padding-top: 3px;
padding-bottom: 3px;
}
.table-condensed tbody>tr>td {
padding-top: 0;
padding-bottom: 0;
}
.table .progress {
margin-bottom: inherit;
}
.table-borderless th, .table-borderless td {
border: 0 !important;
}
.table tbody tr.covered-by-large-tests, .table tbody tr.covered-by-large-tests td, li.covered-by-large-tests, tr.success, tr.success td, td.success, li.success, span.success {
background-color: var(--phpunit-success-low);
}
.table tbody tr.covered-by-medium-tests, .table tbody tr.covered-by-medium-tests td, li.covered-by-medium-tests {
background-color: var(--phpunit-success-medium);
}
.table tbody tr.covered-by-small-tests, .table tbody tr.covered-by-small-tests td, li.covered-by-small-tests {
background-color: var(--phpunit-success-high);
}
.table tbody tr.warning, .table tbody tr.warning td, .table tbody td.warning, li.warning, span.warning {
background-color: var(--phpunit-warning);
}
.table tbody tr.danger, .table tbody tr.danger td, .table tbody td.danger, li.danger, span.danger {
background-color: var(--phpunit-danger);
}
.table tbody td.info {
background-color: rgb(from var(--bs-info) r g b / 0.25);
}
td.big {
vertical-align: middle;
width: 117px;
}
td.small {
}
td.codeLine {
font-family: "Source Code Pro", var(--bs-font-monospace);
white-space: pre-wrap;
}
td span.comment {
color: var(--bs-secondary-color);
}
td span.default {
color: var(--bs-body-color);
}
td span.html {
color: var(--bs-secondary-color);
}
td span.keyword {
color: var(--bs-body-color);
font-weight: bold;
}
pre span.string {
color: var(--bs-body-color);
}
span.success, span.warning, span.danger {
margin-right: 2px;
padding-left: 10px;
padding-right: 10px;
text-align: center;
}
#toplink {
position: fixed;
left: 5px;
bottom: 5px;
outline: 0;
}
svg text {
font-family: var(--bs-font-sans-serif);
font-size: 11px;
color: var(--bs-gray);
fill: var(--bs-gray);
}
.scrollbox {
height:245px;
overflow-x:scroll;
overflow-y:scroll;
}
table + .structure-heading {
border-top: 1px solid var(--bs-gray-200);
padding-top: 0.5em;
}
table#code td:first-of-type {
padding-left: .75em;
padding-right: .75em;
}
table#code td:first-of-type a {
text-decoration: none;
}
.legend {
font-weight: bold;
margin-right: 2px;
padding-left: 10px;
padding-right: 10px;
text-align: center;
}
.covered-by-small-tests {
background-color: var(--phpunit-success-high);
}
.covered-by-medium-tests {
background-color: var(--phpunit-success-medium);
}
.covered-by-large-tests {
background-color: var(--phpunit-success-low);
}
.not-covered {
background-color: var(--phpunit-danger);
}
.not-coverable {
background-color: var(--phpunit-warning);
}
.progress-bar.bg-success {
background-color: var(--phpunit-success-bar) !important;
}
.progress-bar.bg-warning {
background-color: var(--phpunit-warning-bar) !important;
}
.progress-bar.bg-danger {
background-color: var(--phpunit-danger-bar) !important;
}
@@ -0,0 +1,299 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dashboard for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/billboard.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h2>Classes</h2>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="classesCoverageDistribution" style="height: 300px;">
<svg></svg>
</div>
</div>
<div class="col-md-6">
<h3>Complexity</h3>
<div id="classComplexity" style="height: 300px;">
<svg></svg>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Class</th>
<th class="text-right">Coverage</th>
</tr>
</thead>
<tbody>
{{insufficient_coverage_classes}}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h3 style="margin-top: 2rem;">Project Risks</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Class</th>
<th class="text-right">Coverage</th>
<th class="text-right">Complexity</th>
<th class="text-right"><abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr></th>
</tr>
</thead>
<tbody>
{{project_risks_classes}}
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h2 style="margin-top: 3rem;">Methods</h2>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="methodsCoverageDistribution" style="height: 300px;">
<svg></svg>
</div>
</div>
<div class="col-md-6">
<h3>Complexity</h3>
<div id="methodComplexity" style="height: 300px;">
<svg></svg>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Method</th>
<th class="text-right">Coverage</th>
</tr>
</thead>
<tbody>
{{insufficient_coverage_methods}}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h3 style="margin-top: 2rem;">Project Risks</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Method</th>
<th class="text-right">Coverage</th>
<th class="text-right">Complexity</th>
<th class="text-right"><abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr></th>
</tr>
</thead>
<tbody>
{{project_risks_methods}}
</tbody>
</table>
</div>
</div>
</div>
<footer>
<hr/>
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
</footer>
</div>
<script src="{{path_to_root}}_js/billboard.pkgd.min.js?v={{version}}" type="text/javascript"></script>
<script type="application/json" id="complexity_class">{{complexity_class}}</script>
<script type="application/json" id="complexity_method">{{complexity_method}}</script>
<script type="text/javascript" defer>
const barLabels = [
'0%',
'0-10%',
'10-20%',
'20-30%',
'30-40%',
'40-50%',
'50-60%',
'60-70%',
'70-80%',
'80-90%',
'90-100%',
'100%'
];
const barConfig = (name, fullName, values) => ({
axis: {
x: {
type: "category",
categories: barLabels,
},
y: {
label: {
text: `#${name}`,
position: 'outer-top',
},
},
},
bindto: `#${name}CoverageDistribution`,
data: {
columns: [
[fullName].concat(values),
],
colors: {
[fullName]: "rgba(69, 114, 167, 0.75)",
},
type: "bar",
},
bar: {
width: {
ratio: 0.9,
},
},
grid: {
focus: {
show: false,
},
x: {
show: true,
},
y: {
show: true,
},
},
legend: {
show: false,
},
tooltip: {
contents: function (data) {
return `<table class="bb-tooltip"><tbody>
<tr><th colspan="2">Coverage ${barLabels[data[0].x]}</th></tr>
<tr><td class="value">${data[0].value} ${name}</td></tr>
</tbody></table>`;
},
grouped: false,
},
});
bb.generate(
barConfig('classes', "Class Coverage", {{class_coverage_distribution}})
);
bb.generate(
barConfig('methods', "Method Coverage", {{method_coverage_distribution}})
);
const scatterConfig = (name, complexityData) => ({
axis: {
x: {
label: {
text: 'Code Coverage (in percent)',
position: 'outer-right',
},
tick: {
values: [0, 20, 40, 60, 80, 100],
},
},
y: {
label: {
text: 'Cyclomatic Complexity',
position: 'outer-top',
},
min: 0,
padding: {
bottom: 0,
top: 5,
},
},
},
bindto: `#${name}Complexity`,
data: {
columns: [
["complexity_x"].concat(complexityData.map(d => d[0])),
["complexity"].concat(complexityData.map(d => d[1])),
],
onclick: function(data, element) {
window.location = complexityData[data.index][2];
},
type: "scatter",
xs: {
"complexity": "complexity_x",
},
},
grid: {
focus: {
show: true,
y: true,
},
x: {
show: true,
},
y: {
show: true,
},
},
legend: {
show: false,
},
tooltip: {
contents: function (data) {
const coverage = Math.round(data[0].x);
const complexity = data[0].value;
const targetName = complexityData[data[0].index][3];
const crap = complexityData[data[0].index][4];
return `<table class="bb-tooltip"><tbody>
<tr><th colspan="2">${targetName}</th></tr>
<tr class="bb-tooltip-name-complexity"><td>Coverage</td><td class="name">${coverage}%</td></tr>
<tr class="bb-tooltip-name-complexity"><td>Complexity</td><td class="value">${complexity}</td></tr>
<tr class="bb-tooltip-name-complexity"><td>Crap</td><td class="value">${crap}</td></tr>
</tbody></table>`;
},
grouped: false,
},
});
const classComplexityData = JSON.parse(document.getElementById('complexity_class').textContent);
bb.generate(
scatterConfig("class", classComplexityData)
);
const methodComplexityData = JSON.parse(document.getElementById('complexity_method').textContent);
bb.generate(
scatterConfig("method", methodComplexityData),
);
</script>
</body>
</html>
@@ -0,0 +1,293 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dashboard for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/billboard.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h2>Classes</h2>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="classesCoverageDistribution" style="height: 300px;">
<svg></svg>
</div>
</div>
<div class="col-md-6">
<h3>Complexity</h3>
<div id="classComplexity" style="height: 300px;">
<svg></svg>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Class</th>
<th class="text-right">Coverage</th>
</tr>
</thead>
<tbody>
{{insufficient_coverage_classes}}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h3 style="margin-top: 2rem;">Project Risks</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Class</th>
<th class="text-right"><abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr></th>
</tr>
</thead>
<tbody>
{{project_risks_classes}}
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h2 style="margin-top: 3rem;">Methods</h2>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="methodsCoverageDistribution" style="height: 300px;">
<svg></svg>
</div>
</div>
<div class="col-md-6">
<h3>Complexity</h3>
<div id="methodComplexity" style="height: 300px;">
<svg></svg>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3 style="margin-top: 2rem;">Insufficient Coverage</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Method</th>
<th class="text-right">Coverage</th>
</tr>
</thead>
<tbody>
{{insufficient_coverage_methods}}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h3 style="margin-top: 2rem;">Project Risks</h3>
<div class="scrollbox">
<table class="table">
<thead>
<tr>
<th>Method</th>
<th class="text-right"><abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr></th>
</tr>
</thead>
<tbody>
{{project_risks_methods}}
</tbody>
</table>
</div>
</div>
</div>
<footer>
<hr/>
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
</footer>
</div>
<script src="_js/billboard.pkgd.min.js?v={{version}}" type="text/javascript"></script>
<script type="application/json" id="complexity_class">{{complexity_class}}</script>
<script type="application/json" id="complexity_method">{{complexity_method}}</script>
<script type="text/javascript" defer>
const barLabels = [
'0%',
'0-10%',
'10-20%',
'20-30%',
'30-40%',
'40-50%',
'50-60%',
'60-70%',
'70-80%',
'80-90%',
'90-100%',
'100%'
];
const barConfig = (name, fullName, values) => ({
axis: {
x: {
type: "category",
categories: barLabels,
},
y: {
label: {
text: `#${name}`,
position: 'outer-top',
},
},
},
bindto: `#${name}CoverageDistribution`,
data: {
columns: [
[fullName].concat(values),
],
colors: {
[fullName]: "rgba(69, 114, 167, 0.75)",
},
type: "bar",
},
bar: {
width: {
ratio: 0.9,
},
},
grid: {
focus: {
show: false,
},
x: {
show: true,
},
y: {
show: true,
},
},
legend: {
show: false,
},
tooltip: {
contents: function (data) {
return `<table class="bb-tooltip"><tbody>
<tr><th colspan="2">Coverage ${barLabels[data[0].x]}</th></tr>
<tr><td class="value">${data[0].value} ${name}</td></tr>
</tbody></table>`;
},
grouped: false,
},
});
bb.generate(
barConfig('classes', "Class Coverage", {{class_coverage_distribution}})
);
bb.generate(
barConfig('methods', "Method Coverage", {{method_coverage_distribution}})
);
const scatterConfig = (name, complexityData) => ({
axis: {
x: {
label: {
text: 'Code Coverage (in percent)',
position: 'outer-right',
},
tick: {
values: [0, 20, 40, 60, 80, 100],
},
},
y: {
label: {
text: 'Cyclomatic Complexity',
position: 'outer-top',
},
min: 0,
padding: {
bottom: 0,
top: 5,
},
},
},
bindto: `#${name}Complexity`,
data: {
columns: [
["complexity_x"].concat(complexityData.map(d => d[0])),
["complexity"].concat(complexityData.map(d => d[1])),
],
onclick: function(data, element) {
window.location = complexityData[data.index][2];
},
type: "scatter",
xs: {
"complexity": "complexity_x",
},
},
grid: {
focus: {
show: true,
y: true,
},
x: {
show: true,
},
y: {
show: true,
},
},
legend: {
show: false,
},
tooltip: {
contents: function (data) {
const coverage = Math.round(data[0].x);
const complexity = data[0].value;
const targetName = complexityData[data[0].index][3];
return `<table class="bb-tooltip"><tbody>
<tr><th colspan="2">${targetName}</th></tr>
<tr class="bb-tooltip-name-complexity"><td>Coverage</td><td class="name">${coverage}%</td></tr>
<tr class="bb-tooltip-name-complexity"><td>Complexity</td><td class="value">${complexity}</td></tr>
</tbody></table>`;
},
grouped: false,
},
});
const classComplexityData = JSON.parse(document.getElementById('complexity_class').textContent);
bb.generate(
scatterConfig("class", classComplexityData)
);
const methodComplexityData = JSON.parse(document.getElementById('complexity_method').textContent);
bb.generate(
scatterConfig("method", methodComplexityData),
);
</script>
</body>
</html>
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<td>&nbsp;</td>
<td colspan="9"><div align="center"><strong>Code Coverage</strong></div></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="3"><div align="center"><strong>Lines</strong></div></td>
<td colspan="3"><div align="center"><strong>Functions and Methods</strong></div></td>
<td colspan="3"><div align="center"><strong>Classes and Traits</strong></div></td>
</tr>
</thead>
<tbody>
{{items}}
</tbody>
</table>
</div>
<footer>
<hr/>
<h4>Legend</h4>
<p>
<span class="danger"><strong>Low</strong>: 0% to {{low_upper_bound}}%</span>
<span class="warning"><strong>Medium</strong>: {{low_upper_bound}}% to {{high_lower_bound}}%</span>
<span class="success"><strong>High</strong>: {{high_lower_bound}}% to 100%</span>
</p>
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<td>&nbsp;</td>
<td colspan="15"><div align="center"><strong>Code Coverage</strong></div></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="3"><div align="center"><strong>Lines</strong></div></td>
<td colspan="3"><div align="center"><strong>Branches</strong></div></td>
<td colspan="3"><div align="center"><strong>Paths</strong></div></td>
<td colspan="3"><div align="center"><strong>Functions and Methods</strong></div></td>
<td colspan="3"><div align="center"><strong>Classes and Traits</strong></div></td>
</tr>
</thead>
<tbody>
{{items}}
</tbody>
</table>
</div>
<footer>
<hr/>
<h4>Legend</h4>
<p>
<span class="danger"><strong>Low</strong>: 0% to {{low_upper_bound}}%</span>
<span class="warning"><strong>Medium</strong>: {{low_upper_bound}}% to {{high_lower_bound}}%</span>
<span class="success"><strong>High</strong>: {{high_lower_bound}}% to 100%</span>
</p>
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,13 @@
<tr>
<td class="{{lines_level}}">{{icon}}{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{classes_level}} big">{{classes_bar}}</td>
<td class="{{classes_level}} small"><div align="right">{{classes_tested_percent}}</div></td>
<td class="{{classes_level}} small"><div align="right">{{classes_number}}</div></td>
</tr>
@@ -0,0 +1,19 @@
<tr>
<td class="{{lines_level}}">{{icon}}{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{branches_level}} big">{{branches_bar}}</td>
<td class="{{branches_level}} small"><div align="right">{{branches_executed_percent}}</div></td>
<td class="{{branches_level}} small"><div align="right">{{branches_number}}</div></td>
<td class="{{paths_level}} big">{{paths_bar}}</td>
<td class="{{paths_level}} small"><div align="right">{{paths_executed_percent}}</div></td>
<td class="{{paths_level}} small"><div align="right">{{paths_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{classes_level}} big">{{classes_bar}}</td>
<td class="{{classes_level}} small"><div align="right">{{classes_tested_percent}}</div></td>
<td class="{{classes_level}} small"><div align="right">{{classes_number}}</div></td>
</tr>
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<td>&nbsp;</td>
<td colspan="10"><div align="center"><strong>Code Coverage</strong></div></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="3"><div align="center"><strong>Lines</strong></div></td>
<td colspan="4"><div align="center"><strong>Functions and Methods</strong></div></td>
<td colspan="3"><div align="center"><strong>Classes and Traits</strong></div></td>
</tr>
</thead>
<tbody>
{{items}}
</tbody>
</table>
</div>
{{lines}}
{{structure}}
<footer>
<hr/>
<h4>Legend</h4>
{{legend}}
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
<a title="Back to the top" id="toplink" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16"><path fill-rule="evenodd" d="M12 11L6 5l-6 6h12z"/></svg>
</a>
</footer>
</div>
<script src="{{path_to_root}}_js/jquery.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/bootstrap.bundle.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/file.js?v={{version}}" type="text/javascript"></script>
</body>
</html>
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css?v={{version}}" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<td>&nbsp;</td>
<td colspan="16"><div align="center"><strong>Code Coverage</strong></div></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="3"><div align="center"><strong>Lines</strong></div></td>
<td colspan="3"><div align="center"><strong>Branches</strong></div></td>
<td colspan="3"><div align="center"><strong>Paths</strong></div></td>
<td colspan="4"><div align="center"><strong>Functions and Methods</strong></div></td>
<td colspan="3"><div align="center"><strong>Classes and Traits</strong></div></td>
</tr>
</thead>
<tbody>
{{items}}
</tbody>
</table>
</div>
{{lines}}
{{structure}}
<footer>
<hr/>
<h4>Legend</h4>
{{legend}}
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
<a title="Back to the top" id="toplink" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16"><path fill-rule="evenodd" d="M12 11L6 5l-6 6h12z"/></svg>
</a>
</footer>
</div>
<script src="{{path_to_root}}_js/jquery.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/bootstrap.bundle.min.js?v={{version}}" type="text/javascript"></script>
<script src="{{path_to_root}}_js/file.js?v={{version}}" type="text/javascript"></script>
</body>
</html>
@@ -0,0 +1,14 @@
<tr>
<td class="{{lines_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{methods_level}} small">{{crap}}</td>
<td class="{{classes_level}} big">{{classes_bar}}</td>
<td class="{{classes_level}} small"><div align="right">{{classes_tested_percent}}</div></td>
<td class="{{classes_level}} small"><div align="right">{{classes_number}}</div></td>
</tr>
@@ -0,0 +1,20 @@
<tr>
<td class="{{lines_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{branches_level}} big">{{branches_bar}}</td>
<td class="{{branches_level}} small"><div align="right">{{branches_executed_percent}}</div></td>
<td class="{{branches_level}} small"><div align="right">{{branches_number}}</div></td>
<td class="{{paths_level}} big">{{paths_bar}}</td>
<td class="{{paths_level}} small"><div align="right">{{paths_executed_percent}}</div></td>
<td class="{{paths_level}} small"><div align="right">{{paths_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{methods_level}} small">{{crap}}</td>
<td class="{{classes_level}} big">{{classes_bar}}</td>
<td class="{{classes_level}} small"><div align="right">{{classes_tested_percent}}</div></td>
<td class="{{classes_level}} small"><div align="right">{{classes_number}}</div></td>
</tr>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16"><path fill-rule="evenodd" d="M8.5 1H1c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V4.5L8.5 1zM11 14H1V2h7l3 3v9zM5 6.98L3.5 8.5 5 10l-.5 1L2 8.5 4.5 6l.5.98zM7.5 6L10 8.5 7.5 11l-.5-.98L8.5 8.5 7 7l.5-1z"/></svg>

After

Width:  |  Height:  |  Size: 304 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16"><path fill-rule="evenodd" d="M13 4H7V3c0-.66-.31-1-1-1H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zM6 4H1V3h5v1z"/></svg>

After

Width:  |  Height:  |  Size: 234 B

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,53 @@
$(function () {
var $window = $(window)
, $top_link = $('#toplink')
, $body = $('body, html')
, offset = $('#code').offset().top;
$top_link.hide().click(function (event) {
event.preventDefault();
$body.animate({scrollTop: 0}, 800);
});
$window.scroll(function () {
if ($window.scrollTop() > offset) {
$top_link.fadeIn();
} else {
$top_link.fadeOut();
}
});
var $popovers = $('.popin > :first-child');
$('.popin').on({
'click.popover': function (event) {
event.stopPropagation();
var $container = $(this).children().first();
//Close all other popovers:
$popovers.each(function () {
var $current = $(this);
if (!$current.is($container)) {
$current.popover('hide');
}
});
// Toggle this popover:
$container.popover('toggle');
},
});
//Hide all popovers on outside click:
$(document).click(function (event) {
if ($(event.target).closest($('.popover')).length === 0) {
$popovers.popover('hide');
}
});
//Hide all popovers on escape:
$(document).keyup(function (event) {
if (event.key === 'Escape') {
$popovers.popover('hide');
}
});
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
<tr class="{{class}} d-flex"><td {{popover}} class="col-1 text-end"><a id="{{lineNumber}}" href="#{{lineNumber}}">{{lineNumber}}</a></td><td class="col-11 codeLine">{{lineContent}}</td></tr>
@@ -0,0 +1,5 @@
<table id="code" class="table table-borderless table-condensed">
<tbody>
{{lines}}
</tbody>
</table>
@@ -0,0 +1,12 @@
<tr>
<td class="{{lines_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{methods_level}} small">{{crap}}</td>
<td class="{{methods_level}}" colspan="3"></td>
</tr>
@@ -0,0 +1,18 @@
<tr>
<td class="{{lines_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{branches_level}} big">{{branches_bar}}</td>
<td class="{{branches_level}} small"><div align="right">{{branches_executed_percent}}</div></td>
<td class="{{branches_level}} small"><div align="right">{{branches_number}}</div></td>
<td class="{{paths_level}} big">{{paths_bar}}</td>
<td class="{{paths_level}} small"><div align="right">{{paths_executed_percent}}</div></td>
<td class="{{paths_level}} small"><div align="right">{{paths_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{methods_level}} small">{{crap}}</td>
<td class="{{methods_level}}" colspan="3"></td>
</tr>
@@ -0,0 +1,9 @@
<hr/>
<h4>Paths</h4>
<p>
Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not
necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once.
Please also be aware that some paths may include implicit rather than explicit branches, e.g. an <code>if</code> statement
<i>always</i> has an <code>else</code> as part of its logical flow even if you didn't write one.
</p>
{{paths}}
@@ -0,0 +1,249 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use function assert;
use function basename;
use function count;
use function is_string;
use function ksort;
use function max;
use function range;
use function str_replace;
use function time;
use DOMDocument;
use DOMElement;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Util\Xml;
use SebastianBergmann\CodeCoverage\Version;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
final class OpenClover
{
/**
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
{
$time = (string) time();
$xmlDocument = new DOMDocument('1.0', 'UTF-8');
$xmlDocument->formatOutput = true;
$xmlCoverage = $xmlDocument->createElement('coverage');
$xmlCoverage->setAttribute('clover', Version::id());
$xmlCoverage->setAttribute('generated', $time);
$xmlDocument->appendChild($xmlCoverage);
$xmlProject = $xmlDocument->createElement('project');
$xmlProject->setAttribute('timestamp', $time);
if (is_string($name)) {
$xmlProject->setAttribute('name', $name);
}
$xmlCoverage->appendChild($xmlProject);
/** @var array<non-empty-string, DOMElement> $packages */
$packages = [];
$report = $coverage->getReport();
foreach ($report as $item) {
if (!$item instanceof File) {
continue;
}
$xmlFile = $xmlDocument->createElement('file');
$xmlFile->setAttribute('name', basename($item->pathAsString()));
$xmlFile->setAttribute('path', $item->pathAsString());
$classes = $item->classesAndTraits();
$coverageData = $item->lineCoverageData();
$lines = [];
$namespace = 'global';
foreach ($classes as $className => $class) {
$classStatements = 0;
$coveredClassStatements = 0;
$coveredMethods = 0;
$classMethods = 0;
// Assumption: one namespace per file
if ($class->namespace !== '') {
$namespace = $class->namespace;
}
foreach ($class->methods as $methodName => $method) {
/** @phpstan-ignore equal.notAllowed */
if ($method->executableLines == 0) {
continue;
}
$classMethods++;
$classStatements += $method->executableLines;
$coveredClassStatements += $method->executedLines;
/** @phpstan-ignore equal.notAllowed */
if ($method->coverage == 100) {
$coveredMethods++;
}
$methodCount = 0;
foreach (range($method->startLine, $method->endLine) as $line) {
if (isset($coverageData[$line])) {
$methodCount = max($methodCount, count($coverageData[$line]));
}
}
$lines[$method->startLine] = [
'ccn' => $method->ccn,
'count' => $methodCount,
'type' => 'method',
'signature' => $method->signature,
'visibility' => $method->visibility,
];
}
$xmlClass = $xmlDocument->createElement('class');
$xmlClass->setAttribute('name', str_replace($class->namespace . '\\', '', $className));
$xmlFile->appendChild($xmlClass);
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('complexity', (string) $class->ccn);
$xmlMetrics->setAttribute('elements', (string) ($classMethods + $classStatements + $class->executableBranches));
$xmlMetrics->setAttribute('coveredelements', (string) ($coveredMethods + $coveredClassStatements + $class->executedBranches));
$xmlMetrics->setAttribute('conditionals', (string) $class->executableBranches);
$xmlMetrics->setAttribute('coveredconditionals', (string) $class->executedBranches);
$xmlMetrics->setAttribute('statements', (string) $classStatements);
$xmlMetrics->setAttribute('coveredstatements', (string) $coveredClassStatements);
$xmlMetrics->setAttribute('methods', (string) $classMethods);
$xmlMetrics->setAttribute('coveredmethods', (string) $coveredMethods);
$xmlClass->insertBefore($xmlMetrics, $xmlClass->firstChild);
}
foreach ($coverageData as $line => $data) {
if ($data === null || isset($lines[$line])) {
continue;
}
$lines[$line] = [
'count' => count($data),
'type' => 'stmt',
];
}
ksort($lines);
foreach ($lines as $line => $data) {
$xmlLine = $xmlDocument->createElement('line');
$xmlLine->setAttribute('num', (string) $line);
$xmlLine->setAttribute('type', $data['type']);
if (isset($data['ccn'])) {
$xmlLine->setAttribute('complexity', (string) $data['ccn']);
}
$xmlLine->setAttribute('count', (string) $data['count']);
if (isset($data['signature'])) {
$xmlLine->setAttribute('signature', $data['signature']);
}
if (isset($data['visibility'])) {
$xmlLine->setAttribute('visibility', $data['visibility']);
}
$xmlFile->appendChild($xmlLine);
}
$linesOfCode = $item->linesOfCode();
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode());
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode());
$xmlMetrics->setAttribute('classes', (string) $item->numberOfClassesAndTraits());
$xmlMetrics->setAttribute('complexity', (string) $item->cyclomaticComplexity());
$xmlMetrics->setAttribute('elements', (string) ($item->numberOfMethods() + $item->numberOfExecutableLines() + $item->numberOfExecutableBranches()));
$xmlMetrics->setAttribute('coveredelements', (string) ($item->numberOfTestedMethods() + $item->numberOfExecutedLines() + $item->numberOfExecutedBranches()));
$xmlMetrics->setAttribute('conditionals', (string) $item->numberOfExecutableBranches());
$xmlMetrics->setAttribute('coveredconditionals', (string) $item->numberOfExecutedBranches());
$xmlMetrics->setAttribute('statements', (string) $item->numberOfExecutableLines());
$xmlMetrics->setAttribute('coveredstatements', (string) $item->numberOfExecutedLines());
$xmlMetrics->setAttribute('methods', (string) $item->numberOfMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $item->numberOfTestedMethods());
$xmlFile->insertBefore($xmlMetrics, $xmlFile->firstChild);
if (!isset($packages[$namespace])) {
$packages[$namespace] = $xmlDocument->createElement('package');
$packages[$namespace]->setAttribute('name', $namespace);
$xmlPackageMetrics = $xmlDocument->createElement('metrics');
$xmlPackageMetrics->setAttribute('complexity', '0');
$xmlPackageMetrics->setAttribute('elements', '0');
$xmlPackageMetrics->setAttribute('coveredelements', '0');
$xmlPackageMetrics->setAttribute('conditionals', '0');
$xmlPackageMetrics->setAttribute('coveredconditionals', '0');
$xmlPackageMetrics->setAttribute('statements', '0');
$xmlPackageMetrics->setAttribute('coveredstatements', '0');
$xmlPackageMetrics->setAttribute('methods', '0');
$xmlPackageMetrics->setAttribute('coveredmethods', '0');
$packages[$namespace]->appendChild($xmlPackageMetrics);
$xmlProject->appendChild($packages[$namespace]);
}
$xmlPackageMetrics = $packages[$namespace]->firstChild;
assert($xmlPackageMetrics instanceof DOMElement);
$xmlPackageMetrics->setAttribute('complexity', (string) ((int) $xmlPackageMetrics->getAttribute('complexity') + $item->cyclomaticComplexity()));
$xmlPackageMetrics->setAttribute('elements', (string) ((int) $xmlPackageMetrics->getAttribute('elements') + $item->numberOfMethods() + $item->numberOfExecutableLines() + $item->numberOfExecutableBranches()));
$xmlPackageMetrics->setAttribute('coveredelements', (string) ((int) $xmlPackageMetrics->getAttribute('coveredelements') + $item->numberOfTestedMethods() + $item->numberOfExecutedLines() + $item->numberOfExecutedBranches()));
$xmlPackageMetrics->setAttribute('conditionals', (string) ((int) $xmlPackageMetrics->getAttribute('conditionals') + $item->numberOfExecutableBranches()));
$xmlPackageMetrics->setAttribute('coveredconditionals', (string) ((int) $xmlPackageMetrics->getAttribute('coveredconditionals') + $item->numberOfExecutedBranches()));
$xmlPackageMetrics->setAttribute('statements', (string) ((int) $xmlPackageMetrics->getAttribute('statements') + $item->numberOfExecutableLines()));
$xmlPackageMetrics->setAttribute('coveredstatements', (string) ((int) $xmlPackageMetrics->getAttribute('coveredstatements') + $item->numberOfExecutedLines()));
$xmlPackageMetrics->setAttribute('methods', (string) ((int) $xmlPackageMetrics->getAttribute('methods') + $item->numberOfMethods()));
$xmlPackageMetrics->setAttribute('coveredmethods', (string) ((int) $xmlPackageMetrics->getAttribute('coveredmethods') + $item->numberOfTestedMethods()));
$packages[$namespace]->appendChild($xmlFile);
}
$linesOfCode = $report->linesOfCode();
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('files', (string) count($report));
$xmlMetrics->setAttribute('loc', (string) $linesOfCode->linesOfCode());
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode->nonCommentLinesOfCode());
$xmlMetrics->setAttribute('classes', (string) $report->numberOfClassesAndTraits());
$xmlMetrics->setAttribute('complexity', (string) $report->cyclomaticComplexity());
$xmlMetrics->setAttribute('elements', (string) ($report->numberOfMethods() + $report->numberOfExecutableLines() + $report->numberOfExecutableBranches()));
$xmlMetrics->setAttribute('coveredelements', (string) ($report->numberOfTestedMethods() + $report->numberOfExecutedLines() + $report->numberOfExecutedBranches()));
$xmlMetrics->setAttribute('conditionals', (string) $report->numberOfExecutableBranches());
$xmlMetrics->setAttribute('coveredconditionals', (string) $report->numberOfExecutedBranches());
$xmlMetrics->setAttribute('statements', (string) $report->numberOfExecutableLines());
$xmlMetrics->setAttribute('coveredstatements', (string) $report->numberOfExecutedLines());
$xmlMetrics->setAttribute('methods', (string) $report->numberOfMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $report->numberOfTestedMethods());
$xmlProject->insertBefore($xmlMetrics, $xmlProject->firstChild);
$buffer = Xml::asString($xmlDocument);
if ($target !== null) {
Filesystem::write($target, $buffer);
}
return $buffer;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use const PHP_EOL;
use function serialize;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
final class PHP
{
/**
* @param null|non-empty-string $target
*
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null): string
{
$coverage->clearCache();
$buffer = "<?php
return \unserialize(<<<'END_OF_COVERAGE_SERIALIZATION'" . PHP_EOL . serialize($coverage) . PHP_EOL . 'END_OF_COVERAGE_SERIALIZATION' . PHP_EOL . ');';
if ($target !== null) {
Filesystem::write($target, $buffer);
}
return $buffer;
}
}
+299
View File
@@ -0,0 +1,299 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use const PHP_EOL;
use function array_map;
use function date;
use function ksort;
use function max;
use function sprintf;
use function str_pad;
use function strlen;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Percentage;
final class Text
{
private const string COLOR_GREEN = "\x1b[30;42m";
private const string COLOR_YELLOW = "\x1b[30;43m";
private const string COLOR_RED = "\x1b[37;41m";
private const string COLOR_HEADER = "\x1b[1;37;40m";
private const string COLOR_RESET = "\x1b[0m";
private readonly Thresholds $thresholds;
private readonly bool $showUncoveredFiles;
private readonly bool $showOnlySummary;
public function __construct(Thresholds $thresholds, bool $showUncoveredFiles = false, bool $showOnlySummary = false)
{
$this->thresholds = $thresholds;
$this->showUncoveredFiles = $showUncoveredFiles;
$this->showOnlySummary = $showOnlySummary;
}
public function process(CodeCoverage $coverage, bool $showColors = false): string
{
$hasBranchCoverage = $coverage->getData(true)->functionCoverage() !== [];
$output = PHP_EOL . PHP_EOL;
$report = $coverage->getReport();
$colors = [
'header' => '',
'classes' => '',
'methods' => '',
'lines' => '',
'branches' => '',
'paths' => '',
'reset' => '',
];
if ($showColors) {
$colors['classes'] = $this->coverageColor(
$report->numberOfTestedClassesAndTraits(),
$report->numberOfClassesAndTraits(),
);
$colors['methods'] = $this->coverageColor(
$report->numberOfTestedMethods(),
$report->numberOfMethods(),
);
$colors['lines'] = $this->coverageColor(
$report->numberOfExecutedLines(),
$report->numberOfExecutableLines(),
);
$colors['branches'] = $this->coverageColor(
$report->numberOfExecutedBranches(),
$report->numberOfExecutableBranches(),
);
$colors['paths'] = $this->coverageColor(
$report->numberOfExecutedPaths(),
$report->numberOfExecutablePaths(),
);
$colors['reset'] = self::COLOR_RESET;
$colors['header'] = self::COLOR_HEADER;
}
$classes = sprintf(
' Classes: %6s (%d/%d)',
Percentage::fromFractionAndTotal(
$report->numberOfTestedClassesAndTraits(),
$report->numberOfClassesAndTraits(),
)->asString(),
$report->numberOfTestedClassesAndTraits(),
$report->numberOfClassesAndTraits(),
);
$methods = sprintf(
' Methods: %6s (%d/%d)',
Percentage::fromFractionAndTotal(
$report->numberOfTestedMethods(),
$report->numberOfMethods(),
)->asString(),
$report->numberOfTestedMethods(),
$report->numberOfMethods(),
);
$paths = '';
$branches = '';
if ($hasBranchCoverage) {
$paths = sprintf(
' Paths: %6s (%d/%d)',
Percentage::fromFractionAndTotal(
$report->numberOfExecutedPaths(),
$report->numberOfExecutablePaths(),
)->asString(),
$report->numberOfExecutedPaths(),
$report->numberOfExecutablePaths(),
);
$branches = sprintf(
' Branches: %6s (%d/%d)',
Percentage::fromFractionAndTotal(
$report->numberOfExecutedBranches(),
$report->numberOfExecutableBranches(),
)->asString(),
$report->numberOfExecutedBranches(),
$report->numberOfExecutableBranches(),
);
}
$lines = sprintf(
' Lines: %6s (%d/%d)',
Percentage::fromFractionAndTotal(
$report->numberOfExecutedLines(),
$report->numberOfExecutableLines(),
)->asString(),
$report->numberOfExecutedLines(),
$report->numberOfExecutableLines(),
);
$padding = max(array_map('strlen', [$classes, $methods, $lines]));
if ($this->showOnlySummary) {
$title = 'Code Coverage Report Summary:';
$padding = max($padding, strlen($title));
$output .= $this->format($colors['header'], $padding, $title);
} else {
$date = date(' Y-m-d H:i:s');
$title = 'Code Coverage Report:';
$output .= $this->format($colors['header'], $padding, $title);
$output .= $this->format($colors['header'], $padding, $date);
$output .= $this->format($colors['header'], $padding, '');
$output .= $this->format($colors['header'], $padding, ' Summary:');
}
$output .= $this->format($colors['classes'], $padding, $classes);
$output .= $this->format($colors['methods'], $padding, $methods);
if ($hasBranchCoverage) {
$output .= $this->format($colors['paths'], $padding, $paths);
$output .= $this->format($colors['branches'], $padding, $branches);
}
$output .= $this->format($colors['lines'], $padding, $lines);
if ($this->showOnlySummary) {
return $output . PHP_EOL;
}
$classCoverage = [];
foreach ($report as $item) {
if (!$item instanceof File) {
continue;
}
$classes = $item->classesAndTraits();
foreach ($classes as $className => $class) {
$classExecutableLines = 0;
$classExecutedLines = 0;
$classExecutableBranches = 0;
$classExecutedBranches = 0;
$classExecutablePaths = 0;
$classExecutedPaths = 0;
$coveredMethods = 0;
$classMethods = 0;
foreach ($class->methods as $method) {
/** @phpstan-ignore equal.notAllowed */
if ($method->executableLines == 0) {
continue;
}
$classMethods++;
$classExecutableLines += $method->executableLines;
$classExecutedLines += $method->executedLines;
$classExecutableBranches += $method->executableBranches;
$classExecutedBranches += $method->executedBranches;
$classExecutablePaths += $method->executablePaths;
$classExecutedPaths += $method->executedPaths;
/** @phpstan-ignore equal.notAllowed */
if ($method->coverage == 100) {
$coveredMethods++;
}
}
$classCoverage[$className] = [
'namespace' => $class->namespace,
'className' => $className,
'methodsCovered' => $coveredMethods,
'methodCount' => $classMethods,
'statementsCovered' => $classExecutedLines,
'statementCount' => $classExecutableLines,
'branchesCovered' => $classExecutedBranches,
'branchesCount' => $classExecutableBranches,
'pathsCovered' => $classExecutedPaths,
'pathsCount' => $classExecutablePaths,
];
}
}
ksort($classCoverage);
$methodColor = '';
$pathsColor = '';
$branchesColor = '';
$linesColor = '';
$resetColor = '';
foreach ($classCoverage as $fullQualifiedPath => $classInfo) {
/** @phpstan-ignore notEqual.notAllowed */
if ($this->showUncoveredFiles || $classInfo['statementsCovered'] != 0) {
if ($showColors) {
$methodColor = $this->coverageColor($classInfo['methodsCovered'], $classInfo['methodCount']);
$pathsColor = $this->coverageColor($classInfo['pathsCovered'], $classInfo['pathsCount']);
$branchesColor = $this->coverageColor($classInfo['branchesCovered'], $classInfo['branchesCount']);
$linesColor = $this->coverageColor($classInfo['statementsCovered'], $classInfo['statementCount']);
$resetColor = $colors['reset'];
}
$output .= PHP_EOL . $fullQualifiedPath . PHP_EOL
. ' ' . $methodColor . 'Methods: ' . $this->printCoverageCounts($classInfo['methodsCovered'], $classInfo['methodCount'], 2) . $resetColor . ' ';
if ($hasBranchCoverage) {
$output .= ' ' . $pathsColor . 'Paths: ' . $this->printCoverageCounts($classInfo['pathsCovered'], $classInfo['pathsCount'], 3) . $resetColor . ' '
. ' ' . $branchesColor . 'Branches: ' . $this->printCoverageCounts($classInfo['branchesCovered'], $classInfo['branchesCount'], 3) . $resetColor . ' ';
}
$output .= ' ' . $linesColor . 'Lines: ' . $this->printCoverageCounts($classInfo['statementsCovered'], $classInfo['statementCount'], 3) . $resetColor;
}
}
return $output . PHP_EOL;
}
private function coverageColor(int $numberOfCoveredElements, int $totalNumberOfElements): string
{
$coverage = Percentage::fromFractionAndTotal(
$numberOfCoveredElements,
$totalNumberOfElements,
);
if ($coverage->asFloat() >= $this->thresholds->highLowerBound()) {
return self::COLOR_GREEN;
}
if ($coverage->asFloat() > $this->thresholds->lowUpperBound()) {
return self::COLOR_YELLOW;
}
return self::COLOR_RED;
}
private function printCoverageCounts(int $numberOfCoveredElements, int $totalNumberOfElements, int $precision): string
{
$format = '%' . $precision . 's';
return Percentage::fromFractionAndTotal(
$numberOfCoveredElements,
$totalNumberOfElements,
)->asFixedWidthString() .
' (' . sprintf($format, $numberOfCoveredElements) . '/' .
sprintf($format, $totalNumberOfElements) . ')';
}
private function format(string $color, int $padding, false|string $string): string
{
if ($color === '') {
return (string) $string . PHP_EOL;
}
return $color . str_pad((string) $string, $padding) . self::COLOR_RESET . PHP_EOL;
}
}
@@ -0,0 +1,56 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report;
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
/**
* @immutable
*/
final readonly class Thresholds
{
private int $lowUpperBound;
private int $highLowerBound;
public static function default(): self
{
return new self(50, 90);
}
/**
* @throws InvalidArgumentException
*/
public static function from(int $lowUpperBound, int $highLowerBound): self
{
if ($lowUpperBound > $highLowerBound) {
throw new InvalidArgumentException(
'$lowUpperBound must not be larger than $highLowerBound',
);
}
return new self($lowUpperBound, $highLowerBound);
}
private function __construct(int $lowUpperBound, int $highLowerBound)
{
$this->lowUpperBound = $lowUpperBound;
$this->highLowerBound = $highLowerBound;
}
public function lowUpperBound(): int
{
return $this->lowUpperBound;
}
public function highLowerBound(): int
{
return $this->highLowerBound;
}
}
@@ -0,0 +1,48 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use DateTimeImmutable;
use SebastianBergmann\Environment\Runtime;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final readonly class BuildInformation
{
public function __construct(
XMLWriter $xmlWriter,
Runtime $runtime,
DateTimeImmutable $buildDate,
string $phpUnitVersion,
string $coverageVersion,
string $driverExtensionName,
string $driverExtensionVersion,
) {
$xmlWriter->startElement('build');
$xmlWriter->writeAttribute('time', $buildDate->format('D M j G:i:s T Y'));
$xmlWriter->writeAttribute('phpunit', $phpUnitVersion);
$xmlWriter->writeAttribute('coverage', $coverageVersion);
$xmlWriter->startElement('runtime');
$xmlWriter->writeAttribute('name', $runtime->getName());
$xmlWriter->writeAttribute('version', $runtime->getVersion());
$xmlWriter->writeAttribute('url', $runtime->getVendorUrl());
$xmlWriter->endElement();
$xmlWriter->startElement('driver');
$xmlWriter->writeAttribute('name', $driverExtensionName);
$xmlWriter->writeAttribute('version', $driverExtensionVersion);
$xmlWriter->endElement();
$xmlWriter->endElement();
}
}
@@ -0,0 +1,43 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Coverage
{
private readonly XMLWriter $xmlWriter;
private readonly string $line;
public function __construct(
XMLWriter $xmlWriter,
string $line
) {
$this->xmlWriter = $xmlWriter;
$this->line = $line;
}
public function finalize(array $tests): void
{
$writer = $this->xmlWriter;
$writer->startElement('line');
$writer->writeAttribute('nr', $this->line);
foreach ($tests as $test) {
$writer->startElement('covered');
$writer->writeAttribute('by', $test);
$writer->endElement();
}
$writer->endElement();
}
}
@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Directory extends Node
{
}
@@ -0,0 +1,350 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use const DIRECTORY_SEPARATOR;
use function count;
use function dirname;
use function file_get_contents;
use function is_array;
use function is_dir;
use function is_file;
use function is_writable;
use function phpversion;
use function sprintf;
use function strlen;
use function substr;
use DateTimeImmutable;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Data\ProcessedClassType;
use SebastianBergmann\CodeCoverage\Data\ProcessedFunctionType;
use SebastianBergmann\CodeCoverage\Data\ProcessedTraitType;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\PathExistsButIsNotDirectoryException;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SebastianBergmann\CodeCoverage\Version;
use SebastianBergmann\CodeCoverage\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\XmlException;
use SebastianBergmann\Environment\Runtime;
use XMLWriter;
/**
* @phpstan-import-type TestType from CodeCoverage
*/
final class Facade
{
public const string XML_NAMESPACE = 'https://schema.phpunit.de/coverage/1.0';
private string $target;
private Project $project;
private readonly string $phpUnitVersion;
private readonly bool $includeSource;
public function __construct(string $version, bool $includeSource = true)
{
$this->phpUnitVersion = $version;
$this->includeSource = $includeSource;
}
/**
* @throws XmlException
*/
public function process(CodeCoverage $coverage, string $target): void
{
if (substr($target, -1, 1) !== DIRECTORY_SEPARATOR) {
$target .= DIRECTORY_SEPARATOR;
}
$this->target = $target;
$this->initTargetDirectory($target);
$report = $coverage->getReport();
$writer = new XMLWriter;
$writer->openUri($this->targetFilePath('index'));
$writer->setIndent(true);
$writer->setIndentString(' ');
$this->project = new Project(
$writer,
$coverage->getReport()->name(),
);
$this->setBuildInformation($coverage);
$this->project->startProject();
$this->processTests($coverage->getTests());
$this->processDirectory($report, $this->project);
$this->project->finalize();
}
private function setBuildInformation(CodeCoverage $coverage): void
{
if ($coverage->driverIsPcov()) {
$driverExtensionName = 'pcov';
$driverExtensionVersion = phpversion('pcov');
} elseif ($coverage->driverIsXdebug()) {
$driverExtensionName = 'xdebug';
$driverExtensionVersion = phpversion('xdebug');
} else {
// @codeCoverageIgnoreStart
$driverExtensionName = 'unknown';
$driverExtensionVersion = 'unknown';
// @codeCoverageIgnoreEnd
}
$this->project->buildInformation(
new Runtime,
new DateTimeImmutable,
$this->phpUnitVersion,
Version::id(),
$driverExtensionName,
$driverExtensionVersion,
);
}
/**
* @throws PathExistsButIsNotDirectoryException
* @throws WriteOperationFailedException
*/
private function initTargetDirectory(string $directory): void
{
if (is_file($directory)) {
// @codeCoverageIgnoreStart
if (!is_dir($directory)) {
throw new PathExistsButIsNotDirectoryException($directory);
}
if (!is_writable($directory)) {
throw new WriteOperationFailedException($directory);
}
// @codeCoverageIgnoreEnd
}
Filesystem::createDirectory($directory);
}
/**
* @throws XmlException
*/
private function processDirectory(DirectoryNode $directory, Node $context): void
{
$directoryName = $directory->name();
if ($this->project->projectSourceDirectory() === $directoryName) {
$directoryName = '/';
}
$writer = $this->project->getWriter();
$writer->startElement('directory');
$writer->writeAttribute('name', $directoryName);
$directoryObject = $context->addDirectory();
$this->setTotals($directory, $directoryObject->totals());
foreach ($directory->directories() as $node) {
$this->processDirectory($node, $directoryObject);
}
foreach ($directory->files() as $node) {
$this->processFile($node, $directoryObject);
}
$writer->endElement();
}
/**
* @throws XmlException
*/
private function processFile(FileNode $file, Directory $context): void
{
$context->getWriter()->startElement('file');
$context->getWriter()->writeAttribute('name', $file->name());
$context->getWriter()->writeAttribute('href', $file->id() . '.xml');
$context->getWriter()->writeAttribute('hash', $file->sha1());
$fileObject = $context->addFile();
$this->setTotals($file, $fileObject->totals());
$context->getWriter()->endElement();
$path = substr(
$file->pathAsString(),
strlen($this->project->projectSourceDirectory()),
);
$writer = new XMLWriter;
$writer->openUri($this->targetFilePath($file->id()));
$writer->setIndent(true);
$writer->setIndentString(' ');
$fileReport = new Report($writer, $path, $file->sha1());
$this->setTotals($file, $fileReport->totals());
foreach ($file->classesAndTraits() as $unit) {
$this->processUnit($unit, $fileReport);
}
foreach ($file->functions() as $function) {
$this->processFunction($function, $fileReport);
}
$fileReport->getWriter()->startElement('coverage');
foreach ($file->lineCoverageData() as $line => $tests) {
if (!is_array($tests) || count($tests) === 0) {
continue;
}
$coverage = $fileReport->lineCoverage((string) $line);
$coverage->finalize($tests);
}
$fileReport->getWriter()->endElement();
if ($this->includeSource) {
$fileReport->source()->setSourceCode(
file_get_contents($file->pathAsString()),
);
}
$fileReport->finalize();
}
private function processUnit(ProcessedClassType|ProcessedTraitType $unit, Report $report): void
{
if ($unit instanceof ProcessedClassType) {
$report->getWriter()->startElement('class');
$unitObject = $report->classObject(
$unit->className,
$unit->namespace,
$unit->startLine,
$unit->executableLines,
$unit->executedLines,
(float) $unit->crap,
);
} else {
$report->getWriter()->startElement('trait');
$unitObject = $report->traitObject(
$unit->traitName,
$unit->namespace,
$unit->startLine,
$unit->executableLines,
$unit->executedLines,
(float) $unit->crap,
);
}
foreach ($unit->methods as $method) {
$report->getWriter()->startElement('method');
$unitObject->addMethod(
$method->methodName,
$method->signature,
(string) $method->startLine,
(string) $method->endLine,
(string) $method->executableLines,
(string) $method->executedLines,
(string) $method->coverage,
$method->crap,
);
$report->getWriter()->endElement();
}
$report->getWriter()->endElement();
}
private function processFunction(ProcessedFunctionType $function, Report $report): void
{
$report->getWriter()->startElement('function');
$report->functionObject(
$function->functionName,
$function->signature,
(string) $function->startLine,
null,
(string) $function->executableLines,
(string) $function->executedLines,
(string) $function->coverage,
$function->crap,
);
$report->getWriter()->endElement();
}
/**
* @param array<string, TestType> $tests
*/
private function processTests(array $tests): void
{
$this->project->getWriter()->startElement('tests');
$testsObject = $this->project->tests();
foreach ($tests as $test => $result) {
$testsObject->addTest($test, $result);
}
$this->project->getWriter()->endElement();
}
private function setTotals(AbstractNode $node, Totals $totals): void
{
$totals->getWriter()->startElement('totals');
$loc = $node->linesOfCode();
$totals->setNumLines(
$loc->linesOfCode(),
$loc->commentLinesOfCode(),
$loc->nonCommentLinesOfCode(),
$node->numberOfExecutableLines(),
$node->numberOfExecutedLines(),
);
$totals->setNumMethods(
$node->numberOfMethods(),
$node->numberOfTestedMethods(),
);
$totals->setNumFunctions(
$node->numberOfFunctions(),
$node->numberOfTestedFunctions(),
);
$totals->setNumClasses(
$node->numberOfClasses(),
$node->numberOfTestedClasses(),
);
$totals->setNumTraits(
$node->numberOfTraits(),
$node->numberOfTestedTraits(),
);
$totals->getWriter()->endElement();
}
private function targetDirectory(): string
{
return $this->target;
}
private function targetFilePath(string $name): string
{
$filename = sprintf('%s/%s.xml', $this->targetDirectory(), $name);
$this->initTargetDirectory(dirname($filename));
return $filename;
}
}
@@ -0,0 +1,40 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
class File
{
protected XMLWriter $xmlWriter;
public function __construct(XMLWriter $xmlWriter)
{
$this->xmlWriter = $xmlWriter;
}
public function getWriter(): XMLWriter
{
return $this->xmlWriter;
}
public function totals(): Totals
{
return new Totals($this->xmlWriter);
}
public function lineCoverage(string $line): Coverage
{
return new Coverage($this->xmlWriter, $line);
}
}
@@ -0,0 +1,49 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final readonly class Method
{
private XMLWriter $xmlWriter;
public function __construct(
XMLWriter $xmlWriter,
string $name,
string $signature,
string $start,
?string $end,
string $executable,
string $executed,
string $coverage,
string $crap
) {
$this->xmlWriter = $xmlWriter;
$this->xmlWriter->writeAttribute('name', $name);
$this->xmlWriter->writeAttribute('signature', $signature);
$this->xmlWriter->writeAttribute('start', $start);
if ($end !== null) {
$this->xmlWriter->writeAttribute('end', $end);
}
$this->xmlWriter->writeAttribute('crap', $crap);
$this->xmlWriter->writeAttribute('executable', $executable);
$this->xmlWriter->writeAttribute('executed', $executed);
$this->xmlWriter->writeAttribute('coverage', $coverage);
}
}
@@ -0,0 +1,45 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
abstract class Node
{
protected readonly XMLWriter $xmlWriter;
public function __construct(XMLWriter $xmlWriter)
{
$this->xmlWriter = $xmlWriter;
}
public function totals(): Totals
{
return new Totals($this->xmlWriter);
}
public function addDirectory(): Directory
{
return new Directory($this->xmlWriter);
}
public function addFile(): File
{
return new File($this->xmlWriter);
}
public function getWriter(): XMLWriter
{
return $this->xmlWriter;
}
}
@@ -0,0 +1,81 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use DateTimeImmutable;
use SebastianBergmann\Environment\Runtime;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Project extends Node
{
private readonly string $directory;
public function __construct(XMLWriter $xmlWriter, string $directory)
{
$this->directory = $directory;
parent::__construct($xmlWriter);
$this->xmlWriter->startDocument();
$this->xmlWriter->startElement('phpunit');
$this->xmlWriter->writeAttribute('xmlns', Facade::XML_NAMESPACE);
}
public function projectSourceDirectory(): string
{
return $this->directory;
}
public function buildInformation(
Runtime $runtime,
DateTimeImmutable $buildDate,
string $phpUnitVersion,
string $coverageVersion,
string $driverExtensionName,
string $driverExtensionVersion,
): void {
new BuildInformation(
$this->xmlWriter,
$runtime,
$buildDate,
$phpUnitVersion,
$coverageVersion,
$driverExtensionName,
$driverExtensionVersion,
);
}
public function tests(): Tests
{
return new Tests($this->xmlWriter);
}
public function getWriter(): XMLWriter
{
return $this->xmlWriter;
}
public function startProject(): void
{
$this->xmlWriter->startElement('project');
$this->xmlWriter->writeAttribute('source', $this->directory);
}
public function finalize(): void
{
$this->xmlWriter->endElement();
$this->xmlWriter->endDocument();
$this->xmlWriter->flush();
}
}
@@ -0,0 +1,108 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use function basename;
use function dirname;
use DOMDocument;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Report extends File
{
private readonly string $name;
private readonly string $sha1;
public function __construct(XMLWriter $xmlWriter, string $name, string $sha1)
{
/*
$dom = new DOMDocument;
$dom->loadXML('<?xml version="1.0" ?><phpunit xmlns="https://schema.phpunit.de/coverage/1.0"><file /></phpunit>');
$contextNode = $dom->getElementsByTagNameNS(
Facade::XML_NAMESPACE,
'file',
)->item(0);
*/
parent::__construct($xmlWriter);
$this->name = $name;
$this->sha1 = $sha1;
$xmlWriter->startDocument();
$xmlWriter->startElement('phpunit');
$xmlWriter->writeAttribute('xmlns', Facade::XML_NAMESPACE);
$xmlWriter->startElement('file');
$xmlWriter->writeAttribute('name', basename($this->name));
$xmlWriter->writeAttribute('path', dirname($this->name));
$xmlWriter->writeAttribute('hash', $this->sha1);
}
public function finalize(): void
{
$this->xmlWriter->endElement();
$this->xmlWriter->endElement();
$this->xmlWriter->endDocument();
$this->xmlWriter->flush();
}
public function functionObject(
string $name,
string $signature,
string $start,
?string $end,
string $executable,
string $executed,
string $coverage,
string $crap
): void {
new Method(
$this->xmlWriter,
$name,
$signature,
$start,
$end,
$executable,
$executed,
$coverage,
$crap,
);
}
public function classObject(
string $name,
string $namespace,
int $start,
int $executable,
int $executed,
float $crap
): Unit {
return new Unit($this->xmlWriter, $name, $namespace, $start, $executable, $executed, $crap);
}
public function traitObject(
string $name,
string $namespace,
int $start,
int $executable,
int $executed,
float $crap
): Unit {
return new Unit($this->xmlWriter, $name, $namespace, $start, $executable, $executed, $crap);
}
public function source(): Source
{
return new Source($this->xmlWriter);
}
}
@@ -0,0 +1,34 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use TheSeer\Tokenizer\NamespaceUri;
use TheSeer\Tokenizer\Tokenizer;
use TheSeer\Tokenizer\XMLSerializer;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final readonly class Source
{
private XMLWriter $xmlWriter;
public function __construct(XMLWriter $xmlWriter)
{
$this->xmlWriter = $xmlWriter;
}
public function setSourceCode(string $source): void
{
$tokens = (new Tokenizer)->parse($source);
(new XMLSerializer(new NamespaceUri(Facade::XML_NAMESPACE)))->appendToWriter($this->xmlWriter, $tokens);
}
}
@@ -0,0 +1,44 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use function sprintf;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*
* @phpstan-import-type TestType from CodeCoverage
*/
final readonly class Tests
{
private readonly XMLWriter $xmlWriter;
public function __construct(XMLWriter $xmlWriter)
{
$this->xmlWriter = $xmlWriter;
}
/**
* @param TestType $result
*/
public function addTest(string $test, array $result): void
{
$this->xmlWriter->startElement('test');
$this->xmlWriter->writeAttribute('name', $test);
$this->xmlWriter->writeAttribute('size', $result['size']);
$this->xmlWriter->writeAttribute('status', $result['status']);
$this->xmlWriter->writeAttribute('time', sprintf('%F', $result['time']));
$this->xmlWriter->endElement();
}
}
@@ -0,0 +1,95 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use function sprintf;
use SebastianBergmann\CodeCoverage\Util\Percentage;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final readonly class Totals
{
private XMLWriter $xmlWriter;
public function __construct(XMLWriter $xmlWriter)
{
$this->xmlWriter = $xmlWriter;
}
public function setNumLines(int $loc, int $cloc, int $ncloc, int $executable, int $executed): void
{
$this->xmlWriter->startElement('lines');
$this->xmlWriter->writeAttribute('total', (string) $loc);
$this->xmlWriter->writeAttribute('comments', (string) $cloc);
$this->xmlWriter->writeAttribute('code', (string) $ncloc);
$this->xmlWriter->writeAttribute('executable', (string) $executable);
$this->xmlWriter->writeAttribute('executed', (string) $executed);
$this->xmlWriter->writeAttribute(
'percent',
$executable === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($executed, $executable)->asFloat()),
);
$this->xmlWriter->endElement();
}
public function setNumClasses(int $count, int $tested): void
{
$this->xmlWriter->startElement('classes');
$this->xmlWriter->writeAttribute('count', (string) $count);
$this->xmlWriter->writeAttribute('tested', (string) $tested);
$this->xmlWriter->writeAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
);
$this->xmlWriter->endElement();
}
public function setNumTraits(int $count, int $tested): void
{
$this->xmlWriter->startElement('traits');
$this->xmlWriter->writeAttribute('count', (string) $count);
$this->xmlWriter->writeAttribute('tested', (string) $tested);
$this->xmlWriter->writeAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
);
$this->xmlWriter->endElement();
}
public function setNumMethods(int $count, int $tested): void
{
$this->xmlWriter->startElement('methods');
$this->xmlWriter->writeAttribute('count', (string) $count);
$this->xmlWriter->writeAttribute('tested', (string) $tested);
$this->xmlWriter->writeAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
);
$this->xmlWriter->endElement();
}
public function setNumFunctions(int $count, int $tested): void
{
$this->xmlWriter->startElement('functions');
$this->xmlWriter->writeAttribute('count', (string) $count);
$this->xmlWriter->writeAttribute('tested', (string) $tested);
$this->xmlWriter->writeAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat()),
);
$this->xmlWriter->endElement();
}
public function getWriter(): XMLWriter
{
return $this->xmlWriter;
}
}
@@ -0,0 +1,65 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final readonly class Unit
{
private XMLWriter $xmlWriter;
public function __construct(
XMLWriter $xmlWriter,
string $name,
string $namespace,
int $start,
int $executable,
int $executed,
float $crap
) {
$this->xmlWriter = $xmlWriter;
$this->xmlWriter->writeAttribute('name', $name);
$this->xmlWriter->writeAttribute('start', (string) $start);
$this->xmlWriter->writeAttribute('executable', (string) $executable);
$this->xmlWriter->writeAttribute('executed', (string) $executed);
$this->xmlWriter->writeAttribute('crap', (string) $crap);
$this->xmlWriter->startElement('namespace');
$this->xmlWriter->writeAttribute('name', $namespace);
$this->xmlWriter->endElement();
}
public function addMethod(
string $name,
string $signature,
string $start,
?string $end,
string $executable,
string $executed,
string $coverage,
string $crap
): void {
new Method(
$this->xmlWriter,
$name,
$signature,
$start,
$end,
$executable,
$executed,
$coverage,
$crap,
);
}
}