First Commit
This commit is contained in:
commit
7a7440daab
8955 files changed
+1117958
No files matched your search
@@ -0,0 +1,691 @@
|
||||
<?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;
|
||||
|
||||
use function array_diff;
|
||||
use function array_diff_key;
|
||||
use function array_flip;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function is_file;
|
||||
use function sort;
|
||||
use ReflectionClass;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData;
|
||||
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
|
||||
use SebastianBergmann\CodeCoverage\Driver\Driver;
|
||||
use SebastianBergmann\CodeCoverage\Node\Builder;
|
||||
use SebastianBergmann\CodeCoverage\Node\Directory;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingSourceAnalyser;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingSourceAnalyser;
|
||||
use SebastianBergmann\CodeCoverage\Test\Target\MapBuilder;
|
||||
use SebastianBergmann\CodeCoverage\Test\Target\Mapper;
|
||||
use SebastianBergmann\CodeCoverage\Test\Target\TargetCollection;
|
||||
use SebastianBergmann\CodeCoverage\Test\Target\TargetCollectionValidator;
|
||||
use SebastianBergmann\CodeCoverage\Test\Target\ValidationResult;
|
||||
use SebastianBergmann\CodeCoverage\Test\TestSize\TestSize;
|
||||
use SebastianBergmann\CodeCoverage\Test\TestStatus\TestStatus;
|
||||
|
||||
/**
|
||||
* Provides collection functionality for PHP code coverage information.
|
||||
*
|
||||
* @phpstan-type TestType array{size: string, status: string, time: float}
|
||||
* @phpstan-type TargetedLines array<non-empty-string, list<positive-int>>
|
||||
*/
|
||||
final class CodeCoverage
|
||||
{
|
||||
private const string UNCOVERED_FILES = 'UNCOVERED_FILES';
|
||||
private readonly Driver $driver;
|
||||
private readonly Filter $filter;
|
||||
private ?FileAnalyser $analyser = null;
|
||||
private ?Mapper $targetMapper = null;
|
||||
private ?string $cacheDirectory = null;
|
||||
private bool $checkForUnintentionallyCoveredCode = false;
|
||||
private bool $collectBranchAndPathCoverage = false;
|
||||
private bool $includeUncoveredFiles = true;
|
||||
private bool $ignoreDeprecatedCode = false;
|
||||
private bool $useAnnotationsForIgnoringCode = true;
|
||||
|
||||
/**
|
||||
* @var list<class-string>
|
||||
*/
|
||||
private array $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
|
||||
private ?string $currentId = null;
|
||||
private ?TestSize $currentSize = null;
|
||||
private ProcessedCodeCoverageData $data;
|
||||
|
||||
/**
|
||||
* @var array<string, TestType>
|
||||
*/
|
||||
private array $tests = [];
|
||||
private ?Directory $cachedReport = null;
|
||||
|
||||
public function __construct(Driver $driver, Filter $filter)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
$this->filter = $filter;
|
||||
$this->data = new ProcessedCodeCoverageData;
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
$prefix = "\x00" . self::class . "\x00";
|
||||
|
||||
return [
|
||||
// Configuration
|
||||
$prefix . 'cacheDirectory' => $this->cacheDirectory,
|
||||
$prefix . 'checkForUnintentionallyCoveredCode' => $this->checkForUnintentionallyCoveredCode,
|
||||
$prefix . 'includeUncoveredFiles' => $this->includeUncoveredFiles,
|
||||
$prefix . 'ignoreDeprecatedCode' => $this->ignoreDeprecatedCode,
|
||||
$prefix . 'parentClassesExcludedFromUnintentionallyCoveredCodeCheck' => $this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck,
|
||||
$prefix . 'useAnnotationsForIgnoringCode' => $this->useAnnotationsForIgnoringCode,
|
||||
$prefix . 'filter' => $this->filter,
|
||||
|
||||
// Data
|
||||
$prefix . 'data' => $this->data,
|
||||
$prefix . 'tests' => $this->tests,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the code coverage information as a graph of node objects.
|
||||
*/
|
||||
public function getReport(): Directory
|
||||
{
|
||||
if ($this->cachedReport === null) {
|
||||
$this->cachedReport = (new Builder($this->analyser()))->build($this);
|
||||
}
|
||||
|
||||
return $this->cachedReport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears collected code coverage data.
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->currentId = null;
|
||||
$this->currentSize = null;
|
||||
$this->data = new ProcessedCodeCoverageData;
|
||||
$this->tests = [];
|
||||
$this->cachedReport = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->cachedReport = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filter object used.
|
||||
*/
|
||||
public function filter(): Filter
|
||||
{
|
||||
return $this->filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collected code coverage data.
|
||||
*/
|
||||
public function getData(bool $raw = false): ProcessedCodeCoverageData
|
||||
{
|
||||
if (!$raw) {
|
||||
if ($this->includeUncoveredFiles) {
|
||||
$this->addUncoveredFilesFromFilter();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the coverage data.
|
||||
*/
|
||||
public function setData(ProcessedCodeCoverageData $data): void
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, TestType>
|
||||
*/
|
||||
public function getTests(): array
|
||||
{
|
||||
return $this->tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, TestType> $tests
|
||||
*/
|
||||
public function setTests(array $tests): void
|
||||
{
|
||||
$this->tests = $tests;
|
||||
}
|
||||
|
||||
public function start(string $id, ?TestSize $size = null, bool $clear = false): void
|
||||
{
|
||||
if ($clear) {
|
||||
$this->clear();
|
||||
}
|
||||
|
||||
$this->currentId = $id;
|
||||
$this->currentSize = $size;
|
||||
|
||||
$this->driver->start();
|
||||
|
||||
$this->cachedReport = null;
|
||||
}
|
||||
|
||||
public function stop(bool $append = true, ?TestStatus $status = null, null|false|TargetCollection $covers = null, ?TargetCollection $uses = null, float $time = 0.0): RawCodeCoverageData
|
||||
{
|
||||
$data = $this->driver->stop();
|
||||
|
||||
$this->append($data, null, $append, $status, $covers, $uses, $time);
|
||||
|
||||
$this->currentId = null;
|
||||
$this->currentSize = null;
|
||||
$this->cachedReport = null;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ReflectionException
|
||||
* @throws TestIdMissingException
|
||||
* @throws UnintentionallyCoveredCodeException
|
||||
*/
|
||||
public function append(RawCodeCoverageData $rawData, ?string $id = null, bool $append = true, ?TestStatus $status = null, null|false|TargetCollection $covers = null, ?TargetCollection $uses = null, float $time = 0.0): void
|
||||
{
|
||||
if ($id === null) {
|
||||
$id = $this->currentId;
|
||||
}
|
||||
|
||||
if ($id === null) {
|
||||
throw new TestIdMissingException;
|
||||
}
|
||||
|
||||
if ($status === null) {
|
||||
$status = TestStatus::unknown();
|
||||
}
|
||||
|
||||
if ($covers === null) {
|
||||
$covers = TargetCollection::fromArray([]);
|
||||
}
|
||||
|
||||
if ($uses === null) {
|
||||
$uses = TargetCollection::fromArray([]);
|
||||
}
|
||||
|
||||
$size = $this->currentSize;
|
||||
|
||||
if ($size === null) {
|
||||
$size = TestSize::unknown();
|
||||
}
|
||||
|
||||
$this->cachedReport = null;
|
||||
|
||||
$this->applyFilter($rawData);
|
||||
|
||||
$this->applyExecutableLinesFilter($rawData);
|
||||
|
||||
if ($this->useAnnotationsForIgnoringCode) {
|
||||
$this->applyIgnoredLinesFilter($rawData);
|
||||
}
|
||||
|
||||
$this->data->initializeUnseenData($rawData);
|
||||
|
||||
if (!$append) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($id === self::UNCOVERED_FILES) {
|
||||
return;
|
||||
}
|
||||
|
||||
$linesToBeCovered = false;
|
||||
$linesToBeUsed = [];
|
||||
|
||||
if ($covers !== false) {
|
||||
$linesToBeCovered = $this->targetMapper()->mapTargets($covers);
|
||||
}
|
||||
|
||||
if ($linesToBeCovered !== false) {
|
||||
$linesToBeUsed = $this->targetMapper()->mapTargets($uses);
|
||||
}
|
||||
|
||||
$this->applyCoversAndUsesFilter(
|
||||
$rawData,
|
||||
$linesToBeCovered,
|
||||
$linesToBeUsed,
|
||||
$size,
|
||||
);
|
||||
|
||||
if ($rawData->lineCoverage() === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tests[$id] = [
|
||||
'size' => $size->asString(),
|
||||
'status' => $status->asString(),
|
||||
'time' => $time,
|
||||
];
|
||||
|
||||
$this->data->markCodeAsExecutedByTestCase($id, $rawData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the data from another instance.
|
||||
*/
|
||||
public function merge(self $that): void
|
||||
{
|
||||
$this->filter->includeFiles(
|
||||
$that->filter()->files(),
|
||||
);
|
||||
|
||||
$this->data->merge($that->data);
|
||||
|
||||
$this->tests = array_merge($this->tests, $that->getTests());
|
||||
|
||||
$this->cachedReport = null;
|
||||
}
|
||||
|
||||
public function enableCheckForUnintentionallyCoveredCode(): void
|
||||
{
|
||||
$this->checkForUnintentionallyCoveredCode = true;
|
||||
}
|
||||
|
||||
public function disableCheckForUnintentionallyCoveredCode(): void
|
||||
{
|
||||
$this->checkForUnintentionallyCoveredCode = false;
|
||||
}
|
||||
|
||||
public function includeUncoveredFiles(): void
|
||||
{
|
||||
$this->includeUncoveredFiles = true;
|
||||
}
|
||||
|
||||
public function excludeUncoveredFiles(): void
|
||||
{
|
||||
$this->includeUncoveredFiles = false;
|
||||
}
|
||||
|
||||
public function enableAnnotationsForIgnoringCode(): void
|
||||
{
|
||||
$this->useAnnotationsForIgnoringCode = true;
|
||||
}
|
||||
|
||||
public function disableAnnotationsForIgnoringCode(): void
|
||||
{
|
||||
$this->useAnnotationsForIgnoringCode = false;
|
||||
}
|
||||
|
||||
public function ignoreDeprecatedCode(): void
|
||||
{
|
||||
$this->ignoreDeprecatedCode = true;
|
||||
}
|
||||
|
||||
public function doNotIgnoreDeprecatedCode(): void
|
||||
{
|
||||
$this->ignoreDeprecatedCode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-assert-if-true !null $this->cacheDirectory
|
||||
*/
|
||||
public function cachesStaticAnalysis(): bool
|
||||
{
|
||||
return $this->cacheDirectory !== null;
|
||||
}
|
||||
|
||||
public function cacheStaticAnalysis(string $directory): void
|
||||
{
|
||||
$this->cacheDirectory = $directory;
|
||||
}
|
||||
|
||||
public function doNotCacheStaticAnalysis(): void
|
||||
{
|
||||
$this->cacheDirectory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StaticAnalysisCacheNotConfiguredException
|
||||
*/
|
||||
public function cacheDirectory(): string
|
||||
{
|
||||
if (!$this->cachesStaticAnalysis()) {
|
||||
throw new StaticAnalysisCacheNotConfiguredException(
|
||||
'The static analysis cache is not configured',
|
||||
);
|
||||
}
|
||||
|
||||
return $this->cacheDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $className
|
||||
*/
|
||||
public function excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck(string $className): void
|
||||
{
|
||||
$this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck[] = $className;
|
||||
}
|
||||
|
||||
public function enableBranchAndPathCoverage(): void
|
||||
{
|
||||
$this->driver->enableBranchAndPathCoverage();
|
||||
|
||||
$this->collectBranchAndPathCoverage = true;
|
||||
}
|
||||
|
||||
public function disableBranchAndPathCoverage(): void
|
||||
{
|
||||
$this->driver->disableBranchAndPathCoverage();
|
||||
|
||||
$this->collectBranchAndPathCoverage = false;
|
||||
}
|
||||
|
||||
public function collectsBranchAndPathCoverage(): bool
|
||||
{
|
||||
return $this->collectBranchAndPathCoverage;
|
||||
}
|
||||
|
||||
public function validate(TargetCollection $targets): ValidationResult
|
||||
{
|
||||
return (new TargetCollectionValidator)->validate($this->targetMapper(), $targets);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function driverIsPcov(): bool
|
||||
{
|
||||
/** @phpstan-ignore isset.initializedProperty */
|
||||
return isset($this->driver) && $this->driver->isPcov();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function driverIsXdebug(): bool
|
||||
{
|
||||
/** @phpstan-ignore isset.initializedProperty */
|
||||
return isset($this->driver) && $this->driver->isXdebug();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param false|TargetedLines $linesToBeCovered
|
||||
* @param TargetedLines $linesToBeUsed
|
||||
*
|
||||
* @throws ReflectionException
|
||||
* @throws UnintentionallyCoveredCodeException
|
||||
*/
|
||||
private function applyCoversAndUsesFilter(RawCodeCoverageData $rawData, array|false $linesToBeCovered, array $linesToBeUsed, TestSize $size): void
|
||||
{
|
||||
if ($linesToBeCovered === false) {
|
||||
$rawData->clear();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($linesToBeCovered === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->checkForUnintentionallyCoveredCode && !$size->isMedium() && !$size->isLarge()) {
|
||||
$this->performUnintentionallyCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed);
|
||||
}
|
||||
|
||||
$rawLineData = $rawData->lineCoverage();
|
||||
$filesWithNoCoverage = array_diff_key($rawLineData, $linesToBeCovered);
|
||||
|
||||
foreach (array_keys($filesWithNoCoverage) as $fileWithNoCoverage) {
|
||||
$rawData->removeCoverageDataForFile($fileWithNoCoverage);
|
||||
}
|
||||
|
||||
foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) {
|
||||
$rawData->keepLineCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
|
||||
$rawData->keepFunctionCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyFilter(RawCodeCoverageData $data): void
|
||||
{
|
||||
if (!$this->filter->isEmpty()) {
|
||||
foreach (array_keys($data->lineCoverage()) as $filename) {
|
||||
if ($this->filter->isExcluded($filename)) {
|
||||
$data->removeCoverageDataForFile($filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data->skipEmptyLines();
|
||||
}
|
||||
|
||||
private function applyExecutableLinesFilter(RawCodeCoverageData $data): void
|
||||
{
|
||||
foreach (array_keys($data->lineCoverage()) as $filename) {
|
||||
if (!$this->filter->isFile($filename)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$linesToBranchMap = $this->analyser()->analyse($filename)->executableLines();
|
||||
|
||||
$data->keepLineCoverageDataOnlyForLines(
|
||||
$filename,
|
||||
array_keys($linesToBranchMap),
|
||||
);
|
||||
|
||||
$data->markExecutableLineByBranch(
|
||||
$filename,
|
||||
$linesToBranchMap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void
|
||||
{
|
||||
foreach (array_keys($data->lineCoverage()) as $filename) {
|
||||
if (!$this->filter->isFile($filename)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data->removeCoverageDataForLines(
|
||||
$filename,
|
||||
$this->analyser()->analyse($filename)->ignoredLines(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnintentionallyCoveredCodeException
|
||||
*/
|
||||
private function addUncoveredFilesFromFilter(): void
|
||||
{
|
||||
$uncoveredFiles = array_diff(
|
||||
$this->filter->files(),
|
||||
$this->data->coveredFiles(),
|
||||
);
|
||||
|
||||
foreach ($uncoveredFiles as $uncoveredFile) {
|
||||
if (is_file($uncoveredFile)) {
|
||||
$this->append(
|
||||
RawCodeCoverageData::fromUncoveredFile(
|
||||
$uncoveredFile,
|
||||
$this->analyser(),
|
||||
),
|
||||
self::UNCOVERED_FILES,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TargetedLines $linesToBeCovered
|
||||
* @param TargetedLines $linesToBeUsed
|
||||
*
|
||||
* @throws ReflectionException
|
||||
* @throws UnintentionallyCoveredCodeException
|
||||
*/
|
||||
private function performUnintentionallyCoveredCodeCheck(RawCodeCoverageData $data, array $linesToBeCovered, array $linesToBeUsed): void
|
||||
{
|
||||
$allowedLines = $this->getAllowedLines(
|
||||
$linesToBeCovered,
|
||||
$linesToBeUsed,
|
||||
);
|
||||
|
||||
$unintentionallyCoveredUnits = [];
|
||||
|
||||
foreach ($data->lineCoverage() as $file => $_data) {
|
||||
foreach ($_data as $line => $flag) {
|
||||
if ($flag === 1 && !isset($allowedLines[$file][$line])) {
|
||||
$unintentionallyCoveredUnits[] = $this->targetMapper->lookup($file, $line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
|
||||
|
||||
if ($unintentionallyCoveredUnits !== []) {
|
||||
throw new UnintentionallyCoveredCodeException(
|
||||
$unintentionallyCoveredUnits,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TargetedLines $linesToBeCovered
|
||||
* @param TargetedLines $linesToBeUsed
|
||||
*
|
||||
* @return TargetedLines
|
||||
*/
|
||||
private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed): array
|
||||
{
|
||||
$allowedLines = [];
|
||||
|
||||
foreach (array_keys($linesToBeCovered) as $file) {
|
||||
if (!isset($allowedLines[$file])) {
|
||||
$allowedLines[$file] = [];
|
||||
}
|
||||
|
||||
$allowedLines[$file] = array_merge(
|
||||
$allowedLines[$file],
|
||||
$linesToBeCovered[$file],
|
||||
);
|
||||
}
|
||||
|
||||
foreach (array_keys($linesToBeUsed) as $file) {
|
||||
if (!isset($allowedLines[$file])) {
|
||||
$allowedLines[$file] = [];
|
||||
}
|
||||
|
||||
$allowedLines[$file] = array_merge(
|
||||
$allowedLines[$file],
|
||||
$linesToBeUsed[$file],
|
||||
);
|
||||
}
|
||||
|
||||
foreach (array_keys($allowedLines) as $file) {
|
||||
$allowedLines[$file] = array_flip(
|
||||
array_unique($allowedLines[$file]),
|
||||
);
|
||||
}
|
||||
|
||||
return $allowedLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $unintentionallyCoveredUnits
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array
|
||||
{
|
||||
$unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits);
|
||||
$processed = [];
|
||||
|
||||
foreach ($unintentionallyCoveredUnits as $unintentionallyCoveredUnit) {
|
||||
$tmp = explode('::', $unintentionallyCoveredUnit);
|
||||
|
||||
if (count($tmp) !== 2) {
|
||||
$processed[] = $unintentionallyCoveredUnit;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$class = new ReflectionClass($tmp[0]);
|
||||
|
||||
foreach ($this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck as $parentClass) {
|
||||
if ($class->isSubclassOf($parentClass)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
} catch (\ReflectionException $e) {
|
||||
throw new ReflectionException(
|
||||
$e->getMessage(),
|
||||
$e->getCode(),
|
||||
$e,
|
||||
);
|
||||
}
|
||||
|
||||
$processed[] = $tmp[0];
|
||||
}
|
||||
|
||||
$processed = array_unique($processed);
|
||||
|
||||
sort($processed);
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
private function targetMapper(): Mapper
|
||||
{
|
||||
if ($this->targetMapper !== null) {
|
||||
return $this->targetMapper;
|
||||
}
|
||||
|
||||
$this->targetMapper = new Mapper(
|
||||
(new MapBuilder)->build($this->filter, $this->analyser()),
|
||||
);
|
||||
|
||||
return $this->targetMapper;
|
||||
}
|
||||
|
||||
private function analyser(): FileAnalyser
|
||||
{
|
||||
if ($this->analyser !== null) {
|
||||
return $this->analyser;
|
||||
}
|
||||
|
||||
$sourceAnalyser = new ParsingSourceAnalyser;
|
||||
|
||||
if ($this->cachesStaticAnalysis()) {
|
||||
$sourceAnalyser = new CachingSourceAnalyser(
|
||||
$this->cacheDirectory,
|
||||
$sourceAnalyser,
|
||||
);
|
||||
}
|
||||
|
||||
$this->analyser = new FileAnalyser(
|
||||
$sourceAnalyser,
|
||||
$this->useAnnotationsForIgnoringCode,
|
||||
$this->ignoreDeprecatedCode,
|
||||
);
|
||||
|
||||
return $this->analyser;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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\Data;
|
||||
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
use NoDiscard;
|
||||
use SebastianBergmann\CodeCoverage\Driver\XdebugDriver;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type TestIdType from ProcessedCodeCoverageData
|
||||
* @phpstan-import-type XdebugBranchCoverageType from XdebugDriver
|
||||
*/
|
||||
final class ProcessedBranchCoverageData
|
||||
{
|
||||
public readonly int $op_start;
|
||||
public readonly int $op_end;
|
||||
public readonly int $line_start;
|
||||
public readonly int $line_end;
|
||||
|
||||
/** @var list<TestIdType> */
|
||||
public array $hit;
|
||||
|
||||
/** @var array<int, int> */
|
||||
public readonly array $out;
|
||||
|
||||
/** @var array<int, int> */
|
||||
public readonly array $out_hit;
|
||||
|
||||
/**
|
||||
* @param XdebugBranchCoverageType $xdebugCoverageData
|
||||
*/
|
||||
public static function fromXdebugCoverage(array $xdebugCoverageData): self
|
||||
{
|
||||
return new self(
|
||||
$xdebugCoverageData['op_start'],
|
||||
$xdebugCoverageData['op_end'],
|
||||
$xdebugCoverageData['line_start'],
|
||||
$xdebugCoverageData['line_end'],
|
||||
[],
|
||||
$xdebugCoverageData['out'],
|
||||
$xdebugCoverageData['out_hit'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<TestIdType> $hit
|
||||
* @param array<int, int> $out
|
||||
* @param array<int, int> $out_hit
|
||||
*/
|
||||
public function __construct(
|
||||
int $op_start,
|
||||
int $op_end,
|
||||
int $line_start,
|
||||
int $line_end,
|
||||
array $hit,
|
||||
array $out,
|
||||
array $out_hit,
|
||||
) {
|
||||
$this->out_hit = $out_hit;
|
||||
$this->out = $out;
|
||||
$this->hit = $hit;
|
||||
$this->line_end = $line_end;
|
||||
$this->line_start = $line_start;
|
||||
$this->op_end = $op_end;
|
||||
$this->op_start = $op_start;
|
||||
}
|
||||
|
||||
#[NoDiscard]
|
||||
public function merge(self $data): self
|
||||
{
|
||||
if ($data->hit === []) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return new self(
|
||||
$this->op_start,
|
||||
$this->op_end,
|
||||
$this->line_start,
|
||||
$this->line_end,
|
||||
array_unique(array_merge($this->hit, $data->hit)),
|
||||
$this->out,
|
||||
$this->out_hit,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TestIdType $testCaseId
|
||||
*/
|
||||
public function recordHit(string $testCaseId): void
|
||||
{
|
||||
$this->hit[] = $testCaseId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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\Data;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class ProcessedClassType
|
||||
{
|
||||
public readonly string $className;
|
||||
public readonly string $namespace;
|
||||
|
||||
/**
|
||||
* @var array<string, ProcessedMethodType>
|
||||
*/
|
||||
public array $methods;
|
||||
public readonly int $startLine;
|
||||
public int $executableLines;
|
||||
public int $executedLines;
|
||||
public int $executableBranches;
|
||||
public int $executedBranches;
|
||||
public int $executablePaths;
|
||||
public int $executedPaths;
|
||||
public int $ccn;
|
||||
public float|int $coverage;
|
||||
public int|string $crap;
|
||||
public readonly string $link;
|
||||
|
||||
public function __construct(
|
||||
string $className,
|
||||
string $namespace,
|
||||
/**
|
||||
* @var array<string, ProcessedMethodType>
|
||||
*/
|
||||
array $methods,
|
||||
int $startLine,
|
||||
int $executableLines,
|
||||
int $executedLines,
|
||||
int $executableBranches,
|
||||
int $executedBranches,
|
||||
int $executablePaths,
|
||||
int $executedPaths,
|
||||
int $ccn,
|
||||
float|int $coverage,
|
||||
int|string $crap,
|
||||
string $link,
|
||||
) {
|
||||
$this->className = $className;
|
||||
$this->namespace = $namespace;
|
||||
$this->methods = $methods;
|
||||
$this->startLine = $startLine;
|
||||
$this->executableLines = $executableLines;
|
||||
$this->executedLines = $executedLines;
|
||||
$this->executableBranches = $executableBranches;
|
||||
$this->executedBranches = $executedBranches;
|
||||
$this->executablePaths = $executablePaths;
|
||||
$this->executedPaths = $executedPaths;
|
||||
$this->ccn = $ccn;
|
||||
$this->coverage = $coverage;
|
||||
$this->crap = $crap;
|
||||
$this->link = $link;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
<?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\Data;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function ksort;
|
||||
use SebastianBergmann\CodeCoverage\Driver\Driver;
|
||||
use SebastianBergmann\CodeCoverage\Driver\XdebugDriver;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type XdebugFunctionCoverageType from XdebugDriver
|
||||
*
|
||||
* @phpstan-type TestIdType string
|
||||
* @phpstan-type FunctionCoverageType array<string, array<string, ProcessedFunctionCoverageData>>
|
||||
* @phpstan-type LineCoverageType array<string, array<int, null|list<TestIdType>>>
|
||||
*/
|
||||
final class ProcessedCodeCoverageData
|
||||
{
|
||||
/**
|
||||
* Line coverage data.
|
||||
* An array of filenames, each having an array of linenumbers, each executable line having an array of testcase ids.
|
||||
*
|
||||
* @var LineCoverageType
|
||||
*/
|
||||
private array $lineCoverage = [];
|
||||
|
||||
/**
|
||||
* Function coverage data.
|
||||
* Maintains base format of raw data (@see https://xdebug.org/docs/code_coverage), but each 'hit' entry is an array
|
||||
* of testcase ids.
|
||||
*
|
||||
* @var FunctionCoverageType
|
||||
*/
|
||||
private array $functionCoverage = [];
|
||||
|
||||
public function initializeUnseenData(RawCodeCoverageData $rawData): void
|
||||
{
|
||||
foreach ($rawData->lineCoverage() as $file => $lines) {
|
||||
if (!isset($this->lineCoverage[$file])) {
|
||||
$this->lineCoverage[$file] = [];
|
||||
|
||||
foreach ($lines as $k => $v) {
|
||||
$this->lineCoverage[$file][$k] = $v === Driver::LINE_NOT_EXECUTABLE ? null : [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rawData->functionCoverage() as $file => $functions) {
|
||||
foreach ($functions as $functionName => $functionData) {
|
||||
if (isset($this->functionCoverage[$file][$functionName])) {
|
||||
$this->initPreviouslySeenFunction($file, $functionName, $functionData);
|
||||
} else {
|
||||
$this->initPreviouslyUnseenFunction($file, $functionName, $functionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function markCodeAsExecutedByTestCase(string $testCaseId, RawCodeCoverageData $executedCode): void
|
||||
{
|
||||
foreach ($executedCode->lineCoverage() as $file => $lines) {
|
||||
foreach ($lines as $k => $v) {
|
||||
if ($v === Driver::LINE_EXECUTED) {
|
||||
$this->lineCoverage[$file][$k][] = $testCaseId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($executedCode->functionCoverage() as $file => $functions) {
|
||||
foreach ($functions as $functionName => $functionData) {
|
||||
foreach ($functionData['branches'] as $branchId => $branchData) {
|
||||
if ($branchData['hit'] === Driver::BRANCH_HIT) {
|
||||
$this->functionCoverage[$file][$functionName]->recordBranchHit($branchId, $testCaseId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($functionData['paths'] as $pathId => $pathData) {
|
||||
if ($pathData['hit'] === Driver::BRANCH_HIT) {
|
||||
$this->functionCoverage[$file][$functionName]->recordPathHit($pathId, $testCaseId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LineCoverageType $lineCoverage
|
||||
*/
|
||||
public function setLineCoverage(array $lineCoverage): void
|
||||
{
|
||||
$this->lineCoverage = $lineCoverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LineCoverageType
|
||||
*/
|
||||
public function lineCoverage(): array
|
||||
{
|
||||
ksort($this->lineCoverage);
|
||||
|
||||
return $this->lineCoverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FunctionCoverageType $functionCoverage
|
||||
*/
|
||||
public function setFunctionCoverage(array $functionCoverage): void
|
||||
{
|
||||
$this->functionCoverage = $functionCoverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FunctionCoverageType
|
||||
*/
|
||||
public function functionCoverage(): array
|
||||
{
|
||||
ksort($this->functionCoverage);
|
||||
|
||||
return $this->functionCoverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function coveredFiles(): array
|
||||
{
|
||||
ksort($this->lineCoverage);
|
||||
|
||||
return array_keys($this->lineCoverage);
|
||||
}
|
||||
|
||||
public function renameFile(string $oldFile, string $newFile): void
|
||||
{
|
||||
$this->lineCoverage[$newFile] = $this->lineCoverage[$oldFile];
|
||||
|
||||
if (isset($this->functionCoverage[$oldFile])) {
|
||||
$this->functionCoverage[$newFile] = $this->functionCoverage[$oldFile];
|
||||
}
|
||||
|
||||
unset($this->lineCoverage[$oldFile], $this->functionCoverage[$oldFile]);
|
||||
}
|
||||
|
||||
public function merge(self $newData): void
|
||||
{
|
||||
foreach ($newData->lineCoverage as $file => $lines) {
|
||||
if (!isset($this->lineCoverage[$file])) {
|
||||
$this->lineCoverage[$file] = $lines;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// we should compare the lines if any of two contains data
|
||||
$compareLineNumbers = array_unique(
|
||||
array_merge(
|
||||
array_keys($this->lineCoverage[$file]),
|
||||
array_keys($newData->lineCoverage[$file]),
|
||||
),
|
||||
);
|
||||
|
||||
foreach ($compareLineNumbers as $line) {
|
||||
$thatPriority = $this->priorityForLine($newData->lineCoverage[$file], $line);
|
||||
$thisPriority = $this->priorityForLine($this->lineCoverage[$file], $line);
|
||||
|
||||
if ($thatPriority > $thisPriority) {
|
||||
$this->lineCoverage[$file][$line] = $newData->lineCoverage[$file][$line];
|
||||
} elseif ($thatPriority === $thisPriority && is_array($this->lineCoverage[$file][$line])) {
|
||||
$this->lineCoverage[$file][$line] = array_unique(
|
||||
array_merge($this->lineCoverage[$file][$line], $newData->lineCoverage[$file][$line]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($newData->functionCoverage as $file => $functions) {
|
||||
if (!isset($this->functionCoverage[$file])) {
|
||||
$this->functionCoverage[$file] = $functions;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($functions as $functionName => $functionData) {
|
||||
if (isset($this->functionCoverage[$file][$functionName])) {
|
||||
$this->initPreviouslySeenFunction($file, $functionName, $functionData);
|
||||
} else {
|
||||
$this->initPreviouslyUnseenFunction($file, $functionName, $functionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the priority for a line.
|
||||
*
|
||||
* 1 = the line is not set
|
||||
* 2 = the line has not been tested
|
||||
* 3 = the line is dead code
|
||||
* 4 = the line has been tested
|
||||
*
|
||||
* During a merge, a higher number is better.
|
||||
*
|
||||
* @return 1|2|3|4
|
||||
*/
|
||||
private function priorityForLine(array $data, int $line): int
|
||||
{
|
||||
if (!array_key_exists($line, $data)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (is_array($data[$line]) && count($data[$line]) === 0) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if ($data[$line] === null) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a function we have never seen before, copy all data over and simply init the 'hit' array.
|
||||
*
|
||||
* @param ProcessedFunctionCoverageData|XdebugFunctionCoverageType $functionData
|
||||
*/
|
||||
private function initPreviouslyUnseenFunction(string $file, string $functionName, array|ProcessedFunctionCoverageData $functionData): void
|
||||
{
|
||||
if (is_array($functionData)) {
|
||||
$functionData = ProcessedFunctionCoverageData::fromXdebugCoverage($functionData);
|
||||
}
|
||||
|
||||
$this->functionCoverage[$file][$functionName] = $functionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a function we have seen before, only copy over and init the 'hit' array for any unseen branches and paths.
|
||||
* Techniques such as mocking and where the contents of a file are different vary during tests (e.g. compiling
|
||||
* containers) mean that the functions inside a file cannot be relied upon to be static.
|
||||
*
|
||||
* @param ProcessedFunctionCoverageData|XdebugFunctionCoverageType $functionData
|
||||
*/
|
||||
private function initPreviouslySeenFunction(string $file, string $functionName, array|ProcessedFunctionCoverageData $functionData): void
|
||||
{
|
||||
if (is_array($functionData)) {
|
||||
$functionData = ProcessedFunctionCoverageData::fromXdebugCoverage($functionData);
|
||||
}
|
||||
|
||||
$this->functionCoverage[$file][$functionName] = $this->functionCoverage[$file][$functionName]->merge(
|
||||
$functionData,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?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\Data;
|
||||
|
||||
use NoDiscard;
|
||||
use SebastianBergmann\CodeCoverage\Driver\XdebugDriver;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type TestIdType from ProcessedCodeCoverageData
|
||||
* @phpstan-import-type XdebugFunctionCoverageType from XdebugDriver
|
||||
*/
|
||||
final readonly class ProcessedFunctionCoverageData
|
||||
{
|
||||
/** @var array<int, ProcessedBranchCoverageData> */
|
||||
public array $branches;
|
||||
|
||||
/** @var array<int, ProcessedPathCoverageData> */
|
||||
public array $paths;
|
||||
|
||||
/**
|
||||
* @param XdebugFunctionCoverageType $xdebugCoverageData
|
||||
*/
|
||||
public static function fromXdebugCoverage(array $xdebugCoverageData): self
|
||||
{
|
||||
$branches = [];
|
||||
|
||||
foreach ($xdebugCoverageData['branches'] as $branchId => $branch) {
|
||||
$branches[$branchId] = ProcessedBranchCoverageData::fromXdebugCoverage($branch);
|
||||
}
|
||||
$paths = [];
|
||||
|
||||
foreach ($xdebugCoverageData['paths'] as $pathId => $path) {
|
||||
$paths[$pathId] = ProcessedPathCoverageData::fromXdebugCoverage($path);
|
||||
}
|
||||
|
||||
return new self(
|
||||
$branches,
|
||||
$paths,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, ProcessedBranchCoverageData> $branches
|
||||
* @param array<int, ProcessedPathCoverageData> $paths
|
||||
*/
|
||||
public function __construct(
|
||||
array $branches,
|
||||
array $paths,
|
||||
) {
|
||||
$this->paths = $paths;
|
||||
$this->branches = $branches;
|
||||
}
|
||||
|
||||
#[NoDiscard]
|
||||
public function merge(self $data): self
|
||||
{
|
||||
$branches = null;
|
||||
|
||||
if ($data->branches !== $this->branches) {
|
||||
$branches = $this->branches;
|
||||
|
||||
foreach ($data->branches as $branchId => $branch) {
|
||||
if (!isset($branches[$branchId])) {
|
||||
$branches[$branchId] = $branch;
|
||||
} else {
|
||||
$branches[$branchId] = $branches[$branchId]->merge($branch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$paths = null;
|
||||
|
||||
if ($data->paths !== $this->paths) {
|
||||
$paths = $this->paths;
|
||||
|
||||
foreach ($data->paths as $pathId => $path) {
|
||||
if (!isset($paths[$pathId])) {
|
||||
$paths[$pathId] = $path;
|
||||
} else {
|
||||
$paths[$pathId] = $paths[$pathId]->merge($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($branches === null && $paths === null) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return new self(
|
||||
$branches ?? $this->branches,
|
||||
$paths ?? $this->paths,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TestIdType $testCaseId
|
||||
*/
|
||||
public function recordBranchHit(int $branchId, string $testCaseId): void
|
||||
{
|
||||
$this->branches[$branchId]->recordHit($testCaseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TestIdType $testCaseId
|
||||
*/
|
||||
public function recordPathHit(int $pathId, string $testCaseId): void
|
||||
{
|
||||
$this->paths[$pathId]->recordHit($testCaseId);
|
||||
}
|
||||
}
|
||||
@@ -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\Data;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class ProcessedFunctionType
|
||||
{
|
||||
public readonly string $functionName;
|
||||
public readonly string $namespace;
|
||||
public readonly string $signature;
|
||||
public readonly int $startLine;
|
||||
public readonly int $endLine;
|
||||
public int $executableLines;
|
||||
public int $executedLines;
|
||||
public int $executableBranches;
|
||||
public int $executedBranches;
|
||||
public int $executablePaths;
|
||||
public int $executedPaths;
|
||||
public int $ccn;
|
||||
public float|int $coverage;
|
||||
public int|string $crap;
|
||||
public readonly string $link;
|
||||
|
||||
public function __construct(
|
||||
string $functionName,
|
||||
string $namespace,
|
||||
string $signature,
|
||||
int $startLine,
|
||||
int $endLine,
|
||||
int $executableLines,
|
||||
int $executedLines,
|
||||
int $executableBranches,
|
||||
int $executedBranches,
|
||||
int $executablePaths,
|
||||
int $executedPaths,
|
||||
int $ccn,
|
||||
float|int $coverage,
|
||||
int|string $crap,
|
||||
string $link,
|
||||
) {
|
||||
$this->link = $link;
|
||||
$this->crap = $crap;
|
||||
$this->coverage = $coverage;
|
||||
$this->ccn = $ccn;
|
||||
$this->executedPaths = $executedPaths;
|
||||
$this->executablePaths = $executablePaths;
|
||||
$this->executedBranches = $executedBranches;
|
||||
$this->executableBranches = $executableBranches;
|
||||
$this->executedLines = $executedLines;
|
||||
$this->executableLines = $executableLines;
|
||||
$this->endLine = $endLine;
|
||||
$this->startLine = $startLine;
|
||||
$this->signature = $signature;
|
||||
$this->namespace = $namespace;
|
||||
$this->functionName = $functionName;
|
||||
}
|
||||
}
|
||||
@@ -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\Data;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class ProcessedMethodType
|
||||
{
|
||||
public readonly string $methodName;
|
||||
public readonly string $visibility;
|
||||
public readonly string $signature;
|
||||
public readonly int $startLine;
|
||||
public readonly int $endLine;
|
||||
public int $executableLines;
|
||||
public int $executedLines;
|
||||
public int $executableBranches;
|
||||
public int $executedBranches;
|
||||
public int $executablePaths;
|
||||
public int $executedPaths;
|
||||
public int $ccn;
|
||||
public float|int $coverage;
|
||||
public int|string $crap;
|
||||
public readonly string $link;
|
||||
|
||||
public function __construct(
|
||||
string $methodName,
|
||||
string $visibility,
|
||||
string $signature,
|
||||
int $startLine,
|
||||
int $endLine,
|
||||
int $executableLines,
|
||||
int $executedLines,
|
||||
int $executableBranches,
|
||||
int $executedBranches,
|
||||
int $executablePaths,
|
||||
int $executedPaths,
|
||||
int $ccn,
|
||||
float|int $coverage,
|
||||
int|string $crap,
|
||||
string $link,
|
||||
) {
|
||||
$this->link = $link;
|
||||
$this->crap = $crap;
|
||||
$this->coverage = $coverage;
|
||||
$this->ccn = $ccn;
|
||||
$this->executedPaths = $executedPaths;
|
||||
$this->executablePaths = $executablePaths;
|
||||
$this->executedBranches = $executedBranches;
|
||||
$this->executableBranches = $executableBranches;
|
||||
$this->executedLines = $executedLines;
|
||||
$this->executableLines = $executableLines;
|
||||
$this->endLine = $endLine;
|
||||
$this->startLine = $startLine;
|
||||
$this->signature = $signature;
|
||||
$this->visibility = $visibility;
|
||||
$this->methodName = $methodName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?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\Data;
|
||||
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
use NoDiscard;
|
||||
use SebastianBergmann\CodeCoverage\Driver\XdebugDriver;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type TestIdType from ProcessedCodeCoverageData
|
||||
* @phpstan-import-type XdebugPathCoverageType from XdebugDriver
|
||||
*/
|
||||
final class ProcessedPathCoverageData
|
||||
{
|
||||
/** @var array<int, int> */
|
||||
public readonly array $path;
|
||||
|
||||
/** @var list<TestIdType> */
|
||||
public array $hit;
|
||||
|
||||
/**
|
||||
* @param XdebugPathCoverageType $xdebugCoverageData
|
||||
*/
|
||||
public static function fromXdebugCoverage(array $xdebugCoverageData): self
|
||||
{
|
||||
return new self(
|
||||
$xdebugCoverageData['path'],
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $path
|
||||
* @param list<TestIdType> $hit
|
||||
*/
|
||||
public function __construct(
|
||||
array $path,
|
||||
array $hit,
|
||||
) {
|
||||
$this->hit = $hit;
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
#[NoDiscard]
|
||||
public function merge(self $data): self
|
||||
{
|
||||
if ($data->hit === []) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return new self(
|
||||
$this->path,
|
||||
array_unique(array_merge($this->hit, $data->hit)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TestIdType $testCaseId
|
||||
*/
|
||||
public function recordHit(string $testCaseId): void
|
||||
{
|
||||
$this->hit[] = $testCaseId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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\Data;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class ProcessedTraitType
|
||||
{
|
||||
public readonly string $traitName;
|
||||
public readonly string $namespace;
|
||||
|
||||
/**
|
||||
* @var array<string, ProcessedMethodType>
|
||||
*/
|
||||
public array $methods;
|
||||
public readonly int $startLine;
|
||||
public int $executableLines;
|
||||
public int $executedLines;
|
||||
public int $executableBranches;
|
||||
public int $executedBranches;
|
||||
public int $executablePaths;
|
||||
public int $executedPaths;
|
||||
public int $ccn;
|
||||
public float|int $coverage;
|
||||
public int|string $crap;
|
||||
public readonly string $link;
|
||||
|
||||
public function __construct(
|
||||
string $traitName,
|
||||
string $namespace,
|
||||
/**
|
||||
* @var array<string, ProcessedMethodType>
|
||||
*/
|
||||
array $methods,
|
||||
int $startLine,
|
||||
int $executableLines,
|
||||
int $executedLines,
|
||||
int $executableBranches,
|
||||
int $executedBranches,
|
||||
int $executablePaths,
|
||||
int $executedPaths,
|
||||
int $ccn,
|
||||
float|int $coverage,
|
||||
int|string $crap,
|
||||
string $link,
|
||||
) {
|
||||
$this->link = $link;
|
||||
$this->crap = $crap;
|
||||
$this->coverage = $coverage;
|
||||
$this->ccn = $ccn;
|
||||
$this->executedPaths = $executedPaths;
|
||||
$this->executablePaths = $executablePaths;
|
||||
$this->executedBranches = $executedBranches;
|
||||
$this->executableBranches = $executableBranches;
|
||||
$this->executedLines = $executedLines;
|
||||
$this->executableLines = $executableLines;
|
||||
$this->startLine = $startLine;
|
||||
$this->methods = $methods;
|
||||
$this->namespace = $namespace;
|
||||
$this->traitName = $traitName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?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\Data;
|
||||
|
||||
use function array_diff;
|
||||
use function array_diff_key;
|
||||
use function array_flip;
|
||||
use function array_intersect;
|
||||
use function array_intersect_key;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function file_get_contents;
|
||||
use function in_array;
|
||||
use function is_file;
|
||||
use function preg_replace;
|
||||
use function range;
|
||||
use function str_ends_with;
|
||||
use function str_starts_with;
|
||||
use function trim;
|
||||
use SebastianBergmann\CodeCoverage\Driver\Driver;
|
||||
use SebastianBergmann\CodeCoverage\Driver\XdebugDriver;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type XdebugFunctionsCoverageType from XdebugDriver
|
||||
* @phpstan-import-type XdebugCodeCoverageWithoutPathCoverageType from XdebugDriver
|
||||
* @phpstan-import-type XdebugCodeCoverageWithPathCoverageType from XdebugDriver
|
||||
*/
|
||||
final class RawCodeCoverageData
|
||||
{
|
||||
/**
|
||||
* @var array<string, array<int>>
|
||||
*/
|
||||
private static array $emptyLineCache = [];
|
||||
|
||||
/**
|
||||
* @var XdebugCodeCoverageWithoutPathCoverageType
|
||||
*/
|
||||
private array $lineCoverage;
|
||||
|
||||
/**
|
||||
* @var array<string, XdebugFunctionsCoverageType>
|
||||
*/
|
||||
private array $functionCoverage;
|
||||
|
||||
/**
|
||||
* @param XdebugCodeCoverageWithoutPathCoverageType $rawCoverage
|
||||
*/
|
||||
public static function fromXdebugWithoutPathCoverage(array $rawCoverage): self
|
||||
{
|
||||
return new self($rawCoverage, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XdebugCodeCoverageWithPathCoverageType $rawCoverage
|
||||
*/
|
||||
public static function fromXdebugWithPathCoverage(array $rawCoverage): self
|
||||
{
|
||||
$lineCoverage = [];
|
||||
$functionCoverage = [];
|
||||
|
||||
foreach ($rawCoverage as $file => $fileCoverageData) {
|
||||
// Xdebug annotates the function name of traits, strip that off
|
||||
foreach ($fileCoverageData['functions'] as $existingKey => $data) {
|
||||
if (str_ends_with($existingKey, '}') && !str_starts_with($existingKey, '{')) { // don't want to catch {main}
|
||||
$newKey = preg_replace('/\{.*}$/', '', $existingKey);
|
||||
$fileCoverageData['functions'][$newKey] = $data;
|
||||
unset($fileCoverageData['functions'][$existingKey]);
|
||||
}
|
||||
}
|
||||
|
||||
$lineCoverage[$file] = $fileCoverageData['lines'];
|
||||
$functionCoverage[$file] = $fileCoverageData['functions'];
|
||||
}
|
||||
|
||||
return new self($lineCoverage, $functionCoverage);
|
||||
}
|
||||
|
||||
public static function fromUncoveredFile(string $filename, FileAnalyser $analyser): self
|
||||
{
|
||||
$lineCoverage = array_map(
|
||||
static fn (): int => Driver::LINE_NOT_EXECUTED,
|
||||
$analyser->analyse($filename)->executableLines(),
|
||||
);
|
||||
|
||||
return new self([$filename => $lineCoverage], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XdebugCodeCoverageWithoutPathCoverageType $lineCoverage
|
||||
* @param array<string, XdebugFunctionsCoverageType> $functionCoverage
|
||||
*/
|
||||
private function __construct(array $lineCoverage, array $functionCoverage)
|
||||
{
|
||||
$this->lineCoverage = $lineCoverage;
|
||||
$this->functionCoverage = $functionCoverage;
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->lineCoverage = $this->functionCoverage = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return XdebugCodeCoverageWithoutPathCoverageType
|
||||
*/
|
||||
public function lineCoverage(): array
|
||||
{
|
||||
return $this->lineCoverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, XdebugFunctionsCoverageType>
|
||||
*/
|
||||
public function functionCoverage(): array
|
||||
{
|
||||
return $this->functionCoverage;
|
||||
}
|
||||
|
||||
public function removeCoverageDataForFile(string $filename): void
|
||||
{
|
||||
unset($this->lineCoverage[$filename], $this->functionCoverage[$filename]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $lines
|
||||
*/
|
||||
public function keepLineCoverageDataOnlyForLines(string $filename, array $lines): void
|
||||
{
|
||||
if (!isset($this->lineCoverage[$filename])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->lineCoverage[$filename] = array_intersect_key(
|
||||
$this->lineCoverage[$filename],
|
||||
array_flip($lines),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $linesToBranchMap
|
||||
*/
|
||||
public function markExecutableLineByBranch(string $filename, array $linesToBranchMap): void
|
||||
{
|
||||
if (!isset($this->lineCoverage[$filename])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$linesByBranch = [];
|
||||
|
||||
foreach ($linesToBranchMap as $line => $branch) {
|
||||
$linesByBranch[$branch][] = $line;
|
||||
}
|
||||
|
||||
foreach ($this->lineCoverage[$filename] as $line => $lineStatus) {
|
||||
if (!isset($linesToBranchMap[$line])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$branch = $linesToBranchMap[$line];
|
||||
|
||||
if (!isset($linesByBranch[$branch])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($linesByBranch[$branch] as $lineInBranch) {
|
||||
$this->lineCoverage[$filename][$lineInBranch] = $lineStatus;
|
||||
}
|
||||
|
||||
if (Driver::LINE_EXECUTED === $lineStatus) {
|
||||
unset($linesByBranch[$branch]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $lines
|
||||
*/
|
||||
public function keepFunctionCoverageDataOnlyForLines(string $filename, array $lines): void
|
||||
{
|
||||
if (!isset($this->functionCoverage[$filename])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->functionCoverage[$filename] as $functionName => $functionData) {
|
||||
foreach ($functionData['branches'] as $branchId => $branch) {
|
||||
if (count(array_diff(range($branch['line_start'], $branch['line_end']), $lines)) > 0) {
|
||||
unset($this->functionCoverage[$filename][$functionName]['branches'][$branchId]);
|
||||
|
||||
foreach ($functionData['paths'] as $pathId => $path) {
|
||||
if (in_array($branchId, $path['path'], true)) {
|
||||
unset($this->functionCoverage[$filename][$functionName]['paths'][$pathId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $lines
|
||||
*/
|
||||
public function removeCoverageDataForLines(string $filename, array $lines): void
|
||||
{
|
||||
if ($lines === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($this->lineCoverage[$filename])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->lineCoverage[$filename] = array_diff_key(
|
||||
$this->lineCoverage[$filename],
|
||||
array_flip($lines),
|
||||
);
|
||||
|
||||
if (isset($this->functionCoverage[$filename])) {
|
||||
foreach ($this->functionCoverage[$filename] as $functionName => $functionData) {
|
||||
foreach ($functionData['branches'] as $branchId => $branch) {
|
||||
if (count(array_intersect($lines, range($branch['line_start'], $branch['line_end']))) > 0) {
|
||||
unset($this->functionCoverage[$filename][$functionName]['branches'][$branchId]);
|
||||
|
||||
foreach ($functionData['paths'] as $pathId => $path) {
|
||||
if (in_array($branchId, $path['path'], true)) {
|
||||
unset($this->functionCoverage[$filename][$functionName]['paths'][$pathId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* At the end of a file, the PHP interpreter always sees an implicit return. Where this occurs in a file that has
|
||||
* e.g. a class definition, that line cannot be invoked from a test and results in confusing coverage. This engine
|
||||
* implementation detail therefore needs to be masked which is done here by simply ensuring that all empty lines
|
||||
* are skipped over for coverage purposes.
|
||||
*
|
||||
* @see https://github.com/sebastianbergmann/php-code-coverage/issues/799
|
||||
*/
|
||||
public function skipEmptyLines(): void
|
||||
{
|
||||
foreach ($this->lineCoverage as $filename => $coverage) {
|
||||
foreach ($this->getEmptyLinesForFile($filename) as $emptyLine) {
|
||||
unset($this->lineCoverage[$filename][$emptyLine]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
private function getEmptyLinesForFile(string $filename): array
|
||||
{
|
||||
if (!isset(self::$emptyLineCache[$filename])) {
|
||||
self::$emptyLineCache[$filename] = [];
|
||||
|
||||
if (is_file($filename)) {
|
||||
$sourceLines = explode("\n", file_get_contents($filename));
|
||||
|
||||
foreach ($sourceLines as $line => $source) {
|
||||
if (trim($source) === '') {
|
||||
self::$emptyLineCache[$filename][] = ($line + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::$emptyLineCache[$filename];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?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\Driver;
|
||||
|
||||
use function sprintf;
|
||||
use SebastianBergmann\CodeCoverage\BranchAndPathCoverageNotSupportedException;
|
||||
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
abstract class Driver
|
||||
{
|
||||
/**
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const int LINE_NOT_EXECUTABLE = -2;
|
||||
|
||||
/**
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const int LINE_NOT_EXECUTED = -1;
|
||||
|
||||
/**
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const int LINE_EXECUTED = 1;
|
||||
|
||||
/**
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const int BRANCH_NOT_HIT = 0;
|
||||
|
||||
/**
|
||||
* @see http://xdebug.org/docs/code_coverage
|
||||
*/
|
||||
public const int BRANCH_HIT = 1;
|
||||
private bool $collectBranchAndPathCoverage = false;
|
||||
|
||||
public function canCollectBranchAndPathCoverage(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function collectsBranchAndPathCoverage(): bool
|
||||
{
|
||||
return $this->collectBranchAndPathCoverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BranchAndPathCoverageNotSupportedException
|
||||
*/
|
||||
public function enableBranchAndPathCoverage(): void
|
||||
{
|
||||
if (!$this->canCollectBranchAndPathCoverage()) {
|
||||
throw new BranchAndPathCoverageNotSupportedException(
|
||||
sprintf(
|
||||
'%s does not support branch and path coverage',
|
||||
$this->nameAndVersion(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$this->collectBranchAndPathCoverage = true;
|
||||
}
|
||||
|
||||
public function disableBranchAndPathCoverage(): void
|
||||
{
|
||||
$this->collectBranchAndPathCoverage = false;
|
||||
}
|
||||
|
||||
public function isPcov(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isXdebug(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
abstract public function nameAndVersion(): string;
|
||||
|
||||
abstract public function start(): void;
|
||||
|
||||
abstract public function stop(): RawCodeCoverageData;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?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\Driver;
|
||||
|
||||
use const pcov\inclusive;
|
||||
use function array_intersect;
|
||||
use function extension_loaded;
|
||||
use function pcov\clear;
|
||||
use function pcov\collect;
|
||||
use function pcov\start;
|
||||
use function pcov\stop;
|
||||
use function pcov\waiting;
|
||||
use function phpversion;
|
||||
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
|
||||
use SebastianBergmann\CodeCoverage\Filter;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class PcovDriver extends Driver
|
||||
{
|
||||
private readonly Filter $filter;
|
||||
|
||||
/**
|
||||
* @throws PcovNotAvailableException
|
||||
*/
|
||||
public function __construct(Filter $filter)
|
||||
{
|
||||
$this->ensurePcovIsAvailable();
|
||||
|
||||
$this->filter = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function start(): void
|
||||
{
|
||||
start();
|
||||
}
|
||||
|
||||
public function stop(): RawCodeCoverageData
|
||||
{
|
||||
stop();
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
$filesToCollectCoverageFor = waiting();
|
||||
$collected = [];
|
||||
|
||||
if ($filesToCollectCoverageFor !== []) {
|
||||
if (!$this->filter->isEmpty()) {
|
||||
$filesToCollectCoverageFor = array_intersect($filesToCollectCoverageFor, $this->filter->files());
|
||||
}
|
||||
|
||||
$collected = collect(inclusive, $filesToCollectCoverageFor);
|
||||
|
||||
clear();
|
||||
}
|
||||
|
||||
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($collected);
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
public function nameAndVersion(): string
|
||||
{
|
||||
return 'PCOV ' . phpversion('pcov');
|
||||
}
|
||||
|
||||
public function isPcov(): true
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PcovNotAvailableException
|
||||
*/
|
||||
private function ensurePcovIsAvailable(): void
|
||||
{
|
||||
if (!extension_loaded('pcov')) {
|
||||
throw new PcovNotAvailableException;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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\Driver;
|
||||
|
||||
use SebastianBergmann\CodeCoverage\Filter;
|
||||
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverAvailableException;
|
||||
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverWithPathCoverageSupportAvailableException;
|
||||
use SebastianBergmann\Environment\Runtime;
|
||||
|
||||
final class Selector
|
||||
{
|
||||
/**
|
||||
* @throws NoCodeCoverageDriverAvailableException
|
||||
* @throws PcovNotAvailableException
|
||||
* @throws XdebugNotAvailableException
|
||||
* @throws XdebugNotEnabledException
|
||||
* @throws XdebugVersionNotSupportedException
|
||||
*/
|
||||
public function forLineCoverage(Filter $filter): Driver
|
||||
{
|
||||
$runtime = new Runtime;
|
||||
|
||||
if ($runtime->hasPCOV()) {
|
||||
return new PcovDriver($filter);
|
||||
}
|
||||
|
||||
if ($runtime->hasXdebug()) {
|
||||
return new XdebugDriver($filter);
|
||||
}
|
||||
|
||||
throw new NoCodeCoverageDriverAvailableException;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException
|
||||
* @throws XdebugNotAvailableException
|
||||
* @throws XdebugNotEnabledException
|
||||
* @throws XdebugVersionNotSupportedException
|
||||
*/
|
||||
public function forLineAndPathCoverage(Filter $filter): Driver
|
||||
{
|
||||
if ((new Runtime)->hasXdebug()) {
|
||||
$driver = new XdebugDriver($filter);
|
||||
|
||||
$driver->enableBranchAndPathCoverage();
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
throw new NoCodeCoverageDriverWithPathCoverageSupportAvailableException;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?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\Driver;
|
||||
|
||||
use const XDEBUG_CC_BRANCH_CHECK;
|
||||
use const XDEBUG_CC_DEAD_CODE;
|
||||
use const XDEBUG_CC_UNUSED;
|
||||
use const XDEBUG_FILTER_CODE_COVERAGE;
|
||||
use const XDEBUG_PATH_INCLUDE;
|
||||
use function extension_loaded;
|
||||
use function in_array;
|
||||
use function phpversion;
|
||||
use function version_compare;
|
||||
use function xdebug_get_code_coverage;
|
||||
use function xdebug_info;
|
||||
use function xdebug_set_filter;
|
||||
use function xdebug_start_code_coverage;
|
||||
use function xdebug_stop_code_coverage;
|
||||
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
|
||||
use SebastianBergmann\CodeCoverage\Filter;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @see https://xdebug.org/docs/code_coverage#xdebug_get_code_coverage
|
||||
*
|
||||
* @phpstan-type XdebugLinesCoverageType array<int, int>
|
||||
* @phpstan-type XdebugBranchCoverageType array{
|
||||
* op_start: int,
|
||||
* op_end: int,
|
||||
* line_start: int,
|
||||
* line_end: int,
|
||||
* hit: int,
|
||||
* out: array<int, int>,
|
||||
* out_hit: array<int, int>,
|
||||
* }
|
||||
* @phpstan-type XdebugPathCoverageType array{
|
||||
* path: array<int, int>,
|
||||
* hit: int,
|
||||
* }
|
||||
* @phpstan-type XdebugFunctionCoverageType array{
|
||||
* branches: array<int, XdebugBranchCoverageType>,
|
||||
* paths: array<int, XdebugPathCoverageType>,
|
||||
* }
|
||||
* @phpstan-type XdebugFunctionsCoverageType array<string, XdebugFunctionCoverageType>
|
||||
* @phpstan-type XdebugPathAndBranchesCoverageType array{
|
||||
* lines: XdebugLinesCoverageType,
|
||||
* functions: XdebugFunctionsCoverageType,
|
||||
* }
|
||||
* @phpstan-type XdebugCodeCoverageWithoutPathCoverageType array<string, XdebugLinesCoverageType>
|
||||
* @phpstan-type XdebugCodeCoverageWithPathCoverageType array<string, XdebugPathAndBranchesCoverageType>
|
||||
*/
|
||||
final class XdebugDriver extends Driver
|
||||
{
|
||||
/**
|
||||
* @throws XdebugNotAvailableException
|
||||
* @throws XdebugNotEnabledException
|
||||
* @throws XdebugVersionNotSupportedException
|
||||
*/
|
||||
public function __construct(Filter $filter)
|
||||
{
|
||||
$this->ensureXdebugIsAvailable();
|
||||
|
||||
if (!$filter->isEmpty()) {
|
||||
xdebug_set_filter(
|
||||
XDEBUG_FILTER_CODE_COVERAGE,
|
||||
XDEBUG_PATH_INCLUDE,
|
||||
$filter->files(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function canCollectBranchAndPathCoverage(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
$flags = XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE;
|
||||
|
||||
if ($this->collectsBranchAndPathCoverage()) {
|
||||
$flags |= XDEBUG_CC_BRANCH_CHECK;
|
||||
}
|
||||
|
||||
xdebug_start_code_coverage($flags);
|
||||
}
|
||||
|
||||
public function stop(): RawCodeCoverageData
|
||||
{
|
||||
$data = xdebug_get_code_coverage();
|
||||
|
||||
xdebug_stop_code_coverage();
|
||||
|
||||
if ($this->collectsBranchAndPathCoverage()) {
|
||||
/* @var XdebugCodeCoverageWithPathCoverageType $data */
|
||||
return RawCodeCoverageData::fromXdebugWithPathCoverage($data);
|
||||
}
|
||||
|
||||
/* @var XdebugCodeCoverageWithoutPathCoverageType $data */
|
||||
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data);
|
||||
}
|
||||
|
||||
public function nameAndVersion(): string
|
||||
{
|
||||
return 'Xdebug ' . phpversion('xdebug');
|
||||
}
|
||||
|
||||
public function isXdebug(): true
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws XdebugNotAvailableException
|
||||
* @throws XdebugNotEnabledException
|
||||
* @throws XdebugVersionNotSupportedException
|
||||
*/
|
||||
private function ensureXdebugIsAvailable(): void
|
||||
{
|
||||
if (!extension_loaded('xdebug')) {
|
||||
throw new XdebugNotAvailableException;
|
||||
}
|
||||
|
||||
if (!version_compare(phpversion('xdebug'), '3.1', '>=')) {
|
||||
throw new XdebugVersionNotSupportedException(phpversion('xdebug'));
|
||||
}
|
||||
|
||||
if (!in_array('coverage', xdebug_info('mode'), true)) {
|
||||
throw new XdebugNotEnabledException;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
<?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;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class BranchAndPathCoverageNotSupportedException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
+17
@@ -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\Util;
|
||||
|
||||
use RuntimeException;
|
||||
use SebastianBergmann\CodeCoverage\Exception;
|
||||
|
||||
final class DirectoryCouldNotBeCreatedException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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;
|
||||
|
||||
use Throwable;
|
||||
|
||||
interface Exception extends Throwable
|
||||
{
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?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;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class FileCouldNotBeWrittenException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?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;
|
||||
|
||||
final class InvalidArgumentException extends \InvalidArgumentException implements Exception
|
||||
{
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?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\Test\Target;
|
||||
|
||||
use function sprintf;
|
||||
use RuntimeException;
|
||||
use SebastianBergmann\CodeCoverage\Exception;
|
||||
|
||||
final class InvalidCodeCoverageTargetException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct(Target $target)
|
||||
{
|
||||
parent::__construct(
|
||||
sprintf(
|
||||
'%s is not a valid target for code coverage',
|
||||
$target->description(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class NoCodeCoverageDriverAvailableException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('No code coverage driver available');
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class NoCodeCoverageDriverWithPathCoverageSupportAvailableException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('No code coverage driver with path coverage support available');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class ParserException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?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;
|
||||
|
||||
use function sprintf;
|
||||
use RuntimeException;
|
||||
|
||||
final class PathExistsButIsNotDirectoryException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct(string $path)
|
||||
{
|
||||
parent::__construct(sprintf('"%s" exists but is not a directory', $path));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Driver;
|
||||
|
||||
use RuntimeException;
|
||||
use SebastianBergmann\CodeCoverage\Exception;
|
||||
|
||||
final class PcovNotAvailableException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('The PCOV extension is not available');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class ReflectionException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class ReportAlreadyFinalizedException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('The code coverage report has already been finalized');
|
||||
}
|
||||
}
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
<?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;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class StaticAnalysisCacheNotConfiguredException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class TestIdMissingException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Test ID is missing');
|
||||
}
|
||||
}
|
||||
+50
@@ -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;
|
||||
|
||||
use function rtrim;
|
||||
use RuntimeException;
|
||||
|
||||
final class UnintentionallyCoveredCodeException extends RuntimeException implements Exception
|
||||
{
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private readonly array $unintentionallyCoveredUnits;
|
||||
|
||||
/**
|
||||
* @param list<string> $unintentionallyCoveredUnits
|
||||
*/
|
||||
public function __construct(array $unintentionallyCoveredUnits)
|
||||
{
|
||||
$this->unintentionallyCoveredUnits = $unintentionallyCoveredUnits;
|
||||
|
||||
parent::__construct($this->toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getUnintentionallyCoveredUnits(): array
|
||||
{
|
||||
return $this->unintentionallyCoveredUnits;
|
||||
}
|
||||
|
||||
private function toString(): string
|
||||
{
|
||||
$message = '';
|
||||
|
||||
foreach ($this->unintentionallyCoveredUnits as $unit) {
|
||||
$message .= '- ' . $unit . "\n";
|
||||
}
|
||||
|
||||
return rtrim($message);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?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;
|
||||
|
||||
use function sprintf;
|
||||
use RuntimeException;
|
||||
|
||||
final class WriteOperationFailedException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct(string $path)
|
||||
{
|
||||
parent::__construct(sprintf('Cannot write to "%s"', $path));
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?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\Driver;
|
||||
|
||||
use RuntimeException;
|
||||
use SebastianBergmann\CodeCoverage\Exception;
|
||||
|
||||
final class XdebugNotAvailableException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('The Xdebug extension is not available');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Driver;
|
||||
|
||||
use RuntimeException;
|
||||
use SebastianBergmann\CodeCoverage\Exception;
|
||||
|
||||
final class XdebugNotEnabledException extends RuntimeException implements Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('XDEBUG_MODE=coverage (environment variable) or xdebug.mode=coverage (PHP configuration setting) has to be set');
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?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\Driver;
|
||||
|
||||
use function sprintf;
|
||||
use RuntimeException;
|
||||
use SebastianBergmann\CodeCoverage\Exception;
|
||||
|
||||
final class XdebugVersionNotSupportedException extends RuntimeException implements Exception
|
||||
{
|
||||
/**
|
||||
* @param non-empty-string $version
|
||||
*/
|
||||
public function __construct(string $version)
|
||||
{
|
||||
parent::__construct(
|
||||
sprintf(
|
||||
'Version %s of the Xdebug extension is not supported',
|
||||
$version,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class XmlException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<?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;
|
||||
|
||||
use function array_keys;
|
||||
use function is_file;
|
||||
use function realpath;
|
||||
use function str_contains;
|
||||
use function str_starts_with;
|
||||
|
||||
final class Filter
|
||||
{
|
||||
/**
|
||||
* @var array<string,true>
|
||||
*/
|
||||
private array $files = [];
|
||||
|
||||
/**
|
||||
* @var array<string,bool>
|
||||
*/
|
||||
private array $isFileCache = [];
|
||||
|
||||
/**
|
||||
* @param list<string> $filenames
|
||||
*/
|
||||
public function includeFiles(array $filenames): void
|
||||
{
|
||||
foreach ($filenames as $filename) {
|
||||
$this->includeFile($filename);
|
||||
}
|
||||
}
|
||||
|
||||
public function includeFile(string $filename): void
|
||||
{
|
||||
$filename = realpath($filename);
|
||||
|
||||
if (!$filename) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->files[$filename] = true;
|
||||
}
|
||||
|
||||
public function isFile(string $filename): bool
|
||||
{
|
||||
if (isset($this->isFileCache[$filename])) {
|
||||
return $this->isFileCache[$filename];
|
||||
}
|
||||
|
||||
if ($filename === '-' ||
|
||||
str_starts_with($filename, 'vfs://') ||
|
||||
str_contains($filename, 'xdebug://debug-eval') ||
|
||||
str_contains($filename, 'eval()\'d code') ||
|
||||
str_contains($filename, 'runtime-created function') ||
|
||||
str_contains($filename, 'runkit created function') ||
|
||||
str_contains($filename, 'assert code') ||
|
||||
str_contains($filename, 'regexp code') ||
|
||||
str_contains($filename, 'Standard input code')) {
|
||||
$isFile = false;
|
||||
} else {
|
||||
$isFile = is_file($filename);
|
||||
}
|
||||
|
||||
$this->isFileCache[$filename] = $isFile;
|
||||
|
||||
return $isFile;
|
||||
}
|
||||
|
||||
public function isExcluded(string $filename): bool
|
||||
{
|
||||
return !isset($this->files[$filename]) || !$this->isFile($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function files(): array
|
||||
{
|
||||
return array_keys($this->files);
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->files === [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
<?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\Node;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use function array_merge;
|
||||
use function str_ends_with;
|
||||
use function str_replace;
|
||||
use function substr;
|
||||
use Countable;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedClassType;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedFunctionType;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedTraitType;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode;
|
||||
use SebastianBergmann\CodeCoverage\Util\Percentage;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
abstract class AbstractNode implements Countable
|
||||
{
|
||||
private readonly string $name;
|
||||
private string $pathAsString;
|
||||
|
||||
/**
|
||||
* @var non-empty-list<self>
|
||||
*/
|
||||
private array $pathAsArray;
|
||||
private readonly ?AbstractNode $parent;
|
||||
private string $id;
|
||||
|
||||
public function __construct(string $name, ?self $parent = null)
|
||||
{
|
||||
if (str_ends_with($name, DIRECTORY_SEPARATOR)) {
|
||||
$name = substr($name, 0, -1);
|
||||
}
|
||||
|
||||
$this->name = $name;
|
||||
$this->parent = $parent;
|
||||
|
||||
$this->processId();
|
||||
$this->processPath();
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function pathAsString(): string
|
||||
{
|
||||
return $this->pathAsString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<self>
|
||||
*/
|
||||
public function pathAsArray(): array
|
||||
{
|
||||
return $this->pathAsArray;
|
||||
}
|
||||
|
||||
public function parent(): ?self
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
public function percentageOfTestedClasses(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedClasses(),
|
||||
$this->numberOfClasses(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfTestedTraits(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedTraits(),
|
||||
$this->numberOfTraits(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfTestedClassesAndTraits(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedClassesAndTraits(),
|
||||
$this->numberOfClassesAndTraits(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfTestedFunctions(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedFunctions(),
|
||||
$this->numberOfFunctions(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfTestedMethods(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedMethods(),
|
||||
$this->numberOfMethods(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfTestedFunctionsAndMethods(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfTestedFunctionsAndMethods(),
|
||||
$this->numberOfFunctionsAndMethods(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfExecutedLines(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfExecutedLines(),
|
||||
$this->numberOfExecutableLines(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfExecutedBranches(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfExecutedBranches(),
|
||||
$this->numberOfExecutableBranches(),
|
||||
);
|
||||
}
|
||||
|
||||
public function percentageOfExecutedPaths(): Percentage
|
||||
{
|
||||
return Percentage::fromFractionAndTotal(
|
||||
$this->numberOfExecutedPaths(),
|
||||
$this->numberOfExecutablePaths(),
|
||||
);
|
||||
}
|
||||
|
||||
public function numberOfClassesAndTraits(): int
|
||||
{
|
||||
return $this->numberOfClasses() + $this->numberOfTraits();
|
||||
}
|
||||
|
||||
public function numberOfTestedClassesAndTraits(): int
|
||||
{
|
||||
return $this->numberOfTestedClasses() + $this->numberOfTestedTraits();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedClassType|ProcessedTraitType>
|
||||
*/
|
||||
public function classesAndTraits(): array
|
||||
{
|
||||
return array_merge($this->classes(), $this->traits());
|
||||
}
|
||||
|
||||
public function numberOfFunctionsAndMethods(): int
|
||||
{
|
||||
return $this->numberOfFunctions() + $this->numberOfMethods();
|
||||
}
|
||||
|
||||
public function numberOfTestedFunctionsAndMethods(): int
|
||||
{
|
||||
return $this->numberOfTestedFunctions() + $this->numberOfTestedMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-negative-int
|
||||
*/
|
||||
public function cyclomaticComplexity(): int
|
||||
{
|
||||
$ccn = 0;
|
||||
|
||||
foreach ($this->classesAndTraits() as $classLike) {
|
||||
$ccn += $classLike->ccn;
|
||||
}
|
||||
|
||||
foreach ($this->functions() as $function) {
|
||||
$ccn += $function->ccn;
|
||||
}
|
||||
|
||||
return $ccn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedClassType>
|
||||
*/
|
||||
abstract public function classes(): array;
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedTraitType>
|
||||
*/
|
||||
abstract public function traits(): array;
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedFunctionType>
|
||||
*/
|
||||
abstract public function functions(): array;
|
||||
|
||||
abstract public function linesOfCode(): LinesOfCode;
|
||||
|
||||
abstract public function numberOfExecutableLines(): int;
|
||||
|
||||
abstract public function numberOfExecutedLines(): int;
|
||||
|
||||
abstract public function numberOfExecutableBranches(): int;
|
||||
|
||||
abstract public function numberOfExecutedBranches(): int;
|
||||
|
||||
abstract public function numberOfExecutablePaths(): int;
|
||||
|
||||
abstract public function numberOfExecutedPaths(): int;
|
||||
|
||||
abstract public function numberOfClasses(): int;
|
||||
|
||||
abstract public function numberOfTestedClasses(): int;
|
||||
|
||||
abstract public function numberOfTraits(): int;
|
||||
|
||||
abstract public function numberOfTestedTraits(): int;
|
||||
|
||||
abstract public function numberOfMethods(): int;
|
||||
|
||||
abstract public function numberOfTestedMethods(): int;
|
||||
|
||||
abstract public function numberOfFunctions(): int;
|
||||
|
||||
abstract public function numberOfTestedFunctions(): int;
|
||||
|
||||
private function processId(): void
|
||||
{
|
||||
if ($this->parent === null) {
|
||||
$this->id = 'index';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$parentId = $this->parent->id();
|
||||
|
||||
if ($parentId === 'index') {
|
||||
$this->id = str_replace(':', '_', $this->name);
|
||||
} else {
|
||||
$this->id = $parentId . '/' . $this->name;
|
||||
}
|
||||
}
|
||||
|
||||
private function processPath(): void
|
||||
{
|
||||
if ($this->parent === null) {
|
||||
$this->pathAsArray = [$this];
|
||||
$this->pathAsString = $this->name;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->pathAsArray = $this->parent->pathAsArray();
|
||||
$this->pathAsString = $this->parent->pathAsString() . DIRECTORY_SEPARATOR . $this->name;
|
||||
|
||||
$this->pathAsArray[] = $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?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\Node;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use function array_shift;
|
||||
use function basename;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function is_file;
|
||||
use function sha1_file;
|
||||
use function str_ends_with;
|
||||
use function str_replace;
|
||||
use function str_starts_with;
|
||||
use function substr;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedCodeCoverageData;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
|
||||
|
||||
/**
|
||||
* @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 Builder
|
||||
{
|
||||
private FileAnalyser $analyser;
|
||||
|
||||
public function __construct(FileAnalyser $analyser)
|
||||
{
|
||||
$this->analyser = $analyser;
|
||||
}
|
||||
|
||||
public function build(CodeCoverage $coverage): Directory
|
||||
{
|
||||
$data = clone $coverage->getData(); // clone because path munging is destructive to the original data
|
||||
$commonPath = $this->reducePaths($data);
|
||||
$root = new Directory(
|
||||
$commonPath,
|
||||
null,
|
||||
);
|
||||
|
||||
$this->addItems(
|
||||
$root,
|
||||
$this->buildDirectoryStructure($data),
|
||||
$coverage->getTests(),
|
||||
);
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, TestType> $tests
|
||||
*/
|
||||
private function addItems(Directory $root, array $items, array $tests): void
|
||||
{
|
||||
foreach ($items as $key => $value) {
|
||||
$key = (string) $key;
|
||||
|
||||
if (str_ends_with($key, '/f')) {
|
||||
$key = substr($key, 0, -2);
|
||||
$filename = $root->pathAsString() . DIRECTORY_SEPARATOR . $key;
|
||||
|
||||
if (is_file($filename)) {
|
||||
$analysisResult = $this->analyser->analyse($filename);
|
||||
|
||||
$root->addFile(
|
||||
new File(
|
||||
$key,
|
||||
$root,
|
||||
sha1_file($filename),
|
||||
$value['lineCoverage'],
|
||||
$value['functionCoverage'],
|
||||
$tests,
|
||||
$analysisResult->classes(),
|
||||
$analysisResult->traits(),
|
||||
$analysisResult->functions(),
|
||||
$analysisResult->linesOfCode(),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$child = $root->addDirectory($key);
|
||||
|
||||
$this->addItems($child, $value, $tests);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an array representation of the directory structure.
|
||||
*
|
||||
* For instance,
|
||||
*
|
||||
* <code>
|
||||
* Array
|
||||
* (
|
||||
* [Money.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
*
|
||||
* [MoneyBag.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
* )
|
||||
* </code>
|
||||
*
|
||||
* is transformed into
|
||||
*
|
||||
* <code>
|
||||
* Array
|
||||
* (
|
||||
* [.] => Array
|
||||
* (
|
||||
* [Money.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
*
|
||||
* [MoneyBag.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* </code>
|
||||
*
|
||||
* @return array<string, array<string, array{lineCoverage: array<int, int>, functionCoverage: array<string, array<int, int>>}>>
|
||||
*/
|
||||
private function buildDirectoryStructure(ProcessedCodeCoverageData $data): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$lineCoverage = $data->lineCoverage();
|
||||
$functionCoverage = $data->functionCoverage();
|
||||
|
||||
foreach ($data->coveredFiles() as $originalPath) {
|
||||
$path = explode(DIRECTORY_SEPARATOR, $originalPath);
|
||||
$pointer = &$result;
|
||||
$max = count($path);
|
||||
|
||||
for ($i = 0; $i < $max; $i++) {
|
||||
$type = '';
|
||||
|
||||
if ($i === ($max - 1)) {
|
||||
$type = '/f';
|
||||
}
|
||||
|
||||
$pointer = &$pointer[$path[$i] . $type];
|
||||
}
|
||||
|
||||
$pointer = [
|
||||
'lineCoverage' => $lineCoverage[$originalPath] ?? [],
|
||||
'functionCoverage' => $functionCoverage[$originalPath] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the paths by cutting the longest common start path.
|
||||
*
|
||||
* For instance,
|
||||
*
|
||||
* <code>
|
||||
* Array
|
||||
* (
|
||||
* [/home/sb/Money/Money.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
*
|
||||
* [/home/sb/Money/MoneyBag.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
* )
|
||||
* </code>
|
||||
*
|
||||
* is reduced to
|
||||
*
|
||||
* <code>
|
||||
* Array
|
||||
* (
|
||||
* [Money.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
*
|
||||
* [MoneyBag.php] => Array
|
||||
* (
|
||||
* ...
|
||||
* )
|
||||
* )
|
||||
* </code>
|
||||
*/
|
||||
private function reducePaths(ProcessedCodeCoverageData $coverage): string
|
||||
{
|
||||
$coveredFiles = $coverage->coveredFiles();
|
||||
|
||||
if ($coveredFiles === []) {
|
||||
return '.';
|
||||
}
|
||||
|
||||
$commonPath = '';
|
||||
$paths = $coveredFiles;
|
||||
|
||||
if (count($paths) === 1) {
|
||||
$commonPath = dirname($paths[0]) . DIRECTORY_SEPARATOR;
|
||||
$coverage->renameFile($paths[0], basename($paths[0]));
|
||||
|
||||
return $commonPath;
|
||||
}
|
||||
|
||||
$max = count($paths);
|
||||
|
||||
for ($i = 0; $i < $max; $i++) {
|
||||
// strip phar:// prefixes
|
||||
if (str_starts_with($paths[$i], 'phar://')) {
|
||||
$paths[$i] = substr($paths[$i], 7);
|
||||
$paths[$i] = str_replace('/', DIRECTORY_SEPARATOR, $paths[$i]);
|
||||
}
|
||||
|
||||
$paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]);
|
||||
|
||||
if ($paths[$i][0] === '') {
|
||||
$paths[$i][0] = DIRECTORY_SEPARATOR;
|
||||
}
|
||||
}
|
||||
|
||||
$done = false;
|
||||
$max = count($paths);
|
||||
|
||||
while (!$done) {
|
||||
for ($i = 0; $i < $max - 1; $i++) {
|
||||
if (!isset($paths[$i][0]) ||
|
||||
!isset($paths[$i + 1][0]) ||
|
||||
$paths[$i][0] !== $paths[$i + 1][0]) {
|
||||
$done = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$done) {
|
||||
$commonPath .= $paths[0][0];
|
||||
|
||||
if ($paths[0][0] !== DIRECTORY_SEPARATOR) {
|
||||
$commonPath .= DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $max; $i++) {
|
||||
array_shift($paths[$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$original = $coveredFiles;
|
||||
$max = count($original);
|
||||
|
||||
for ($i = 0; $i < $max; $i++) {
|
||||
$coverage->renameFile($original[$i], implode(DIRECTORY_SEPARATOR, $paths[$i]));
|
||||
}
|
||||
|
||||
return substr($commonPath, 0, -1);
|
||||
}
|
||||
}
|
||||
@@ -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\Node;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final readonly class CrapIndex
|
||||
{
|
||||
private int $cyclomaticComplexity;
|
||||
private float $codeCoverage;
|
||||
|
||||
public function __construct(int $cyclomaticComplexity, float $codeCoverage)
|
||||
{
|
||||
$this->cyclomaticComplexity = $cyclomaticComplexity;
|
||||
$this->codeCoverage = $codeCoverage;
|
||||
}
|
||||
|
||||
public function asString(): string
|
||||
{
|
||||
if ($this->codeCoverage === 0.0) {
|
||||
return (string) ($this->cyclomaticComplexity ** 2 + $this->cyclomaticComplexity);
|
||||
}
|
||||
|
||||
if ($this->codeCoverage >= 95) {
|
||||
return (string) $this->cyclomaticComplexity;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'%01.2F',
|
||||
$this->cyclomaticComplexity ** 2 * (1 - $this->codeCoverage / 100) ** 3 + $this->cyclomaticComplexity,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
<?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\Node;
|
||||
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
use function count;
|
||||
use IteratorAggregate;
|
||||
use RecursiveIteratorIterator;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedClassType;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedFunctionType;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedTraitType;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode;
|
||||
|
||||
/**
|
||||
* @template-implements IteratorAggregate<int, AbstractNode>
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Directory extends AbstractNode implements IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* @var list<Directory|File>
|
||||
*/
|
||||
private array $children = [];
|
||||
|
||||
/**
|
||||
* @var list<Directory>
|
||||
*/
|
||||
private array $directories = [];
|
||||
|
||||
/**
|
||||
* @var list<File>
|
||||
*/
|
||||
private array $files = [];
|
||||
|
||||
/**
|
||||
* @var ?array<string, ProcessedClassType>
|
||||
*/
|
||||
private ?array $classes = null;
|
||||
|
||||
/**
|
||||
* @var ?array<string, ProcessedTraitType>
|
||||
*/
|
||||
private ?array $traits = null;
|
||||
|
||||
/**
|
||||
* @var ?array<string, ProcessedFunctionType>
|
||||
*/
|
||||
private ?array $functions = null;
|
||||
private ?LinesOfCode $linesOfCode = null;
|
||||
private int $numFiles = -1;
|
||||
private int $numExecutableLines = -1;
|
||||
private int $numExecutedLines = -1;
|
||||
private int $numExecutableBranches = -1;
|
||||
private int $numExecutedBranches = -1;
|
||||
private int $numExecutablePaths = -1;
|
||||
private int $numExecutedPaths = -1;
|
||||
private int $numClasses = -1;
|
||||
private int $numTestedClasses = -1;
|
||||
private int $numTraits = -1;
|
||||
private int $numTestedTraits = -1;
|
||||
private int $numMethods = -1;
|
||||
private int $numTestedMethods = -1;
|
||||
private int $numFunctions = -1;
|
||||
private int $numTestedFunctions = -1;
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
if ($this->numFiles === -1) {
|
||||
$this->numFiles = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numFiles += count($child);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RecursiveIteratorIterator<Iterator<AbstractNode>>
|
||||
*/
|
||||
public function getIterator(): RecursiveIteratorIterator
|
||||
{
|
||||
return new RecursiveIteratorIterator(
|
||||
new Iterator($this),
|
||||
RecursiveIteratorIterator::SELF_FIRST,
|
||||
);
|
||||
}
|
||||
|
||||
public function addDirectory(string $name): self
|
||||
{
|
||||
$directory = new self($name, $this);
|
||||
|
||||
assert($directory instanceof self);
|
||||
|
||||
$this->children[] = $directory;
|
||||
$this->directories[] = &$this->children[count($this->children) - 1];
|
||||
|
||||
return $directory;
|
||||
}
|
||||
|
||||
public function addFile(File $file): void
|
||||
{
|
||||
$this->children[] = $file;
|
||||
$this->files[] = &$this->children[count($this->children) - 1];
|
||||
|
||||
$this->numExecutableLines = -1;
|
||||
$this->numExecutedLines = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Directory>
|
||||
*/
|
||||
public function directories(): array
|
||||
{
|
||||
return $this->directories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<File>
|
||||
*/
|
||||
public function files(): array
|
||||
{
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Directory|File>
|
||||
*/
|
||||
public function children(): array
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedClassType>
|
||||
*/
|
||||
public function classes(): array
|
||||
{
|
||||
if ($this->classes === null) {
|
||||
$this->classes = [];
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->classes = array_merge(
|
||||
$this->classes,
|
||||
$child->classes(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedTraitType>
|
||||
*/
|
||||
public function traits(): array
|
||||
{
|
||||
if ($this->traits === null) {
|
||||
$this->traits = [];
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->traits = array_merge(
|
||||
$this->traits,
|
||||
$child->traits(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->traits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedFunctionType>
|
||||
*/
|
||||
public function functions(): array
|
||||
{
|
||||
if ($this->functions === null) {
|
||||
$this->functions = [];
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->functions = array_merge(
|
||||
$this->functions,
|
||||
$child->functions(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->functions;
|
||||
}
|
||||
|
||||
public function linesOfCode(): LinesOfCode
|
||||
{
|
||||
if ($this->linesOfCode === null) {
|
||||
$linesOfCode = 0;
|
||||
$commentLinesOfCode = 0;
|
||||
$nonCommentLinesOfCode = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$childLinesOfCode = $child->linesOfCode();
|
||||
|
||||
$linesOfCode += $childLinesOfCode->linesOfCode();
|
||||
$commentLinesOfCode += $childLinesOfCode->commentLinesOfCode();
|
||||
$nonCommentLinesOfCode += $childLinesOfCode->nonCommentLinesOfCode();
|
||||
}
|
||||
|
||||
$this->linesOfCode = new LinesOfCode($linesOfCode, $commentLinesOfCode, $nonCommentLinesOfCode);
|
||||
}
|
||||
|
||||
return $this->linesOfCode;
|
||||
}
|
||||
|
||||
public function numberOfExecutableLines(): int
|
||||
{
|
||||
if ($this->numExecutableLines === -1) {
|
||||
$this->numExecutableLines = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutableLines += $child->numberOfExecutableLines();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutableLines;
|
||||
}
|
||||
|
||||
public function numberOfExecutedLines(): int
|
||||
{
|
||||
if ($this->numExecutedLines === -1) {
|
||||
$this->numExecutedLines = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutedLines += $child->numberOfExecutedLines();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutedLines;
|
||||
}
|
||||
|
||||
public function numberOfExecutableBranches(): int
|
||||
{
|
||||
if ($this->numExecutableBranches === -1) {
|
||||
$this->numExecutableBranches = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutableBranches += $child->numberOfExecutableBranches();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutableBranches;
|
||||
}
|
||||
|
||||
public function numberOfExecutedBranches(): int
|
||||
{
|
||||
if ($this->numExecutedBranches === -1) {
|
||||
$this->numExecutedBranches = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutedBranches += $child->numberOfExecutedBranches();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutedBranches;
|
||||
}
|
||||
|
||||
public function numberOfExecutablePaths(): int
|
||||
{
|
||||
if ($this->numExecutablePaths === -1) {
|
||||
$this->numExecutablePaths = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutablePaths += $child->numberOfExecutablePaths();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutablePaths;
|
||||
}
|
||||
|
||||
public function numberOfExecutedPaths(): int
|
||||
{
|
||||
if ($this->numExecutedPaths === -1) {
|
||||
$this->numExecutedPaths = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numExecutedPaths += $child->numberOfExecutedPaths();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numExecutedPaths;
|
||||
}
|
||||
|
||||
public function numberOfClasses(): int
|
||||
{
|
||||
if ($this->numClasses === -1) {
|
||||
$this->numClasses = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numClasses += $child->numberOfClasses();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numClasses;
|
||||
}
|
||||
|
||||
public function numberOfTestedClasses(): int
|
||||
{
|
||||
if ($this->numTestedClasses === -1) {
|
||||
$this->numTestedClasses = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numTestedClasses += $child->numberOfTestedClasses();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedClasses;
|
||||
}
|
||||
|
||||
public function numberOfTraits(): int
|
||||
{
|
||||
if ($this->numTraits === -1) {
|
||||
$this->numTraits = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numTraits += $child->numberOfTraits();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTraits;
|
||||
}
|
||||
|
||||
public function numberOfTestedTraits(): int
|
||||
{
|
||||
if ($this->numTestedTraits === -1) {
|
||||
$this->numTestedTraits = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numTestedTraits += $child->numberOfTestedTraits();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedTraits;
|
||||
}
|
||||
|
||||
public function numberOfMethods(): int
|
||||
{
|
||||
if ($this->numMethods === -1) {
|
||||
$this->numMethods = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numMethods += $child->numberOfMethods();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numMethods;
|
||||
}
|
||||
|
||||
public function numberOfTestedMethods(): int
|
||||
{
|
||||
if ($this->numTestedMethods === -1) {
|
||||
$this->numTestedMethods = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numTestedMethods += $child->numberOfTestedMethods();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedMethods;
|
||||
}
|
||||
|
||||
public function numberOfFunctions(): int
|
||||
{
|
||||
if ($this->numFunctions === -1) {
|
||||
$this->numFunctions = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numFunctions += $child->numberOfFunctions();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numFunctions;
|
||||
}
|
||||
|
||||
public function numberOfTestedFunctions(): int
|
||||
{
|
||||
if ($this->numTestedFunctions === -1) {
|
||||
$this->numTestedFunctions = 0;
|
||||
|
||||
foreach ($this->children as $child) {
|
||||
$this->numTestedFunctions += $child->numberOfTestedFunctions();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedFunctions;
|
||||
}
|
||||
}
|
||||
+656
@@ -0,0 +1,656 @@
|
||||
<?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\Node;
|
||||
|
||||
use function array_filter;
|
||||
use function count;
|
||||
use function range;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedBranchCoverageData;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedClassType;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedFunctionType;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedMethodType;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedPathCoverageData;
|
||||
use SebastianBergmann\CodeCoverage\Data\ProcessedTraitType;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\AnalysisResult;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\Class_;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\Function_;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\LinesOfCode;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\Method;
|
||||
use SebastianBergmann\CodeCoverage\StaticAnalysis\Trait_;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*
|
||||
* @phpstan-import-type TestType from CodeCoverage
|
||||
* @phpstan-import-type LinesType from AnalysisResult
|
||||
*/
|
||||
final class File extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private string $sha1;
|
||||
|
||||
/**
|
||||
* @var array<int, ?list<non-empty-string>>
|
||||
*/
|
||||
private array $lineCoverageData;
|
||||
private array $functionCoverageData;
|
||||
|
||||
/**
|
||||
* @var array<string, TestType>
|
||||
*/
|
||||
private readonly array $testData;
|
||||
private int $numExecutableLines = 0;
|
||||
private int $numExecutedLines = 0;
|
||||
private int $numExecutableBranches = 0;
|
||||
private int $numExecutedBranches = 0;
|
||||
private int $numExecutablePaths = 0;
|
||||
private int $numExecutedPaths = 0;
|
||||
|
||||
/**
|
||||
* @var array<string, ProcessedClassType>
|
||||
*/
|
||||
private array $classes = [];
|
||||
|
||||
/**
|
||||
* @var array<string, ProcessedTraitType>
|
||||
*/
|
||||
private array $traits = [];
|
||||
|
||||
/**
|
||||
* @var array<string, ProcessedFunctionType>
|
||||
*/
|
||||
private array $functions = [];
|
||||
private readonly LinesOfCode $linesOfCode;
|
||||
private ?int $numClasses = null;
|
||||
private int $numTestedClasses = 0;
|
||||
private ?int $numTraits = null;
|
||||
private int $numTestedTraits = 0;
|
||||
private ?int $numMethods = null;
|
||||
private ?int $numTestedMethods = null;
|
||||
private ?int $numTestedFunctions = null;
|
||||
|
||||
/**
|
||||
* @var array<int, array|array{0: Class_, 1: string}|array{0: Function_|ProcessedFunctionType|ProcessedMethodType}|array{0: Trait_, 1: string}>
|
||||
*/
|
||||
private array $codeUnitsByLine = [];
|
||||
|
||||
/**
|
||||
* @param non-empty-string $sha1
|
||||
* @param array<int, ?list<non-empty-string>> $lineCoverageData
|
||||
* @param array<string, TestType> $testData
|
||||
* @param array<string, Class_> $classes
|
||||
* @param array<string, Trait_> $traits
|
||||
* @param array<string, Function_> $functions
|
||||
*/
|
||||
public function __construct(string $name, AbstractNode $parent, string $sha1, array $lineCoverageData, array $functionCoverageData, array $testData, array $classes, array $traits, array $functions, LinesOfCode $linesOfCode)
|
||||
{
|
||||
parent::__construct($name, $parent);
|
||||
|
||||
$this->sha1 = $sha1;
|
||||
$this->lineCoverageData = $lineCoverageData;
|
||||
$this->functionCoverageData = $functionCoverageData;
|
||||
$this->testData = $testData;
|
||||
$this->linesOfCode = $linesOfCode;
|
||||
|
||||
$this->calculateStatistics($classes, $traits, $functions);
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function sha1(): string
|
||||
{
|
||||
return $this->sha1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, ?list<non-empty-string>>
|
||||
*/
|
||||
public function lineCoverageData(): array
|
||||
{
|
||||
return $this->lineCoverageData;
|
||||
}
|
||||
|
||||
public function functionCoverageData(): array
|
||||
{
|
||||
return $this->functionCoverageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, TestType>
|
||||
*/
|
||||
public function testData(): array
|
||||
{
|
||||
return $this->testData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedClassType>
|
||||
*/
|
||||
public function classes(): array
|
||||
{
|
||||
return $this->classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedTraitType>
|
||||
*/
|
||||
public function traits(): array
|
||||
{
|
||||
return $this->traits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ProcessedFunctionType>
|
||||
*/
|
||||
public function functions(): array
|
||||
{
|
||||
return $this->functions;
|
||||
}
|
||||
|
||||
public function linesOfCode(): LinesOfCode
|
||||
{
|
||||
return $this->linesOfCode;
|
||||
}
|
||||
|
||||
public function numberOfExecutableLines(): int
|
||||
{
|
||||
return $this->numExecutableLines;
|
||||
}
|
||||
|
||||
public function numberOfExecutedLines(): int
|
||||
{
|
||||
return $this->numExecutedLines;
|
||||
}
|
||||
|
||||
public function numberOfExecutableBranches(): int
|
||||
{
|
||||
return $this->numExecutableBranches;
|
||||
}
|
||||
|
||||
public function numberOfExecutedBranches(): int
|
||||
{
|
||||
return $this->numExecutedBranches;
|
||||
}
|
||||
|
||||
public function numberOfExecutablePaths(): int
|
||||
{
|
||||
return $this->numExecutablePaths;
|
||||
}
|
||||
|
||||
public function numberOfExecutedPaths(): int
|
||||
{
|
||||
return $this->numExecutedPaths;
|
||||
}
|
||||
|
||||
public function numberOfClasses(): int
|
||||
{
|
||||
if ($this->numClasses === null) {
|
||||
$this->numClasses = 0;
|
||||
|
||||
foreach ($this->classes as $class) {
|
||||
foreach ($class->methods as $method) {
|
||||
if ($method->executableLines > 0) {
|
||||
$this->numClasses++;
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numClasses;
|
||||
}
|
||||
|
||||
public function numberOfTestedClasses(): int
|
||||
{
|
||||
return $this->numTestedClasses;
|
||||
}
|
||||
|
||||
public function numberOfTraits(): int
|
||||
{
|
||||
if ($this->numTraits === null) {
|
||||
$this->numTraits = 0;
|
||||
|
||||
foreach ($this->traits as $trait) {
|
||||
foreach ($trait->methods as $method) {
|
||||
if ($method->executableLines > 0) {
|
||||
$this->numTraits++;
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTraits;
|
||||
}
|
||||
|
||||
public function numberOfTestedTraits(): int
|
||||
{
|
||||
return $this->numTestedTraits;
|
||||
}
|
||||
|
||||
public function numberOfMethods(): int
|
||||
{
|
||||
if ($this->numMethods === null) {
|
||||
$this->numMethods = 0;
|
||||
|
||||
foreach ($this->classes as $class) {
|
||||
foreach ($class->methods as $method) {
|
||||
if ($method->executableLines > 0) {
|
||||
$this->numMethods++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->traits as $trait) {
|
||||
foreach ($trait->methods as $method) {
|
||||
if ($method->executableLines > 0) {
|
||||
$this->numMethods++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numMethods;
|
||||
}
|
||||
|
||||
public function numberOfTestedMethods(): int
|
||||
{
|
||||
if ($this->numTestedMethods === null) {
|
||||
$this->numTestedMethods = 0;
|
||||
|
||||
foreach ($this->classes as $class) {
|
||||
foreach ($class->methods as $method) {
|
||||
if ($method->executableLines > 0 &&
|
||||
$method->coverage === 100) {
|
||||
$this->numTestedMethods++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->traits as $trait) {
|
||||
foreach ($trait->methods as $method) {
|
||||
if ($method->executableLines > 0 &&
|
||||
$method->coverage === 100) {
|
||||
$this->numTestedMethods++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedMethods;
|
||||
}
|
||||
|
||||
public function numberOfFunctions(): int
|
||||
{
|
||||
return count($this->functions);
|
||||
}
|
||||
|
||||
public function numberOfTestedFunctions(): int
|
||||
{
|
||||
if ($this->numTestedFunctions === null) {
|
||||
$this->numTestedFunctions = 0;
|
||||
|
||||
foreach ($this->functions as $function) {
|
||||
if ($function->executableLines > 0 &&
|
||||
$function->coverage === 100) {
|
||||
$this->numTestedFunctions++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numTestedFunctions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Class_> $classes
|
||||
* @param array<string, Trait_> $traits
|
||||
* @param array<string, Function_> $functions
|
||||
*/
|
||||
private function calculateStatistics(array $classes, array $traits, array $functions): void
|
||||
{
|
||||
foreach (range(1, $this->linesOfCode->linesOfCode()) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [];
|
||||
}
|
||||
|
||||
$this->processClasses($classes);
|
||||
$this->processTraits($traits);
|
||||
$this->processFunctions($functions);
|
||||
|
||||
foreach (range(1, $this->linesOfCode->linesOfCode()) as $lineNumber) {
|
||||
if (isset($this->lineCoverageData[$lineNumber])) {
|
||||
foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) {
|
||||
$codeUnit->executableLines++;
|
||||
}
|
||||
|
||||
unset($codeUnit);
|
||||
|
||||
$this->numExecutableLines++;
|
||||
|
||||
if (count($this->lineCoverageData[$lineNumber]) > 0) {
|
||||
foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) {
|
||||
$codeUnit->executedLines++;
|
||||
}
|
||||
|
||||
unset($codeUnit);
|
||||
|
||||
$this->numExecutedLines++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->traits as &$trait) {
|
||||
foreach ($trait->methods as &$method) {
|
||||
$methodLineCoverage = $method->executableLines > 0 ? ($method->executedLines / $method->executableLines) * 100 : 100;
|
||||
$methodBranchCoverage = $method->executableBranches > 0 ? ($method->executedBranches / $method->executableBranches) * 100 : 0;
|
||||
$methodPathCoverage = $method->executablePaths > 0 ? ($method->executedPaths / $method->executablePaths) * 100 : 0;
|
||||
|
||||
$method->coverage = $methodBranchCoverage > 0 ? $methodBranchCoverage : $methodLineCoverage;
|
||||
$method->crap = (new CrapIndex($method->ccn, $methodPathCoverage > 0 ? $methodPathCoverage : $methodLineCoverage))->asString();
|
||||
|
||||
$trait->ccn += $method->ccn;
|
||||
}
|
||||
|
||||
unset($method);
|
||||
|
||||
$traitBranchCoverage = $trait->executableBranches > 0 ? ($trait->executedBranches / $trait->executableBranches) * 100 : 0;
|
||||
$traitLineCoverage = $trait->executableLines > 0 ? ($trait->executedLines / $trait->executableLines) * 100 : 100;
|
||||
$traitPathCoverage = $trait->executablePaths > 0 ? ($trait->executedPaths / $trait->executablePaths) * 100 : 0;
|
||||
|
||||
$trait->coverage = $traitBranchCoverage > 0 ? $traitBranchCoverage : $traitLineCoverage;
|
||||
$trait->crap = (new CrapIndex($trait->ccn, $traitPathCoverage > 0 ? $traitPathCoverage : $traitLineCoverage))->asString();
|
||||
|
||||
if ($trait->executableLines > 0 && $trait->coverage === 100) {
|
||||
$this->numTestedTraits++;
|
||||
}
|
||||
}
|
||||
|
||||
unset($trait);
|
||||
|
||||
foreach ($this->classes as &$class) {
|
||||
foreach ($class->methods as &$method) {
|
||||
$methodLineCoverage = $method->executableLines > 0 ? ($method->executedLines / $method->executableLines) * 100 : 100;
|
||||
$methodBranchCoverage = $method->executableBranches > 0 ? ($method->executedBranches / $method->executableBranches) * 100 : 0;
|
||||
$methodPathCoverage = $method->executablePaths > 0 ? ($method->executedPaths / $method->executablePaths) * 100 : 0;
|
||||
|
||||
$method->coverage = $methodBranchCoverage > 0 ? $methodBranchCoverage : $methodLineCoverage;
|
||||
$method->crap = (new CrapIndex($method->ccn, $methodPathCoverage > 0 ? $methodPathCoverage : $methodLineCoverage))->asString();
|
||||
|
||||
$class->ccn += $method->ccn;
|
||||
}
|
||||
|
||||
unset($method);
|
||||
|
||||
$classLineCoverage = $class->executableLines > 0 ? ($class->executedLines / $class->executableLines) * 100 : 100;
|
||||
$classBranchCoverage = $class->executableBranches > 0 ? ($class->executedBranches / $class->executableBranches) * 100 : 0;
|
||||
$classPathCoverage = $class->executablePaths > 0 ? ($class->executedPaths / $class->executablePaths) * 100 : 0;
|
||||
|
||||
$class->coverage = $classBranchCoverage > 0 ? $classBranchCoverage : $classLineCoverage;
|
||||
$class->crap = (new CrapIndex($class->ccn, $classPathCoverage > 0 ? $classPathCoverage : $classLineCoverage))->asString();
|
||||
|
||||
if ($class->executableLines > 0 && $class->coverage === 100) {
|
||||
$this->numTestedClasses++;
|
||||
}
|
||||
}
|
||||
|
||||
unset($class);
|
||||
|
||||
foreach ($this->functions as &$function) {
|
||||
$functionLineCoverage = $function->executableLines > 0 ? ($function->executedLines / $function->executableLines) * 100 : 100;
|
||||
$functionBranchCoverage = $function->executableBranches > 0 ? ($function->executedBranches / $function->executableBranches) * 100 : 0;
|
||||
$functionPathCoverage = $function->executablePaths > 0 ? ($function->executedPaths / $function->executablePaths) * 100 : 0;
|
||||
|
||||
$function->coverage = $functionBranchCoverage > 0 ? $functionBranchCoverage : $functionLineCoverage;
|
||||
$function->crap = (new CrapIndex($function->ccn, $functionPathCoverage > 0 ? $functionPathCoverage : $functionLineCoverage))->asString();
|
||||
|
||||
if ($function->coverage === 100) {
|
||||
$this->numTestedFunctions++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Class_> $classes
|
||||
*/
|
||||
private function processClasses(array $classes): void
|
||||
{
|
||||
$link = $this->id() . '.html#';
|
||||
|
||||
foreach ($classes as $className => $class) {
|
||||
$this->classes[$className] = new ProcessedClassType(
|
||||
$className,
|
||||
$class->namespace(),
|
||||
[],
|
||||
$class->startLine(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$link . $class->startLine(),
|
||||
);
|
||||
|
||||
foreach ($class->methods() as $methodName => $method) {
|
||||
$methodData = $this->newMethod($className, $method, $link);
|
||||
$this->classes[$className]->methods[$methodName] = $methodData;
|
||||
|
||||
$this->classes[$className]->executableBranches += $methodData->executableBranches;
|
||||
$this->classes[$className]->executedBranches += $methodData->executedBranches;
|
||||
$this->classes[$className]->executablePaths += $methodData->executablePaths;
|
||||
$this->classes[$className]->executedPaths += $methodData->executedPaths;
|
||||
|
||||
$this->numExecutableBranches += $methodData->executableBranches;
|
||||
$this->numExecutedBranches += $methodData->executedBranches;
|
||||
$this->numExecutablePaths += $methodData->executablePaths;
|
||||
$this->numExecutedPaths += $methodData->executedPaths;
|
||||
|
||||
foreach (range($method->startLine(), $method->endLine()) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [
|
||||
&$this->classes[$className],
|
||||
&$this->classes[$className]->methods[$methodName],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Trait_> $traits
|
||||
*/
|
||||
private function processTraits(array $traits): void
|
||||
{
|
||||
$link = $this->id() . '.html#';
|
||||
|
||||
foreach ($traits as $traitName => $trait) {
|
||||
$this->traits[$traitName] = new ProcessedTraitType(
|
||||
$traitName,
|
||||
$trait->namespace(),
|
||||
[],
|
||||
$trait->startLine(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$link . $trait->startLine(),
|
||||
);
|
||||
|
||||
foreach ($trait->methods() as $methodName => $method) {
|
||||
$methodData = $this->newMethod($traitName, $method, $link);
|
||||
$this->traits[$traitName]->methods[$methodName] = $methodData;
|
||||
|
||||
$this->traits[$traitName]->executableBranches += $methodData->executableBranches;
|
||||
$this->traits[$traitName]->executedBranches += $methodData->executedBranches;
|
||||
$this->traits[$traitName]->executablePaths += $methodData->executablePaths;
|
||||
$this->traits[$traitName]->executedPaths += $methodData->executedPaths;
|
||||
|
||||
$this->numExecutableBranches += $methodData->executableBranches;
|
||||
$this->numExecutedBranches += $methodData->executedBranches;
|
||||
$this->numExecutablePaths += $methodData->executablePaths;
|
||||
$this->numExecutedPaths += $methodData->executedPaths;
|
||||
|
||||
foreach (range($method->startLine(), $method->endLine()) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [
|
||||
&$this->traits[$traitName],
|
||||
&$this->traits[$traitName]->methods[$methodName],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Function_> $functions
|
||||
*/
|
||||
private function processFunctions(array $functions): void
|
||||
{
|
||||
$link = $this->id() . '.html#';
|
||||
|
||||
foreach ($functions as $functionName => $function) {
|
||||
$this->functions[$functionName] = new ProcessedFunctionType(
|
||||
$functionName,
|
||||
$function->namespace(),
|
||||
$function->signature(),
|
||||
$function->startLine(),
|
||||
$function->endLine(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$function->cyclomaticComplexity(),
|
||||
0,
|
||||
0,
|
||||
$link . $function->startLine(),
|
||||
);
|
||||
|
||||
foreach (range($function->startLine(), $function->endLine()) as $lineNumber) {
|
||||
$this->codeUnitsByLine[$lineNumber] = [&$this->functions[$functionName]];
|
||||
}
|
||||
|
||||
if (isset($this->functionCoverageData[$functionName])) {
|
||||
$this->functions[$functionName]->executableBranches = count(
|
||||
$this->functionCoverageData[$functionName]->branches,
|
||||
);
|
||||
|
||||
$this->functions[$functionName]->executedBranches = count(
|
||||
array_filter(
|
||||
$this->functionCoverageData[$functionName]->branches,
|
||||
static function (ProcessedBranchCoverageData $branch)
|
||||
{
|
||||
return (bool) $branch->hit;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($this->functionCoverageData[$functionName])) {
|
||||
$this->functions[$functionName]->executablePaths = count(
|
||||
$this->functionCoverageData[$functionName]->paths,
|
||||
);
|
||||
|
||||
$this->functions[$functionName]->executedPaths = count(
|
||||
array_filter(
|
||||
$this->functionCoverageData[$functionName]->paths,
|
||||
static function (ProcessedPathCoverageData $path)
|
||||
{
|
||||
return (bool) $path->hit;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$this->numExecutableBranches += $this->functions[$functionName]->executableBranches;
|
||||
$this->numExecutedBranches += $this->functions[$functionName]->executedBranches;
|
||||
$this->numExecutablePaths += $this->functions[$functionName]->executablePaths;
|
||||
$this->numExecutedPaths += $this->functions[$functionName]->executedPaths;
|
||||
}
|
||||
}
|
||||
|
||||
private function newMethod(string $className, Method $method, string $link): ProcessedMethodType
|
||||
{
|
||||
$key = $className . '->' . $method->name();
|
||||
|
||||
$executableBranches = 0;
|
||||
$executedBranches = 0;
|
||||
|
||||
if (isset($this->functionCoverageData[$key])) {
|
||||
$executableBranches = count(
|
||||
$this->functionCoverageData[$key]->branches,
|
||||
);
|
||||
|
||||
$executedBranches = count(
|
||||
array_filter(
|
||||
$this->functionCoverageData[$key]->branches,
|
||||
static function (ProcessedBranchCoverageData $branch)
|
||||
{
|
||||
return (bool) $branch->hit;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$executablePaths = 0;
|
||||
$executedPaths = 0;
|
||||
|
||||
if (isset($this->functionCoverageData[$key])) {
|
||||
$executablePaths = count(
|
||||
$this->functionCoverageData[$key]->paths,
|
||||
);
|
||||
|
||||
$executedPaths = count(
|
||||
array_filter(
|
||||
$this->functionCoverageData[$key]->paths,
|
||||
static function (ProcessedPathCoverageData $path)
|
||||
{
|
||||
return (bool) $path->hit;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return new ProcessedMethodType(
|
||||
$method->name(),
|
||||
$method->visibility()->value,
|
||||
$method->signature(),
|
||||
$method->startLine(),
|
||||
$method->endLine(),
|
||||
0,
|
||||
0,
|
||||
$executableBranches,
|
||||
$executedBranches,
|
||||
$executablePaths,
|
||||
$executedPaths,
|
||||
$method->cyclomaticComplexity(),
|
||||
0,
|
||||
0,
|
||||
$link . $method->startLine(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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\Node;
|
||||
|
||||
use function assert;
|
||||
use function count;
|
||||
use RecursiveIterator;
|
||||
|
||||
/**
|
||||
* @template-implements RecursiveIterator<int, AbstractNode>
|
||||
*
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class Iterator implements RecursiveIterator
|
||||
{
|
||||
private int $position;
|
||||
|
||||
/**
|
||||
* @var list<AbstractNode>
|
||||
*/
|
||||
private readonly array $nodes;
|
||||
|
||||
public function __construct(Directory $node)
|
||||
{
|
||||
$this->nodes = $node->children();
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return $this->position < count($this->nodes);
|
||||
}
|
||||
|
||||
public function key(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function current(): ?AbstractNode
|
||||
{
|
||||
return $this->valid() ? $this->nodes[$this->position] : null;
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
$this->position++;
|
||||
}
|
||||
|
||||
public function getChildren(): self
|
||||
{
|
||||
assert($this->nodes[$this->position] instanceof Directory);
|
||||
|
||||
return new self($this->nodes[$this->position]);
|
||||
}
|
||||
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
return $this->nodes[$this->position] instanceof Directory;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,298 @@
|
||||
<?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 ENT_HTML5;
|
||||
use const ENT_QUOTES;
|
||||
use const ENT_SUBSTITUTE;
|
||||
use function array_pop;
|
||||
use function count;
|
||||
use function htmlspecialchars;
|
||||
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 = ' / ';
|
||||
|
||||
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' => $this->escapeHtml($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 escapeHtml(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5);
|
||||
}
|
||||
|
||||
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",
|
||||
$this->escapeHtml($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,
|
||||
$this->escapeHtml($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",
|
||||
$this->escapeHtml($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,124 @@
|
||||
<?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 {
|
||||
$name = $this->escapeHtml($node->name());
|
||||
$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>',
|
||||
$name,
|
||||
$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>',
|
||||
$name,
|
||||
$name,
|
||||
$name,
|
||||
$name,
|
||||
);
|
||||
} else {
|
||||
$data['name'] = sprintf(
|
||||
'<a href="%s.html">%s</a>',
|
||||
$name,
|
||||
$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
+9
@@ -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}}
|
||||
+5
@@ -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>
|
||||
Vendored
+5
@@ -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>
|
||||
+10
@@ -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}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
@charset "UTF-8";/*!
|
||||
* Bootstrap v5.3.6 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherLine truncated
|
||||
/*# sourceMappingURL=bootstrap.min.css.map */
|
||||
Whitespace-only changes.
+5
@@ -0,0 +1,5 @@
|
||||
.octicon {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
fill: currentColor;
|
||||
}
|
||||
+274
@@ -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;
|
||||
}
|
||||
+299
@@ -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>
|
||||
Vendored
+293
@@ -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>
|
||||
+60
@@ -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> </td>
|
||||
<td colspan="9"><div align="center"><strong>Code Coverage</strong></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </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>
|
||||
Vendored
+62
@@ -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> </td>
|
||||
<td colspan="15"><div align="center"><strong>Code Coverage</strong></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </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>
|
||||
Vendored
+13
@@ -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>
|
||||
|
||||
Vendored
+19
@@ -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>
|
||||
|
||||
+64
@@ -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> </td>
|
||||
<td colspan="10"><div align="center"><strong>Code Coverage</strong></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </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>
|
||||
+66
@@ -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> </td>
|
||||
<td colspan="16"><div align="center"><strong>Code Coverage</strong></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </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>
|
||||
+14
@@ -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>
|
||||
|
||||
Vendored
+20
@@ -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>
|
||||
|
||||
+1
@@ -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 |
Vendored
+1
@@ -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 |
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
/*!
|
||||
* 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
|
||||
*
|
||||
* All-in-one packaged file for ease use of 'billboard.js' with dependant d3.js modules & polyfills.
|
||||
* - @types/d3-selection ^3.0.11
|
||||
* - @types/d3-transition ^3.0.9
|
||||
* - d3-axis ^3.0.0
|
||||
* - d3-brush ^3.0.0
|
||||
* - d3-drag ^3.0.0
|
||||
* - d3-dsv ^3.0.1
|
||||
* - d3-ease ^3.0.1
|
||||
* - d3-hierarchy ^3.1.2
|
||||
* - d3-interpolate ^3.0.1
|
||||
* - d3-scale ^4.0.2
|
||||
* - d3-selection ^3.0.0
|
||||
* - d3-shape ^3.2.0
|
||||
* - d3-time-format ^4.1.0
|
||||
* - d3-transition ^3.0.1
|
||||
* - d3-zoom ^3.0.0
|
||||
*/(function(Xa,zn){if(typeof exports=="object"&&typeof module=="object")module.exports=zn();else if(typeof define=="function"&&define.amd)define([],zn);else{var Ha=zn();for(var x in Ha)(typeof exports=="object"?exports:Xa)[x]=Ha[x]}})(this,function(){return function(){"use strict";var Cs=[function(x,b,r){r(1),r(97),r(98),r(99),r(100),r(101),r(102),r(103),r(104),r(105),r(106),r(107),r(108),r(109),r(110),r(111),r(124),r(126),r(136),r(137),r(139),r(143),r(146),r(148),r(150),r(151),r(152),r(153),r(155),r(156),r(158),r(159),r(161),r(165),r(166),r(167),r(168),r(173),r(174),r(176),r(177),r(178),r(180),r(184),r(185),r(186),r(187),r(188),r(193),r(195),r(196),r(198),r(201),r(202),r(203),r(204),r(205),r(207),r(218),r(220),r(221),r(223),r(224),r(227),r(230),r(236),r(237),r(238),r(239),r(240),r(241),r(245),r(246),r(248),r(249),r(250),r(252),r(253),r(254),r(255),r(256),r(261),r(262),r(263),r(264),r(266),r(267),r(268),r(270),r(271),r(272),r(273),r(93),r(274),r(275),r(283),r(285),r(287),r(288),r(289),r(290),r(291),r(293),r(294),r(295),r(296),r(297),r(298),r(300),r(301),r(302),r(303),r(304),r(305),r(306),r(307),r(311),r(312),r(314),r(316),r(317),r(318),r(319),r(320),r(322),r(324),r(325),r(326),r(327),r(329),r(330),r(332),r(333),r(334),r(335),r(337),r(338),r(339),r(340),r(341),r(342),r(343),r(344),r(345),r(347),r(348),r(349),r(350),r(351),r(352),r(353),r(354),r(355),r(356),r(357),r(359),r(360),r(361),r(362),r(386),r(387),r(388),r(389),r(390),r(391),r(392),r(393),r(394),r(395),r(397),r(398),r(399),r(400),r(401),r(402),r(403),r(404),r(405),r(406),r(413),r(415),r(416),r(418),r(419),r(420),r(421),r(422),r(424),r(434),r(436),r(438),r(440),r(442),r(444),r(446),r(447),r(449),r(452),r(453),r(454),r(455),r(456),r(460),r(461),r(463),r(464),r(465),r(466),r(468),r(469),r(470),r(471),r(472),r(473),r(474),r(476),r(479),r(482),r(485),r(486),r(487),r(488),r(489),r(490),r(491),r(492),r(493),r(494),r(495),r(496),r(497),r(505),r(506),r(507),r(508),r(509),r(510),r(511),r(512),r(513),r(514),r(515),r(516),r(517),r(519),r(520),r(521),r(522),r(523),r(524),r(525),r(526),r(527),r(528),r(529),r(530),r(531),r(532),r(533),r(534),r(535),r(536),r(537),r(538),r(539),r(540),r(541),r(542),r(543),r(544),r(545),r(546),r(549),r(551),r(553),r(554),r(557),r(558),r(560),r(561),r(562),r(566),r(567),r(568),r(569),r(572),r(577),r(578),r(579),r(580),r(581),r(582),r(583),r(80)},function(x,b,r){r(2),r(90),r(92),r(93),r(96)},function(x,b,r){var u=r(3),d=r(4),h=r(8),p=r(14),y=r(36),T=r(6),$=r(26),A=r(7),E=r(38),R=r(24),I=r(46),O=r(12),C=r(18),D=r(68),M=r(11),F=r(71),z=r(73),U=r(57),j=r(75),G=r(66),B=r(5),V=r(44),Y=r(72),Z=r(10),J=r(47),q=r(77),nt=r(34),rt=r(53),_=r(54),tt=r(40),et=r(33),lt=r(78),mt=r(79),gt=r(81),xt=r(82),yt=r(51),Ut=r(83).forEach,Dt=rt("hidden"),Xt="Symbol",Qt="prototype",kt=yt.set,me=yt.getterFor(Xt),ge=Object[Qt],ae=d.Symbol,Mt=ae&&ae[Qt],Ht=d.RangeError,re=d.TypeError,se=d.QObject,ee=B.f,fe=V.f,Pe=j.f,Me=Z.f,$e=p([].push),ce=nt("symbols"),Ae=nt("op-symbols"),Te=nt("wks"),de=!se||!se[Qt]||!se[Qt].findChild,bt=function(It,Pt,Ct){var Nt=ee(ge,Pt);Nt&&delete ge[Pt],fe(It,Pt,Ct),Nt&&It!==ge&&fe(ge,Pt,Nt)},Ft=T&&A(function(){return F(fe({},"a",{get:function(){return fe(this,"a",{value:7}).a}})).a!==7})?bt:fe,Tt=function(It,Pt){var Ct=ce[It]=F(Mt);return kt(Ct,{type:Xt,tag:It,description:Pt}),T||(Ct.description=Pt),Ct},qt=function(Pt,Ct,Nt){Pt===ge&&qt(Ae,Ct,Nt),I(Pt);var Et=C(Ct);return I(Nt),E(ce,Et)?(Nt.enumerable?(E(Pt,Dt)&&Pt[Dt][Et]&&(Pt[Dt][Et]=!1),Nt=F(Nt,{enumerable:M(0,!1)})):(E(Pt,Dt)||fe(Pt,Dt,M(1,F(null))),Pt[Dt][Et]=!0),Ft(Pt,Et,Nt)):fe(Pt,Et,Nt)},te=function(Pt,Ct){I(Pt);var Nt=O(Ct),Et=z(Nt).concat(ut(Nt));return Ut(Et,function(ie){(!T||h(Yt,Nt,ie))&&qt(Pt,ie,Nt[ie])}),Pt},Zt=function(Pt,Ct){return Ct===void 0?F(Pt):te(F(Pt),Ct)},Yt=function(Pt){var Ct=C(Pt),Nt=h(Me,this,Ct);return this===ge&&E(ce,Ct)&&!E(Ae,Ct)?!1:Nt||!E(this,Ct)||!E(ce,Ct)||E(this,Dt)&&this[Dt][Ct]?Nt:!0},Ye=function(Pt,Ct){var Nt=O(Pt),Et=C(Ct);if(!(Nt===ge&&E(ce,Et)&&!E(Ae,Et))){var ie=ee(Nt,Et);return ie&&E(ce,Et)&&!(E(Nt,Dt)&&Nt[Dt][Et])&&(ie.enumerable=!0),ie}},Ze=function(Pt){var Ct=Pe(O(Pt)),Nt=[];return Ut(Ct,function(Et){!E(ce,Et)&&!E(_,Et)&&$e(Nt,Et)}),Nt},ut=function(It){var Pt=It===ge,Ct=Pe(Pt?Ae:O(It)),Nt=[];return Ut(Ct,function(Et){E(ce,Et)&&(!Pt||E(ge,Et))&&$e(Nt,ce[Et])}),Nt};$||(ae=function(){if(R(Mt,this))throw new re("Symbol is not a constructor");var Pt=!arguments.length||arguments[0]===void 0?void 0:D(arguments[0]),Ct=tt(Pt),Nt=function(Et){var ie=this===void 0?d:this;ie===ge&&h(Nt,Ae,Et),E(ie,Dt)&&E(ie[Dt],Ct)&&(ie[Dt][Ct]=!1);var we=M(1,Et);try{Ft(ie,Ct,we)}catch(Rt){if(!(Rt instanceof Ht))throw Rt;bt(ie,Ct,we)}};return T&&de&&Ft(ge,Ct,{configurable:!0,set:Nt}),Tt(Ct,Pt)},Mt=ae[Qt],J(Mt,"toString",function(){return me(this).tag}),J(ae,"withoutSetter",function(It){return Tt(tt(It),It)}),Z.f=Yt,V.f=qt,Y.f=te,B.f=Ye,U.f=j.f=Ze,G.f=ut,lt.f=function(It){return Tt(et(It),It)},T&&(q(Mt,"description",{configurable:!0,get:function(){return me(this).descriptLine truncated
|
||||
\v\f\r \xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF`},function(x,b,r){var u=r(3);u({target:"Number",stat:!0,nonConfigurable:!0,nonWritable:!0},{EPSILON:Math.pow(2,-52)})},function(x,b,r){var u=r(3),d=r(313);u({target:"Number",stat:!0},{isFinite:d})},function(x,b,r){var u=r(4),d=u.isFinite;x.exports=Number.isFinite||function(p){return typeof p=="number"&&d(p)}},function(x,b,r){var u=r(3),d=r(315);u({target:"Number",stat:!0},{isInteger:d})},function(x,b,r){var u=r(20),d=Math.floor;x.exports=Number.isInteger||function(p){return!u(p)&&isFinite(p)&&d(p)===p}},function(x,b,r){var u=r(3);u({target:"Number",stat:!0},{isNaN:function(h){return h!==h}})},function(x,b,r){var u=r(3),d=r(315),h=Math.abs;u({target:"Number",stat:!0},{isSafeInteger:function(y){return d(y)&&h(y)<=9007199254740991}})},function(x,b,r){var u=r(3);u({target:"Number",stat:!0,nonConfigurable:!0,nonWritable:!0},{MAX_SAFE_INTEGER:9007199254740991})},function(x,b,r){var u=r(3);u({target:"Number",stat:!0,nonConfigurable:!0,nonWritable:!0},{MIN_SAFE_INTEGER:-9007199254740991})},function(x,b,r){var u=r(3),d=r(321);u({target:"Number",stat:!0,forced:Number.parseFloat!==d},{parseFloat:d})},function(x,b,r){var u=r(4),d=r(7),h=r(14),p=r(68),y=r(309).trim,T=r(310),$=h("".charAt),A=u.parseFloat,E=u.Symbol,R=E&&E.iterator,I=1/A(T+"-0")!==-1/0||R&&!d(function(){A(Object(R))});x.exports=I?function(C){var D=y(p(C)),M=A(D);return M===0&&$(D,0)==="-"?-0:M}:A},function(x,b,r){var u=r(3),d=r(323);u({target:"Number",stat:!0,forced:Number.parseInt!==d},{parseInt:d})},function(x,b,r){var u=r(4),d=r(7),h=r(14),p=r(68),y=r(309).trim,T=r(310),$=u.parseInt,A=u.Symbol,E=A&&A.iterator,R=/^[+-]?0x/i,I=h(R.exec),O=$(T+"08")!==8||$(T+"0x16")!==22||E&&!d(function(){$(Object(E))});x.exports=O?function(D,M){var F=y(p(D));return $(F,M>>>0||(I(R,F)?16:10))}:$},function(x,b,r){var u=r(3),d=r(14),h=r(61),p=r(308),y=r(244),T=r(299),$=r(7),A=RangeError,E=String,R=isFinite,I=Math.abs,O=Math.floor,C=Math.pow,D=Math.round,M=d(1 .toExponential),F=d(y),z=d("".slice),U=M(-69e-12,4)==="-6.9000e-11"&&M(1.255,2)==="1.25e+0"&&M(12345,3)==="1.235e+4"&&M(25,0)==="3e+1",j=function(){return $(function(){M(1,1/0)})&&$(function(){M(1,-1/0)})},G=function(){return!$(function(){M(1/0,1/0),M(NaN,1/0)})},B=!U||!j()||!G();u({target:"Number",proto:!0,forced:B},{toExponential:function(Y){var Z=p(this);if(Y===void 0)return M(Z);var J=h(Y);if(!R(Z))return String(Z);if(J<0||J>20)throw new A("Incorrect fraction digits");if(U)return M(Z,J);var q="",nt,rt,_,tt;if(Z<0&&(q="-",Z=-Z),Z===0)rt=0,nt=F("0",J+1);else{var et=T(Z);rt=O(et);var lt=C(10,rt-J),mt=D(Z/lt);2*Z>=(2*mt+1)*lt&&(mt+=1),mt>=C(10,J+1)&&(mt/=10,rt+=1),nt=E(mt)}return J!==0&&(nt=z(nt,0,1)+"."+z(nt,1)),rt===0?(_="+",tt="0"):(_=rt>0?"+":"-",tt=E(I(rt))),nt+="e"+_+tt,q+nt}})},function(x,b,r){var u=r(3),d=r(14),h=r(61),p=r(308),y=r(244),T=r(7),$=RangeError,A=String,E=Math.floor,R=d(y),I=d("".slice),O=d(1 .toFixed),C=function(j,G,B){return G===0?B:G%2===1?C(j,G-1,B*j):C(j*j,G/2,B)},D=function(j){for(var G=0,B=j;B>=4096;)G+=12,B/=4096;for(;B>=2;)G+=1,B/=2;return G},M=function(j,G,B){for(var V=-1,Y=B;++V<6;)Y+=G*j[V],j[V]=Y%1e7,Y=E(Y/1e7)},F=function(j,G){for(var B=6,V=0;--B>=0;)V+=j[B],j[B]=E(V/G),V=V%G*1e7},z=function(j){for(var G=6,B="";--G>=0;)if(B!==""||G===0||j[G]!==0){var V=A(j[G]);B=B===""?V:B+R("0",7-V.length)+V}return B},U=T(function(){return O(8e-5,3)!=="0.000"||O(.9,0)!=="1"||O(1.255,2)!=="1.25"||O(0xde0b6b3a7640080,0)!=="1000000000000000128"})||!T(function(){O({})});u({target:"Number",proto:!0,forced:U},{toFixed:function(G){var B=p(this),V=h(G),Y=[0,0,0,0,0,0],Z="",J="0",q,nt,rt,_;if(V<0||V>20)throw new $("Incorrect fraction digits");if(B!==B)return"NaN";if(B<=-1e21||B>=1e21)return A(B);if(B<0&&(Z="-",B=-B),B>1e-21)if(q=D(B*C(2,69,1))-69,nt=q<0?B*C(2,-q,1):B/C(2,q,1),nt*=4503599627370496,q=52-q,q>0){for(M(Y,0,nt),rt=V;rt>=7;)M(Y,1e7,0),rt-=7;for(M(Y,C(10,rt,1),0),rt=q-1;rt>=23;)F(Y,8388608),rt-=23;F(Y,1<<rt),M(Y,1,1),F(Y,2),J=z(Y)}else M(Y,0,nt),M(Y,1<<-q,0),J=z(Y)+R("0",V);return V>0?(_=J.length,J=Z+(_<=V?"0."+R("0",V-_)+J:I(J,0,_-V)+"."+I(J,_-V))):J=Z+J,J}})},function(x,b,r){var u=r(3),d=r(14),h=r(7),p=r(308),y=d(1 .toPrecision),T=h(function(){return y(1,void 0)!=="1"})||!h(function(){y({})});u({target:"Number",proto:!0,forced:T},{toPrecision:function(A){return A===void 0?y(p(this)):y(p(this),A)}})},function(x,b,r){var u=r(3),d=r(328);u({target:"Object",stat:!0,arity:2,forced:Object.assign!==d},{assign:d})},function(x,b,r){var u=r(6),d=r(14),h=r(8),p=r(7),y=r(73),T=r(66),$=r(10),A=r(39),E=r(13),R=Object.assign,I=Object.defineProperty,O=d([].concat);x.exports=!R||p(function(){if(u&&R({b:1},R(I({},"a",{enumerable:!0,get:function(){I(this,"b",{value:3,enumerable:!1})}}),{b:2})).b!==1)return!0;var C={},D={},M=Symbol("assign detection"),F="abcdefghijklmnopqrst";return C[M]=7,F.split("").forEach(function(z){D[z]=z}),R({},C)[M]!==7||y(R({},D)).join("")!==F})?function(D,M){for(var F=A(D),
Line truncated
|
||||
`)&&p.flags==="s")})},function(x,b,r){var u=r(7),d=r(4),h=d.RegExp;x.exports=u(function(){var p=h("(?<a>b)","g");return p.exec("b").groups.a!=="b"||"b".replace(p,"$<a>c")!=="bc"})},function(x,b,r){var u=r(3),d=r(14),h=r(414),p=r(38),y=r(243).start,T=r(310),$=Array,A=RegExp.escape,E=d("".charAt),R=d("".charCodeAt),I=d(1.1.toString),O=d([].join),C=/^[0-9a-z]/i,D=/^[$()*+./?[\\\]^{|}]/,M=RegExp("^[!\"#%&',\\-:;<=>@`~"+T+"]"),F=d(C.exec),z={" ":"t","\n":"n","\v":"v","\f":"f","\r":"r"},U=function(G){var B=I(R(G,0),16);return B.length<3?"\\x"+y(B,2,"0"):"\\u"+y(B,4,"0")},j=!A||A("ab")!=="\\x61b";u({target:"RegExp",stat:!0,forced:j},{escape:function(B){h(B);for(var V=B.length,Y=$(V),Z=0;Z<V;Z++){var J=E(B,Z);if(Z===0&&F(C,J))Y[Z]=U(J);else if(p(z,J))Y[Z]="\\"+z[J];else if(F(D,J))Y[Z]="\\"+J;else if(F(M,J))Y[Z]=U(J);else{var q=R(J,0);(q&63488)!==55296?Y[Z]=J:q>=56320||Z+1>=V||(R(B,Z+1)&64512)!==56320?Y[Z]=U(J):(Y[Z]=J,Y[++Z]=E(B,Z))}}return O(Y,"")}})},function(x){var b=TypeError;x.exports=function(r){if(typeof r=="string")return r;throw new b("Argument is not a string")}},function(x,b,r){var u=r(6),d=r(411),h=r(15),p=r(77),y=r(51).get,T=RegExp.prototype,$=TypeError;u&&d&&p(T,"dotAll",{configurable:!0,get:function(){if(this!==T){if(h(this)==="RegExp")return!!y(this).dotAll;throw new $("Incompatible receiver, RegExp required")}}})},function(x,b,r){var u=r(3),d=r(417);u({target:"RegExp",proto:!0,forced:/./.exec!==d},{exec:d})},function(x,b,r){var u=r(8),d=r(14),h=r(68),p=r(409),y=r(410),T=r(34),$=r(71),A=r(51).get,E=r(411),R=r(412),I=T("native-string-replace",String.prototype.replace),O=RegExp.prototype.exec,C=O,D=d("".charAt),M=d("".indexOf),F=d("".replace),z=d("".slice),U=function(){var V=/a/,Y=/b*/g;return u(O,V,"a"),u(O,Y,"a"),V.lastIndex!==0||Y.lastIndex!==0}(),j=y.BROKEN_CARET,G=/()??/.exec("")[1]!==void 0,B=U||G||j||E||R;B&&(C=function(Y){var Z=this,J=A(Z),q=h(Y),nt=J.raw,rt,_,tt,et,lt,mt,gt;if(nt)return nt.lastIndex=Z.lastIndex,rt=u(C,nt,q),Z.lastIndex=nt.lastIndex,rt;var xt=J.groups,yt=j&&Z.sticky,Ut=u(p,Z),Dt=Z.source,Xt=0,Qt=q;if(yt&&(Ut=F(Ut,"y",""),M(Ut,"g")===-1&&(Ut+="g"),Qt=z(q,Z.lastIndex),Z.lastIndex>0&&(!Z.multiline||Z.multiline&&D(q,Z.lastIndex-1)!==`
|
||||
`)&&(Dt="(?: "+Dt+")",Qt=" "+Qt,Xt++),_=new RegExp("^(?:"+Dt+")",Ut)),G&&(_=new RegExp("^"+Dt+"$(?!\\s)",Ut)),U&&(tt=Z.lastIndex),et=u(O,yt?_:Z,Qt),yt?et?(et.input=z(et.input,Xt),et[0]=z(et[0],Xt),et.index=Z.lastIndex,Z.lastIndex+=et[0].length):Z.lastIndex=0:U&&et&&(Z.lastIndex=Z.global?et.index+et[0].length:tt),G&&et&&et.length>1&&u(I,et[0],_,function(){for(lt=1;lt<arguments.length-2;lt++)arguments[lt]===void 0&&(et[lt]=void 0)}),et&&xt)for(et.groups=mt=$(null),lt=0;lt<xt.length;lt++)gt=xt[lt],mt[gt[0]]=et[gt[1]];return et}),x.exports=C},function(x,b,r){var u=r(4),d=r(6),h=r(77),p=r(409),y=r(7),T=u.RegExp,$=T.prototype,A=d&&y(function(){var E=!0;try{T(".","d")}catch(z){E=!1}var R={},I="",O=E?"dgimsy":"gimsy",C=function(z,U){Object.defineProperty(R,z,{get:function(){return I+=U,!0}})},D={dotAll:"s",global:"g",ignoreCase:"i",multiline:"m",sticky:"y"};E&&(D.hasIndices="d");for(var M in D)C(M,D[M]);var F=Object.getOwnPropertyDescriptor($,"flags").get.call(R);return F!==O||I!==O});A&&h($,"flags",{configurable:!0,get:p})},function(x,b,r){var u=r(6),d=r(410).MISSED_STICKY,h=r(15),p=r(77),y=r(51).get,T=RegExp.prototype,$=TypeError;u&&d&&p(T,"sticky",{configurable:!0,get:function(){if(this!==T){if(h(this)==="RegExp")return!!y(this).sticky;throw new $("Incompatible receiver, RegExp required")}}})},function(x,b,r){r(416);var u=r(3),d=r(8),h=r(21),p=r(46),y=r(68),T=function(){var A=!1,E=/[ac]/;return E.exec=function(){return A=!0,/./.exec.apply(this,arguments)},E.test("abc")===!0&&A}(),$=/./.test;u({target:"RegExp",proto:!0,forced:!T},{test:function(A){var E=p(this),R=y(A),I=E.exec;if(!h(I))return d($,E,R);var O=d(I,E,R);return O===null?!1:(p(O),!0)}})},function(x,b,r){var u=r(49).PROPER,d=r(47),h=r(46),p=r(68),y=r(7),T=r(408),$="toString",A=RegExp.prototype,E=A[$],R=y(function(){return E.call({source:"a",flags:"b"})!=="/a/b"}),I=u&&E.name!==$;(R||I)&&d(A,$,function(){var C=h(this),D=p(C.source),M=p(T(C));return"/"+D+"/"+M},{unsafe:!0})},function(x,b,r){r(423)},function(x,b,r){var u=r(277),d=r(282);u("Set",function(h){return function(){return h(this,arguments.length?arguments[0]:void 0)}},d)},function(x,b,r){var u=r(3),d=r(425),h=r(433),p=!h("difference",function(y){return y.size===0});u({target:"Set",proto:!0,real:!0,forced:p},{difference:d})},function(x,b,r){var u=r(426),d=r(427),h=r(428),p=r(431),y=r(432),T=r(429),$=r(430),A=d.has,E=d.remove;x.exports=function(I){var O=u(this),C=y(I),D=h(O);return p(O)<=C.size?T(O,function(M){C.includes(M)&&E(D,M)}):$(C.getIterator(),function(M){A(O,M)&&E(D,M)}),D}},function(x,b,r){var u=r(427).has;x.exports=function(d){return u(d),d}},function(x,b,r){var u=r(14),d=Set.prototype;x.exports={Set,add:u(d.add),has:u(d.has),remove:u(d.delete),proto:d}},function(x,b,r){var u=r(427),d=r(429),h=u.Set,p=u.add;x.exports=function(y){var T=new h;return d(y,function($){p(T,$)}),T}},function(x,b,r){var u=r(14),d=r(430),h=r(427),p=h.Set,y=h.proto,T=u(y.forEach),$=u(y.keys),A=$(new p).next;x.exports=function(E,R,I){return I?d({iterator:$(E),next:A},R):T(E,R)}},function(x,b,r){var u=r(8);x.exports=function(d,h,p){for(var y=p?d:d.iterator,T=d.next,$,A;!($=u(T,y)).done;)if(A=h($.value),A!==void 0)return A}},function(x,b,r){var u=r(114),d=r(427);x.exports=u(d.proto,"size","get")||function(h){return h.size}},function(x,b,r){var u=r(30),d=r(46),h=r(8),p=r(61),y=r(257),T="Invalid size",$=RangeError,A=TypeError,E=Math.max,R=function(I,O){this.set=I,this.size=E(O,0),this.has=u(I.has),this.keys=u(I.keys)};R.prototype={getIterator:function(){return y(d(h(this.keys,this.set)))},includes:function(I){return h(this.has,this.set,I)}},x.exports=function(I){d(I);var O=+I.size;if(O!==O)throw new A(T);var C=p(O);if(C<0)throw new $(T);return new R(I,C)}},function(x,b,r){var u=r(23),d=function(p){return{size:p,has:function(){return!1},keys:function(){return{next:function(){return{done:!0}}}}}},h=function(p){return{size:p,has:function(){return!0},keys:function(){throw new Error("e")}}};x.exports=function(p,y){var T=u("Set");try{new T()[p](d(0));try{return new T()[p](d(-1)),!1}catch(A){if(!y)return!0;try{return new T()[p](h(-1/0)),!1}catch(E){var $=new T;return $.add(1),$.add(2),y($[p](h(1/0)))}}}catch(A){return!1}}},function(x,b,r){var u=r(3),d=r(7),h=r(435),p=r(433),y=!p("intersection",function(T){return T.size===2&&T.has(1)&&T.has(2)})||d(function(){return String(Array.from(new Set([1,2,3]).intersection(new Set([3,2]))))!=="3,2"});u({target:"Set",proto:!0,real:!0,forced:y},{intersection:h})},function(x,b,r){var u=r(426),d=r(427),h=r(431),p=r(432),y=r(429),T=r(430),$=d.Set,A=d.add,E=d.has;x.exports=function(I){var O=u(this),C=p(I),D=new $;return h(O)>C.size?T(C.getIterator(),function(M){E(O,M)&&A(D,M)}):y(O,function(M){C.includes(M)&&A(D,M)}),D}},function(x,b,r){var u=r(3),d=r(437),h=r(433),p=!h("isDisjointFrom",function(y){return!y});u({target:"Set",proto:!0,real:!0,forced:p},{isDisjointFrom:d})},function(x,b,r){var u=r(426),d=r(427).has,h=r(431),p=r(432),y=r(429),T=r(430),$=r(135);x.exports=function(E){var R=u(this)
Line truncated
|
||||
`)===-1)t.text(e);else{const i=[t.text(),e].map(o=>o.replace(/[\s\n]/g,""));if(i[0]!==i[1]){const o=e.split(`
|
||||
`),s=a?o.length-1:1;t.html(""),o.forEach((l,c)=>{t.append("tspan").attr("x",0).attr("dy",`${c===0?n[0]*s:n[1]}em`).text(l)})}}}function Hl(t){const{x:e,y:n,width:a,height:i}=t.getBBox();return[{x:e,y:n+i},{x:e,y:n},{x:e+a,y:n},{x:e+a,y:n+i}]}function Yl(t){const{width:e,height:n}=t.getBoundingClientRect(),a=Hl(t),i=a[0].x,o=Math.min(a[0].y,a[1].y);return{x:i,y:o,width:e,height:n}}function Hn(t,e){var n;const a=t&&((n=t.touches||t.sourceEvent&&t.sourceEvent.touches)==null?void 0:n[0]);let i=[0,0];try{i=Xn(a||t,e)}catch(o){}return i.map(o=>isNaN(o)?0:o)}function Wl(t){const{event:e,$el:n}=t,a=n.subchart.main||n.main;let i;return e&&e.type==="brush"?i=e.selection:a&&(i=a.select(".bb-brush").node())&&(i=zl(i)),i}function Ma(t){return!("rect"in t)||"rect"in t&&t.hasAttribute("width")&&t.rect.width!==+t.getAttribute("width")?t.rect=t.getBoundingClientRect():t.rect}function gr(t=!0,e=0,n=1e4){const a=Ke.crypto||Ke.msCrypto,i=a?e+a.getRandomValues(new Uint32Array(1))[0]%(n-e+1):Math.floor(Math.random()*(n-e)+e);return t?String(i):i}function wo(t,e,n,a,i){if(n>a)return-1;const o=Math.floor((n+a)/2);let{x:s,w:l=0}=t[o];return i&&(s=t[o].y,l=t[o].h),e>=s&&e<=s+l?o:e<s?wo(t,e,n,o-1,i):wo(t,e,o+1,a,i)}function Kl(t){const e=Wl(t);return e?e[0]===e[1]:!0}function kg(...t){const e=n=>{if(Be(n)&&n.constructor){const a=new n.constructor;for(const i in n)a[i]=e(n[i]);return a}return n};return t.map(n=>e(n)).reduce((n,a)=>Xl(Xl({},n),a))}function yn(t={},e){je(e)&&e.forEach(n=>yn(t,n));for(const n in e)/^\d+$/.test(n)||n in t||(t[n]=e[n]);return t}const Cn=t=>t.charAt(0).toUpperCase()+t.slice(1);function qg(t,e="-"){return t.split(e).map((n,a)=>a?n.charAt(0).toUpperCase()+n.slice(1).toLowerCase():n.toLowerCase()).join("")}const Lr=t=>[].slice.call(t);function _g(t,e,n){const{rootSelector:a="",sheet:i}=t,s=`${a} ${(l=>l.replace(/\s?(bb-)/g,".$1").replace(/\.+/g,"."))(e)} {${n.join(";")}}`;return i[i.insertRule?"insertRule":"addRule"](s,i.cssRules.length)}function tv(t){let e=[];return t.forEach(n=>{var a;try{n.cssRules&&n.cssRules.length&&(e=e.concat(Lr(n.cssRules)))}catch(i){(a=Ke.console)==null||a.warn(`Error while reading rules from ${n.href}: ${i.toString()}`)}}),e}function Zl(t){var e,n,a,i,o,s;return{x:((n=(e=Ke.pageXOffset)!=null?e:Ke.scrollX)!=null?n:0)+((a=t.scrollLeft)!=null?a:0),y:((o=(i=Ke.pageYOffset)!=null?i:Ke.scrollY)!=null?o:0)+((s=t.scrollTop)!=null?s:0)}}function Ai(t,e=0,n=0,a=!0){const i=new DOMPoint(e,n),o=t.getScreenCTM(),s=i.matrixTransform(a?o==null?void 0:o.inverse():o);if(a===!1){const l=t.getBoundingClientRect();s.x-=l.x,s.y-=l.y}return s}function Jl(t){const e=t?t.transform:null,n=e&&e.baseVal;return n&&n.numberOfItems?n.getItem(0).matrix:{a:0,b:0,c:0,d:0,e:0,f:0}}function Mo(t){const e=t[0]instanceof Date,n=(e?t.map(Number):t).filter((a,i,o)=>o.indexOf(a)===i);return e?n.map(a=>new Date(a)):n}function Do(t){return t&&t.length?t.reduce((e,n)=>e.concat(n)):[]}function ea(t,...e){if(!e.length||e.length===1&&!e[0])return t;const n=e.shift();return Be(t)&&Be(n)&&Object.keys(n).forEach(a=>{if(!/^(__proto__|constructor|prototype)$/i.test(a)){const i=n[a];Be(i)?(!t[a]&&(t[a]={}),t[a]=ea(t[a],i)):t[a]=je(i)?i.concat():i}}),ea(t,...e)}function na(t,e=!0){let n;return t[0]instanceof Date?n=e?(a,i)=>a-i:(a,i)=>i-a:e&&!t.every(isNaN)?n=(a,i)=>a-i:e||(n=(a,i)=>a>i&&-1||a<i&&1||a===i&&0),t.concat().sort(n)}function _n(t,e){let n=e.filter(a=>cn(a));return n.length?he(n[0])?n=Math[t](...n):n[0]instanceof Date&&(n=na(n,t==="min")[0]):n=void 0,n}const Ei=(t,e,n=1)=>{const a=[],i=Math.max(0,Math.ceil((e-t)/n))|0;for(let o=t;o<i;o++)a.push(t+o*n);return a},ev={mouse:(()=>{const t=()=>({bubbles:!1,cancelable:!1,screenX:0,screenY:0,clientX:0,clientY:0});try{return new MouseEvent("t"),(e,n,a=t())=>{e.dispatchEvent(new MouseEvent(n,a))}}catch(e){return(n,a,i=t())=>{const o=gn.createEvent("MouseEvent");o.initMouseEvent(a,i.bubbles,i.cancelable,Ke,0,i.screenX,i.screenY,i.clientX,i.clientY,!1,!1,!1,!1,0,null),n.dispatchEvent(o)}}})(),touch:(t,e,n)=>{const a=new Touch(ea({identifier:Date.now(),target:t,radiusX:2.5,radiusY:2.5,rotationAngle:10,force:.5},n));t.dispatchEvent(new TouchEvent(e,{cancelable:!0,bubbles:!0,shiftKey:!0,touches:[a],targetTouches:[],changedTouches:[a]}))}};function bi(t,e){let n=t;for(const a in e)n=n.replace(new RegExp(`{=${a}}`,"g"),e[a]);return n}function Yn(t){var e;let n;if(t instanceof Date)n=t;else if(ze(t)){const{config:a,format:i}=this;n=(e=i.dataTime(a.data_xFormat)(t))!=null?e:new Date(t)}else he(t)&&!isNaN(t)&&(n=new Date(+t));return(!n||isNaN(+n))&&console&&console.error&&console.error(`Failed to parse x '${t}' to Date object`),n}function Lo(t){const e=t.attr("viewBox");return e?/(\d+(\.\d+)?){3}/.test(e):!1}function nv(t,e,n=!1){const a=!!t.node;let i=!1;for(const[o,s]of Object.entries(e))if(i=a?t.style(o)===s:t.style[o]===s,n===!1&&i)break;return i}function Da(){var t,e;return((t=gn)==null?void 0:t.hidden)===!1||((e=gn)==null?void 0:e.visibilityState)==="visible"}function rv(t,e){const{Do
Line truncated
|
||||
|
||||
self.onmessage=function({data}) {
|
||||
const result = (${a}).apply(null, data);
|
||||
self.postMessage(result);
|
||||
};`],{type:"text/javascript"})),Ke.URL.createObjectURL(Bo[i])}function $v(t){const e=new Ke.Worker(t);return e.onerror=function(n){console.error?console.error(n):console.log(n)},e}function Uo(t=!0,e,n,a){let i=function(...o){const s=e(...o);n(s)};if(Ke.Worker&&t){const o=Tv(e,a),s=$v(o);i=function(...l){s.postMessage(l),s.onmessage=function(c){return Ke.URL.revokeObjectURL(o),n(c.data)}}}return i}var nc={},zo={},jo=34,La=10,Vo=13;function rc(t){return new Function("d","return {"+t.map(function(e,n){return JSON.stringify(e)+": d["+n+'] || ""'}).join(",")+"}")}function Sv(t,e){var n=rc(t);return function(a,i){return e(n(a),i,t)}}function ac(t){var e=Object.create(null),n=[];return t.forEach(function(a){for(var i in a)i in e||n.push(e[i]=i)}),n}function Nn(t,e){var n=t+"",a=n.length;return a<e?new Array(e-a+1).join(0)+n:n}function Av(t){return t<0?"-"+Nn(-t,6):t>9999?"+"+Nn(t,6):Nn(t,4)}function Ev(t){var e=t.getUTCHours(),n=t.getUTCMinutes(),a=t.getUTCSeconds(),i=t.getUTCMilliseconds();return isNaN(t)?"Invalid Date":Av(t.getUTCFullYear(),4)+"-"+Nn(t.getUTCMonth()+1,2)+"-"+Nn(t.getUTCDate(),2)+(i?"T"+Nn(e,2)+":"+Nn(n,2)+":"+Nn(a,2)+"."+Nn(i,3)+"Z":a?"T"+Nn(e,2)+":"+Nn(n,2)+":"+Nn(a,2)+"Z":n||e?"T"+Nn(e,2)+":"+Nn(n,2)+"Z":"")}function ic(t){var e=new RegExp('["'+t+`
|
||||
\r]`),n=t.charCodeAt(0);function a(v,m){var S,P,N=i(v,function(L,w){if(S)return S(L,w-1);P=L,S=m?Sv(L,m):rc(L)});return N.columns=P||[],N}function i(v,m){var S=[],P=v.length,N=0,L=0,w,X=P<=0,W=!1;v.charCodeAt(P-1)===La&&--P,v.charCodeAt(P-1)===Vo&&--P;function H(){if(X)return zo;if(W)return W=!1,nc;var K,at=N,ht;if(v.charCodeAt(at)===jo){for(;N++<P&&v.charCodeAt(N)!==jo||v.charCodeAt(++N)===jo;);return(K=N)>=P?X=!0:(ht=v.charCodeAt(N++))===La?W=!0:ht===Vo&&(W=!0,v.charCodeAt(N)===La&&++N),v.slice(at+1,K-1).replace(/""/g,'"')}for(;N<P;){if((ht=v.charCodeAt(K=N++))===La)W=!0;else if(ht===Vo)W=!0,v.charCodeAt(N)===La&&++N;else if(ht!==n)continue;return v.slice(at,K)}return X=!0,v.slice(at,P)}for(;(w=H())!==zo;){for(var k=[];w!==nc&&w!==zo;)k.push(w),w=H();m&&(k=m(k,L++))==null||S.push(k)}return S}function o(v,m){return v.map(function(S){return m.map(function(P){return g(S[P])}).join(t)})}function s(v,m){return m==null&&(m=ac(v)),[m.map(g).join(t)].concat(o(v,m)).join(`
|
||||
`)}function l(v,m){return m==null&&(m=ac(v)),o(v,m).join(`
|
||||
`)}function c(v){return v.map(f).join(`
|
||||
`)}function f(v){return v.map(g).join(t)}function g(v){return v==null?"":v instanceof Date?Ev(v):e.test(v+="")?'"'+v.replace(/"/g,'""')+'"':v}return{parse:a,parseRows:i,format:s,formatBody:l,formatRows:c,formatRow:f,formatValue:g}}var Fr=ic(","),bv=Fr.parse,Rv=Fr.parseRows,e1=Fr.format,n1=Fr.formatBody,r1=Fr.formatRows,a1=Fr.formatRow,i1=Fr.formatValue,Br=ic(" "),Iv=Br.parse,Ov=Br.parseRows,o1=Br.format,s1=Br.formatBody,l1=Br.formatRows,c1=Br.formatRow,u1=Br.formatValue;function Go(t){const e=[];return t.forEach(function(n,a){const i=n[0];n.forEach(function(o,s){if(s>0){if(typeof e[s-1]=="undefined"&&(e[s-1]={}),typeof o=="undefined")throw new Error(`Source data is missing a component at (${a}, ${s})!`);e[s-1][i]=o}})}),e}function Xo(t){const e=t[0],n=[];return t.forEach(function(a,i){if(i>0){const o={};a.forEach(function(s,l){if(typeof s=="undefined")throw new Error(`Source data is missing a component at (${i}, ${l})!`);o[e[l]]=s}),n.push(o)}}),n}function oc(t,e){const n=[];let a,i;if(Array.isArray(t)){const o=function(s,l){if(s[l]!==void 0)return s[l];const f=l.replace(/\[(\w+)\]/g,".$1").replace(/^\./,"").split(".");let g=s;return f.some(function(v){return!(g=g&&v in g?g[v]:void 0)}),g};e.x?a=e.value.concat(e.x):a=e.value,n.push(a),t.forEach(function(s){const l=a.map(function(c){let f=o(s,c);return typeof f=="undefined"&&(f=null),f});n.push(l)}),i=Xo(n)}else Object.keys(t).forEach(function(o){var s;const l=t[o].concat();(s=l.unshift)==null||s.call(l,o),n.push(l)}),i=Go(n);return i}function Cv(t,e="csv",n,a,i){const o=new XMLHttpRequest,s={csv:Pv,tsv:wv,json:oc};o.open("GET",t),n&&Object.keys(n).forEach(function(l){o.setRequestHeader(l,n[l])}),o.onreadystatechange=function(){if(o.readyState===4)if(o.status===200){const l=o.responseText;l&&i.call(this,s[e](e==="json"?JSON.parse(l):l,a))}else throw new Error(`${t}: Something went wrong loading!`)},o.send()}function sc(t,e){const n=t.rows(e);let a;return n.length===1?(a=[{}],n[0].forEach(i=>{a[0][i]=null})):a=t.parse(e),a}function Pv(t){return sc({rows:Rv,parse:bv},t)}function wv(t){return sc({rows:Ov,parse:Iv},t)}function lc(t,e){const n=t||(e==null?void 0:e.data_keys);return n!=null&&n.x&&(e.data_x=n.x),n}var Mv={convertData(t,e){const{config:n}=this,a=n.boost_useWorker;let i=t;if(t.bindto&&(i={},["url","mimeType","headers","keys","json","keys","rows","columns"].forEach(o=>{const s=`data_${o}`;s in t&&(i[o]=t[s])})),i.url&&e)Cv(i.url,i.mimeType,i.headers,lc(i.keys,n),e);else if(i.json)Uo(a,oc,e,[Go,Xo])(i.json,lc(i.keys,n));else if(i.rows)Uo(a,Xo,e)(i.rows);else if(i.columns)Uo(a,Go,e)(i.columns);else if(t.bindto)throw Error("url or json or rows or columns is required.")},convertDataToTargets(t,e){const n=this,{axis:a,config:i,state:o}=n,s=i.data_type;let l=!1,c=!1,f=!1;a&&(l=a.isCategorized(),c=a.isTimeSeries(),f=a.isCustomX());const g=Object.keys(t[0]||{}),v=g.length?g.filter(n.isNotX,n):[],m=g.length?g.filter(n.isX,n):[];let S;v.forEach(N=>{const L=this.getXKey(N);f||c?m.indexOf(L)>=0?S=(e&&n.data.xs[N]||[]).concat(t.map(w=>w[L]).filter(De).map((w,X)=>n.generateTargetX(w,N,X))):i.data_x?S=this.getOtherTargetXs():cn(i.data_xs)&&(S=n.getXValuesOfXKey(L,n.data.targets)):S=t.map((w,X)=>X),S&&(this.data.xs[N]=S)}),v.forEach(N=>{if(!this.data.xs[N])throw new Error(`x is not defined for id = "${N}".`)});const P=v.map((N,L)=>{const w=i.data_idConverter.bind(n.api)(N),X=n.getXKey(N),W=f&&l,H=W&&t.map(at=>at.x).every(at=>i.axis_x_categories.indexOf(at)>-1),k=t.__append__,K=X===null&&k?n.api.data.values(N).length:0;return{id:w,id_org:N,values:t.map((at,ht)=>{const $t=at[X];let dt=at[N],st;return dt=dt!==null&&!isNaN(dt)&&!Be(dt)?+dt:je(dt)||Be(dt)?dt:null,(W||o.hasRadar)&&L===0&&!ln($t)?(!H&&L===0&&ht===0&&!k&&(i.axis_x_categories=[]),st=i.axis_x_categories.indexOf($t),st===-1&&(st=i.axis_x_categories.length,i.axis_x_categories.push($t))):st=n.generateTargetX($t,N,K+ht),(ln(dt)||n.data.xs[N].length<=ht)&&(st=void 0),{x:st,value:dt,id:w,index:-1}}).filter(at=>Qe(at.x))}});if(P.forEach(N=>{var L;i.data_xSort&&(N.values=N.values.sort((w,X)=>{const W=w.x||w.x===0?w.x:1/0,H=X.x||X.x===0?X.x:1/0;return W-H})),N.values.forEach((w,X)=>w.index=X),(L=n.data.xs[N.id])==null||L.sort((w,X)=>w-X)}),o.hasNegativeValue=n.hasNegativeValueInTargets(P),o.hasPositiveValue=n.hasPositiveValueInTargets(P),s&&n.isValidChartType(s)){const N=n.mapToIds(P).filter(L=>!(L in i.data_types)||!n.isValidChartType(i.data_types[L]));n.setTargetType(N,s)}return P.forEach(N=>n.cache.add(N.id_org,N,!0)),P}},Dv={isX(t){const e=this,{config:n}=e,a=n.data_x&&t===n.data_x,i=cn(n.data_xs)&&Qg(n.data_xs,t);return a||i},isNotX(t){return!this.isX(t)},isStackNormalized(){const{config:t}=this;return!!(t.data_stack_normalize&&t.data_groups.length)},isGrouped(t){const e=this.config.data_groups;return t?e.some(n=>n.indexOf(t)>=0&&n.length>1):e.length>0},getXKey(t){const e=this,{config:n}=e;return n.data_x?n.data_x:cn(n.data_xs)?n.data_xs[t]:null},getXValuesOfXKey(t,e){const n=this,a=e&&cn(e)?n.mapToIds(e):[];let
Line truncated
|
||||
<feComposite in="SourceGraphic" />`)})}},getGradienColortUrl(t){return`url(#${this.state.datetimeId}-gradient${this.getTargetSelectorSuffix(t)})`},updateLinearGradient(){const t=this,{config:e,data:{targets:n},state:{datetimeId:a},$el:{defs:i}}=t;n.forEach(o=>{const s=`${a}-gradient${t.getTargetSelectorSuffix(o.id)}`,l=t.hasPointType()&&e.point_radialGradient,c=t.isAreaType(o)&&"area"||t.isBarType(o)&&"bar";if((l||c)&&i.select(`#${s}`).empty()){const f=t.color(o),g={defs:null,stops:[]};if(l){const{cx:v=.3,cy:m=.3,r:S=.7,stops:P=[[.1,f,0],[.9,f,1]]}=l;g.stops=P,g.defs=i.append("radialGradient").attr("id",`${s}`).attr("cx",v).attr("cy",m).attr("r",S)}else{const v=e.axis_rotated,{x:m=v?[1,0]:[0,0],y:S=v?[0,0]:[0,1],stops:P=[[0,f,1],[1,f,0]]}=e[`${c}_linearGradient`];g.stops=P,g.defs=i.append("linearGradient").attr("id",`${s}`).attr("x1",m[0]).attr("x2",m[1]).attr("y1",S[0]).attr("y2",S[1])}g.stops.forEach(v=>{const[m,S,P]=v,N=ve(S)?S.bind(t.api)(o.id):S;g.defs&&g.defs.append("stop").attr("offset",m).attr("stop-color",N||f).attr("stop-opacity",P)})}})},setOverColor(t,e){const n=this,{config:a,$el:{main:i}}=n,o=a.color_onover;let s=t?o:n.color;Be(s)?s=({id:l})=>l in o?o[l]:n.color(l):ze(s)?s=()=>o:ve(o)&&(s=s.bind(n.api)),i.selectAll(Be(e)?`.${Ve.arc}${n.getTargetSelectorSuffix(e.id)}`:`.${sn.shape}-${e}`).style("fill",s)}},Wv={getYDomainMinMax(t,e){const n=this,{axis:a,config:i}=n,o=e==="min",s=i.data_groups,l=n.mapToIds(t),c=n.getValuesAsIdKeyed(t);if(s.length>0){const f=n[`has${o?"Negative":"Positive"}ValueInTargets`](t);s.forEach(g=>{const v=g.filter(m=>l.indexOf(m)>=0);if(v.length){const m=v[0],S=a.getId(m);f&&c[m]&&(c[m]=c[m].map(P=>(o?P<0:P>0)?P:0)),v.filter((P,N)=>N>0).forEach(P=>{if(c[P]){const N=a.getId(P);c[P].forEach((L,w)=>{const X=+L,W=o?X>0:X<0;N===S&&!(f&&W)&&(c[m][w]+=X)})}})}})}return _n(e,Object.keys(c).map(f=>_n(e,c[f])))},isHiddenTargetWithYDomain(t){const e=this;return e.state.hiddenTargetIds.some(n=>e.axis.getId(n)===t)},getYDomain(t,e,n){const a=this,{axis:i,config:o,scale:s}=a,l=`axis_${e}`;if(a.isStackNormalized())return[0,100];const c=(s==null?void 0:s[e])&&s[e].type==="log",f=t.filter(dt=>i.getId(dt.id)===e),g=n?a.filterByXDomain(f,n):f;if(g.length===0)return a.isHiddenTargetWithYDomain(e)?s[e].domain():e==="y2"?s.y.domain():a.getYDomain(t,"y2",n);const v=o[`${l}_min`],m=o[`${l}_max`],S=o[`${l}_center`],P=o[`${l}_inverted`],N=a.hasDataLabel()&&o.axis_rotated,L=a.hasDataLabel()&&!o.axis_rotated;let w=a.getYDomainMinMax(g,"min"),X=a.getYDomainMinMax(g,"max"),W=[oe.BAR,oe.BUBBLE,oe.SCATTER,...Sr.Line].some(dt=>{const st=dt.indexOf("area")>-1?"area":dt;return a.hasType(dt,g,!0)&&o[`${st}_zerobased`]});w=De(v)?v:De(m)?w<=m?w:m-10:w,X=De(m)?m:De(v)?v<=X?X:v+10:X,isNaN(w)&&(w=0),isNaN(X)&&(X=w),w===X&&(w<0?X=0:w=0);const H=w>=0&&X>=0,k=w<=0&&X<=0;(De(v)&&H||De(m)&&k)&&(W=!1),W&&(H&&(w=0),k&&(X=0));const K=Math.abs(X-w);let at={top:K*.1,bottom:K*.1};if(Qe(S)){const dt=Math.max(Math.abs(w),Math.abs(X));X=S+dt,w=S-dt}if(N){const dt=Dr(s.y.range()),st=a.getDataLabelLength(w,X,"width").map(Vt=>Vt/dt);["bottom","top"].forEach((Vt,vt)=>{at[Vt]+=K*(st[vt]/(1-st[0]-st[1]))})}else if(L){const dt=a.getDataLabelLength(w,X,"height");["bottom","top"].forEach((st,Vt)=>{at[st]+=a.convertPixelToScale("y",dt[Vt],K)})}at=a.getResettedPadding(at);const ht=o[`${l}_padding`];cn(ht)&&["bottom","top"].forEach(dt=>{at[dt]=i.getPadding(ht,dt,at[dt],K)}),W&&(H&&(at.bottom=w),k&&(at.top=-X));const $t=c?[w,X].map(dt=>dt<0?0:dt):[w-at.bottom,X+at.top];return P?$t.reverse():$t},getXDomainMinMax(t,e){var n;const a=this,i=a.config[`axis_x_${e}`],o=_n(e,t.map(l=>_n(e,l.values.map(c=>c.x))));let s=Be(i)?i.value:i;return s=Qe(s)&&((n=a.axis)!=null&&n.isTimeSeries())?Yn.bind(this)(s):s,Be(i)&&i.fit&&(e==="min"&&s<o||e==="max"&&s>o)&&(s=void 0),Qe(s)?s:o},getXDomainPadding(t,e){const n=this,{axis:a,config:i}=n,o=i.axis_x_padding,s=a.isTimeSeries()&&e,l=Dr(t);let c;if(a.isCategorized()||s)c=0;else if(n.hasType("bar")){const v=n.getMaxDataCount();c=v>1?l/(v-1)/2:.5}else c=n.getResettedPadding(l*.01);let{left:f=c,right:g=c}=he(o)?{left:o,right:o}:o;if(o.unit==="px"){const v=Math.abs(l+l*.2);f=a.getPadding(o,"left",c,v),g=a.getPadding(o,"right",c,v)}else{const v=l+f+g;if(s&&v){const m=l/e/v;f=f/v/m,g=g/v/m}}return{left:f,right:g}},getXDomain(t){const e=this,{axis:n,config:a,scale:{x:i}}=e,o=a.axis_x_inverted,s=[e.getXDomainMinMax(t,"min"),e.getXDomainMinMax(t,"max")];let[l=0,c=0]=s;if(i.type!=="log"){const f=n.isCategorized(),g=n.isTimeSeries(),v=e.getXDomainPadding(s);let[m,S]=s;m-S===0&&!f&&(g?(m=new Date(m.getTime()*.5),S=new Date(S.getTime()*1.5)):(m=m===0?1:m*.5,S=S===0?-1:S*1.5)),(m||m===0)&&(l=g?new Date(m.getTime()-v.left):m-v.left),(S||S===0)&&(c=g?new Date(S.getTime()+v.right):S+v.right)}return o?[c,l]:[l,c]},updateXDomain(t,e,n,a,i){var o;const s=this,{config:l,org:c,scale:{x:f,subX:g}}=s,v=l.zoom_enabled;if(n&&(f.domain(i||na(s.getXDomain(t),!l.axis_x_inverted)),c.xDomain=f.domain(),g.domain(f.domain()),(o=s.brush
Line truncated
|
||||
{=TITLE}
|
||||
{{<tr class="{=CLASS_TOOLTIP_NAME}">
|
||||
<td class="name">${this.patterns?"{=COLOR}":'<span style="background-color:{=COLOR}"></span>'}{=NAME}</td>
|
||||
<td class="value">{=VALUE}</td>
|
||||
</tr>}}
|
||||
</tbody></table>`).replace(/(\r?\n|\t)/g,"").split(/{{(.*)}}/)},setTooltipPosition(t,e){var n,a;const i=this,{config:o,scale:s,state:l,$el:{eventRect:c,tooltip:f,svg:g}}=i,{bindto:v}=o.tooltip_contents,m=o.axis_rotated,S=f==null?void 0:f.datum();if(!v&&S){const P=t!=null?t:JSON.parse(S.current),[N,L]=Hn(l.event,e!=null?e:c==null?void 0:c.node()),w={x:N,y:L};if(l.hasAxis&&s.x&&S&&"x"in S){const k=(K=0,at,ht="y")=>{var $t;const dt=s[at?($t=i.axis)==null?void 0:$t.getId(at):ht];return dt?dt(K)+(m?l.margin.left:l.margin.top):0};w.xAxis=s.x(S.x)+(o.tooltip_position?m?l.margin.top:l.margin.left:0),P.length===1?w.yAxis=k(P[0].value,P[0].id):w.yAxis=k}const{width:X=0,height:W=0}=S,H=(a=(n=o.tooltip_position)==null?void 0:n.bind(i.api)(P,X,W,c==null?void 0:c.node(),w))!=null?a:Lo(g)?i.getTooltipPositionViewBox.bind(i)(X,W,w):i.getTooltipPosition.bind(i)(X,W,w);["top","left"].forEach(k=>{const K=H[k];f.style(k,`${K}px`),k==="left"&&!S.xPosInPercent&&(S.xPosInPercent=K/l.current.width*100)})}},getTooltipPositionViewBox(t,e,n){var a,i;const o=this,{$el:{eventRect:s,svg:l},config:c,state:f}=o,g=c.axis_rotated,v=o.hasArcType()||f.hasFunnel||f.hasTreemap,m=(i=(a=v?l:s)==null?void 0:a.node())!=null?i:f.event.target;let{x:S,y:P}=n;f.hasAxis&&(S=g?S:n.xAxis,P=g?n.xAxis:P);const N=Ai(m,S,P,!1),L=m.getBoundingClientRect(),w=Ai(m,20,0,!1).x;let X=N.y,W=N.x+t/2+w;return v&&(f.hasFunnel||f.hasTreemap||f.hasRadar?(W-=t/2+w,X+=e):(X+=L.height/2,W+=L.width/2-(t-w))),W+t>L.width&&(W=L.width-t-w),X+e>L.height&&(X-=e*2),{top:X,left:W}},getTooltipPosition(t,e,n){var a,i,o;const s=this,{config:l,scale:c,state:f}=s,{width:g,height:v,current:m,hasFunnel:S,hasRadar:P,hasTreemap:N,isLegendRight:L,inputType:w}=f,X=s.hasType("gauge")&&!l.gauge_fullCircle,W=l.axis_rotated,H=s.hasArcType(),k=s.getSvgLeft(!0);let K=k+m.width-s.getCurrentPaddingByDirection("right");const at=20;let{x:ht,y:$t}=n;if(P)ht+=ht>=g/2?15:-(t+15),$t+=15;else if(H){if(w!=="touch"){let Vt=(i=(a=s.getTitlePadding)==null?void 0:a.call(s))!=null?i:0;Vt&&X&&((o=l.arc_rangeText_values)!=null&&o.length)&&(Vt+=10),ht+=(g-(L?s.getLegendWidth():0))/2,$t+=(X?v:v/2+e)+Vt}}else if(S||N)$t+=e;else{const st={top:s.getCurrentPaddingByDirection("top",!0),left:s.getCurrentPaddingByDirection("left",!0)};W?(ht+=k+st.left+at,$t=st.top+n.xAxis+at,K-=k):(ht=k+st.left+at+(c.zoom?ht:n.xAxis),$t+=st.top-5)}if(ht+t+15>K&&(ht-=t+(S||N||H?0:W?at*2:38)),$t+e>m.height){const st=N?e+10:30;$t-=X?e*1.5:e+st}const dt={top:$t,left:ht};return Object.keys(dt).forEach(st=>{dt[st]<0&&(dt[st]=0)}),dt},showTooltip(t,e){const n=this,{config:a,$el:{tooltip:i}}=n,o=t.filter(c=>c&&De(n.getBaseValue(c)));if(!i||o.length===0||!a.tooltip_show)return;let s=i.datum();const l=JSON.stringify(t);if(!s||s.current!==l){const{index:c,x:f}=t.concat().sort()[0];_e(a.tooltip_onshow,n.api,t),i.html(n.getTooltipHTML(t,n.axis?n.axis.getXAxisTickFormat():n.categoryName.bind(n),n.getDefaultValueFormat(),n.color)).style("display",null).style("visibility",null).datum(s={index:c,x:f,current:l,width:i.property("offsetWidth"),height:i.property("offsetHeight")}),_e(a.tooltip_onshown,n.api,t),n._handleLinkedCharts(!0,c)}n.setTooltipPosition(o,e)},bindTooltipResizePos(){const t=this,{resizeFunction:e,state:n,$el:{tooltip:a}}=t;e.add(()=>{if(a.style("display")==="block"){const{current:i}=n,{width:o,xPosInPercent:s}=a.datum();let l=i.width/100*s;const c=i.width-(l+o);c<0&&(l+=c),a.style("left",`${l}px`)}})},hideTooltip(t){var e;const n=this,{api:a,config:i,$el:{tooltip:o}}=n;if(o&&o.style("display")!=="none"&&(!i.tooltip_doNotHide||t)){const s=JSON.parse((e=o.datum().current)!=null?e:{});_e(i.tooltip_onhide,a,s),o.style("display","none").style("visibility","hidden").datum(null),_e(i.tooltip_onhidden,a,s)}},_handleLinkedCharts(t,e){const n=this,{charts:a,config:i,state:{event:o}}=n;if(o!=null&&o.isTrusted&&i.tooltip_linked&&a.length>1){const s=i.tooltip_linked_name;a.filter(l=>l!==n.api).forEach(l=>{const{config:c,$el:f}=l.internal,g=c.tooltip_linked,v=c.tooltip_linked_name,m=gn.body.contains(f.chart.node());if(g&&s===v&&m){const S=f.tooltip.data()[0],P=e!==(S==null?void 0:S.index);try{l.tooltip[t&&P?"show":"hide"]({index:e})}catch(N){}}})}},updateTooltipOnRedraw(t,e){var n;const a=this,{config:i,$el:{eventRect:o,svg:s,tooltip:l},state:{event:c,hasAxis:f,hasRadar:g,hasTreemap:v}}=a;if((l==null?void 0:l.style("display"))==="block"&&c){const m=t!=null?t:(n=g?s:o)==null?void 0:n.node();if(f||g)if(a.isMultipleX())a.selectRectForMultipleXs(m,!1);else{const S=e!=null?e:a.getDataIndexFromEvent(c);e===-1?a.api.tooltip.hide():(a.selectRectForSingle(m,S),a.setExpand(S,null,!0))}else{const{clientX:S,clientY:P}=c;setTimeout(()=>{let N=[S,P].every(Number.isFinite)&&gn.elementFromPoint(S,P);const L=N&&ot(N).datum();if(L){const w=a.hasArcType()?a.convertToArcData(a.updateAngle(L)):L==null?void 0:L.data;v&&(N=s.node()),w&&a.showTooltip([w],N)}else a.api.tooltip.hide()},i.transition_duration)}}}},Yp={getTranslate(t,e=0){var n;const a=this,{config:i,state:o}=a,sLine truncated
|
||||
`)));const g=o.serializeToString(f),v=`<svg xmlns="${ae.svg}" width="${a}" height="${i}"
|
||||
viewBox="0 0 ${n.width} ${n.height}"
|
||||
preserveAspectRatio="${(e==null?void 0:e.preserveAspectRatio)===!1?"none":"xMinYMid meet"}">
|
||||
<foreignObject width="100%" height="100%">
|
||||
${g}
|
||||
${c.replace(/(url\()[^#]+/g,"$1")}
|
||||
</foreignObject></svg>`;return`data:image/svg+xml;base64,${Tm(v)}`}function Sm(t,e){const{top:n,left:a}=e,{x:i,y:o}=t.getBBox(),{a:s,b:l,c,d:f,e:g,f:v}=t.getScreenCTM(),{width:m,height:S}=t.getBoundingClientRect();return{x:s*i+c*o+g-a,y:l*i+f*o+v-n+(S-Math.round(S/4)),width:m,height:S}}function Am(t){const{left:e,top:n}=t.getBoundingClientRect(),a=o=>o.textContent||o.childElementCount,i=[];return Lr(t.querySelectorAll("text")).filter(a).forEach(o=>{const s=l=>{const{fill:c,fontFamily:f,fontSize:g,textAnchor:v,transform:m}=Ke.getComputedStyle(l),{x:S,y:P,width:N,height:L}=Sm(l,{left:e,top:n});return{[l.textContent]:{x:S,y:P,width:N,height:L,fill:c,fontFamily:f,fontSize:g,textAnchor:v,transform:m}}};if(o.childElementCount>1){const l=[];return Lr(o.querySelectorAll("tspan")).filter(a).forEach(c=>{i.push(s(c))}),l}else i.push(s(o))}),i}function Em(t,e){e.forEach(n=>{Object.keys(n).forEach(a=>{const{x:i,y:o,width:s,height:l,fill:c,fontFamily:f,fontSize:g,transform:v}=n[a];if(t.save(),t.font=`${g} ${f}`,t.fillStyle=c,v==="none")t.fillText(a,i,o);else{const m=v.replace(/(matrix|\(|\))/g,"").split(",");m.splice(4).every(S=>+S==0)?(m.push(i+s-s/4),m.push(o-l+l/3)):(m.push(i),m.push(o)),t.transform(...m),t.fillText(a,0,0)}t.restore()})})}var bm={export(t,e){const n=this.internal,{state:a,$el:{chart:i,svg:o}}=n,{width:s,height:l}=a.current,c=ea(Object.create(null),{width:s,height:l,preserveAspectRatio:!0,preserveFontStyle:!1,mimeType:"image/png"},t),f=$m(i.node(),c,{width:s,height:l}),g=c.preserveFontStyle?Am(o.node()):[];if(e&&ve(e)){const v=new Image;v.crossOrigin="Anonymous",v.onload=()=>{const m=gn.createElement("canvas"),S=m.getContext("2d");m.width=c.width||s,m.height=c.height||l,S.drawImage(v,0,0),g.length&&(Em(S,g),g.length=0),e.bind(this)(m.toDataURL(c.mimeType))},v.src=f}return f}},Rm={focus(t){const e=this.internal,{state:n}=e,a=e.mapToTargetIds(t),i=e.$el.svg.selectAll(e.selectorTargets(a.filter(e.isTargetToShow,e)));this.revert(),this.defocus(),i.classed(qe.focused,!0).classed(qe.defocused,!1),e.hasArcType()&&!n.hasRadar&&(e.expandArc(a),e.hasType("gauge")&&e.markOverlapped(t,e,`.${Un.gaugeValue}`)),e.toggleFocusLegend(a,!0),n.focusedTargetIds=a,n.defocusedTargetIds=n.defocusedTargetIds.filter(o=>a.indexOf(o)<0)},defocus(t){const e=this.internal,{state:n}=e,a=e.mapToTargetIds(t);e.$el.svg.selectAll(e.selectorTargets(a.filter(e.isTargetToShow,e))).classed(qe.focused,!1).classed(qe.defocused,!0),e.hasArcType(null,["polar"])&&(e.unexpandArc(a),e.hasType("gauge")&&e.undoMarkOverlapped(e,`.${Un.gaugeValue}`)),e.toggleFocusLegend(a,!1),n.focusedTargetIds=n.focusedTargetIds.filter(o=>a.indexOf(o)<0),n.defocusedTargetIds=a},revert(t){const e=this.internal,{config:n,state:a,$el:i}=e,o=e.mapToTargetIds(t);i.svg.selectAll(e.selectorTargets(o)).classed(qe.focused,!1).classed(qe.defocused,!1),e.hasArcType(null,["polar"])&&e.unexpandArc(o),n.legend_show&&(e.showLegend(o.filter(e.isLegendToShow.bind(e))),i.legend.selectAll(e.selectorLegends(o)).filter(function(){return ot(this).classed(qe.legendItemFocused)}).classed(qe.legendItemFocused,!1)),a.focusedTargetIds=[],a.defocusedTargetIds=[]}},Im={legend:{show:function(t){const e=this.internal;e.showLegend(e.mapToTargetIds(t)),e.updateAndRedraw({withLegend:!0})},hide:function(t){const e=this.internal;e.hideLegend(e.mapToTargetIds(t)),e.updateAndRedraw({withLegend:!0})}}},Om={load(t){const e=this.internal,{config:n}=e;t.xs&&e.addXs(t.xs),"names"in t&&this.data.names(t.names),"classes"in t&&Object.keys(t.classes).forEach(a=>{n.data_classes[a]=t.classes[a]}),"categories"in t&&e.axis.isCategorized()&&(n.axis_x_categories=t.categories),"axes"in t&&Object.keys(t.axes).forEach(a=>{n.data_axes[a]=t.axes[a]}),"colors"in t&&Object.keys(t.colors).forEach(a=>{n.data_colors[a]=t.colors[a]}),"unload"in t&&t.unload!==!1?e.unload(e.mapToTargetIds(t.unload===!0?null:t.unload),()=>{jl(()=>e.loadFromArgs(t))}):e.loadFromArgs(t)},unload(t){const e=this.internal;let n=t||{};qn(n)&&this.tooltip.hide(),je(n)?n={ids:n}:ze(n)&&(n={ids:[n]});const a=e.mapToTargetIds(n.ids);e.unload(a,()=>{e.redraw({withUpdateOrgXDomain:!0,withUpdateXDomain:!0,withLegend:!0}),e.cache.remove(a),cc.call(e,n.done,n.resizeAfter)})}};function iu(t,e,n){const a=this.internal,i=a.mapToTargetIds(e),o=a.state.hiddenTargetIds.map(c=>i.indexOf(c)>-1&&c).filter(Boolean);a.state.toggling=!0,a[`${t?"remove":"add"}HiddenTargetIds`](i);const s=a.$el.svg.selectAll(a.selectorTargets(i)),l=t?null:"0";t&&o.length&&(s.style("display",null),_e(a.config.data_onshown,this,o)),a.$T(s).style("opacity",l,"important").call(Si,()=>{var c;!t&&o.length===0&&(s.style("display","none"),_e((c=a.config)==null?void 0:c.data_onhidden,this,i)),s.style("opacity",l)}),n.withLegend&&a[`${t?"show":"hide"}Legend`](i),a.redraw({withUpdateOrgXDomain:!0,withUpdateXDomain:!0,withLegend:!0}),a.state.toggling=!1}var Cm={show(t,e={}){iu.call(this,!0,t,e)},hide(t,e={}){iu.call(this,!1,t,e)},toggle(t,e={}){const n=this.internal,a={show:[],hide:[]};n.mapToTargetIdsLine truncated
|
||||
`)>-1?l.split(`
|
||||
`):[];if(c.length)return c;if(je(l))return l;let f=s.tickWidth;(!f||f<=0)&&(f=i?95:s.isCategory?(s.isInverted?n(a[0])-n(a[1]):n(a[1])-n(a[0]))-12:110);function g(v,m){let S,P,N;for(let L=1;L<m.length;L++)if(m.charAt(L)===" "&&(P=L),S=m.substr(0,L+1),N=o*S.length,f<N)return g(v.concat(m.substr(0,P||L)),m.slice(P?P+1:L));return v.concat(m)}return g(c,String(l))}scale(e){return arguments.length?(this.helper.scale=e,this):this.helper.scale}orient(e){return arguments.length?(this.config.orient=e in{top:1,right:1,bottom:1,left:1}?String(e):"bottom",this):this.config.orient}tickFormat(e){const{config:n}=this;return arguments.length?(n.tickFormat=e,this):n.tickFormat}tickCentered(e){const{config:n}=this;return arguments.length?(n.tickCentered=e,this):n.tickCentered}tickOffset(){return this.config.tickOffset}tickInterval(e){var n;const{outerTickSize:a,tickOffset:i,tickValues:o}=this.config;let s;if(this.params.isCategory)s=i*2;else{const l=(n=this.params.owner.scale.zoom)!=null?n:this.helper.scale;s=(this.g.select("path.domain").node().getTotalLength()-a*2)/(e||this.g.selectAll("line").size());const f=o?o.map((g,v,m)=>{const S=v+1;return S<m.length?l(m[S])-l(g):null}).filter(Boolean):[];s=Math.min(...f,s)}return s===1/0?0:s}ticks(...e){const{config:n}=this;return e.length?(n.tickArguments=Lr(e),this):n.tickArguments}tickCulling(e){const{config:n}=this;return arguments.length?(n.tickCulling=e,this):n.tickCulling}tickValues(e){const{config:n}=this;if(ve(e))n.tickValues=()=>e(this.helper.scale.domain());else{if(!arguments.length)return n.tickValues;n.tickValues=e}return this}setTransition(e){return this.config.transition=e,this}}var Iy=Object.defineProperty,Oy=(t,e,n)=>e in t?Iy(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,pr=(t,e,n)=>Oy(t,typeof e!="symbol"?e+"":e,n),Cy={getAxisInstance:function(){return this.axis||new Py(this)}};class Py{constructor(e){pr(this,"owner"),pr(this,"x"),pr(this,"subX"),pr(this,"y"),pr(this,"y2"),pr(this,"axesList",{}),pr(this,"tick",{x:null,y:null,y2:null}),pr(this,"xs",[]),pr(this,"orient",{x:"bottom",y:"left",y2:"right",subX:"bottom"}),this.owner=e,this.setOrient()}getAxisClassName(e){return`${Tn.axis} ${Tn[`axis${Cn(e)}`]}`}isHorizontal(e,n){const a=e.config.axis_rotated;return n?a:!a}isCategorized(){const{config:e,state:n}=this.owner;return e.axis_x_type.indexOf("category")>=0||n.hasRadar}isCustomX(){const{config:e}=this.owner;return!this.isTimeSeries()&&(e.data_x||cn(e.data_xs))}isTimeSeries(e="x"){return this.owner.config[`axis_${e}_type`]==="timeseries"}isLog(e="x"){return this.owner.config[`axis_${e}_type`]==="log"}isTimeSeriesY(){return this.isTimeSeries("y")}getAxisType(e="x"){let n="linear";return this.isTimeSeries(e)?n=this.owner.config.axis_x_localtime?"time":"utc":this.isLog(e)&&(n="log"),n}getExtent(){const e=this.owner,{config:n,scale:a}=e;let i=n.axis_x_extent;if(i){if(ve(i))i=i.bind(e.api)(e.getXDomain(e.data.targets),a.subX);else if(this.isTimeSeries()&&i.every(isNaN)){const o=Yn.bind(e);i=i.map(s=>a.subX(o(s)))}}return i}init(){const e=this.owner,{config:n,$el:{main:a,axis:i},state:{clip:o}}=e,s=["x","y"];n.axis_y2_show&&s.push("y2"),s.forEach(l=>{const c=this.getAxisClassName(l);i[l]=a.append("g").attr("class",c).attr("clip-path",()=>{let f=null;return l==="x"?f=o.pathXAxis:l==="y"&&(f=o.pathYAxis),f}).attr("transform",e.getTranslate(l)).style("visibility",n[`axis_${l}_show`]?null:"hidden"),this.generateAxes(l)})}setOrient(){const e=this.owner,{axis_rotated:n,axis_y_inner:a,axis_y2_inner:i}=e.config;this.orient={x:n?"left":"bottom",y:n?a?"top":"bottom":a?"right":"left",y2:n?i?"bottom":"top":i?"left":"right",subX:n?"left":"bottom"}}generateAxes(e){const n=this.owner,{config:a}=n,i=[],o=a[`axis_${e}_axes`],s=a.axis_rotated;let l;e==="x"?l=s?Iu:Ru:e==="y"?l=s?Ru:Iu:e==="y2"&&(l=s?Ty:$y),o.length&&o.forEach(c=>{const f=c.tick||{},g=n.scale[e].copy();c.domain&&g.domain(c.domain),i.push(l(g).ticks(f.count).tickFormat(ve(f.format)?f.format.bind(n.api):v=>v).tickValues(f.values).tickSizeOuter(f.outer===!1?0:6))}),this.axesList[e]=i}updateAxes(){const e=this.owner,{config:n,$el:{main:a},$T:i}=e;Object.keys(this.axesList).forEach(o=>{const s=n[`axis_${o}_axes`],l=e.scale[o].copy(),c=l.range();this.axesList[o].forEach((f,g)=>{const v=f.scale().range();c.every((P,N)=>P===v[N])||f.scale().range(c);const m=`${this.getAxisClassName(o)}-${g+1}`;let S=a.select(`.${m.replace(/\s/,".")}`);S.empty()?S=a.append("g").attr("class",m).style("visibility",n[`axis_${o}_show`]?null:"hidden").call(f):(s[g].domain&&l.domain(s[g].domain),i(S).call(f.scale(l))),S.attr("transform",e.getTranslate(o,g+1))})})}setAxis(e,n,a,i){const o=this.owner;e!=="subX"&&(this.tick[e]=this.getTickValues(e)),this[e]=this.getAxis(e,n,a,e==="x"&&(o.scale.zoom||o.config.subchart_show||o.state.resizing)?!0:i)}getAxis(e,n,a,i,o){const s=this.owner,{config:l}=s,c=/^(x|subX)$/.test(e),f=c?"x":e,g=c&&this.isCategorized(),v=this.orient[e],m=o?0:s.getAxisTickRotate(f);let S;if(c)S=e==="subX"?s.format.subXAxi
Line truncated
|
||||
${l}%`}}},Xr={point_show:!0,point_r:2.5,point_radialGradient:!1,point_sensitivity:10,point_focus_expand_enabled:!0,point_focus_expand_r:void 0,point_focus_only:!1,point_opacity:void 0,point_pattern:[],point_select_r:void 0,point_type:"circle"},fa={area_above:!1,area_below:!1,area_front:!0,area_linearGradient:!1,area_zerobased:!0},v0={bar_front:!1,bar_indices_removeNull:!1,bar_label_threshold:0,bar_linearGradient:!1,bar_overlap:!1,bar_padding:0,bar_radius:void 0,bar_radius_ratio:void 0,bar_sensitivity:2,bar_width:void 0,bar_width_ratio:.6,bar_width_max:void 0,bar_zerobased:!0},p0={bubble_maxR:35,bubble_zerobased:!1},m0={candlestick_width:void 0,candlestick_width_ratio:.6,candlestick_width_max:void 0,candlestick_color_down:"red"},y0={line_connectNull:!1,line_step_type:"step",line_step_tooltipMatch:!1,line_zerobased:!1,line_classes:void 0,line_point:!0},x0={scatter_zerobased:!1},Is={spline_interpolation_type:"cardinal"},_i={arc_cornerRadius:0,arc_cornerRadius_ratio:0,arc_needle_show:!1,arc_needle_color:void 0,arc_needle_value:void 0,arc_needle_path:void 0,arc_needle_length:100,arc_needle_top_rx:0,arc_needle_top_ry:0,arc_needle_top_width:0,arc_needle_bottom_rx:1,arc_needle_bottom_ry:1,arc_needle_bottom_width:15,arc_needle_bottom_len:0,arc_rangeText_values:void 0,arc_rangeText_unit:"absolute",arc_rangeText_fixed:!1,arc_rangeText_format:void 0,arc_rangeText_position:void 0},T0={donut_label_show:!0,donut_label_format:void 0,donut_label_threshold:.05,donut_label_ratio:void 0,donut_width:void 0,donut_title:"",donut_expand:{},donut_expand_rate:.98,donut_expand_duration:50,donut_padAngle:0,donut_startingAngle:0},$0={funnel_neck_width:0,funnel_neck_height:0},S0={gauge_background:"",gauge_fullCircle:!1,gauge_label_show:!0,gauge_label_extents:void 0,gauge_label_format:void 0,gauge_label_ratio:void 0,gauge_label_threshold:0,gauge_enforceMinMax:!1,gauge_min:0,gauge_max:100,gauge_type:"single",gauge_startingAngle:-1*Math.PI/2,gauge_arcLength:100,gauge_title:"",gauge_units:void 0,gauge_width:void 0,gauge_arcs_minWidth:5,gauge_expand:{},gauge_expand_rate:.98,gauge_expand_duration:50},A0={pie_label_show:!0,pie_label_format:void 0,pie_label_ratio:void 0,pie_label_threshold:.05,pie_expand:{},pie_expand_rate:.98,pie_expand_duration:50,pie_innerRadius:0,pie_outerRadius:void 0,pie_padAngle:0,pie_padding:0,pie_startingAngle:0},E0={polar_label_show:!0,polar_label_format:void 0,polar_label_threshold:.05,polar_label_ratio:void 0,polar_level_depth:3,polar_level_max:void 0,polar_level_show:!0,polar_level_text_backgroundColor:"#fff",polar_level_text_format:t=>t%1===0?t:t.toFixed(2),polar_level_text_show:!0,polar_padAngle:0,polar_padding:0,polar_startingAngle:0},b0={radar_axis_max:void 0,radar_axis_line_show:!0,radar_axis_text_show:!0,radar_axis_text_position:{},radar_level_depth:3,radar_level_show:!0,radar_level_text_format:t=>t%1===0?t:t.toFixed(2),radar_level_text_show:!0,radar_size_ratio:.87,radar_direction_clockwise:!1},R0={treemap_tile:"binary",treemap_label_format:void 0,treemap_label_threshold:.05,treemap_label_show:!0};function da(t,e){yn(Vr.prototype,Object.values(Du).concat(t)),yn(Er.prototype,Jy),Nr.setOptions(Object.values(Lu).concat(e||[]))}function mr(t,e){da([ca,Ji,Nx].concat(t||[])),Nr.setOptions([Xr,y0].concat(e||[]))}function ha(t,e){yn(Vr.prototype,[px,ca].concat(t||[])),Nr.setOptions([Xr].concat(e||[]))}let rf=()=>(mr(sa,[fa]),(rf=()=>oe.AREA)()),af=()=>(mr(sa,[fa]),(af=()=>oe.AREA_LINE_RANGE)()),of=()=>(mr(sa,[fa]),(of=()=>oe.AREA_STEP_RANGE)()),sf=()=>(mr(sa,[fa,Is]),(sf=()=>oe.AREA_SPLINE)()),lf=()=>(mr(sa,[fa,Is]),(lf=()=>oe.AREA_SPLINE_RANGE)()),cf=()=>(mr(sa,[fa]),(cf=()=>oe.AREA_STEP)()),uf=()=>(mr(),(uf=()=>oe.LINE)()),ff=()=>(mr(void 0,[Is]),(ff=()=>oe.SPLINE)()),df=()=>(mr(),(df=()=>oe.STEP)()),hf=()=>(ha(void 0,[_i,T0]),(hf=()=>oe.DONUT)()),gf=()=>(ha([Mx],[_i,S0]),(gf=()=>oe.GAUGE)()),vf=()=>(ha(void 0,[_i,A0]),(vf=()=>oe.PIE)()),pf=()=>(ha([Ux],[_i,E0]),(pf=()=>oe.POLAR)()),mf=()=>(ha([Du.eventrect,Ji,jx],[Xr,b0,{axis_x_categories:Lu.optAxis.axis_x_categories}]),(mf=()=>oe.RADAR)()),yf=()=>(da([yx,ca],[v0,Xr]),(yf=()=>oe.BAR)()),xf=()=>(da([ca,Ji,xx],[p0,Xr]),(xf=()=>oe.BUBBLE)()),Tf=()=>(da([Ex,ca],[m0,Xr]),(Tf=()=>oe.CANDLESTICK)()),$f=()=>(da([ca,Ji],[Xr,x0]),($f=()=>oe.SCATTER)()),Sf=()=>(ha([wx],[$0]),(Sf=()=>oe.FUNNEL)()),Af=()=>(da([g0],[R0]),(Af=()=>oe.TREEMAP)()),Os=Object.create(null);const Ef={version:"3.15.1",generate(t){const e=ea(Object.create(null),Os,t),n=new Er(e);return n.internal.charts=this.instance,this.instance.push(n),n},defaults(t){return Be(t)&&(Os=t),Os},instance:[],plugin:{}};Object.keys(d).forEach(t=>d[t]()),Object.keys(u).forEach(t=>u[t]())}],Xa={};function zn(x){var b=Xa[x];if(b!==void 0)return b.exports;var r=Xa[x]={exports:{}};return Cs[x].call(r.exports,r,r.exports,zn),r.exports}(function(){zn.d=function(x,b){for(var r in b)zn.o(b,r)&&!zn.o(x,r)&&Object.defineProperty(x,r,{enumerable:!0,get:b[r]})}})(),function(){zn.o=function(x,b){return Object.prototype.hasOwnProperty.ca
Line truncated
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* Bootstrap v5.3.6 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,iLine truncated
|
||||
//# sourceMappingURL=bootstrap.bundle.min.js.map
|
||||
+53
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */
|
||||
!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}function fe(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}ce.fn=ce.prototype={jquery:t,constructor:ce,length:0,toArray:function(){return ae.call(this)},get:function(e){return null==e?ae.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=ce.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return ce.each(this,e)},map:function(n){return this.pushStack(ce.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(ae.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(ce.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(ce.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:s,sort:oe.sort,splice:oe.splice},ce.extend=ce.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||v(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(ce.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||ce.isPlainObject(n)?n:{},i=!1,a[t]=ce.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},ce.extend({expando:"jQuery"+(t+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==i.call(e))&&(!(t=r(e))||"function"==typeof(n=ue.call(t,"constructor")&&t.constructor)&&o.call(n)===a)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){m(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(c(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},text:function(e){var t,n="",r=0,i=e.nodeType;if(!i)while(t=e[r++])n+=ce.text(t);return 1===i||11===i?e.textContent:9===i?e.documentElement.textContent:3===i||4===i?e.nodeValue:n},makeArray:function(e,t){var n=t||[];return null!=e&&(c(Object(e))?ce.merge(n,"string"==typeof e?[e]:e):s.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:se.call(t,e,n)},isXMLDoc:function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!l.test(t||n&&n.nodeName||"HTML")},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(c(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:le}),"function"==typeof Symbol&&(ce.fn[Symbol.iterator]=oe[Symbol.iterator]),ce.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var pe=oe.pop,de=oe.sort,he=oe.splice,ge="[\\x20\\t\\r\\n\\f]",ve=new RegExp("^"+ge+"+|((?:^|[^\\\\])(?:\\\\.)*)"+ge+"+$","g");ce.contains=function(e,t){var n=t&&t.parentNode;return e===n||!(!n||1!==n.nodeType||!(e.contains?e.contains(n):e.compareDocumentPosition&&16&e.compareDocumentPosition(n)))};var f=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g;function p(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e}ce.escapeSelector=function(e){return(e+"").replace(f,p)};var ye=C,me=s;!function(){var e,b,w,o,a,T,r,C,d,i,k=me,S=ce.expando,E=0,n=0,s=W(),c=W(),u=W(),h=W(),l=function(e,t){return e===t&&(a=!0),0},f="checked|selected|async|autofocus|autoplay|conLine truncated
|
||||
+1
@@ -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>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<table id="code" class="table table-borderless table-condensed">
|
||||
<tbody>
|
||||
{{lines}}
|
||||
</tbody>
|
||||
</table>
|
||||
+12
@@ -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>
|
||||
|
||||
Vendored
+18
@@ -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>
|
||||
|
||||
+9
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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\StaticAnalysis;
|
||||
|
||||
use function file_get_contents;
|
||||
use SebastianBergmann\CodeCoverage\Filter;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final readonly class CacheWarmer
|
||||
{
|
||||
/**
|
||||
* @return array{cacheHits: non-negative-int, cacheMisses: non-negative-int}
|
||||
*/
|
||||
public function warmCache(string $cacheDirectory, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode, Filter $filter): array
|
||||
{
|
||||
$analyser = new CachingSourceAnalyser(
|
||||
$cacheDirectory,
|
||||
new ParsingSourceAnalyser,
|
||||
);
|
||||
|
||||
foreach ($filter->files() as $file) {
|
||||
$analyser->analyse(
|
||||
$file,
|
||||
file_get_contents($file),
|
||||
$useAnnotationsForIgnoringCode,
|
||||
$ignoreDeprecatedCode,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'cacheHits' => $analyser->cacheHits(),
|
||||
'cacheMisses' => $analyser->cacheMisses(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+154
@@ -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\StaticAnalysis;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use function file_get_contents;
|
||||
use function file_put_contents;
|
||||
use function hash;
|
||||
use function implode;
|
||||
use function is_file;
|
||||
use function serialize;
|
||||
use function unserialize;
|
||||
use SebastianBergmann\CodeCoverage\Util\Filesystem;
|
||||
use SebastianBergmann\CodeCoverage\Version;
|
||||
|
||||
/**
|
||||
* @internal This interface is not covered by the backward compatibility promise for phpunit/php-code-coverage
|
||||
*/
|
||||
final class CachingSourceAnalyser implements SourceAnalyser
|
||||
{
|
||||
/**
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private readonly string $directory;
|
||||
private readonly SourceAnalyser $sourceAnalyser;
|
||||
|
||||
/**
|
||||
* @var non-negative-int
|
||||
*/
|
||||
private int $cacheHits = 0;
|
||||
|
||||
/**
|
||||
* @var non-negative-int
|
||||
*/
|
||||
private int $cacheMisses = 0;
|
||||
|
||||
public function __construct(string $directory, SourceAnalyser $sourceAnalyser)
|
||||
{
|
||||
Filesystem::createDirectory($directory);
|
||||
|
||||
$this->directory = $directory;
|
||||
$this->sourceAnalyser = $sourceAnalyser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $sourceCodeFile
|
||||
*/
|
||||
public function analyse(string $sourceCodeFile, string $sourceCode, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode): AnalysisResult
|
||||
{
|
||||
$cacheFile = $this->cacheFile(
|
||||
$sourceCode,
|
||||
$useAnnotationsForIgnoringCode,
|
||||
$ignoreDeprecatedCode,
|
||||
);
|
||||
|
||||
$cachedAnalysisResult = $this->read($cacheFile);
|
||||
|
||||
if ($cachedAnalysisResult !== false) {
|
||||
$this->cacheHits++;
|
||||
|
||||
return $cachedAnalysisResult;
|
||||
}
|
||||
|
||||
$this->cacheMisses++;
|
||||
|
||||
$analysisResult = $this->sourceAnalyser->analyse(
|
||||
$sourceCodeFile,
|
||||
$sourceCode,
|
||||
$useAnnotationsForIgnoringCode,
|
||||
$ignoreDeprecatedCode,
|
||||
);
|
||||
|
||||
$this->write($cacheFile, $analysisResult);
|
||||
|
||||
return $analysisResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-negative-int
|
||||
*/
|
||||
public function cacheHits(): int
|
||||
{
|
||||
return $this->cacheHits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-negative-int
|
||||
*/
|
||||
public function cacheMisses(): int
|
||||
{
|
||||
return $this->cacheMisses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $cacheFile
|
||||
*/
|
||||
private function read(string $cacheFile): AnalysisResult|false
|
||||
{
|
||||
if (!is_file($cacheFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return unserialize(
|
||||
file_get_contents($cacheFile),
|
||||
[
|
||||
'allowed_classes' => [
|
||||
AnalysisResult::class,
|
||||
Class_::class,
|
||||
Function_::class,
|
||||
Interface_::class,
|
||||
LinesOfCode::class,
|
||||
Method::class,
|
||||
Trait_::class,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $cacheFile
|
||||
*/
|
||||
private function write(string $cacheFile, AnalysisResult $result): void
|
||||
{
|
||||
file_put_contents(
|
||||
$cacheFile,
|
||||
serialize($result),
|
||||
);
|
||||
}
|
||||
|
||||
private function cacheFile(string $source, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode): string
|
||||
{
|
||||
$cacheKey = hash(
|
||||
'sha256',
|
||||
implode(
|
||||
"\0",
|
||||
[
|
||||
$source,
|
||||
Version::id(),
|
||||
$useAnnotationsForIgnoringCode,
|
||||
$ignoreDeprecatedCode,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return $this->directory . DIRECTORY_SEPARATOR . $cacheKey;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 8955 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user