dep: phpunit 추가

This commit is contained in:
Hide_D committed 2022-03-02 23:29:09 +09:00
1 parent f8ab617e4c
commit 923d7f5178
1222 files changed
+121095 -8

No files matched your search

+676
View File
@@ -0,0 +1,676 @@
<?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 array_values;
use function count;
use function explode;
use function get_class;
use function is_array;
use function is_file;
use function sort;
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\PhptTestCase;
use PHPUnit\Util\Test;
use ReflectionClass;
use SebastianBergmann\CodeCoverage\Driver\Driver;
use SebastianBergmann\CodeCoverage\Node\Builder;
use SebastianBergmann\CodeCoverage\Node\Directory;
use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingFileAnalyser;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;
use SebastianBergmann\CodeUnitReverseLookup\Wizard;
/**
* Provides collection functionality for PHP code coverage information.
*/
final class CodeCoverage
{
private const UNCOVERED_FILES = 'UNCOVERED_FILES';
/**
* @var Driver
*/
private $driver;
/**
* @var Filter
*/
private $filter;
/**
* @var Wizard
*/
private $wizard;
/**
* @var bool
*/
private $checkForUnintentionallyCoveredCode = false;
/**
* @var bool
*/
private $includeUncoveredFiles = true;
/**
* @var bool
*/
private $processUncoveredFiles = false;
/**
* @var bool
*/
private $ignoreDeprecatedCode = false;
/**
* @var PhptTestCase|string|TestCase
*/
private $currentId;
/**
* Code coverage data.
*
* @var ProcessedCodeCoverageData
*/
private $data;
/**
* @var bool
*/
private $useAnnotationsForIgnoringCode = true;
/**
* Test data.
*
* @var array
*/
private $tests = [];
/**
* @psalm-var list<class-string>
*/
private $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
/**
* @var ?FileAnalyser
*/
private $analyser;
/**
* @var ?string
*/
private $cacheDirectory;
public function __construct(Driver $driver, Filter $filter)
{
$this->driver = $driver;
$this->filter = $filter;
$this->data = new ProcessedCodeCoverageData;
$this->wizard = new Wizard;
}
/**
* Returns the code coverage information as a graph of node objects.
*/
public function getReport(): Directory
{
return (new Builder($this->analyser()))->build($this);
}
/**
* Clears collected code coverage data.
*/
public function clear(): void
{
$this->currentId = null;
$this->data = new ProcessedCodeCoverageData;
$this->tests = [];
}
/**
* 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->processUncoveredFiles) {
$this->processUncoveredFilesFromFilter();
} elseif ($this->includeUncoveredFiles) {
$this->addUncoveredFilesFromFilter();
}
}
return $this->data;
}
/**
* Sets the coverage data.
*/
public function setData(ProcessedCodeCoverageData $data): void
{
$this->data = $data;
}
/**
* Returns the test data.
*/
public function getTests(): array
{
return $this->tests;
}
/**
* Sets the test data.
*/
public function setTests(array $tests): void
{
$this->tests = $tests;
}
/**
* Start collection of code coverage information.
*
* @param PhptTestCase|string|TestCase $id
*/
public function start($id, bool $clear = false): void
{
if ($clear) {
$this->clear();
}
$this->currentId = $id;
$this->driver->start();
}
/**
* Stop collection of code coverage information.
*
* @param array|false $linesToBeCovered
*/
public function stop(bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): RawCodeCoverageData
{
if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {
throw new InvalidArgumentException(
'$linesToBeCovered must be an array or false'
);
}
$data = $this->driver->stop();
$this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);
$this->currentId = null;
return $data;
}
/**
* Appends code coverage data.
*
* @param PhptTestCase|string|TestCase $id
* @param array|false $linesToBeCovered
*
* @throws ReflectionException
* @throws TestIdMissingException
* @throws UnintentionallyCoveredCodeException
*/
public function append(RawCodeCoverageData $rawData, $id = null, bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): void
{
if ($id === null) {
$id = $this->currentId;
}
if ($id === null) {
throw new TestIdMissingException;
}
$this->applyFilter($rawData);
$this->applyExecutableLinesFilter($rawData);
if ($this->useAnnotationsForIgnoringCode) {
$this->applyIgnoredLinesFilter($rawData);
}
$this->data->initializeUnseenData($rawData);
if (!$append) {
return;
}
if ($id !== self::UNCOVERED_FILES) {
$this->applyCoversAnnotationFilter(
$rawData,
$linesToBeCovered,
$linesToBeUsed
);
if (empty($rawData->lineCoverage())) {
return;
}
$size = 'unknown';
$status = -1;
$fromTestcase = false;
if ($id instanceof TestCase) {
$fromTestcase = true;
$_size = $id->getSize();
if ($_size === Test::SMALL) {
$size = 'small';
} elseif ($_size === Test::MEDIUM) {
$size = 'medium';
} elseif ($_size === Test::LARGE) {
$size = 'large';
}
$status = $id->getStatus();
$id = get_class($id) . '::' . $id->getName();
} elseif ($id instanceof PhptTestCase) {
$fromTestcase = true;
$size = 'large';
$id = $id->getName();
}
$this->tests[$id] = ['size' => $size, 'status' => $status, 'fromTestcase' => $fromTestcase];
$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());
}
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 processUncoveredFiles(): void
{
$this->processUncoveredFiles = true;
}
public function doNotProcessUncoveredFiles(): void
{
$this->processUncoveredFiles = 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;
}
/**
* @psalm-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;
}
/**
* @psalm-param class-string $className
*/
public function excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck(string $className): void
{
$this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck[] = $className;
}
public function enableBranchAndPathCoverage(): void
{
$this->driver->enableBranchAndPathCoverage();
}
public function disableBranchAndPathCoverage(): void
{
$this->driver->disableBranchAndPathCoverage();
}
public function collectsBranchAndPathCoverage(): bool
{
return $this->driver->collectsBranchAndPathCoverage();
}
public function detectsDeadCode(): bool
{
return $this->driver->detectsDeadCode();
}
/**
* Applies the @covers annotation filtering.
*
* @param array|false $linesToBeCovered
*
* @throws ReflectionException
* @throws UnintentionallyCoveredCodeException
*/
private function applyCoversAnnotationFilter(RawCodeCoverageData $rawData, $linesToBeCovered, array $linesToBeUsed): void
{
if ($linesToBeCovered === false) {
$rawData->clear();
return;
}
if (empty($linesToBeCovered)) {
return;
}
if ($this->checkForUnintentionallyCoveredCode &&
(!$this->currentId instanceof TestCase ||
(!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {
$this->performUnintentionallyCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed);
}
$rawLineData = $rawData->lineCoverage();
$filesWithNoCoverage = array_diff_key($rawLineData, $linesToBeCovered);
foreach (array_keys($filesWithNoCoverage) as $fileWithNoCoverage) {
$rawData->removeCoverageDataForFile($fileWithNoCoverage);
}
if (is_array($linesToBeCovered)) {
foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) {
$rawData->keepLineCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
$rawData->keepFunctionCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
}
}
}
private function applyFilter(RawCodeCoverageData $data): void
{
if ($this->filter->isEmpty()) {
return;
}
foreach (array_keys($data->lineCoverage()) as $filename) {
if ($this->filter->isExcluded($filename)) {
$data->removeCoverageDataForFile($filename);
}
}
}
private function applyExecutableLinesFilter(RawCodeCoverageData $data): void
{
foreach (array_keys($data->lineCoverage()) as $filename) {
if (!$this->filter->isFile($filename)) {
continue;
}
$data->keepLineCoverageDataOnlyForLines(
$filename,
$this->analyser()->executableLinesIn($filename)
);
}
}
private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void
{
foreach (array_keys($data->lineCoverage()) as $filename) {
if (!$this->filter->isFile($filename)) {
continue;
}
$data->removeCoverageDataForLines(
$filename,
$this->analyser()->ignoredLinesFor($filename)
);
}
}
/**
* @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
);
}
}
}
/**
* @throws UnintentionallyCoveredCodeException
*/
private function processUncoveredFilesFromFilter(): void
{
$uncoveredFiles = array_diff(
$this->filter->files(),
$this->data->coveredFiles()
);
$this->driver->start();
foreach ($uncoveredFiles as $uncoveredFile) {
if (is_file($uncoveredFile)) {
include_once $uncoveredFile;
}
}
$this->append($this->driver->stop(), self::UNCOVERED_FILES);
}
/**
* @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->wizard->lookup($file, $line);
}
}
}
$unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
if (!empty($unintentionallyCoveredUnits)) {
throw new UnintentionallyCoveredCodeException(
$unintentionallyCoveredUnits
);
}
}
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;
}
/**
* @throws ReflectionException
*/
private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array
{
$unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits);
sort($unintentionallyCoveredUnits);
foreach (array_keys($unintentionallyCoveredUnits) as $k => $v) {
$unit = explode('::', $unintentionallyCoveredUnits[$k]);
if (count($unit) !== 2) {
continue;
}
try {
$class = new ReflectionClass($unit[0]);
foreach ($this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck as $parentClass) {
if ($class->isSubclassOf($parentClass)) {
unset($unintentionallyCoveredUnits[$k]);
break;
}
}
} catch (\ReflectionException $e) {
throw new ReflectionException(
$e->getMessage(),
(int) $e->getCode(),
$e
);
}
}
return array_values($unintentionallyCoveredUnits);
}
private function analyser(): FileAnalyser
{
if ($this->analyser !== null) {
return $this->analyser;
}
$this->analyser = new ParsingFileAnalyser(
$this->useAnnotationsForIgnoringCode,
$this->ignoreDeprecatedCode
);
if ($this->cachesStaticAnalysis()) {
$this->analyser = new CachingFileAnalyser(
$this->cacheDirectory,
$this->analyser
);
}
return $this->analyser;
}
}
+167
View File
@@ -0,0 +1,167 @@
<?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\DeadCodeDetectionNotSupportedException;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverAvailableException;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverWithPathCoverageSupportAvailableException;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
abstract class Driver
{
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const LINE_NOT_EXECUTABLE = -2;
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const LINE_NOT_EXECUTED = -1;
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const LINE_EXECUTED = 1;
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const BRANCH_NOT_HIT = 0;
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const BRANCH_HIT = 1;
/**
* @var bool
*/
private $collectBranchAndPathCoverage = false;
/**
* @var bool
*/
private $detectDeadCode = false;
/**
* @throws NoCodeCoverageDriverAvailableException
* @throws PcovNotAvailableException
* @throws PhpdbgNotAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
*
* @deprecated Use DriverSelector::forLineCoverage() instead
*/
public static function forLineCoverage(Filter $filter): self
{
return (new Selector)->forLineCoverage($filter);
}
/**
* @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
*
* @deprecated Use DriverSelector::forLineAndPathCoverage() instead
*/
public static function forLineAndPathCoverage(Filter $filter): self
{
return (new Selector)->forLineAndPathCoverage($filter);
}
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 canDetectDeadCode(): bool
{
return false;
}
public function detectsDeadCode(): bool
{
return $this->detectDeadCode;
}
/**
* @throws DeadCodeDetectionNotSupportedException
*/
public function enableDeadCodeDetection(): void
{
if (!$this->canDetectDeadCode()) {
throw new DeadCodeDetectionNotSupportedException(
sprintf(
'%s does not support dead code detection',
$this->nameAndVersion()
)
);
}
$this->detectDeadCode = true;
}
public function disableDeadCodeDetection(): void
{
$this->detectDeadCode = false;
}
abstract public function nameAndVersion(): string;
abstract public function start(): void;
abstract public function stop(): RawCodeCoverageData;
}
@@ -0,0 +1,75 @@
<?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\Filter;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class PcovDriver extends Driver
{
/**
* @var Filter
*/
private $filter;
/**
* @throws PcovNotAvailableException
*/
public function __construct(Filter $filter)
{
if (!extension_loaded('pcov')) {
throw new PcovNotAvailableException;
}
$this->filter = $filter;
}
public function start(): void
{
start();
}
public function stop(): RawCodeCoverageData
{
stop();
$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);
}
public function nameAndVersion(): string
{
return 'PCOV ' . phpversion('pcov');
}
}
@@ -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\Driver;
use const PHP_SAPI;
use const PHP_VERSION;
use function array_diff;
use function array_keys;
use function array_merge;
use function get_included_files;
use function phpdbg_end_oplog;
use function phpdbg_get_executable;
use function phpdbg_start_oplog;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class PhpdbgDriver extends Driver
{
/**
* @throws PhpdbgNotAvailableException
*/
public function __construct()
{
if (PHP_SAPI !== 'phpdbg') {
throw new PhpdbgNotAvailableException;
}
}
public function start(): void
{
phpdbg_start_oplog();
}
public function stop(): RawCodeCoverageData
{
static $fetchedLines = [];
$dbgData = phpdbg_end_oplog();
if ($fetchedLines === []) {
$sourceLines = phpdbg_get_executable();
} else {
$newFiles = array_diff(get_included_files(), array_keys($fetchedLines));
$sourceLines = [];
if ($newFiles) {
$sourceLines = phpdbg_get_executable(['files' => $newFiles]);
}
}
foreach ($sourceLines as $file => $lines) {
foreach ($lines as $lineNo => $numExecuted) {
$sourceLines[$file][$lineNo] = self::LINE_NOT_EXECUTED;
}
}
$fetchedLines = array_merge($fetchedLines, $sourceLines);
return RawCodeCoverageData::fromXdebugWithoutPathCoverage(
$this->detectExecutedLines($fetchedLines, $dbgData)
);
}
public function nameAndVersion(): string
{
return 'PHPDBG ' . PHP_VERSION;
}
private function detectExecutedLines(array $sourceLines, array $dbgData): array
{
foreach ($dbgData as $file => $coveredLines) {
foreach ($coveredLines as $lineNo => $numExecuted) {
// phpdbg also reports $lineNo=0 when e.g. exceptions get thrown.
// make sure we only mark lines executed which are actually executable.
if (isset($sourceLines[$file][$lineNo])) {
$sourceLines[$file][$lineNo] = self::LINE_EXECUTED;
}
}
}
return $sourceLines;
}
}
@@ -0,0 +1,79 @@
<?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 phpversion;
use function version_compare;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverAvailableException;
use SebastianBergmann\CodeCoverage\NoCodeCoverageDriverWithPathCoverageSupportAvailableException;
use SebastianBergmann\Environment\Runtime;
final class Selector
{
/**
* @throws NoCodeCoverageDriverAvailableException
* @throws PcovNotAvailableException
* @throws PhpdbgNotAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
*/
public function forLineCoverage(Filter $filter): Driver
{
$runtime = new Runtime;
if ($runtime->hasPHPDBGCodeCoverage()) {
return new PhpdbgDriver;
}
if ($runtime->hasPCOV()) {
return new PcovDriver($filter);
}
if ($runtime->hasXdebug()) {
if (version_compare(phpversion('xdebug'), '3', '>=')) {
$driver = new Xdebug3Driver($filter);
} else {
$driver = new Xdebug2Driver($filter);
}
$driver->enableDeadCodeDetection();
return $driver;
}
throw new NoCodeCoverageDriverAvailableException;
}
/**
* @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
*/
public function forLineAndPathCoverage(Filter $filter): Driver
{
if ((new Runtime)->hasXdebug()) {
if (version_compare(phpversion('xdebug'), '3', '>=')) {
$driver = new Xdebug3Driver($filter);
} else {
$driver = new Xdebug2Driver($filter);
}
$driver->enableDeadCodeDetection();
$driver->enableBranchAndPathCoverage();
return $driver;
}
throw new NoCodeCoverageDriverWithPathCoverageSupportAvailableException;
}
}
@@ -0,0 +1,128 @@
<?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 const XDEBUG_PATH_WHITELIST;
use function defined;
use function extension_loaded;
use function ini_get;
use function phpversion;
use function sprintf;
use function version_compare;
use function xdebug_get_code_coverage;
use function xdebug_set_filter;
use function xdebug_start_code_coverage;
use function xdebug_stop_code_coverage;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Xdebug2Driver extends Driver
{
/**
* @var bool
*/
private $pathCoverageIsMixedCoverage;
/**
* @throws WrongXdebugVersionException
* @throws Xdebug2NotEnabledException
* @throws XdebugNotAvailableException
*/
public function __construct(Filter $filter)
{
if (!extension_loaded('xdebug')) {
throw new XdebugNotAvailableException;
}
if (version_compare(phpversion('xdebug'), '3', '>=')) {
throw new WrongXdebugVersionException(
sprintf(
'This driver requires Xdebug 2 but version %s is loaded',
phpversion('xdebug')
)
);
}
if (!ini_get('xdebug.coverage_enable')) {
throw new Xdebug2NotEnabledException;
}
if (!$filter->isEmpty()) {
if (defined('XDEBUG_PATH_WHITELIST')) {
$listType = XDEBUG_PATH_WHITELIST;
} else {
$listType = XDEBUG_PATH_INCLUDE;
}
xdebug_set_filter(
XDEBUG_FILTER_CODE_COVERAGE,
$listType,
$filter->files()
);
}
$this->pathCoverageIsMixedCoverage = version_compare(phpversion('xdebug'), '2.9.6', '<');
}
public function canCollectBranchAndPathCoverage(): bool
{
return true;
}
public function canDetectDeadCode(): bool
{
return true;
}
public function start(): void
{
$flags = XDEBUG_CC_UNUSED;
if ($this->detectsDeadCode() || $this->collectsBranchAndPathCoverage()) {
$flags |= 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()) {
if ($this->pathCoverageIsMixedCoverage) {
return RawCodeCoverageData::fromXdebugWithMixedCoverage($data);
}
return RawCodeCoverageData::fromXdebugWithPathCoverage($data);
}
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data);
}
public function nameAndVersion(): string
{
return 'Xdebug ' . phpversion('xdebug');
}
}
@@ -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\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 explode;
use function extension_loaded;
use function getenv;
use function in_array;
use function ini_get;
use function phpversion;
use function sprintf;
use function version_compare;
use function xdebug_get_code_coverage;
use function xdebug_set_filter;
use function xdebug_start_code_coverage;
use function xdebug_stop_code_coverage;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\RawCodeCoverageData;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Xdebug3Driver extends Driver
{
/**
* @throws WrongXdebugVersionException
* @throws Xdebug3NotEnabledException
* @throws XdebugNotAvailableException
*/
public function __construct(Filter $filter)
{
if (!extension_loaded('xdebug')) {
throw new XdebugNotAvailableException;
}
if (version_compare(phpversion('xdebug'), '3', '<')) {
throw new WrongXdebugVersionException(
sprintf(
'This driver requires Xdebug 3 but version %s is loaded',
phpversion('xdebug')
)
);
}
$mode = getenv('XDEBUG_MODE');
if ($mode === false || $mode === '') {
$mode = ini_get('xdebug.mode');
}
if ($mode === false ||
!in_array('coverage', explode(',', $mode), true)) {
throw new Xdebug3NotEnabledException;
}
if (!$filter->isEmpty()) {
xdebug_set_filter(
XDEBUG_FILTER_CODE_COVERAGE,
XDEBUG_PATH_INCLUDE,
$filter->files()
);
}
}
public function canCollectBranchAndPathCoverage(): bool
{
return true;
}
public function canDetectDeadCode(): bool
{
return true;
}
public function start(): void
{
$flags = XDEBUG_CC_UNUSED;
if ($this->detectsDeadCode() || $this->collectsBranchAndPathCoverage()) {
$flags |= 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()) {
return RawCodeCoverageData::fromXdebugWithPathCoverage($data);
}
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($data);
}
public function nameAndVersion(): string
{
return 'Xdebug ' . phpversion('xdebug');
}
}
@@ -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
{
}
@@ -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 DeadCodeDetectionNotSupportedException extends RuntimeException implements Exception
{
}
@@ -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
{
}
@@ -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
{
}
@@ -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');
}
}
@@ -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
{
}
@@ -0,0 +1,22 @@
<?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 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,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 PhpdbgNotAvailableException extends RuntimeException implements Exception
{
public function __construct()
{
parent::__construct('The PHPDBG SAPI 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
{
}
@@ -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');
}
}
@@ -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');
}
}
@@ -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;
use RuntimeException;
final class UnintentionallyCoveredCodeException extends RuntimeException implements Exception
{
/**
* @var array
*/
private $unintentionallyCoveredUnits;
public function __construct(array $unintentionallyCoveredUnits)
{
$this->unintentionallyCoveredUnits = $unintentionallyCoveredUnits;
parent::__construct($this->toString());
}
public function getUnintentionallyCoveredUnits(): array
{
return $this->unintentionallyCoveredUnits;
}
private function toString(): string
{
$message = '';
foreach ($this->unintentionallyCoveredUnits as $unit) {
$message .= '- ' . $unit . "\n";
}
return $message;
}
}
@@ -0,0 +1,22 @@
<?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 WriteOperationFailedException extends RuntimeException implements Exception
{
public function __construct(string $path)
{
parent::__construct(sprintf('Cannot write to "%s"', $path));
}
}
@@ -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\Driver;
use RuntimeException;
use SebastianBergmann\CodeCoverage\Exception;
final class WrongXdebugVersionException extends RuntimeException implements Exception
{
}
@@ -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 Xdebug2NotEnabledException extends RuntimeException implements Exception
{
public function __construct()
{
parent::__construct('xdebug.coverage_enable=On has to be set');
}
}
@@ -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 Xdebug3NotEnabledException extends RuntimeException implements Exception
{
public function __construct()
{
parent::__construct('XDEBUG_MODE=coverage or xdebug.mode=coverage has to be set');
}
}
@@ -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,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
{
}
+122
View File
@@ -0,0 +1,122 @@
<?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 strpos;
use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
final class Filter
{
/**
* @psalm-var array<string,true>
*/
private $files = [];
/**
* @psalm-var array<string,bool>
*/
private $isFileCache = [];
public function includeDirectory(string $directory, string $suffix = '.php', string $prefix = ''): void
{
foreach ((new FileIteratorFacade)->getFilesAsArray($directory, $suffix, $prefix) as $file) {
$this->includeFile($file);
}
}
/**
* @psalm-param list<string> $files
*/
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 excludeDirectory(string $directory, string $suffix = '.php', string $prefix = ''): void
{
foreach ((new FileIteratorFacade)->getFilesAsArray($directory, $suffix, $prefix) as $file) {
$this->excludeFile($file);
}
}
public function excludeFile(string $filename): void
{
$filename = realpath($filename);
if (!$filename || !isset($this->files[$filename])) {
return;
}
unset($this->files[$filename]);
}
public function isFile(string $filename): bool
{
if (isset($this->isFileCache[$filename])) {
return $this->isFileCache[$filename];
}
if ($filename === '-' ||
strpos($filename, 'vfs://') === 0 ||
strpos($filename, 'xdebug://debug-eval') !== false ||
strpos($filename, 'eval()\'d code') !== false ||
strpos($filename, 'runtime-created function') !== false ||
strpos($filename, 'runkit created function') !== false ||
strpos($filename, 'assert code') !== false ||
strpos($filename, 'regexp code') !== false ||
strpos($filename, 'Standard input code') !== false) {
$isFile = false;
} else {
$isFile = is_file($filename);
}
$this->isFileCache[$filename] = $isFile;
return $isFile;
}
public function isExcluded(string $filename): bool
{
if (!$this->isFile($filename)) {
return true;
}
return !isset($this->files[$filename]);
}
/**
* @psalm-return list<string>
*/
public function files(): array
{
return array_keys($this->files);
}
public function isEmpty(): bool
{
return empty($this->files);
}
}
@@ -0,0 +1,253 @@
<?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_replace;
use function substr;
use Countable;
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
{
/**
* @var string
*/
private $name;
/**
* @var string
*/
private $pathAsString;
/**
* @var array
*/
private $pathAsArray;
/**
* @var AbstractNode
*/
private $parent;
/**
* @var string
*/
private $id;
public function __construct(string $name, self $parent = null)
{
if (substr($name, -1) === DIRECTORY_SEPARATOR) {
$name = substr($name, 0, -1);
}
$this->name = $name;
$this->parent = $parent;
}
public function name(): string
{
return $this->name;
}
public function id(): string
{
if ($this->id === null) {
$parent = $this->parent();
if ($parent === null) {
$this->id = 'index';
} else {
$parentId = $parent->id();
if ($parentId === 'index') {
$this->id = str_replace(':', '_', $this->name);
} else {
$this->id = $parentId . '/' . $this->name;
}
}
}
return $this->id;
}
public function pathAsString(): string
{
if ($this->pathAsString === null) {
if ($this->parent === null) {
$this->pathAsString = $this->name;
} else {
$this->pathAsString = $this->parent->pathAsString() . DIRECTORY_SEPARATOR . $this->name;
}
}
return $this->pathAsString;
}
public function pathAsArray(): array
{
if ($this->pathAsArray === null) {
if ($this->parent === null) {
$this->pathAsArray = [];
} else {
$this->pathAsArray = $this->parent->pathAsArray();
}
$this->pathAsArray[] = $this;
}
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();
}
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();
}
abstract public function classes(): array;
abstract public function traits(): array;
abstract public function functions(): array;
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
abstract public function linesOfCode(): array;
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;
}
+264
View File
@@ -0,0 +1,264 @@
<?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 str_replace;
use function strpos;
use function substr;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\ProcessedCodeCoverageData;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Builder
{
/**
* @var FileAnalyser
*/
private $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;
}
private function addItems(Directory $root, array $items, array $tests): void
{
foreach ($items as $key => $value) {
$key = (string) $key;
if (substr($key, -2) === '/f') {
$key = substr($key, 0, -2);
$filename = $root->pathAsString() . DIRECTORY_SEPARATOR . $key;
if (is_file($filename)) {
$root->addFile(
new File(
$key,
$root,
$value['lineCoverage'],
$value['functionCoverage'],
$tests,
$this->analyser->classesIn($filename),
$this->analyser->traitsIn($filename),
$this->analyser->functionsIn($filename),
$this->analyser->linesOfCodeFor($filename)
)
);
}
} 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>
*/
private function buildDirectoryStructure(ProcessedCodeCoverageData $data): array
{
$result = [];
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' => $data->lineCoverage()[$originalPath] ?? [],
'functionCoverage' => $data->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
{
if (empty($coverage->coveredFiles())) {
return '.';
}
$commonPath = '';
$paths = $coverage->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 (strpos($paths[$i], 'phar://') === 0) {
$paths[$i] = substr($paths[$i], 7);
$paths[$i] = str_replace('/', DIRECTORY_SEPARATOR, $paths[$i]);
}
$paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]);
if (empty($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 = $coverage->coveredFiles();
$max = count($original);
for ($i = 0; $i < $max; $i++) {
$coverage->renameFile($original[$i], implode(DIRECTORY_SEPARATOR, $paths[$i]));
}
return substr($commonPath, 0, -1);
}
}
+50
View File
@@ -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\Node;
use function sprintf;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class CrapIndex
{
/**
* @var int
*/
private $cyclomaticComplexity;
/**
* @var float
*/
private $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
);
}
}
+440
View File
@@ -0,0 +1,440 @@
<?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 count;
use IteratorAggregate;
use RecursiveIteratorIterator;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Directory extends AbstractNode implements IteratorAggregate
{
/**
* @var AbstractNode[]
*/
private $children = [];
/**
* @var Directory[]
*/
private $directories = [];
/**
* @var File[]
*/
private $files = [];
/**
* @var array
*/
private $classes;
/**
* @var array
*/
private $traits;
/**
* @var array
*/
private $functions;
/**
* @psalm-var null|array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
private $linesOfCode;
/**
* @var int
*/
private $numFiles = -1;
/**
* @var int
*/
private $numExecutableLines = -1;
/**
* @var int
*/
private $numExecutedLines = -1;
/**
* @var int
*/
private $numExecutableBranches = -1;
/**
* @var int
*/
private $numExecutedBranches = -1;
/**
* @var int
*/
private $numExecutablePaths = -1;
/**
* @var int
*/
private $numExecutedPaths = -1;
/**
* @var int
*/
private $numClasses = -1;
/**
* @var int
*/
private $numTestedClasses = -1;
/**
* @var int
*/
private $numTraits = -1;
/**
* @var int
*/
private $numTestedTraits = -1;
/**
* @var int
*/
private $numMethods = -1;
/**
* @var int
*/
private $numTestedMethods = -1;
/**
* @var int
*/
private $numFunctions = -1;
/**
* @var int
*/
private $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;
}
public function getIterator(): RecursiveIteratorIterator
{
return new RecursiveIteratorIterator(
new Iterator($this),
RecursiveIteratorIterator::SELF_FIRST
);
}
public function addDirectory(string $name): self
{
$directory = new self($name, $this);
$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;
}
public function directories(): array
{
return $this->directories;
}
public function files(): array
{
return $this->files;
}
public function children(): array
{
return $this->children;
}
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;
}
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;
}
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;
}
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
public function linesOfCode(): array
{
if ($this->linesOfCode === null) {
$this->linesOfCode = [
'linesOfCode' => 0,
'commentLinesOfCode' => 0,
'nonCommentLinesOfCode' => 0,
];
foreach ($this->children as $child) {
$childLinesOfCode = $child->linesOfCode();
$this->linesOfCode['linesOfCode'] += $childLinesOfCode['linesOfCode'];
$this->linesOfCode['commentLinesOfCode'] += $childLinesOfCode['commentLinesOfCode'];
$this->linesOfCode['nonCommentLinesOfCode'] += $childLinesOfCode['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;
}
}
+651
View File
@@ -0,0 +1,651 @@
<?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;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class File extends AbstractNode
{
/**
* @var array
*/
private $lineCoverageData;
/**
* @var array
*/
private $functionCoverageData;
/**
* @var array
*/
private $testData;
/**
* @var int
*/
private $numExecutableLines = 0;
/**
* @var int
*/
private $numExecutedLines = 0;
/**
* @var int
*/
private $numExecutableBranches = 0;
/**
* @var int
*/
private $numExecutedBranches = 0;
/**
* @var int
*/
private $numExecutablePaths = 0;
/**
* @var int
*/
private $numExecutedPaths = 0;
/**
* @var array
*/
private $classes = [];
/**
* @var array
*/
private $traits = [];
/**
* @var array
*/
private $functions = [];
/**
* @psalm-var array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
private $linesOfCode;
/**
* @var int
*/
private $numClasses;
/**
* @var int
*/
private $numTestedClasses = 0;
/**
* @var int
*/
private $numTraits;
/**
* @var int
*/
private $numTestedTraits = 0;
/**
* @var int
*/
private $numMethods;
/**
* @var int
*/
private $numTestedMethods;
/**
* @var int
*/
private $numTestedFunctions;
/**
* @var array
*/
private $codeUnitsByLine = [];
/**
* @psalm-param array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int} $linesOfCode
*/
public function __construct(string $name, AbstractNode $parent, array $lineCoverageData, array $functionCoverageData, array $testData, array $classes, array $traits, array $functions, array $linesOfCode)
{
parent::__construct($name, $parent);
$this->lineCoverageData = $lineCoverageData;
$this->functionCoverageData = $functionCoverageData;
$this->testData = $testData;
$this->linesOfCode = $linesOfCode;
$this->calculateStatistics($classes, $traits, $functions);
}
public function count(): int
{
return 1;
}
public function lineCoverageData(): array
{
return $this->lineCoverageData;
}
public function functionCoverageData(): array
{
return $this->functionCoverageData;
}
public function testData(): array
{
return $this->testData;
}
public function classes(): array
{
return $this->classes;
}
public function traits(): array
{
return $this->traits;
}
public function functions(): array
{
return $this->functions;
}
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
public function linesOfCode(): array
{
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;
}
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'] ? ($method['executedLines'] / $method['executableLines']) * 100 : 100;
$methodBranchCoverage = $method['executableBranches'] ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0;
$methodPathCoverage = $method['executablePaths'] ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0;
$method['coverage'] = $methodBranchCoverage ?: $methodLineCoverage;
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage ?: $methodLineCoverage))->asString();
$trait['ccn'] += $method['ccn'];
}
unset($method);
$traitLineCoverage = $trait['executableLines'] ? ($trait['executedLines'] / $trait['executableLines']) * 100 : 100;
$traitBranchCoverage = $trait['executableBranches'] ? ($trait['executedBranches'] / $trait['executableBranches']) * 100 : 0;
$traitPathCoverage = $trait['executablePaths'] ? ($trait['executedPaths'] / $trait['executablePaths']) * 100 : 0;
$trait['coverage'] = $traitBranchCoverage ?: $traitLineCoverage;
$trait['crap'] = (new CrapIndex($trait['ccn'], $traitPathCoverage ?: $traitLineCoverage))->asString();
if ($trait['executableLines'] > 0 && $trait['coverage'] === 100) {
$this->numTestedClasses++;
}
}
unset($trait);
foreach ($this->classes as &$class) {
foreach ($class['methods'] as &$method) {
$methodLineCoverage = $method['executableLines'] ? ($method['executedLines'] / $method['executableLines']) * 100 : 100;
$methodBranchCoverage = $method['executableBranches'] ? ($method['executedBranches'] / $method['executableBranches']) * 100 : 0;
$methodPathCoverage = $method['executablePaths'] ? ($method['executedPaths'] / $method['executablePaths']) * 100 : 0;
$method['coverage'] = $methodBranchCoverage ?: $methodLineCoverage;
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage ?: $methodLineCoverage))->asString();
$class['ccn'] += $method['ccn'];
}
unset($method);
$classLineCoverage = $class['executableLines'] ? ($class['executedLines'] / $class['executableLines']) * 100 : 100;
$classBranchCoverage = $class['executableBranches'] ? ($class['executedBranches'] / $class['executableBranches']) * 100 : 0;
$classPathCoverage = $class['executablePaths'] ? ($class['executedPaths'] / $class['executablePaths']) * 100 : 0;
$class['coverage'] = $classBranchCoverage ?: $classLineCoverage;
$class['crap'] = (new CrapIndex($class['ccn'], $classPathCoverage ?: $classLineCoverage))->asString();
if ($class['executableLines'] > 0 && $class['coverage'] === 100) {
$this->numTestedClasses++;
}
}
unset($class);
foreach ($this->functions as &$function) {
$functionLineCoverage = $function['executableLines'] ? ($function['executedLines'] / $function['executableLines']) * 100 : 100;
$functionBranchCoverage = $function['executableBranches'] ? ($function['executedBranches'] / $function['executableBranches']) * 100 : 0;
$functionPathCoverage = $function['executablePaths'] ? ($function['executedPaths'] / $function['executablePaths']) * 100 : 0;
$function['coverage'] = $functionBranchCoverage ?: $functionLineCoverage;
$function['crap'] = (new CrapIndex($function['ccn'], $functionPathCoverage ?: $functionLineCoverage))->asString();
if ($function['coverage'] === 100) {
$this->numTestedFunctions++;
}
}
}
private function processClasses(array $classes): void
{
$link = $this->id() . '.html#';
foreach ($classes as $className => $class) {
$this->classes[$className] = [
'className' => $className,
'namespace' => $class['namespace'],
'methods' => [],
'startLine' => $class['startLine'],
'executableLines' => 0,
'executedLines' => 0,
'executableBranches' => 0,
'executedBranches' => 0,
'executablePaths' => 0,
'executedPaths' => 0,
'ccn' => 0,
'coverage' => 0,
'crap' => 0,
'link' => $link . $class['startLine'],
];
foreach ($class['methods'] as $methodName => $method) {
$methodData = $this->newMethod($className, $methodName, $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],
];
}
}
}
}
private function processTraits(array $traits): void
{
$link = $this->id() . '.html#';
foreach ($traits as $traitName => $trait) {
$this->traits[$traitName] = [
'traitName' => $traitName,
'namespace' => $trait['namespace'],
'methods' => [],
'startLine' => $trait['startLine'],
'executableLines' => 0,
'executedLines' => 0,
'executableBranches' => 0,
'executedBranches' => 0,
'executablePaths' => 0,
'executedPaths' => 0,
'ccn' => 0,
'coverage' => 0,
'crap' => 0,
'link' => $link . $trait['startLine'],
];
foreach ($trait['methods'] as $methodName => $method) {
$methodData = $this->newMethod($traitName, $methodName, $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],
];
}
}
}
}
private function processFunctions(array $functions): void
{
$link = $this->id() . '.html#';
foreach ($functions as $functionName => $function) {
$this->functions[$functionName] = [
'functionName' => $functionName,
'namespace' => $function['namespace'],
'signature' => $function['signature'],
'startLine' => $function['startLine'],
'endLine' => $function['endLine'],
'executableLines' => 0,
'executedLines' => 0,
'executableBranches' => 0,
'executedBranches' => 0,
'executablePaths' => 0,
'executedPaths' => 0,
'ccn' => $function['ccn'],
'coverage' => 0,
'crap' => 0,
'link' => $link . $function['startLine'],
];
foreach (range($function['startLine'], $function['endLine']) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [&$this->functions[$functionName]];
}
if (isset($this->functionCoverageData[$functionName]['branches'])) {
$this->functions[$functionName]['executableBranches'] = count(
$this->functionCoverageData[$functionName]['branches']
);
$this->functions[$functionName]['executedBranches'] = count(
array_filter(
$this->functionCoverageData[$functionName]['branches'],
static function (array $branch)
{
return (bool) $branch['hit'];
}
)
);
}
if (isset($this->functionCoverageData[$functionName]['paths'])) {
$this->functions[$functionName]['executablePaths'] = count(
$this->functionCoverageData[$functionName]['paths']
);
$this->functions[$functionName]['executedPaths'] = count(
array_filter(
$this->functionCoverageData[$functionName]['paths'],
static function (array $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, string $methodName, array $method, string $link): array
{
$methodData = [
'methodName' => $methodName,
'visibility' => $method['visibility'],
'signature' => $method['signature'],
'startLine' => $method['startLine'],
'endLine' => $method['endLine'],
'executableLines' => 0,
'executedLines' => 0,
'executableBranches' => 0,
'executedBranches' => 0,
'executablePaths' => 0,
'executedPaths' => 0,
'ccn' => $method['ccn'],
'coverage' => 0,
'crap' => 0,
'link' => $link . $method['startLine'],
];
$key = $className . '->' . $methodName;
if (isset($this->functionCoverageData[$key]['branches'])) {
$methodData['executableBranches'] = count(
$this->functionCoverageData[$key]['branches']
);
$methodData['executedBranches'] = count(
array_filter(
$this->functionCoverageData[$key]['branches'],
static function (array $branch)
{
return (bool) $branch['hit'];
}
)
);
}
if (isset($this->functionCoverageData[$key]['paths'])) {
$methodData['executablePaths'] = count(
$this->functionCoverageData[$key]['paths']
);
$methodData['executedPaths'] = count(
array_filter(
$this->functionCoverageData[$key]['paths'],
static function (array $path)
{
return (bool) $path['hit'];
}
)
);
}
return $methodData;
}
}
+92
View File
@@ -0,0 +1,92 @@
<?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 count;
use RecursiveIterator;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Iterator implements RecursiveIterator
{
/**
* @var int
*/
private $position;
/**
* @var AbstractNode[]
*/
private $nodes;
public function __construct(Directory $node)
{
$this->nodes = $node->children();
}
/**
* Rewinds the Iterator to the first element.
*/
public function rewind(): void
{
$this->position = 0;
}
/**
* Checks if there is a current element after calls to rewind() or next().
*/
public function valid(): bool
{
return $this->position < count($this->nodes);
}
/**
* Returns the key of the current element.
*/
public function key(): int
{
return $this->position;
}
/**
* Returns the current element.
*/
public function current(): ?AbstractNode
{
return $this->valid() ? $this->nodes[$this->position] : null;
}
/**
* Moves forward to next element.
*/
public function next(): void
{
$this->position++;
}
/**
* Returns the sub iterator for the current element.
*
* @return Iterator
*/
public function getChildren(): self
{
return new self($this->nodes[$this->position]);
}
/**
* Checks whether the current element has children.
*/
public function hasChildren(): bool
{
return $this->nodes[$this->position] instanceof Directory;
}
}
@@ -0,0 +1,255 @@
<?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_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;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
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 array
*/
private $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 array
*/
private $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]['branches'][$branchId]['hit'][] = $testCaseId;
}
}
foreach ($functionData['paths'] as $pathId => $pathData) {
if ($pathData['hit'] === Driver::BRANCH_HIT) {
$this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'][] = $testCaseId;
}
}
}
}
}
public function setLineCoverage(array $lineCoverage): void
{
$this->lineCoverage = $lineCoverage;
}
public function lineCoverage(): array
{
ksort($this->lineCoverage);
return $this->lineCoverage;
}
public function setFunctionCoverage(array $functionCoverage): void
{
$this->functionCoverage = $functionCoverage;
}
public function functionCoverage(): array
{
ksort($this->functionCoverage);
return $this->functionCoverage;
}
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);
}
foreach ($functionData['branches'] as $branchId => $branchData) {
$this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = array_unique(array_merge($this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'], $branchData['hit']));
}
foreach ($functionData['paths'] as $pathId => $pathData) {
$this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = array_unique(array_merge($this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'], $pathData['hit']));
}
}
}
}
/**
* 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.
*/
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.
*/
private function initPreviouslyUnseenFunction(string $file, string $functionName, array $functionData): void
{
$this->functionCoverage[$file][$functionName] = $functionData;
foreach (array_keys($functionData['branches']) as $branchId) {
$this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = [];
}
foreach (array_keys($functionData['paths']) as $pathId) {
$this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = [];
}
}
/**
* 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.
*/
private function initPreviouslySeenFunction(string $file, string $functionName, array $functionData): void
{
foreach ($functionData['branches'] as $branchId => $branchData) {
if (!isset($this->functionCoverage[$file][$functionName]['branches'][$branchId])) {
$this->functionCoverage[$file][$functionName]['branches'][$branchId] = $branchData;
$this->functionCoverage[$file][$functionName]['branches'][$branchId]['hit'] = [];
}
}
foreach ($functionData['paths'] as $pathId => $pathData) {
if (!isset($this->functionCoverage[$file][$functionName]['paths'][$pathId])) {
$this->functionCoverage[$file][$functionName]['paths'][$pathId] = $pathData;
$this->functionCoverage[$file][$functionName]['paths'][$pathId]['hit'] = [];
}
}
}
}
@@ -0,0 +1,234 @@
<?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_intersect;
use function array_intersect_key;
use function count;
use function in_array;
use function range;
use SebastianBergmann\CodeCoverage\Driver\Driver;
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class RawCodeCoverageData
{
/**
* @var array<string, array<int>>
*/
private static $emptyLineCache = [];
/**
* @var array
*
* @see https://xdebug.org/docs/code_coverage for format
*/
private $lineCoverage;
/**
* @var array
*
* @see https://xdebug.org/docs/code_coverage for format
*/
private $functionCoverage;
public static function fromXdebugWithoutPathCoverage(array $rawCoverage): self
{
return new self($rawCoverage, []);
}
public static function fromXdebugWithPathCoverage(array $rawCoverage): self
{
$lineCoverage = [];
$functionCoverage = [];
foreach ($rawCoverage as $file => $fileCoverageData) {
$lineCoverage[$file] = $fileCoverageData['lines'];
$functionCoverage[$file] = $fileCoverageData['functions'];
}
return new self($lineCoverage, $functionCoverage);
}
public static function fromXdebugWithMixedCoverage(array $rawCoverage): self
{
$lineCoverage = [];
$functionCoverage = [];
foreach ($rawCoverage as $file => $fileCoverageData) {
if (!isset($fileCoverageData['functions'])) {
// Current file does not have functions, so line coverage
// is stored in $fileCoverageData, not in $fileCoverageData['lines']
$lineCoverage[$file] = $fileCoverageData;
continue;
}
$lineCoverage[$file] = $fileCoverageData['lines'];
$functionCoverage[$file] = $fileCoverageData['functions'];
}
return new self($lineCoverage, $functionCoverage);
}
public static function fromUncoveredFile(string $filename, FileAnalyser $analyser): self
{
$lineCoverage = [];
foreach ($analyser->executableLinesIn($filename) as $line) {
$lineCoverage[$line] = Driver::LINE_NOT_EXECUTED;
}
return new self([$filename => $lineCoverage], []);
}
private function __construct(array $lineCoverage, array $functionCoverage)
{
$this->lineCoverage = $lineCoverage;
$this->functionCoverage = $functionCoverage;
$this->skipEmptyLines();
}
public function clear(): void
{
$this->lineCoverage = $this->functionCoverage = [];
}
public function lineCoverage(): array
{
return $this->lineCoverage;
}
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[] $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 (empty($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
*/
private function skipEmptyLines(): void
{
foreach ($this->lineCoverage as $filename => $coverage) {
foreach ($this->getEmptyLinesForFile($filename) as $emptyLine) {
unset($this->lineCoverage[$filename][$emptyLine]);
}
}
}
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];
}
}
+255
View File
@@ -0,0 +1,255 @@
<?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 dirname;
use function file_put_contents;
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\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
final class Clover
{
/**
* @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('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;
foreach ($class['methods'] as $methodName => $method) {
if ($method['executableLines'] == 0) {
continue;
}
$classMethods++;
$classStatements += $method['executableLines'];
$coveredClassStatements += $method['executedLines'];
if ($method['coverage'] == 100) {
$coveredMethods++;
}
$methodCount = 0;
foreach (range($method['startLine'], $method['endLine']) as $line) {
if (isset($coverageData[$line]) && ($coverageData[$line] !== null)) {
$methodCount = max($methodCount, count($coverageData[$line]));
}
}
$lines[$method['startLine']] = [
'ccn' => $method['ccn'],
'count' => $methodCount,
'crap' => $method['crap'],
'type' => 'method',
'visibility' => $method['visibility'],
'name' => $methodName,
];
}
if (!empty($class['package']['namespace'])) {
$namespace = $class['package']['namespace'];
}
$xmlClass = $xmlDocument->createElement('class');
$xmlClass->setAttribute('name', $className);
$xmlClass->setAttribute('namespace', $namespace);
if (!empty($class['package']['fullPackage'])) {
$xmlClass->setAttribute(
'fullPackage',
$class['package']['fullPackage']
);
}
if (!empty($class['package']['category'])) {
$xmlClass->setAttribute(
'category',
$class['package']['category']
);
}
if (!empty($class['package']['package'])) {
$xmlClass->setAttribute(
'package',
$class['package']['package']
);
}
if (!empty($class['package']['subpackage'])) {
$xmlClass->setAttribute(
'subpackage',
$class['package']['subpackage']
);
}
$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 = $xmlDocument->saveXML();
if ($target !== null) {
Filesystem::createDirectory(dirname($target));
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
}
return $buffer;
}
}
@@ -0,0 +1,304 @@
<?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 dirname;
use function file_put_contents;
use function range;
use function time;
use DOMImplementation;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
final class Cobertura
{
/**
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = 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';
$document->formatOutput = true;
$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;
$packageName = $name ?? '';
$packageElement->setAttribute('name', $packageName);
$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'];
if (!empty($class['package']['namespace'])) {
$className = $class['package']['namespace'] . '\\' . $className;
}
$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 = $linesValid === 0 ? 0 : ($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]) || $coverageData[$line] === null) {
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 ($report->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 = $report->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 = $linesValid === 0 ? 0 : ($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]) || $coverageData[$line] === null) {
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 = $document->saveXML();
if ($target !== null) {
Filesystem::createDirectory(dirname($target));
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
}
return $buffer;
}
}
+153
View File
@@ -0,0 +1,153 @@
<?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 dirname;
use function file_put_contents;
use function htmlspecialchars;
use function is_string;
use function round;
use DOMDocument;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
final class Crap4j
{
/**
* @var int
*/
private $threshold;
public function __construct(int $threshold = 30)
{
$this->threshold = $threshold;
}
/**
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
{
$document = new DOMDocument('1.0', 'UTF-8');
$document->formatOutput = true;
$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 (!empty($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 = $document->saveXML();
if ($target !== null) {
Filesystem::createDirectory(dirname($target));
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
}
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,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\Html;
use const DIRECTORY_SEPARATOR;
use function copy;
use function date;
use function dirname;
use function substr;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
final class Facade
{
/**
* @var string
*/
private $templatePath;
/**
* @var string
*/
private $generator;
/**
* @var int
*/
private $lowUpperBound;
/**
* @var int
*/
private $highLowerBound;
public function __construct(int $lowUpperBound = 50, int $highLowerBound = 90, string $generator = '')
{
if ($lowUpperBound > $highLowerBound) {
throw new InvalidArgumentException(
'$lowUpperBound must not be larger than $highLowerBound'
);
}
$this->generator = $generator;
$this->highLowerBound = $highLowerBound;
$this->lowUpperBound = $lowUpperBound;
$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');
$dashboard = new Dashboard(
$this->templatePath,
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound,
$coverage->collectsBranchAndPathCoverage()
);
$directory = new Directory(
$this->templatePath,
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound,
$coverage->collectsBranchAndPathCoverage()
);
$file = new File(
$this->templatePath,
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound,
$coverage->collectsBranchAndPathCoverage()
);
$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);
}
private function copyFiles(string $target): void
{
$dir = $this->directory($target . '_css');
copy($this->templatePath . 'css/bootstrap.min.css', $dir . 'bootstrap.min.css');
copy($this->templatePath . 'css/nv.d3.min.css', $dir . 'nv.d3.min.css');
copy($this->templatePath . 'css/style.css', $dir . 'style.css');
copy($this->templatePath . 'css/custom.css', $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/bootstrap.min.js', $dir . 'bootstrap.min.js');
copy($this->templatePath . 'js/popper.min.js', $dir . 'popper.min.js');
copy($this->templatePath . 'js/d3.min.js', $dir . 'd3.min.js');
copy($this->templatePath . 'js/jquery.min.js', $dir . 'jquery.min.js');
copy($this->templatePath . 'js/nv.d3.min.js', $dir . 'nv.d3.min.js');
copy($this->templatePath . 'js/file.js', $dir . 'file.js');
}
private function directory(string $directory): string
{
if (substr($directory, -1, 1) != DIRECTORY_SEPARATOR) {
$directory .= DIRECTORY_SEPARATOR;
}
Filesystem::createDirectory($directory);
return $directory;
}
}
@@ -0,0 +1,314 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Report\Html;
use function array_pop;
use function count;
use function sprintf;
use function str_repeat;
use function substr_count;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\Version;
use SebastianBergmann\Environment\Runtime;
use SebastianBergmann\Template\Template;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
abstract class Renderer
{
/**
* @var string
*/
protected $templatePath;
/**
* @var string
*/
protected $generator;
/**
* @var string
*/
protected $date;
/**
* @var int
*/
protected $lowUpperBound;
/**
* @var int
*/
protected $highLowerBound;
/**
* @var bool
*/
protected $hasBranchCoverage;
/**
* @var string
*/
protected $version;
public function __construct(string $templatePath, string $generator, string $date, int $lowUpperBound, int $highLowerBound, bool $hasBranchCoverage)
{
$this->templatePath = $templatePath;
$this->generator = $generator;
$this->date = $date;
$this->lowUpperBound = $lowUpperBound;
$this->highLowerBound = $highLowerBound;
$this->version = Version::id();
$this->hasBranchCoverage = $hasBranchCoverage;
}
protected function renderItemTemplate(Template $template, array $data): string
{
$numSeparator = '&nbsp;/&nbsp;';
if (isset($data['numClasses']) && $data['numClasses'] > 0) {
$classesLevel = $this->colorLevel($data['testedClassesPercent']);
$classesNumber = $data['numTestedClasses'] . $numSeparator .
$data['numClasses'];
$classesBar = $this->coverageBar(
$data['testedClassesPercent']
);
} else {
$classesLevel = '';
$classesNumber = '0' . $numSeparator . '0';
$classesBar = '';
$data['testedClassesPercentAsString'] = 'n/a';
}
if ($data['numMethods'] > 0) {
$methodsLevel = $this->colorLevel($data['testedMethodsPercent']);
$methodsNumber = $data['numTestedMethods'] . $numSeparator .
$data['numMethods'];
$methodsBar = $this->coverageBar(
$data['testedMethodsPercent']
);
} else {
$methodsLevel = '';
$methodsNumber = '0' . $numSeparator . '0';
$methodsBar = '';
$data['testedMethodsPercentAsString'] = 'n/a';
}
if ($data['numExecutableLines'] > 0) {
$linesLevel = $this->colorLevel($data['linesExecutedPercent']);
$linesNumber = $data['numExecutedLines'] . $numSeparator .
$data['numExecutableLines'];
$linesBar = $this->coverageBar(
$data['linesExecutedPercent']
);
} else {
$linesLevel = '';
$linesNumber = '0' . $numSeparator . '0';
$linesBar = '';
$data['linesExecutedPercentAsString'] = 'n/a';
}
if ($data['numExecutablePaths'] > 0) {
$pathsLevel = $this->colorLevel($data['pathsExecutedPercent']);
$pathsNumber = $data['numExecutedPaths'] . $numSeparator .
$data['numExecutablePaths'];
$pathsBar = $this->coverageBar(
$data['pathsExecutedPercent']
);
} else {
$pathsLevel = '';
$pathsNumber = '0' . $numSeparator . '0';
$pathsBar = '';
$data['pathsExecutedPercentAsString'] = 'n/a';
}
if ($data['numExecutableBranches'] > 0) {
$branchesLevel = $this->colorLevel($data['branchesExecutedPercent']);
$branchesNumber = $data['numExecutedBranches'] . $numSeparator .
$data['numExecutableBranches'];
$branchesBar = $this->coverageBar(
$data['branchesExecutedPercent']
);
} else {
$branchesLevel = '';
$branchesNumber = '0' . $numSeparator . '0';
$branchesBar = '';
$data['branchesExecutedPercentAsString'] = 'n/a';
}
$template->setVar(
[
'icon' => $data['icon'] ?? '',
'crap' => $data['crap'] ?? '',
'name' => $data['name'],
'lines_bar' => $linesBar,
'lines_executed_percent' => $data['linesExecutedPercentAsString'],
'lines_level' => $linesLevel,
'lines_number' => $linesNumber,
'paths_bar' => $pathsBar,
'paths_executed_percent' => $data['pathsExecutedPercentAsString'],
'paths_level' => $pathsLevel,
'paths_number' => $pathsNumber,
'branches_bar' => $branchesBar,
'branches_executed_percent' => $data['branchesExecutedPercentAsString'],
'branches_level' => $branchesLevel,
'branches_number' => $branchesNumber,
'methods_bar' => $methodsBar,
'methods_tested_percent' => $data['testedMethodsPercentAsString'],
'methods_level' => $methodsLevel,
'methods_number' => $methodsNumber,
'classes_bar' => $classesBar,
'classes_tested_percent' => $data['testedClassesPercentAsString'] ?? '',
'classes_level' => $classesLevel,
'classes_number' => $classesNumber,
]
);
return $template->render();
}
protected function setCommonTemplateVariables(Template $template, AbstractNode $node): void
{
$template->setVar(
[
'id' => $node->id(),
'full_path' => $node->pathAsString(),
'path_to_root' => $this->pathToRoot($node),
'breadcrumbs' => $this->breadcrumbs($node),
'date' => $this->date,
'version' => $this->version,
'runtime' => $this->runtimeString(),
'generator' => $this->generator,
'low_upper_bound' => $this->lowUpperBound,
'high_lower_bound' => $this->highLowerBound,
]
);
}
protected function breadcrumbs(AbstractNode $node): string
{
$breadcrumbs = '';
$path = $node->pathAsArray();
$pathToRoot = [];
$max = count($path);
if ($node instanceof FileNode) {
$max--;
}
for ($i = 0; $i < $max; $i++) {
$pathToRoot[] = str_repeat('../', $i);
}
foreach ($path as $step) {
if ($step !== $node) {
$breadcrumbs .= $this->inactiveBreadcrumb(
$step,
array_pop($pathToRoot)
);
} else {
$breadcrumbs .= $this->activeBreadcrumb($step);
}
}
return $breadcrumbs;
}
protected function activeBreadcrumb(AbstractNode $node): string
{
$buffer = sprintf(
' <li class="breadcrumb-item active">%s</li>' . "\n",
$node->name()
);
if ($node instanceof DirectoryNode) {
$buffer .= ' <li class="breadcrumb-item">(<a href="dashboard.html">Dashboard</a>)</li>' . "\n";
}
return $buffer;
}
protected function inactiveBreadcrumb(AbstractNode $node, string $pathToRoot): string
{
return sprintf(
' <li class="breadcrumb-item"><a href="%sindex.html">%s</a></li>' . "\n",
$pathToRoot,
$node->name()
);
}
protected function pathToRoot(AbstractNode $node): string
{
$id = $node->id();
$depth = substr_count($id, '/');
if ($id !== 'index' &&
$node instanceof DirectoryNode) {
$depth++;
}
return str_repeat('../', $depth);
}
protected function coverageBar(float $percent): string
{
$level = $this->colorLevel($percent);
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'coverage_bar_branch.html' : 'coverage_bar.html');
$template = new Template(
$templateName,
'{{',
'}}'
);
$template->setVar(['level' => $level, 'percent' => sprintf('%.2F', $percent)]);
return $template->render();
}
protected function colorLevel(float $percent): string
{
if ($percent <= $this->lowUpperBound) {
return 'danger';
}
if ($percent > $this->lowUpperBound &&
$percent < $this->highLowerBound) {
return 'warning';
}
return 'success';
}
private function runtimeString(): string
{
$runtime = new Runtime;
return sprintf(
'<a href="%s" target="_top">%s %s</a>',
$runtime->getVendorUrl(),
$runtime->getName(),
$runtime->getVersion()
);
}
}
@@ -0,0 +1,288 @@
<?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 arsort;
use function asort;
use function count;
use function explode;
use function floor;
use function json_encode;
use function sprintf;
use function str_replace;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
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'],
]
);
$template->renderTo($file);
}
protected function activeBreadcrumb(AbstractNode $node): string
{
return sprintf(
' <li class="breadcrumb-item"><a href="index.html">%s</a></li>' . "\n" .
' <li class="breadcrumb-item active">(Dashboard)</li>' . "\n",
$node->name()
);
}
/**
* Returns the data for the Class/Method Complexity charts.
*/
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'],
sprintf(
'<a href="%s">%s</a>',
str_replace($baseLink, '', $method['link']),
$methodName
),
];
}
$result['class'][] = [
$class['coverage'],
$class['ccn'],
sprintf(
'<a href="%s">%s</a>',
str_replace($baseLink, '', $class['link']),
$className
),
];
}
return [
'class' => json_encode($result['class']),
'method' => json_encode($result['method']),
];
}
/**
* Returns the data for the Class / Method Coverage Distribution chart.
*/
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 $methodName => $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]++;
}
}
return [
'class' => json_encode(array_values($result['class'])),
'method' => json_encode(array_values($result['method'])),
];
}
/**
* Returns the classes / methods with insufficient coverage.
*/
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->highLowerBound) {
$key = $methodName;
if ($className !== '*') {
$key = $className . '::' . $methodName;
}
$leastTestedMethods[$key] = $method['coverage'];
}
}
if ($class['coverage'] < $this->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;
}
/**
* Returns the project risks according to the CRAP index.
*/
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->highLowerBound && $method['ccn'] > 1) {
$key = $methodName;
if ($className !== '*') {
$key = $className . '::' . $methodName;
}
$methodRisks[$key] = $method['crap'];
}
}
if ($class['coverage'] < $this->highLowerBound &&
$class['ccn'] > count($class['methods'])) {
$classRisks[$className] = $class['crap'];
}
}
arsort($classRisks);
arsort($methodRisks);
foreach ($classRisks as $className => $crap) {
$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,
$crap
);
}
foreach ($methodRisks as $methodName => $crap) {
[$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,
$crap
);
}
return $result;
}
}
@@ -0,0 +1,113 @@
<?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\Node\AbstractNode as Node;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
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,
]
);
$template->renderTo($file);
}
private function renderItem(Node $node, bool $total = false): string
{
$data = [
'numClasses' => $node->numberOfClassesAndTraits(),
'numTestedClasses' => $node->numberOfTestedClassesAndTraits(),
'numMethods' => $node->numberOfFunctionsAndMethods(),
'numTestedMethods' => $node->numberOfTestedFunctionsAndMethods(),
'linesExecutedPercent' => $node->percentageOfExecutedLines()->asFloat(),
'linesExecutedPercentAsString' => $node->percentageOfExecutedLines()->asString(),
'numExecutedLines' => $node->numberOfExecutedLines(),
'numExecutableLines' => $node->numberOfExecutableLines(),
'branchesExecutedPercent' => $node->percentageOfExecutedBranches()->asFloat(),
'branchesExecutedPercentAsString' => $node->percentageOfExecutedBranches()->asString(),
'numExecutedBranches' => $node->numberOfExecutedBranches(),
'numExecutableBranches' => $node->numberOfExecutableBranches(),
'pathsExecutedPercent' => $node->percentageOfExecutedPaths()->asFloat(),
'pathsExecutedPercentAsString' => $node->percentageOfExecutedPaths()->asString(),
'numExecutedPaths' => $node->numberOfExecutedPaths(),
'numExecutablePaths' => $node->numberOfExecutablePaths(),
'testedMethodsPercent' => $node->percentageOfTestedFunctionsAndMethods()->asFloat(),
'testedMethodsPercentAsString' => $node->percentageOfTestedFunctionsAndMethods()->asString(),
'testedClassesPercent' => $node->percentageOfTestedClassesAndTraits()->asFloat(),
'testedClassesPercentAsString' => $node->percentageOfTestedClassesAndTraits()->asString(),
];
if ($total) {
$data['name'] = 'Total';
} else {
$up = str_repeat('../', count($node->pathAsArray()) - 2);
$data['icon'] = sprintf('<img src="%s_icons/file-code.svg" class="octicon" />', $up);
if ($node instanceof DirectoryNode) {
$data['name'] = sprintf(
'<a href="%s/index.html">%s</a>',
$node->name(),
$node->name()
);
$data['icon'] = sprintf('<img src="%s_icons/file-directory.svg" class="octicon" />', $up);
} elseif ($this->hasBranchCoverage) {
$data['name'] = sprintf(
'%s <a class="small" href="%s.html">[line]</a> <a class="small" href="%s_branch.html">[branch]</a> <a class="small" href="%s_path.html">[path]</a>',
$node->name(),
$node->name(),
$node->name(),
$node->name()
);
} else {
$data['name'] = sprintf(
'<a href="%s.html">%s</a>',
$node->name(),
$node->name()
);
}
}
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'directory_item_branch.html' : 'directory_item.html');
return $this->renderItemTemplate(
new Template($templateName, '{{', '}}'),
$data
);
}
}
@@ -0,0 +1,1149 @@
<?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_COMPAT;
use const ENT_HTML401;
use const ENT_SUBSTITUTE;
use const T_ABSTRACT;
use const T_ARRAY;
use const T_AS;
use const T_BREAK;
use const T_CALLABLE;
use const T_CASE;
use const T_CATCH;
use const T_CLASS;
use const T_CLONE;
use const T_COMMENT;
use const T_CONST;
use const T_CONTINUE;
use const T_DECLARE;
use const T_DEFAULT;
use const T_DO;
use const T_DOC_COMMENT;
use const T_ECHO;
use const T_ELSE;
use const T_ELSEIF;
use const T_EMPTY;
use const T_ENDDECLARE;
use const T_ENDFOR;
use const T_ENDFOREACH;
use const T_ENDIF;
use const T_ENDSWITCH;
use const T_ENDWHILE;
use const T_EVAL;
use const T_EXIT;
use const T_EXTENDS;
use const T_FINAL;
use const T_FINALLY;
use const T_FOR;
use const T_FOREACH;
use const T_FUNCTION;
use const T_GLOBAL;
use const T_GOTO;
use const T_HALT_COMPILER;
use const T_IF;
use const T_IMPLEMENTS;
use const T_INCLUDE;
use const T_INCLUDE_ONCE;
use const T_INLINE_HTML;
use const T_INSTANCEOF;
use const T_INSTEADOF;
use const T_INTERFACE;
use const T_ISSET;
use const T_LIST;
use const T_NAMESPACE;
use const T_NEW;
use const T_PRINT;
use const T_PRIVATE;
use const T_PROTECTED;
use const T_PUBLIC;
use const T_REQUIRE;
use const T_REQUIRE_ONCE;
use const T_RETURN;
use const T_STATIC;
use const T_SWITCH;
use const T_THROW;
use const T_TRAIT;
use const T_TRY;
use const T_UNSET;
use const T_USE;
use const T_VAR;
use const T_WHILE;
use const T_YIELD;
use const T_YIELD_FROM;
use function array_key_exists;
use function array_pop;
use function array_unique;
use function constant;
use function count;
use function defined;
use function explode;
use function file_get_contents;
use function htmlspecialchars;
use function is_string;
use function sprintf;
use function str_replace;
use function substr;
use function token_get_all;
use function trim;
use PHPUnit\Runner\BaseTestRunner;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\Util\Percentage;
use SebastianBergmann\Template\Template;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class File extends Renderer
{
/**
* @psalm-var array<int,true>
*/
private static $keywordTokens = [];
/**
* @var array
*/
private static $formattedSourceCache = [];
/**
* @var int
*/
private $htmlSpecialCharsFlags = ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE;
public function render(FileNode $node, string $file): void
{
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'file_branch.html' : 'file.html');
$template = new Template($templateName, '{{', '}}');
$this->setCommonTemplateVariables($template, $node);
$template->setVar(
[
'items' => $this->renderItems($node),
'lines' => $this->renderSourceWithLineCoverage($node),
'legend' => '<p><span class="success"><strong>Executed</strong></span><span class="danger"><strong>Not Executed</strong></span><span class="warning"><strong>Dead Code</strong></span></p>',
'structure' => '',
]
);
$template->renderTo($file . '.html');
if ($this->hasBranchCoverage) {
$template->setVar(
[
'items' => $this->renderItems($node),
'lines' => $this->renderSourceWithBranchCoverage($node),
'legend' => '<p><span class="success"><strong>Fully covered</strong></span><span class="warning"><strong>Partially covered</strong></span><span class="danger"><strong>Not covered</strong></span></p>',
'structure' => $this->renderBranchStructure($node),
]
);
$template->renderTo($file . '_branch.html');
$template->setVar(
[
'items' => $this->renderItems($node),
'lines' => $this->renderSourceWithPathCoverage($node),
'legend' => '<p><span class="success"><strong>Fully covered</strong></span><span class="warning"><strong>Partially covered</strong></span><span class="danger"><strong>Not covered</strong></span></p>',
'structure' => $this->renderPathStructure($node),
]
);
$template->renderTo($file . '_path.html');
}
}
private function renderItems(FileNode $node): string
{
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'file_item_branch.html' : 'file_item.html');
$template = new Template($templateName, '{{', '}}');
$methodTemplateName = $this->templatePath . ($this->hasBranchCoverage ? 'method_item_branch.html' : 'method_item.html');
$methodItemTemplate = new Template(
$methodTemplateName,
'{{',
'}}'
);
$items = $this->renderItemTemplate(
$template,
[
'name' => 'Total',
'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(),
'crap' => '<abbr title="Change Risk Anti-Patterns (CRAP) Index">CRAP</abbr>',
]
);
$items .= $this->renderFunctionItems(
$node->functions(),
$methodItemTemplate
);
$items .= $this->renderTraitOrClassItems(
$node->traits(),
$template,
$methodItemTemplate
);
$items .= $this->renderTraitOrClassItems(
$node->classes(),
$template,
$methodItemTemplate
);
return $items;
}
private function renderTraitOrClassItems(array $items, Template $template, Template $methodItemTemplate): string
{
$buffer = '';
if (empty($items)) {
return $buffer;
}
foreach ($items as $name => $item) {
$numMethods = 0;
$numTestedMethods = 0;
foreach ($item['methods'] as $method) {
if ($method['executableLines'] > 0) {
$numMethods++;
if ($method['executedLines'] === $method['executableLines']) {
$numTestedMethods++;
}
}
}
if ($item['executableLines'] > 0) {
$numClasses = 1;
$numTestedClasses = $numTestedMethods === $numMethods ? 1 : 0;
$linesExecutedPercentAsString = Percentage::fromFractionAndTotal(
$item['executedLines'],
$item['executableLines']
)->asString();
$branchesExecutedPercentAsString = Percentage::fromFractionAndTotal(
$item['executedBranches'],
$item['executableBranches']
)->asString();
$pathsExecutedPercentAsString = Percentage::fromFractionAndTotal(
$item['executedPaths'],
$item['executablePaths']
)->asString();
} else {
$numClasses = 0;
$numTestedClasses = 0;
$linesExecutedPercentAsString = 'n/a';
$branchesExecutedPercentAsString = 'n/a';
$pathsExecutedPercentAsString = 'n/a';
}
$testedMethodsPercentage = Percentage::fromFractionAndTotal(
$numTestedMethods,
$numMethods
);
$testedClassesPercentage = Percentage::fromFractionAndTotal(
$numTestedMethods === $numMethods ? 1 : 0,
1
);
$buffer .= $this->renderItemTemplate(
$template,
[
'name' => $this->abbreviateClassName($name),
'numClasses' => $numClasses,
'numTestedClasses' => $numTestedClasses,
'numMethods' => $numMethods,
'numTestedMethods' => $numTestedMethods,
'linesExecutedPercent' => Percentage::fromFractionAndTotal(
$item['executedLines'],
$item['executableLines'],
)->asFloat(),
'linesExecutedPercentAsString' => $linesExecutedPercentAsString,
'numExecutedLines' => $item['executedLines'],
'numExecutableLines' => $item['executableLines'],
'branchesExecutedPercent' => Percentage::fromFractionAndTotal(
$item['executedBranches'],
$item['executableBranches'],
)->asFloat(),
'branchesExecutedPercentAsString' => $branchesExecutedPercentAsString,
'numExecutedBranches' => $item['executedBranches'],
'numExecutableBranches' => $item['executableBranches'],
'pathsExecutedPercent' => Percentage::fromFractionAndTotal(
$item['executedPaths'],
$item['executablePaths']
)->asFloat(),
'pathsExecutedPercentAsString' => $pathsExecutedPercentAsString,
'numExecutedPaths' => $item['executedPaths'],
'numExecutablePaths' => $item['executablePaths'],
'testedMethodsPercent' => $testedMethodsPercentage->asFloat(),
'testedMethodsPercentAsString' => $testedMethodsPercentage->asString(),
'testedClassesPercent' => $testedClassesPercentage->asFloat(),
'testedClassesPercentAsString' => $testedClassesPercentage->asString(),
'crap' => $item['crap'],
]
);
foreach ($item['methods'] as $method) {
$buffer .= $this->renderFunctionOrMethodItem(
$methodItemTemplate,
$method,
'&nbsp;'
);
}
}
return $buffer;
}
private function renderFunctionItems(array $functions, Template $template): string
{
if (empty($functions)) {
return '';
}
$buffer = '';
foreach ($functions as $function) {
$buffer .= $this->renderFunctionOrMethodItem(
$template,
$function
);
}
return $buffer;
}
private function renderFunctionOrMethodItem(Template $template, array $item, string $indent = ''): string
{
$numMethods = 0;
$numTestedMethods = 0;
if ($item['executableLines'] > 0) {
$numMethods = 1;
if ($item['executedLines'] === $item['executableLines']) {
$numTestedMethods = 1;
}
}
$executedLinesPercentage = Percentage::fromFractionAndTotal(
$item['executedLines'],
$item['executableLines']
);
$executedBranchesPercentage = Percentage::fromFractionAndTotal(
$item['executedBranches'],
$item['executableBranches']
);
$executedPathsPercentage = Percentage::fromFractionAndTotal(
$item['executedPaths'],
$item['executablePaths']
);
$testedMethodsPercentage = Percentage::fromFractionAndTotal(
$numTestedMethods,
1
);
return $this->renderItemTemplate(
$template,
[
'name' => sprintf(
'%s<a href="#%d"><abbr title="%s">%s</abbr></a>',
$indent,
$item['startLine'],
htmlspecialchars($item['signature'], $this->htmlSpecialCharsFlags),
$item['functionName'] ?? $item['methodName']
),
'numMethods' => $numMethods,
'numTestedMethods' => $numTestedMethods,
'linesExecutedPercent' => $executedLinesPercentage->asFloat(),
'linesExecutedPercentAsString' => $executedLinesPercentage->asString(),
'numExecutedLines' => $item['executedLines'],
'numExecutableLines' => $item['executableLines'],
'branchesExecutedPercent' => $executedBranchesPercentage->asFloat(),
'branchesExecutedPercentAsString' => $executedBranchesPercentage->asString(),
'numExecutedBranches' => $item['executedBranches'],
'numExecutableBranches' => $item['executableBranches'],
'pathsExecutedPercent' => $executedPathsPercentage->asFloat(),
'pathsExecutedPercentAsString' => $executedPathsPercentage->asString(),
'numExecutedPaths' => $item['executedPaths'],
'numExecutablePaths' => $item['executablePaths'],
'testedMethodsPercent' => $testedMethodsPercentage->asFloat(),
'testedMethodsPercentAsString' => $testedMethodsPercentage->asString(),
'crap' => $item['crap'],
]
);
}
private function renderSourceWithLineCoverage(FileNode $node): string
{
$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');
$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');
$coverageData = $node->lineCoverageData();
$testData = $node->testData();
$codeLines = $this->loadFile($node->pathAsString());
$lines = '';
$i = 1;
foreach ($codeLines as $line) {
$trClass = '';
$popoverContent = '';
$popoverTitle = '';
if (array_key_exists($i, $coverageData)) {
$numTests = ($coverageData[$i] ? count($coverageData[$i]) : 0);
if ($coverageData[$i] === null) {
$trClass = 'warning';
} elseif ($numTests === 0) {
$trClass = 'danger';
} else {
if ($numTests > 1) {
$popoverTitle = $numTests . ' tests cover line ' . $i;
} else {
$popoverTitle = '1 test covers line ' . $i;
}
$lineCss = 'covered-by-large-tests';
$popoverContent = '<ul>';
foreach ($coverageData[$i] as $test) {
if ($lineCss === 'covered-by-large-tests' && $testData[$test]['size'] === 'medium') {
$lineCss = 'covered-by-medium-tests';
} elseif ($testData[$test]['size'] === 'small') {
$lineCss = 'covered-by-small-tests';
}
$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);
}
$popoverContent .= '</ul>';
$trClass = $lineCss . ' popin';
}
}
$popover = '';
if (!empty($popoverTitle)) {
$popover = sprintf(
' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
);
}
$lines .= $this->renderLine($singleLineTemplate, $i, $line, $trClass, $popover);
$i++;
}
$linesTemplate->setVar(['lines' => $lines]);
return $linesTemplate->render();
}
private function renderSourceWithBranchCoverage(FileNode $node): string
{
$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');
$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');
$functionCoverageData = $node->functionCoverageData();
$testData = $node->testData();
$codeLines = $this->loadFile($node->pathAsString());
$lineData = [];
/** @var int $line */
foreach (array_keys($codeLines) as $line) {
$lineData[$line + 1] = [
'includedInBranches' => 0,
'includedInHitBranches' => 0,
'tests' => [],
];
}
foreach ($functionCoverageData as $method) {
foreach ($method['branches'] as $branch) {
foreach (range($branch['line_start'], $branch['line_end']) as $line) {
if (!isset($lineData[$line])) { // blank line at end of file is sometimes included here
continue;
}
$lineData[$line]['includedInBranches']++;
if ($branch['hit']) {
$lineData[$line]['includedInHitBranches']++;
$lineData[$line]['tests'] = array_unique(array_merge($lineData[$line]['tests'], $branch['hit']));
}
}
}
}
$lines = '';
$i = 1;
/** @var string $line */
foreach ($codeLines as $line) {
$trClass = '';
$popover = '';
if ($lineData[$i]['includedInBranches'] > 0) {
$lineCss = 'success';
if ($lineData[$i]['includedInHitBranches'] === 0) {
$lineCss = 'danger';
} elseif ($lineData[$i]['includedInHitBranches'] !== $lineData[$i]['includedInBranches']) {
$lineCss = 'warning';
}
$popoverContent = '<ul>';
if (count($lineData[$i]['tests']) === 1) {
$popoverTitle = '1 test covers line ' . $i;
} else {
$popoverTitle = count($lineData[$i]['tests']) . ' tests cover line ' . $i;
}
$popoverTitle .= '. These are covering ' . $lineData[$i]['includedInHitBranches'] . ' out of the ' . $lineData[$i]['includedInBranches'] . ' code branches.';
foreach ($lineData[$i]['tests'] as $test) {
$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);
}
$popoverContent .= '</ul>';
$trClass = $lineCss . ' popin';
$popover = sprintf(
' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
);
}
$lines .= $this->renderLine($singleLineTemplate, $i, $line, $trClass, $popover);
$i++;
}
$linesTemplate->setVar(['lines' => $lines]);
return $linesTemplate->render();
}
private function renderSourceWithPathCoverage(FileNode $node): string
{
$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');
$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');
$functionCoverageData = $node->functionCoverageData();
$testData = $node->testData();
$codeLines = $this->loadFile($node->pathAsString());
$lineData = [];
/** @var int $line */
foreach (array_keys($codeLines) as $line) {
$lineData[$line + 1] = [
'includedInPaths' => [],
'includedInHitPaths' => [],
'tests' => [],
];
}
foreach ($functionCoverageData as $method) {
foreach ($method['paths'] as $pathId => $path) {
foreach ($path['path'] as $branchTaken) {
foreach (range($method['branches'][$branchTaken]['line_start'], $method['branches'][$branchTaken]['line_end']) as $line) {
if (!isset($lineData[$line])) {
continue;
}
$lineData[$line]['includedInPaths'][] = $pathId;
if ($path['hit']) {
$lineData[$line]['includedInHitPaths'][] = $pathId;
$lineData[$line]['tests'] = array_unique(array_merge($lineData[$line]['tests'], $path['hit']));
}
}
}
}
}
$lines = '';
$i = 1;
/** @var string $line */
foreach ($codeLines as $line) {
$trClass = '';
$popover = '';
$includedInPathsCount = count(array_unique($lineData[$i]['includedInPaths']));
$includedInHitPathsCount = count(array_unique($lineData[$i]['includedInHitPaths']));
if ($includedInPathsCount > 0) {
$lineCss = 'success';
if ($includedInHitPathsCount === 0) {
$lineCss = 'danger';
} elseif ($includedInHitPathsCount !== $includedInPathsCount) {
$lineCss = 'warning';
}
$popoverContent = '<ul>';
if (count($lineData[$i]['tests']) === 1) {
$popoverTitle = '1 test covers line ' . $i;
} else {
$popoverTitle = count($lineData[$i]['tests']) . ' tests cover line ' . $i;
}
$popoverTitle .= '. These are covering ' . $includedInHitPathsCount . ' out of the ' . $includedInPathsCount . ' code paths.';
foreach ($lineData[$i]['tests'] as $test) {
$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);
}
$popoverContent .= '</ul>';
$trClass = $lineCss . ' popin';
$popover = sprintf(
' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
);
}
$lines .= $this->renderLine($singleLineTemplate, $i, $line, $trClass, $popover);
$i++;
}
$linesTemplate->setVar(['lines' => $lines]);
return $linesTemplate->render();
}
private function renderBranchStructure(FileNode $node): string
{
$branchesTemplate = new Template($this->templatePath . 'branches.html.dist', '{{', '}}');
$coverageData = $node->functionCoverageData();
$testData = $node->testData();
$codeLines = $this->loadFile($node->pathAsString());
$branches = '';
ksort($coverageData);
foreach ($coverageData as $methodName => $methodData) {
if (!$methodData['branches']) {
continue;
}
$branchStructure = '';
foreach ($methodData['branches'] as $branch) {
$branchStructure .= $this->renderBranchLines($branch, $codeLines, $testData);
}
if ($branchStructure !== '') { // don't show empty branches
$branches .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, $this->htmlSpecialCharsFlags) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
$branches .= $branchStructure;
}
}
$branchesTemplate->setVar(['branches' => $branches]);
return $branchesTemplate->render();
}
private function renderBranchLines(array $branch, array $codeLines, array $testData): string
{
$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');
$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');
$lines = '';
$branchLines = range($branch['line_start'], $branch['line_end']);
sort($branchLines); // sometimes end_line < start_line
/** @var int $line */
foreach ($branchLines as $line) {
if (!isset($codeLines[$line])) { // blank line at end of file is sometimes included here
continue;
}
$popoverContent = '';
$popoverTitle = '';
$numTests = count($branch['hit']);
if ($numTests === 0) {
$trClass = 'danger';
} else {
$lineCss = 'covered-by-large-tests';
$popoverContent = '<ul>';
if ($numTests > 1) {
$popoverTitle = $numTests . ' tests cover this branch';
} else {
$popoverTitle = '1 test covers this branch';
}
foreach ($branch['hit'] as $test) {
if ($lineCss === 'covered-by-large-tests' && $testData[$test]['size'] === 'medium') {
$lineCss = 'covered-by-medium-tests';
} elseif ($testData[$test]['size'] === 'small') {
$lineCss = 'covered-by-small-tests';
}
$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);
}
$trClass = $lineCss . ' popin';
}
$popover = '';
if (!empty($popoverTitle)) {
$popover = sprintf(
' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
);
}
$lines .= $this->renderLine($singleLineTemplate, $line, $codeLines[$line - 1], $trClass, $popover);
}
if ($lines === '') {
return '';
}
$linesTemplate->setVar(['lines' => $lines]);
return $linesTemplate->render();
}
private function renderPathStructure(FileNode $node): string
{
$pathsTemplate = new Template($this->templatePath . 'paths.html.dist', '{{', '}}');
$coverageData = $node->functionCoverageData();
$testData = $node->testData();
$codeLines = $this->loadFile($node->pathAsString());
$paths = '';
ksort($coverageData);
foreach ($coverageData as $methodName => $methodData) {
if (!$methodData['paths']) {
continue;
}
$pathStructure = '';
if (count($methodData['paths']) > 100) {
$pathStructure .= '<p>' . count($methodData['paths']) . ' is too many paths to sensibly render, consider refactoring your code to bring this number down.</p>';
continue;
}
foreach ($methodData['paths'] as $path) {
$pathStructure .= $this->renderPathLines($path, $methodData['branches'], $codeLines, $testData);
}
if ($pathStructure !== '') {
$paths .= '<h5 class="structure-heading"><a name="' . htmlspecialchars($methodName, $this->htmlSpecialCharsFlags) . '">' . $this->abbreviateMethodName($methodName) . '</a></h5>' . "\n";
$paths .= $pathStructure;
}
}
$pathsTemplate->setVar(['paths' => $paths]);
return $pathsTemplate->render();
}
private function renderPathLines(array $path, array $branches, array $codeLines, array $testData): string
{
$linesTemplate = new Template($this->templatePath . 'lines.html.dist', '{{', '}}');
$singleLineTemplate = new Template($this->templatePath . 'line.html.dist', '{{', '}}');
$lines = '';
foreach ($path['path'] as $branchId) {
$branchLines = range($branches[$branchId]['line_start'], $branches[$branchId]['line_end']);
sort($branchLines); // sometimes end_line < start_line
/** @var int $line */
foreach ($branchLines as $line) {
if (!isset($codeLines[$line])) { // blank line at end of file is sometimes included here
continue;
}
$popoverContent = '';
$popoverTitle = '';
$numTests = count($path['hit']);
if ($numTests === 0) {
$trClass = 'danger';
} else {
$lineCss = 'covered-by-large-tests';
$popoverContent = '<ul>';
if ($numTests > 1) {
$popoverTitle = $numTests . ' tests cover this path';
} else {
$popoverTitle = '1 test covers this path';
}
foreach ($path['hit'] as $test) {
if ($lineCss === 'covered-by-large-tests' && $testData[$test]['size'] === 'medium') {
$lineCss = 'covered-by-medium-tests';
} elseif ($testData[$test]['size'] === 'small') {
$lineCss = 'covered-by-small-tests';
}
$popoverContent .= $this->createPopoverContentForTest($test, $testData[$test]);
}
$trClass = $lineCss . ' popin';
}
$popover = '';
if (!empty($popoverTitle)) {
$popover = sprintf(
' data-title="%s" data-content="%s" data-placement="top" data-html="true"',
$popoverTitle,
htmlspecialchars($popoverContent, $this->htmlSpecialCharsFlags)
);
}
$lines .= $this->renderLine($singleLineTemplate, $line, $codeLines[$line - 1], $trClass, $popover);
}
}
if ($lines === '') {
return '';
}
$linesTemplate->setVar(['lines' => $lines]);
return $linesTemplate->render();
}
private function renderLine(Template $template, int $lineNumber, string $lineContent, string $class, string $popover): string
{
$template->setVar(
[
'lineNumber' => $lineNumber,
'lineContent' => $lineContent,
'class' => $class,
'popover' => $popover,
]
);
return $template->render();
}
private function loadFile(string $file): array
{
if (isset(self::$formattedSourceCache[$file])) {
return self::$formattedSourceCache[$file];
}
$buffer = file_get_contents($file);
$tokens = token_get_all($buffer);
$result = [''];
$i = 0;
$stringFlag = false;
$fileEndsWithNewLine = substr($buffer, -1) === "\n";
unset($buffer);
foreach ($tokens as $j => $token) {
if (is_string($token)) {
if ($token === '"' && $tokens[$j - 1] !== '\\') {
$result[$i] .= sprintf(
'<span class="string">%s</span>',
htmlspecialchars($token, $this->htmlSpecialCharsFlags)
);
$stringFlag = !$stringFlag;
} else {
$result[$i] .= sprintf(
'<span class="keyword">%s</span>',
htmlspecialchars($token, $this->htmlSpecialCharsFlags)
);
}
continue;
}
[$token, $value] = $token;
$value = str_replace(
["\t", ' '],
['&nbsp;&nbsp;&nbsp;&nbsp;', '&nbsp;'],
htmlspecialchars($value, $this->htmlSpecialCharsFlags)
);
if ($value === "\n") {
$result[++$i] = '';
} else {
$lines = explode("\n", $value);
foreach ($lines as $jj => $line) {
$line = trim($line);
if ($line !== '') {
if ($stringFlag) {
$colour = 'string';
} else {
$colour = 'default';
if ($this->isInlineHtml($token)) {
$colour = 'html';
} elseif ($this->isComment($token)) {
$colour = 'comment';
} elseif ($this->isKeyword($token)) {
$colour = 'keyword';
}
}
$result[$i] .= sprintf(
'<span class="%s">%s</span>',
$colour,
$line
);
}
if (isset($lines[$jj + 1])) {
$result[++$i] = '';
}
}
}
}
if ($fileEndsWithNewLine) {
unset($result[count($result) - 1]);
}
self::$formattedSourceCache[$file] = $result;
return $result;
}
private function abbreviateClassName(string $className): string
{
$tmp = explode('\\', $className);
if (count($tmp) > 1) {
$className = sprintf(
'<abbr title="%s">%s</abbr>',
$className,
array_pop($tmp)
);
}
return $className;
}
private function abbreviateMethodName(string $methodName): string
{
$parts = explode('->', $methodName);
if (count($parts) === 2) {
return $this->abbreviateClassName($parts[0]) . '->' . $parts[1];
}
return $methodName;
}
private function createPopoverContentForTest(string $test, array $testData): string
{
$testCSS = '';
if ($testData['fromTestcase']) {
switch ($testData['status']) {
case BaseTestRunner::STATUS_PASSED:
switch ($testData['size']) {
case 'small':
$testCSS = ' class="covered-by-small-tests"';
break;
case 'medium':
$testCSS = ' class="covered-by-medium-tests"';
break;
default:
$testCSS = ' class="covered-by-large-tests"';
break;
}
break;
case BaseTestRunner::STATUS_SKIPPED:
case BaseTestRunner::STATUS_INCOMPLETE:
case BaseTestRunner::STATUS_RISKY:
case BaseTestRunner::STATUS_WARNING:
$testCSS = ' class="warning"';
break;
case BaseTestRunner::STATUS_FAILURE:
case BaseTestRunner::STATUS_ERROR:
$testCSS = ' class="danger"';
break;
}
}
return sprintf(
'<li%s>%s</li>',
$testCSS,
htmlspecialchars($test, $this->htmlSpecialCharsFlags)
);
}
private function isComment(int $token): bool
{
return $token === T_COMMENT || $token === T_DOC_COMMENT;
}
private function isInlineHtml(int $token): bool
{
return $token === T_INLINE_HTML;
}
private function isKeyword(int $token): bool
{
return isset(self::keywordTokens()[$token]);
}
/**
* @psalm-return array<int,true>
*/
private static function keywordTokens(): array
{
if (self::$keywordTokens !== []) {
return self::$keywordTokens;
}
self::$keywordTokens = [
T_ABSTRACT => true,
T_ARRAY => true,
T_AS => true,
T_BREAK => true,
T_CALLABLE => true,
T_CASE => true,
T_CATCH => true,
T_CLASS => true,
T_CLONE => true,
T_CONST => true,
T_CONTINUE => true,
T_DECLARE => true,
T_DEFAULT => true,
T_DO => true,
T_ECHO => true,
T_ELSE => true,
T_ELSEIF => true,
T_EMPTY => true,
T_ENDDECLARE => true,
T_ENDFOR => true,
T_ENDFOREACH => true,
T_ENDIF => true,
T_ENDSWITCH => true,
T_ENDWHILE => true,
T_EVAL => true,
T_EXIT => true,
T_EXTENDS => true,
T_FINAL => true,
T_FINALLY => true,
T_FOR => true,
T_FOREACH => true,
T_FUNCTION => true,
T_GLOBAL => true,
T_GOTO => true,
T_HALT_COMPILER => true,
T_IF => true,
T_IMPLEMENTS => true,
T_INCLUDE => true,
T_INCLUDE_ONCE => true,
T_INSTANCEOF => true,
T_INSTEADOF => true,
T_INTERFACE => true,
T_ISSET => true,
T_LIST => true,
T_NAMESPACE => true,
T_NEW => true,
T_PRINT => true,
T_PRIVATE => true,
T_PROTECTED => true,
T_PUBLIC => true,
T_REQUIRE => true,
T_REQUIRE_ONCE => true,
T_RETURN => true,
T_STATIC => true,
T_SWITCH => true,
T_THROW => true,
T_TRAIT => true,
T_TRY => true,
T_UNSET => true,
T_USE => true,
T_VAR => true,
T_WHILE => true,
T_YIELD => true,
T_YIELD_FROM => true,
];
if (defined('T_FN')) {
self::$keywordTokens[constant('T_FN')] = true;
}
if (defined('T_MATCH')) {
self::$keywordTokens[constant('T_MATCH')] = true;
}
if (defined('T_ENUM')) {
self::$keywordTokens[constant('T_ENUM')] = true;
}
if (defined('T_READONLY')) {
self::$keywordTokens[constant('T_READONLY')] = true;
}
return self::$keywordTokens;
}
}
@@ -0,0 +1,9 @@
<hr/>
<h4>Branches</h4>
<p>
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an <code>if</code> statement
<i>always</i> has an <code>else</code> as part of its logical flow even if you didn't write one.
</p>
{{branches}}
@@ -0,0 +1,5 @@
<div class="progress">
<div class="progress-bar bg-{{level}}" role="progressbar" aria-valuenow="{{percent}}" aria-valuemin="0" aria-valuemax="100" style="width: {{percent}}%">
<span class="sr-only">{{percent}}% covered ({{level}})</span>
</div>
</div>
@@ -0,0 +1,5 @@
<div class="progress">
<div class="progress-bar bg-{{level}}" role="progressbar" aria-valuenow="{{percent}}" aria-valuemin="0" aria-valuemax="100" style="width: {{percent}}%">
<span class="sr-only">{{percent}}% covered ({{level}})</span>
</div>
</div>
@@ -0,0 +1,7 @@
/*!
* Bootstrap v4.6.1 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{maLine truncated
/*# sourceMappingURL=bootstrap.min.css.map */
Whitespace-only changes.
@@ -0,0 +1 @@
.nvd3 .nv-axis{pointer-events:none;opacity:1}.nvd3 .nv-axis path{fill:none;stroke:#000;stroke-opacity:.75;shape-rendering:crispEdges}.nvd3 .nv-axis path.domain{stroke-opacity:.75}.nvd3 .nv-axis.nv-x path.domain{stroke-opacity:0}.nvd3 .nv-axis line{fill:none;stroke:#e5e5e5;shape-rendering:crispEdges}.nvd3 .nv-axis .zero line,.nvd3 .nv-axis line.zero{stroke-opacity:.75}.nvd3 .nv-axis .nv-axisMaxMin text{font-weight:700}.nvd3 .x .nv-axis .nv-axisMaxMin text,.nvd3 .x2 .nv-axis .nv-axisMaxMin text,.nvd3 .x3 .nv-axis .nv-axisMaxMin text{text-anchor:middle}.nvd3 .nv-axis.nv-disabled{opacity:0}.nvd3 .nv-bars rect{fill-opacity:.75;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-bars rect.hover{fill-opacity:1}.nvd3 .nv-bars .hover rect{fill:#add8e6}.nvd3 .nv-bars text{fill:rgba(0,0,0,0)}.nvd3 .nv-bars .hover text{fill:rgba(0,0,0,1)}.nvd3 .nv-multibar .nv-groups rect,.nvd3 .nv-multibarHorizontal .nv-groups rect,.nvd3 .nv-discretebar .nv-groups rect{stroke-opacity:0;transition:fill-opacity 250ms linear;-moz-transition:fill-opacity 250ms linear;-webkit-transition:fill-opacity 250ms linear}.nvd3 .nv-multibar .nv-groups rect:hover,.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,.nvd3 .nv-candlestickBar .nv-ticks rect:hover,.nvd3 .nv-discretebar .nv-groups rect:hover{fill-opacity:1}.nvd3 .nv-discretebar .nv-groups text,.nvd3 .nv-multibarHorizontal .nv-groups text{font-weight:700;fill:rgba(0,0,0,1);strokLine truncated
@@ -0,0 +1,5 @@
.octicon {
display: inline-block;
vertical-align: text-top;
fill: currentColor;
}
@@ -0,0 +1,122 @@
body {
padding-top: 10px;
}
.popover {
max-width: none;
}
.octicon {
margin-right:.25em;
}
.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, li.covered-by-large-tests, tr.success, td.success, li.success, span.success {
background-color: #dff0d8;
}
.table tbody tr.covered-by-medium-tests, li.covered-by-medium-tests {
background-color: #c3e3b5;
}
.table tbody tr.covered-by-small-tests, li.covered-by-small-tests {
background-color: #99cb84;
}
.table tbody tr.danger, .table tbody td.danger, li.danger, span.danger {
background-color: #f2dede;
}
.table tbody tr.warning, .table tbody td.warning, li.warning, span.warning {
background-color: #fcf8e3;
}
.table tbody td.info {
background-color: #d9edf7;
}
td.big {
width: 117px;
}
td.small {
}
td.codeLine {
font-family: "Source Code Pro", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
white-space: pre-wrap;
}
td span.comment {
color: #888a85;
}
td span.default {
color: #2e3436;
}
td span.html {
color: #888a85;
}
td span.keyword {
color: #2e3436;
font-weight: bold;
}
pre span.string {
color: #2e3436;
}
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: "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif;
font-size: 11px;
color: #666;
fill: #666;
}
.scrollbox {
height:245px;
overflow-x:hidden;
overflow-y:scroll;
}
table + .structure-heading {
border-top: 1px solid lightgrey;
padding-top: 0.5em;
}
@@ -0,0 +1,281 @@
<!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" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/nv.d3.min.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" 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="classCoverageDistribution" 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>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>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>Methods</h2>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="methodCoverageDistribution" 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>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>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="{{path_to_root}}_js/jquery.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/d3.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/nv.d3.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
nv.addGraph(function() {
var chart = nv.models.multiBarChart();
chart.tooltips(false)
.showControls(false)
.showLegend(false)
.reduceXTicks(false)
.staggerLabels(true)
.yAxis.tickFormat(d3.format('d'));
d3.select('#classCoverageDistribution svg')
.datum(getCoverageDistributionData({{class_coverage_distribution}}, "Class Coverage"))
.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
nv.addGraph(function() {
var chart = nv.models.multiBarChart();
chart.tooltips(false)
.showControls(false)
.showLegend(false)
.reduceXTicks(false)
.staggerLabels(true)
.yAxis.tickFormat(d3.format('d'));
d3.select('#methodCoverageDistribution svg')
.datum(getCoverageDistributionData({{method_coverage_distribution}}, "Method Coverage"))
.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
function getCoverageDistributionData(data, label) {
var labels = [
'0%',
'0-10%',
'10-20%',
'20-30%',
'30-40%',
'40-50%',
'50-60%',
'60-70%',
'70-80%',
'80-90%',
'90-100%',
'100%'
];
var values = [];
$.each(labels, function(key) {
values.push({x: labels[key], y: data[key]});
});
return [
{
key: label,
values: values,
color: "#4572A7"
}
];
}
nv.addGraph(function() {
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.showLegend(false)
.forceX([0, 100]);
chart.tooltipContent(function(graph) {
return '<p>' + graph.point.class + '</p>';
});
chart.xAxis.axisLabel('Code Coverage (in percent)');
chart.yAxis.axisLabel('Cyclomatic Complexity');
d3.select('#classComplexity svg')
.datum(getComplexityData({{complexity_class}}, 'Class Complexity'))
.transition()
.duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
nv.addGraph(function() {
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.showLegend(false)
.forceX([0, 100]);
chart.tooltipContent(function(graph) {
return '<p>' + graph.point.class + '</p>';
});
chart.xAxis.axisLabel('Code Coverage (in percent)');
chart.yAxis.axisLabel('Method Complexity');
d3.select('#methodComplexity svg')
.datum(getComplexityData({{complexity_method}}, 'Method Complexity'))
.transition()
.duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
function getComplexityData(data, label) {
var values = [];
$.each(data, function(key) {
var value = Math.round(data[key][0]*100) / 100;
values.push({
x: value,
y: data[key][1],
class: data[key][2],
size: 0.05,
shape: 'diamond'
});
});
return [
{
key: label,
values: values,
color: "#4572A7"
}
];
}
});
</script>
</body>
</html>
@@ -0,0 +1,281 @@
<!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" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/nv.d3.min.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" 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="classCoverageDistribution" 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>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>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>Methods</h2>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Coverage Distribution</h3>
<div id="methodCoverageDistribution" 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>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>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="{{path_to_root}}_js/jquery.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/d3.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/nv.d3.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
nv.addGraph(function() {
var chart = nv.models.multiBarChart();
chart.tooltips(false)
.showControls(false)
.showLegend(false)
.reduceXTicks(false)
.staggerLabels(true)
.yAxis.tickFormat(d3.format('d'));
d3.select('#classCoverageDistribution svg')
.datum(getCoverageDistributionData({{class_coverage_distribution}}, "Class Coverage"))
.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
nv.addGraph(function() {
var chart = nv.models.multiBarChart();
chart.tooltips(false)
.showControls(false)
.showLegend(false)
.reduceXTicks(false)
.staggerLabels(true)
.yAxis.tickFormat(d3.format('d'));
d3.select('#methodCoverageDistribution svg')
.datum(getCoverageDistributionData({{method_coverage_distribution}}, "Method Coverage"))
.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
function getCoverageDistributionData(data, label) {
var labels = [
'0%',
'0-10%',
'10-20%',
'20-30%',
'30-40%',
'40-50%',
'50-60%',
'60-70%',
'70-80%',
'80-90%',
'90-100%',
'100%'
];
var values = [];
$.each(labels, function(key) {
values.push({x: labels[key], y: data[key]});
});
return [
{
key: label,
values: values,
color: "#4572A7"
}
];
}
nv.addGraph(function() {
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.showLegend(false)
.forceX([0, 100]);
chart.tooltipContent(function(graph) {
return '<p>' + graph.point.class + '</p>';
});
chart.xAxis.axisLabel('Code Coverage (in percent)');
chart.yAxis.axisLabel('Cyclomatic Complexity');
d3.select('#classComplexity svg')
.datum(getComplexityData({{complexity_class}}, 'Class Complexity'))
.transition()
.duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
nv.addGraph(function() {
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.showLegend(false)
.forceX([0, 100]);
chart.tooltipContent(function(graph) {
return '<p>' + graph.point.class + '</p>';
});
chart.xAxis.axisLabel('Code Coverage (in percent)');
chart.yAxis.axisLabel('Method Complexity');
d3.select('#methodComplexity svg')
.datum(getComplexityData({{complexity_method}}, 'Method Complexity'))
.transition()
.duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
function getComplexityData(data, label) {
var values = [];
$.each(data, function(key) {
var value = Math.round(data[key][0]*100) / 100;
values.push({
x: value,
y: data[key][1],
class: data[key][2],
size: 0.05,
shape: 'diamond'
});
});
return [
{
key: label,
values: values,
color: "#4572A7"
}
];
}
});
</script>
</body>
</html>
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<td>&nbsp;</td>
<td colspan="9"><div align="center"><strong>Code Coverage</strong></div></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="3"><div align="center"><strong>Lines</strong></div></td>
<td colspan="3"><div align="center"><strong>Functions and Methods</strong></div></td>
<td colspan="3"><div align="center"><strong>Classes and Traits</strong></div></td>
</tr>
</thead>
<tbody>
{{items}}
</tbody>
</table>
</div>
<footer>
<hr/>
<h4>Legend</h4>
<p>
<span class="danger"><strong>Low</strong>: 0% to {{low_upper_bound}}%</span>
<span class="warning"><strong>Medium</strong>: {{low_upper_bound}}% to {{high_lower_bound}}%</span>
<span class="success"><strong>High</strong>: {{high_lower_bound}}% to 100%</span>
</p>
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Code Coverage for {{full_path}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{path_to_root}}_css/bootstrap.min.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<td>&nbsp;</td>
<td colspan="15"><div align="center"><strong>Code Coverage</strong></div></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="3"><div align="center"><strong>Lines</strong></div></td>
<td colspan="3"><div align="center"><strong>Branches</strong></div></td>
<td colspan="3"><div align="center"><strong>Paths</strong></div></td>
<td colspan="3"><div align="center"><strong>Functions and Methods</strong></div></td>
<td colspan="3"><div align="center"><strong>Classes and Traits</strong></div></td>
</tr>
</thead>
<tbody>
{{items}}
</tbody>
</table>
</div>
<footer>
<hr/>
<h4>Legend</h4>
<p>
<span class="danger"><strong>Low</strong>: 0% to {{low_upper_bound}}%</span>
<span class="warning"><strong>Medium</strong>: {{low_upper_bound}}% to {{high_lower_bound}}%</span>
<span class="success"><strong>High</strong>: {{high_lower_bound}}% to 100%</span>
</p>
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
</footer>
</div>
</body>
</html>
@@ -0,0 +1,13 @@
<tr>
<td class="{{lines_level}}">{{icon}}{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{classes_level}} big">{{classes_bar}}</td>
<td class="{{classes_level}} small"><div align="right">{{classes_tested_percent}}</div></td>
<td class="{{classes_level}} small"><div align="right">{{classes_number}}</div></td>
</tr>
@@ -0,0 +1,19 @@
<tr>
<td class="{{lines_level}}">{{icon}}{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{branches_level}} big">{{branches_bar}}</td>
<td class="{{branches_level}} small"><div align="right">{{branches_executed_percent}}</div></td>
<td class="{{branches_level}} small"><div align="right">{{branches_number}}</div></td>
<td class="{{paths_level}} big">{{paths_bar}}</td>
<td class="{{paths_level}} small"><div align="right">{{paths_executed_percent}}</div></td>
<td class="{{paths_level}} small"><div align="right">{{paths_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{classes_level}} big">{{classes_bar}}</td>
<td class="{{classes_level}} small"><div align="right">{{classes_tested_percent}}</div></td>
<td class="{{classes_level}} small"><div align="right">{{classes_number}}</div></td>
</tr>
@@ -0,0 +1,65 @@
<!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" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<td>&nbsp;</td>
<td colspan="10"><div align="center"><strong>Code Coverage</strong></div></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="3"><div align="center"><strong>Lines</strong></div></td>
<td colspan="4"><div align="center"><strong>Functions and Methods</strong></div></td>
<td colspan="3"><div align="center"><strong>Classes and Traits</strong></div></td>
</tr>
</thead>
<tbody>
{{items}}
</tbody>
</table>
</div>
{{lines}}
{{structure}}
<footer>
<hr/>
<h4>Legend</h4>
{{legend}}
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
<a title="Back to the top" id="toplink" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16"><path fill-rule="evenodd" d="M12 11L6 5l-6 6h12z"/></svg>
</a>
</footer>
</div>
<script src="{{path_to_root}}_js/jquery.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/popper.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/bootstrap.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/file.js" type="text/javascript"></script>
</body>
</html>
@@ -0,0 +1,67 @@
<!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" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/octicons.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/style.css" rel="stylesheet" type="text/css">
<link href="{{path_to_root}}_css/custom.css" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{{breadcrumbs}}
</ol>
</nav>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<td>&nbsp;</td>
<td colspan="16"><div align="center"><strong>Code Coverage</strong></div></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="3"><div align="center"><strong>Lines</strong></div></td>
<td colspan="3"><div align="center"><strong>Branches</strong></div></td>
<td colspan="3"><div align="center"><strong>Paths</strong></div></td>
<td colspan="4"><div align="center"><strong>Functions and Methods</strong></div></td>
<td colspan="3"><div align="center"><strong>Classes and Traits</strong></div></td>
</tr>
</thead>
<tbody>
{{items}}
</tbody>
</table>
</div>
{{lines}}
{{structure}}
<footer>
<hr/>
<h4>Legend</h4>
{{legend}}
<p>
<small>Generated by <a href="https://github.com/sebastianbergmann/php-code-coverage" target="_top">php-code-coverage {{version}}</a> using {{runtime}}{{generator}} at {{date}}.</small>
</p>
<a title="Back to the top" id="toplink" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16"><path fill-rule="evenodd" d="M12 11L6 5l-6 6h12z"/></svg>
</a>
</footer>
</div>
<script src="{{path_to_root}}_js/jquery.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/popper.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/bootstrap.min.js" type="text/javascript"></script>
<script src="{{path_to_root}}_js/file.js" type="text/javascript"></script>
</body>
</html>
@@ -0,0 +1,14 @@
<tr>
<td class="{{classes_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{methods_level}} small">{{crap}}</td>
<td class="{{classes_level}} big">{{classes_bar}}</td>
<td class="{{classes_level}} small"><div align="right">{{classes_tested_percent}}</div></td>
<td class="{{classes_level}} small"><div align="right">{{classes_number}}</div></td>
</tr>
@@ -0,0 +1,20 @@
<tr>
<td class="{{classes_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{branches_level}} big">{{branches_bar}}</td>
<td class="{{branches_level}} small"><div align="right">{{branches_executed_percent}}</div></td>
<td class="{{branches_level}} small"><div align="right">{{branches_number}}</div></td>
<td class="{{paths_level}} big">{{paths_bar}}</td>
<td class="{{paths_level}} small"><div align="right">{{paths_executed_percent}}</div></td>
<td class="{{paths_level}} small"><div align="right">{{paths_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{methods_level}} small">{{crap}}</td>
<td class="{{classes_level}} big">{{classes_bar}}</td>
<td class="{{classes_level}} small"><div align="right">{{classes_tested_percent}}</div></td>
<td class="{{classes_level}} small"><div align="right">{{classes_number}}</div></td>
</tr>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="16" viewBox="0 0 12 16"><path fill-rule="evenodd" d="M8.5 1H1c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V4.5L8.5 1zM11 14H1V2h7l3 3v9zM5 6.98L3.5 8.5 5 10l-.5 1L2 8.5 4.5 6l.5.98zM7.5 6L10 8.5 7.5 11l-.5-.98L8.5 8.5 7 7l.5-1z"/></svg>

After

Width:  |  Height:  |  Size: 304 B

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

After

Width:  |  Height:  |  Size: 234 B

@@ -0,0 +1,7 @@
/*!
* Bootstrap v4.6.1 (https://getbootstrap.com/)
* Copyright 2011-2021 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?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap={},t.jQuery,t.Popper)}(this,(function(t,e,n){"use strict";function i(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var o=i(e),a=i(n);function s(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,i.key,i)}}function l(t,e,n){return e&&s(t.prototype,e),n&&s(t,n),t}function r(){return r=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(t[i]=n[i])}return t},r.apply(this,arguments)}function u(t,e){return u=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t},u(t,e)}var f="transitionend";var d={TRANSITION_END:"bsTransitionEnd",getUID:function(t){do{t+=~~(1e6*Math.random())}while(document.getElementById(t));return t},getSelectorFromElement:function(t){var e=t.getAttribute("data-target");if(!e||"#"===e){var n=t.getAttribute("href");e=n&&"#"!==n?n.trim():""}try{return document.querySelector(e)?e:null}catch(t){return null}},getTransitionDurationFromElement:function(t){if(!t)return 0;var e=o.default(t).css("transition-duration"),n=o.default(t).css("transition-delay"),i=parseFloat(e),a=parseFloat(n);return i||a?(e=e.split("Line truncated
//# sourceMappingURL=bootstrap.min.js.map
@@ -0,0 +1,5 @@
!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function i(n){return!isNaN(n)}function u(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)<0?r=u+1:i=u}return r},right:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)>0?i=u:r=u+1}return r}}}function o(n){return n.length}function a(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function f(n){return(n+="")===bo||n[0]===_o?_o+n:n}function s(n){return(n+="")[0]===_o?n.slice(1):n}function h(n){return f(n)in this._}function p(n){return(n=f(n))in this._&&delete this._[n]}function g(){var n=[];for(var t in this._)n.push(s(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function y(){this._=Object.create(null)}function m(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=wo.length;r>e;++e){var i=wo[e]+t;if(i in n)return i}}function b(){}function _(){}function w(n){Line truncated
r(),S.point=c,S.lineEnd=f}function c(n,t){u(s=n,h=t),p=M,g=x,v=b,d=_,y=w,S.point=u}function f(){i(M,x,m,b,_,w,p,g,s,v,d,y,a,t),S.lineEnd=o,o()}var s,h,p,g,v,d,y,m,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function i(t,e,r,a,l,c,f,s,h,p,g,v,d,y){var m=f-t,M=s-e,x=m*m+M*M;if(x>4*u&&d--){var b=a+p,_=l+g,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=xo(xo(w)-1)<Uo||xo(r-h)<Uo?(r+h)/2:Math.atan2(_,b),E=n(N,k),A=E[0],C=E[1],z=A-t,L=C-e,q=M*z-m*L;(q*q/x>u||xo((m*z+M*L)/x-.5)>.3||o>a*p+l*g+c*v)&&(i(t,e,r,a,l,c,A,C,N,b/=S,_/=S,w,d,y),y.point(A,C),i(A,C,N,b,_,w,f,s,h,p,g,v,d,y))}}var u=.5,o=Math.cos(30*Yo),a=16;return t.precision=function(n){return arguments.length?(a=(u=n*n)>0&&16,t):Math.sqrt(u)},t}function re(n){var t=ee(function(t,e){return n([t*Zo,e*Zo])});return function(n){return le(t(n))}}function ie(n){this.stream=n}function ue(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function oe(n){return ae(function(){return n})()}function ae(n){function t(n){return n=a(n[0]*Yo,n[1]*Yo),[n[0]*h+l,c-n[1]*h]}function e(n){return n=a.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Zo,n[1]*Zo]}function r(){a=Ct(o=se(y,M,x),u);var n=u(v,d);return l=p-n[0]*h,c=g+n[1]*h,i()}function i(){return f&&(f.valid=!1,f=null),t}var u,o,a,lLine truncated
return p}:En(r),w=i===u?function(){return g}:En(u);++m<M;)o.call(this,h=t[m],m)?(d.push([p=+x.call(this,h,m),g=+b.call(this,h,m)]),y.push([+_.call(this,h,m),+w.call(this,h,m)])):d.length&&(l(),d=[],y=[]);return d.length&&l(),v.length?v.join(""):null}var e=Ce,r=Ce,i=0,u=ze,o=zt,a=xu,l=a.key,c=a,f="L",s=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return arguments.length?(i=u=n,t):u},t.y0=function(n){return arguments.length?(i=n,t):i},t.y1=function(n){return arguments.length?(u=n,t):u},t.defined=function(n){return arguments.length?(o=n,t):o},t.interpolate=function(n){return arguments.length?(l="function"==typeof n?a=n:(a=Tl.get(n)||xu).key,c=a.reverse||a,f=a.closed?"M":"L",t):l},t.tension=function(n){return arguments.length?(s=n,t):s},t}function Iu(n){return n.radius}function Yu(n){return[n.x,n.y]}function Zu(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]-Io;return[e*Math.cos(r),e*Math.sin(r)]}}function Vu(){return 64}function Xu(){return"circle"}function $u(n){var t=Math.sqrt(n/Fo);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+-t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function Bu(n){return function(){var t,e,r;(t=this[n])&&(r=t[e=t.active])&&(r.timer.c=null,r.timer.t=NaN,--t.count?delete t[e]:delete this[n],t.active+=.5,r.event&&r.event.interrupt.call(this,this.__data__,r.index))}}function Wu(n,t,e){return ko(n,Yl),n.namespace=t,n.id=e,n}fLine truncated
shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});ao.format=xa.numberFormat,ao.geo={},ft.prototype={s:0,t:0,add:function(n){st(n,this.t,ba),st(ba.s,this.s,this),this.s?this.t+=ba.t:this.s=ba.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var ba=new ft;ao.geo.stream=function(n,t){n&&_a.hasOwnProperty(n.type)?_a[n.type](n,t):ht(n,t)};var _a={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,i=e.length;++r<i;)ht(e[r].geometry,t)}},wa={Sphere:function(n,t){t.sphere()},Point:function(n,t){n=n.coordinates,t.point(n[0],n[1],n[2])},MultiPoint:function(n,t){for(var e=n.coordinates,r=-1,i=e.length;++r<i;)n=e[r],t.point(n[0],n[1],n[2])},LineString:function(n,t){pt(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coordinates,r=-1,i=e.length;++r<i;)pt(e[r],t,0)},Polygon:function(n,t){gt(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,i=e.length;++r<i;)gt(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,i=e.length;++r<i;)ht(e[r],t)}};ao.geo.area=function(n){return Sa=0,ao.geo.stream(n,Na),Sa};var Sa,ka=new ft,Na={sphere:function(){Sa+=4*Fo},point:b,lineStart:b,lineEnd:b,polygonStart:function(){ka.reset(),Na.lineStart=vt},polygonEnd:function(){var n=2*Line truncated
if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++u<o;)i=n[u],i.x=a,i.y=c,i.dy=f,a+=i.dx=Math.min(e.x+e.dx-a,f?l(i.area/f):0);i.z=!0,i.dx+=e.x+e.dx-a,e.y+=f,e.dy-=f}else{for((r||f>e.dx)&&(f=e.dx);++u<o;)i=n[u],i.x=a,i.y=c,i.dx=f,c+=i.dy=Math.min(e.y+e.dy-c,f?l(i.area/f):0);i.z=!1,i.dy+=e.y+e.dy-c,e.x+=f,e.dx-=f}}function u(r){var i=o||a(r),u=i[0];return u.x=u.y=0,u.value?(u.dx=c[0],u.dy=c[1]):u.dx=u.dy=0,o&&a.revalue(u),n([u],u.dx*u.dy/u.value),(o?e:t)(u),h&&(o=i),i}var o,a=ao.layout.hierarchy(),l=Math.round,c=[1,1],f=null,s=Oi,h=!1,p="squarify",g=.5*(1+Math.sqrt(5));return u.size=function(n){return arguments.length?(c=n,u):c},u.padding=function(n){function t(t){var e=n.call(u,t,t.depth);return null==e?Oi(t):Ii(t,"number"==typeof e?[e,e,e,e]:e)}function e(t){return Ii(t,n)}if(!arguments.length)return f;var r;return s=null==(f=n)?Oi:"function"==(r=typeof n)?t:"number"===r?(n=[n,n,n,n],e):e,u},u.round=function(n){return arguments.length?(l=n?Math.round:Number,u):l!=Number},u.sticky=function(n){return arguments.length?(h=n,o=null,u):h},u.ratio=function(n){return arguments.length?(g=n,u):g},u.mode=function(n){return arguments.length?(p=n+"",u):p},ii(u,a)},ao.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,i;do e=2*Math.random()-1,r=2*Math.random()-1,i=e*e+r*r;while(!i||i>1);return n+t*e*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(){var n=ao.random.normal.apply(ao,arguments);return function(){return Math.exp(n())}},bates:function(n){Line truncated
@@ -0,0 +1,62 @@
$(function() {
var $window = $(window)
, $top_link = $('#toplink')
, $body = $('body, html')
, offset = $('#code').offset().top
, hidePopover = function ($target) {
$target.data('popover-hover', false);
setTimeout(function () {
if (!$target.data('popover-hover')) {
$target.popover('hide');
}
}, 300);
};
$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();
}
}).scroll();
$('.popin')
.popover({trigger: 'manual'})
.on({
'mouseenter.popover': function () {
var $target = $(this);
var $container = $target.children().first();
$target.data('popover-hover', true);
// popover already displayed
if ($target.next('.popover').length) {
return;
}
// show the popover
$container.popover('show');
// register mouse events on the popover
$target.next('.popover:not(.popover-initialized)')
.on({
'mouseenter': function () {
$target.data('popover-hover', true);
},
'mouseleave': function () {
hidePopover($container);
}
})
.addClass('popover-initialized');
},
'mouseleave.popover': function () {
hidePopover($(this).children().first());
}
});
});
@@ -0,0 +1,2 @@
/*! jQuery v3.6.0 | (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(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}S.fn=S.prototype={jquery:f,constructor:S,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=S.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return S.each(this,e)},map:fLine truncated
@@ -0,0 +1,8 @@
/* nvd3 version 1.8.1 (https://github.com/novus/nvd3) 2015-06-15 */
!function(){var a={};a.dev=!1,a.tooltip=a.tooltip||{},a.utils=a.utils||{},a.models=a.models||{},a.charts={},a.logs={},a.dom={},a.dispatch=d3.dispatch("render_start","render_end"),Function.prototype.bind||(Function.prototype.bind=function(a){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var b=Array.prototype.slice.call(arguments,1),c=this,d=function(){},e=function(){return c.apply(this instanceof d&&a?this:a,b.concat(Array.prototype.slice.call(arguments)))};return d.prototype=this.prototype,e.prototype=new d,e}),a.dev&&(a.dispatch.on("render_start",function(){a.logs.startTime=+new Date}),a.dispatch.on("render_end",function(){a.logs.endTime=+new Date,a.logs.totalTime=a.logs.endTime-a.logs.startTime,a.log("total",a.logs.totalTime)})),a.log=function(){if(a.dev&&window.console&&console.log&&console.log.apply)console.log.apply(console,arguments);else if(a.dev&&window.console&&"function"==typeof console.log&&Function.prototype.bind){var b=Function.prototype.bind.call(console.log,console);b.apply(console,arguments)}return arguments[arguments.length-1]},a.deprecated=function(a,b){console&&console.warn&&console.warn("nvd3 warning: `"+a+"` has been deprecated. ",b||"")},a.render=function(b){b=b||1,a.render.active=!0,a.dispatch.render_start();var c=function(){for(var d,e,f=0;b>f&&(e=a.render.queue[f]);f++)d=e.generate(),typeof e.callback==typeof Function&&e.callback(d);a.render.queue.splice(0,f),a.render.queue.lengLine truncated
x.append("g").attr("class","nv-x nv-axis"),x.append("g").attr("class","nv-y nv-axis").append("g").attr("class","nv-zeroLine").append("line"),x.append("g").attr("class","nv-barsWrap"),z.attr("transform","translate("+h.left+","+h.top+")"),n&&z.select(".nv-y.nv-axis").attr("transform","translate("+t+",0)"),e.width(t).height(u);var A=z.select(".nv-barsWrap").datum(k.filter(function(a){return!a.disabled}));if(A.transition().call(e),y.append("clipPath").attr("id","nv-x-label-clip-"+e.id()).append("rect"),z.select("#nv-x-label-clip-"+e.id()+" rect").attr("width",c.rangeBand()*(o?2:1)).attr("height",16).attr("x",-c.rangeBand()/(o?1:2)),l){f.scale(c).ticks(a.utils.calcTicksX(t/100,k)).tickSize(-u,0),z.select(".nv-x.nv-axis").attr("transform","translate(0,"+d.range()[0]+")"),z.select(".nv-x.nv-axis").call(f);var B=z.select(".nv-x.nv-axis").selectAll("g");o&&B.selectAll("text").attr("transform",function(a,b,c){return"translate(0,"+(c%2==0?"5":"17")+")"})}m&&(g.scale(d).ticks(Math.floor(u/36)).tickSize(-t,0),z.select(".nv-y.nv-axis").call(g)),z.select(".nv-zeroLine line").attr("x1",0).attr("x2",t).attr("y1",d(0)).attr("y2",d(0))}),t.renderEnd("nv-boxplot chart immediate"),b}var c,d,e=a.models.boxPlot(),f=a.models.axis(),g=a.models.axis(),h={top:15,right:10,bottom:50,left:60},i=null,j=null,k=a.utils.getColor(),l=!0,m=!0,n=!1,o=!1,p=a.models.tooltip(),q="No Data Available.",r=d3.dispatch("tooltipShow","tooltipHide","beforeUpdate","renderEnd"),s=250;f.orient("bottom").showMaxMin(!1).tickFoLine truncated
var z=x.select(".nv-x.nv-axis").selectAll("g");p&&z.selectAll("text").attr("transform",function(a,b,c){return"translate(0,"+(c%2==0?"5":"17")+")"})}n&&(g.scale(d)._ticks(a.utils.calcTicksY(t/36,h)).tickSize(-q,0),x.select(".nv-y.nv-axis").call(g)),x.select(".nv-zeroLine line").attr("x1",0).attr("x2",q).attr("y1",d(0)).attr("y2",d(0))}),t.renderEnd("discreteBar chart immediate"),b}var c,d,e=a.models.discreteBar(),f=a.models.axis(),g=a.models.axis(),h=a.models.tooltip(),i={top:15,right:10,bottom:50,left:60},j=null,k=null,l=a.utils.getColor(),m=!0,n=!0,o=!1,p=!1,q=null,r=d3.dispatch("beforeUpdate","renderEnd"),s=250;f.orient("bottom").showMaxMin(!1).tickFormat(function(a){return a}),g.orient(o?"right":"left").tickFormat(d3.format(",.1f")),h.duration(0).headerEnabled(!1).valueFormatter(function(a,b){return g.tickFormat()(a,b)}).keyFormatter(function(a,b){return f.tickFormat()(a,b)});var t=a.utils.renderWatch(r,s);return e.dispatch.on("elementMouseover.tooltip",function(a){a.series={key:b.x()(a.data),value:b.y()(a.data),color:a.color},h.data(a).hidden(!1)}),e.dispatch.on("elementMouseout.tooltip",function(){h.hidden(!0)}),e.dispatch.on("elementMousemove.tooltip",function(){h.position({top:d3.event.pageY,left:d3.event.pageX})()}),b.dispatch=r,b.discretebar=e,b.xAxis=f,b.yAxis=g,b.tooltip=h,b.options=a.utils.optionsFunc.bind(b),b._options=Object.create({},{width:{get:function(){return j},set:function(a){j=a}},height:{get:function(){return k},set:function(a){k=a}},staggerLabels:{getLine truncated
}if(!(j&&j.length&&j.filter(function(a){return a.values.length}).length))return a.utils.noData(b,v),b;v.selectAll(".nv-noData").remove(),c=e.xScale(),d=e.yScale();var E=v.selectAll("g.nv-wrap.nv-lineChart").data([j]),F=E.enter().append("g").attr("class","nvd3 nv-wrap nv-lineChart").append("g"),G=E.select("g");F.append("rect").style("opacity",0),F.append("g").attr("class","nv-x nv-axis"),F.append("g").attr("class","nv-y nv-axis"),F.append("g").attr("class","nv-linesWrap"),F.append("g").attr("class","nv-legendWrap"),F.append("g").attr("class","nv-interactive"),G.select("rect").attr("width",B).attr("height",C>0?C:0),o&&(h.width(B),G.select(".nv-legendWrap").datum(j).call(h),k.top!=h.height()&&(k.top=h.height(),C=a.utils.availableHeight(n,v,k)),E.select(".nv-legendWrap").attr("transform","translate(0,"+-k.top+")")),E.attr("transform","translate("+k.left+","+k.top+")"),r&&G.select(".nv-y.nv-axis").attr("transform","translate("+B+",0)"),s&&(i.width(B).height(C).margin({left:k.left,top:k.top}).svgContainer(v).xScale(c),E.select(".nv-interactive").call(i)),e.width(B).height(C).color(j.map(function(a,b){return a.color||l(a,b)}).filter(function(a,b){return!j[b].disabled}));var H=G.select(".nv-linesWrap").datum(j.filter(function(a){return!a.disabled}));H.call(e),p&&(f.scale(c)._ticks(a.utils.calcTicksX(B/100,j)).tickSize(-C,0),G.select(".nv-x.nv-axis").attr("transform","translate(0,"+d.range()[0]+")"),G.select(".nv-x.nv-axis").call(f)),q&&(g.scale(d)._ticks(a.utils.calcTicksY(C/36,j)).Line truncated
var J=z.selectAll("g.nv-wrap.nv-multiBarWithLegend").data([j]),K=J.enter().append("g").attr("class","nvd3 nv-wrap nv-multiBarWithLegend").append("g"),L=J.select("g");if(K.append("g").attr("class","nv-x nv-axis"),K.append("g").attr("class","nv-y nv-axis"),K.append("g").attr("class","nv-barsWrap"),K.append("g").attr("class","nv-legendWrap"),K.append("g").attr("class","nv-controlsWrap"),q&&(h.width(D-B()),L.select(".nv-legendWrap").datum(j).call(h),k.top!=h.height()&&(k.top=h.height(),H=a.utils.availableHeight(m,z,k)),L.select(".nv-legendWrap").attr("transform","translate("+B()+","+-k.top+")")),o){var M=[{key:p.grouped||"Grouped",disabled:e.stacked()},{key:p.stacked||"Stacked",disabled:!e.stacked()}];i.width(B()).color(["#444","#444","#444"]),L.select(".nv-controlsWrap").datum(M).attr("transform","translate(0,"+-k.top+")").call(i)}J.attr("transform","translate("+k.left+","+k.top+")"),t&&L.select(".nv-y.nv-axis").attr("transform","translate("+D+",0)"),e.disabled(j.map(function(a){return a.disabled})).width(D).height(H).color(j.map(function(a,b){return a.color||n(a,b)}).filter(function(a,b){return!j[b].disabled}));var N=L.select(".nv-barsWrap").datum(j.filter(function(a){return!a.disabled}));if(N.call(e),r){f.scale(c)._ticks(a.utils.calcTicksX(D/100,j)).tickSize(-H,0),L.select(".nv-x.nv-axis").attr("transform","translate(0,"+d.range()[0]+")"),L.select(".nv-x.nv-axis").call(f);var O=L.select(".nv-x.nv-axis > g").selectAll("g");if(O.selectAll("line, text").style("opacity",1),v){varLine truncated
}},color:{get:function(){return x},set:function(b){x=a.utils.getColor(b)}}}),a.utils.initOptions(b),b},a.models.parallelCoordinates=function(){"use strict";function b(p){return p.each(function(b){function p(a){return F(h.map(function(b){if(isNaN(a[b])||isNaN(parseFloat(a[b]))){var c=g[b].domain(),d=g[b].range(),e=c[0]-(c[1]-c[0])/9;if(J.indexOf(b)<0){var h=d3.scale.linear().domain([e,c[1]]).range([x-12,d[1]]);g[b].brush.y(h),J.push(b)}return[f(b),g[b](e)]}return J.length>0?(D.style("display","inline"),E.style("display","inline")):(D.style("display","none"),E.style("display","none")),[f(b),g[b](a[b])]}))}function q(){var a=h.filter(function(a){return!g[a].brush.empty()}),b=a.map(function(a){return g[a].brush.extent()});k=[],a.forEach(function(a,c){k[c]={dimension:a,extent:b[c]}}),l=[],M.style("display",function(c){var d=a.every(function(a,d){return isNaN(c[a])&&b[d][0]==g[a].brush.y().domain()[0]?!0:b[d][0]<=c[a]&&c[a]<=b[d][1]});return d&&l.push(c),d?null:"none"}),o.brush({filters:k,active:l})}function r(a){m[a]=this.parentNode.__origin__=f(a),L.attr("visibility","hidden")}function s(a){m[a]=Math.min(w,Math.max(0,this.parentNode.__origin__+=d3.event.x)),M.attr("d",p),h.sort(function(a,b){return u(a)-u(b)}),f.domain(h),N.attr("transform",function(a){return"translate("+u(a)+")"})}function t(a){delete this.parentNode.__origin__,delete m[a],d3.select(this.parentNode).attr("transform","translate("+f(a)+")"),M.attr("d",p),L.attr("d",p).attr("visibility",null)}function u(a){var b=mLine truncated
}},tooltipContent:{get:function(){return i.contentGenerator()},set:function(b){a.deprecated("tooltipContent","use chart.tooltip.contentGenerator() instead"),i.contentGenerator(b)}},tooltipXContent:{get:function(){return i.contentGenerator()},set:function(){a.deprecated("tooltipContent","This option is removed, put values into main tooltip.")}},tooltipYContent:{get:function(){return i.contentGenerator()},set:function(){a.deprecated("tooltipContent","This option is removed, put values into main tooltip.")}},margin:{get:function(){return j},set:function(a){j.top=void 0!==a.top?a.top:j.top,j.right=void 0!==a.right?a.right:j.right,j.bottom=void 0!==a.bottom?a.bottom:j.bottom,j.left=void 0!==a.left?a.left:j.left}},rightAlignYAxis:{get:function(){return v},set:function(a){v=a,e.orient(a?"right":"left")}},color:{get:function(){return n},set:function(b){n=a.utils.getColor(b),f.color(n),g.color(n),h.color(n)}}}),a.utils.inheritOptions(b,c),a.utils.initOptions(b),b},a.models.sparkline=function(){"use strict";function b(k){return k.each(function(b){var k=h-g.left-g.right,q=i-g.top-g.bottom;j=d3.select(this),a.utils.initSVG(j),l.domain(c||d3.extent(b,n)).range(e||[0,k]),m.domain(d||d3.extent(b,o)).range(f||[q,0]);{var r=j.selectAll("g.nv-wrap.nv-sparkline").data([b]),s=r.enter().append("g").attr("class","nvd3 nv-wrap nv-sparkline");s.append("g"),r.select("g")}r.attr("transform","translate("+g.left+","+g.top+")");var t=r.selectAll("path").data(function(a){return[a]});t.enter().append("patLine truncated
@@ -0,0 +1,5 @@
/*
Copyright (C) Federico Zivolo 2020
Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).
*/(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=e.ownerDocument.defaultView,n=o.getComputedStyle(e,null);return t?n[t]:n}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function i(e){return e&&e.referenceNode?e.referenceNode:e}function r(e){return 11===e?re:10===e?pe:re||pe}function p(e){if(!e)return document.documentElement;for(var o=r(10)?document.body:null,n=e.offsetParent||null;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TH','TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?p(n):n:e?e.ownerDocument.documentElement:document.documentElement}function s(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||p(e.firstElementChild)===e)}function d(e){return null===e.parentNode?e:d(e.parentNode)}function a(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();rLine truncated
//# sourceMappingURL=popper.min.js.map
@@ -0,0 +1 @@
<tr class="{{class}} d-flex"><td {{popover}} class="col-1 text-right"><a id="{{lineNumber}}" href="#{{lineNumber}}">{{lineNumber}}</a></td><td class="col-11 codeLine">{{lineContent}}</td></tr>
@@ -0,0 +1,5 @@
<table id="code" class="table table-borderless table-condensed">
<tbody>
{{lines}}
</tbody>
</table>
@@ -0,0 +1,12 @@
<tr>
<td class="{{methods_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{methods_level}} small">{{crap}}</td>
<td class="{{methods_level}}" colspan="3"></td>
</tr>
@@ -0,0 +1,18 @@
<tr>
<td class="{{methods_level}}">{{name}}</td>
<td class="{{lines_level}} big">{{lines_bar}}</td>
<td class="{{lines_level}} small"><div align="right">{{lines_executed_percent}}</div></td>
<td class="{{lines_level}} small"><div align="right">{{lines_number}}</div></td>
<td class="{{branches_level}} big">{{branches_bar}}</td>
<td class="{{branches_level}} small"><div align="right">{{branches_executed_percent}}</div></td>
<td class="{{branches_level}} small"><div align="right">{{branches_number}}</div></td>
<td class="{{paths_level}} big">{{paths_bar}}</td>
<td class="{{paths_level}} small"><div align="right">{{paths_executed_percent}}</div></td>
<td class="{{paths_level}} small"><div align="right">{{paths_number}}</div></td>
<td class="{{methods_level}} big">{{methods_bar}}</td>
<td class="{{methods_level}} small"><div align="right">{{methods_tested_percent}}</div></td>
<td class="{{methods_level}} small"><div align="right">{{methods_number}}</div></td>
<td class="{{methods_level}} small">{{crap}}</td>
<td class="{{methods_level}}" colspan="3"></td>
</tr>
@@ -0,0 +1,9 @@
<hr/>
<h4>Paths</h4>
<p>
Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not
necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once.
Please also be aware that some paths may include implicit rather than explicit branches, e.g. an <code>if</code> statement
<i>always</i> has an <code>else</code> as part of its logical flow even if you didn't write one.
</p>
{{paths}}
+43
View File
@@ -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;
use function dirname;
use function file_put_contents;
use function serialize;
use function sprintf;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
final class PHP
{
public function process(CodeCoverage $coverage, ?string $target = null): string
{
$buffer = sprintf(
"<?php
return \unserialize(<<<'END_OF_COVERAGE_SERIALIZATION'%s%s%sEND_OF_COVERAGE_SERIALIZATION%s);",
PHP_EOL,
serialize($coverage),
PHP_EOL,
PHP_EOL
);
if ($target !== null) {
Filesystem::createDirectory(dirname($target));
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
}
return $buffer;
}
}
+341
View File
@@ -0,0 +1,341 @@
<?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
{
/**
* @var string
*/
private const COLOR_GREEN = "\x1b[30;42m";
/**
* @var string
*/
private const COLOR_YELLOW = "\x1b[30;43m";
/**
* @var string
*/
private const COLOR_RED = "\x1b[37;41m";
/**
* @var string
*/
private const COLOR_HEADER = "\x1b[1;37;40m";
/**
* @var string
*/
private const COLOR_RESET = "\x1b[0m";
/**
* @var string
*/
private const COLOR_EOL = "\x1b[2K";
/**
* @var int
*/
private $lowUpperBound;
/**
* @var int
*/
private $highLowerBound;
/**
* @var bool
*/
private $showUncoveredFiles;
/**
* @var bool
*/
private $showOnlySummary;
public function __construct(int $lowUpperBound = 50, int $highLowerBound = 90, bool $showUncoveredFiles = false, bool $showOnlySummary = false)
{
$this->lowUpperBound = $lowUpperBound;
$this->highLowerBound = $highLowerBound;
$this->showUncoveredFiles = $showUncoveredFiles;
$this->showOnlySummary = $showOnlySummary;
}
public function process(CodeCoverage $coverage, bool $showColors = false): string
{
$hasBranchCoverage = !empty($coverage->getData(true)->functionCoverage());
$output = PHP_EOL . PHP_EOL;
$report = $coverage->getReport();
$colors = [
'header' => '',
'classes' => '',
'methods' => '',
'lines' => '',
'branches' => '',
'paths' => '',
'reset' => '',
'eol' => '',
];
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;
$colors['eol'] = self::COLOR_EOL;
}
$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) {
if ($method['executableLines'] == 0) {
continue;
}
$classMethods++;
$classExecutableLines += $method['executableLines'];
$classExecutedLines += $method['executedLines'];
$classExecutableBranches += $method['executableBranches'];
$classExecutedBranches += $method['executedBranches'];
$classExecutablePaths += $method['executablePaths'];
$classExecutedPaths += $method['executedPaths'];
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) {
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->highLowerBound) {
return self::COLOR_GREEN;
}
if ($coverage->asFloat() > $this->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) . ')';
}
/**
* @param false|string $string
*/
private function format(string $color, int $padding, $string): string
{
$reset = $color ? self::COLOR_RESET : '';
return $color . str_pad((string) $string, $padding) . $reset . PHP_EOL;
}
}
@@ -0,0 +1,88 @@
<?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 constant;
use function phpversion;
use DateTimeImmutable;
use DOMElement;
use SebastianBergmann\Environment\Runtime;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class BuildInformation
{
/**
* @var DOMElement
*/
private $contextNode;
public function __construct(DOMElement $contextNode)
{
$this->contextNode = $contextNode;
}
public function setRuntimeInformation(Runtime $runtime): void
{
$runtimeNode = $this->nodeByName('runtime');
$runtimeNode->setAttribute('name', $runtime->getName());
$runtimeNode->setAttribute('version', $runtime->getVersion());
$runtimeNode->setAttribute('url', $runtime->getVendorUrl());
$driverNode = $this->nodeByName('driver');
if ($runtime->hasPHPDBGCodeCoverage()) {
$driverNode->setAttribute('name', 'phpdbg');
$driverNode->setAttribute('version', constant('PHPDBG_VERSION'));
}
if ($runtime->hasXdebug()) {
$driverNode->setAttribute('name', 'xdebug');
$driverNode->setAttribute('version', phpversion('xdebug'));
}
if ($runtime->hasPCOV()) {
$driverNode->setAttribute('name', 'pcov');
$driverNode->setAttribute('version', phpversion('pcov'));
}
}
public function setBuildTime(DateTimeImmutable $date): void
{
$this->contextNode->setAttribute('time', $date->format('D M j G:i:s T Y'));
}
public function setGeneratorVersions(string $phpUnitVersion, string $coverageVersion): void
{
$this->contextNode->setAttribute('phpunit', $phpUnitVersion);
$this->contextNode->setAttribute('coverage', $coverageVersion);
}
private function nodeByName(string $name): DOMElement
{
$node = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
$name
)->item(0);
if (!$node) {
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
$name
)
);
}
return $node;
}
}
@@ -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\Report\Xml;
use DOMElement;
use SebastianBergmann\CodeCoverage\ReportAlreadyFinalizedException;
use XMLWriter;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Coverage
{
/**
* @var XMLWriter
*/
private $writer;
/**
* @var DOMElement
*/
private $contextNode;
/**
* @var bool
*/
private $finalized = false;
public function __construct(DOMElement $context, string $line)
{
$this->contextNode = $context;
$this->writer = new XMLWriter();
$this->writer->openMemory();
$this->writer->startElementNS(null, $context->nodeName, 'https://schema.phpunit.de/coverage/1.0');
$this->writer->writeAttribute('nr', $line);
}
/**
* @throws ReportAlreadyFinalizedException
*/
public function addTest(string $test): void
{
if ($this->finalized) {
throw new ReportAlreadyFinalizedException;
}
$this->writer->startElement('covered');
$this->writer->writeAttribute('by', $test);
$this->writer->endElement();
}
public function finalize(): void
{
$this->writer->endElement();
$fragment = $this->contextNode->ownerDocument->createDocumentFragment();
$fragment->appendXML($this->writer->outputMemory());
$this->contextNode->parentNode->replaceChild(
$fragment,
$this->contextNode
);
$this->finalized = true;
}
}
@@ -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,315 @@
<?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 const PHP_EOL;
use function count;
use function dirname;
use function file_get_contents;
use function file_put_contents;
use function is_array;
use function is_dir;
use function is_file;
use function is_writable;
use function libxml_clear_errors;
use function libxml_get_errors;
use function libxml_use_internal_errors;
use function sprintf;
use function strlen;
use function substr;
use DateTimeImmutable;
use DOMDocument;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\PathExistsButIsNotDirectoryException;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Node\AbstractNode;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\Node\File as FileNode;
use SebastianBergmann\CodeCoverage\Util\Filesystem as DirectoryUtil;
use SebastianBergmann\CodeCoverage\Version;
use SebastianBergmann\CodeCoverage\XmlException;
use SebastianBergmann\Environment\Runtime;
final class Facade
{
/**
* @var string
*/
private $target;
/**
* @var Project
*/
private $project;
/**
* @var string
*/
private $phpUnitVersion;
public function __construct(string $version)
{
$this->phpUnitVersion = $version;
}
/**
* @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();
$this->project = new Project(
$coverage->getReport()->name()
);
$this->setBuildInformation();
$this->processTests($coverage->getTests());
$this->processDirectory($report, $this->project);
$this->saveDocument($this->project->asDom(), 'index');
}
private function setBuildInformation(): void
{
$buildNode = $this->project->buildInformation();
$buildNode->setRuntimeInformation(new Runtime);
$buildNode->setBuildTime(new DateTimeImmutable);
$buildNode->setGeneratorVersions($this->phpUnitVersion, Version::id());
}
/**
* @throws PathExistsButIsNotDirectoryException
* @throws WriteOperationFailedException
*/
private function initTargetDirectory(string $directory): void
{
if (is_file($directory)) {
if (!is_dir($directory)) {
throw new PathExistsButIsNotDirectoryException($directory);
}
if (!is_writable($directory)) {
throw new WriteOperationFailedException($directory);
}
}
DirectoryUtil::createDirectory($directory);
}
/**
* @throws XmlException
*/
private function processDirectory(DirectoryNode $directory, Node $context): void
{
$directoryName = $directory->name();
if ($this->project->projectSourceDirectory() === $directoryName) {
$directoryName = '/';
}
$directoryObject = $context->addDirectory($directoryName);
$this->setTotals($directory, $directoryObject->totals());
foreach ($directory->directories() as $node) {
$this->processDirectory($node, $directoryObject);
}
foreach ($directory->files() as $node) {
$this->processFile($node, $directoryObject);
}
}
/**
* @throws XmlException
*/
private function processFile(FileNode $file, Directory $context): void
{
$fileObject = $context->addFile(
$file->name(),
$file->id() . '.xml'
);
$this->setTotals($file, $fileObject->totals());
$path = substr(
$file->pathAsString(),
strlen($this->project->projectSourceDirectory())
);
$fileReport = new Report($path);
$this->setTotals($file, $fileReport->totals());
foreach ($file->classesAndTraits() as $unit) {
$this->processUnit($unit, $fileReport);
}
foreach ($file->functions() as $function) {
$this->processFunction($function, $fileReport);
}
foreach ($file->lineCoverageData() as $line => $tests) {
if (!is_array($tests) || count($tests) === 0) {
continue;
}
$coverage = $fileReport->lineCoverage((string) $line);
foreach ($tests as $test) {
$coverage->addTest($test);
}
$coverage->finalize();
}
$fileReport->source()->setSourceCode(
file_get_contents($file->pathAsString())
);
$this->saveDocument($fileReport->asDom(), $file->id());
}
private function processUnit(array $unit, Report $report): void
{
if (isset($unit['className'])) {
$unitObject = $report->classObject($unit['className']);
} else {
$unitObject = $report->traitObject($unit['traitName']);
}
$unitObject->setLines(
$unit['startLine'],
$unit['executableLines'],
$unit['executedLines']
);
$unitObject->setCrap((float) $unit['crap']);
$unitObject->setNamespace($unit['namespace']);
foreach ($unit['methods'] as $method) {
$methodObject = $unitObject->addMethod($method['methodName']);
$methodObject->setSignature($method['signature']);
$methodObject->setLines((string) $method['startLine'], (string) $method['endLine']);
$methodObject->setCrap($method['crap']);
$methodObject->setTotals(
(string) $method['executableLines'],
(string) $method['executedLines'],
(string) $method['coverage']
);
}
}
private function processFunction(array $function, Report $report): void
{
$functionObject = $report->functionObject($function['functionName']);
$functionObject->setSignature($function['signature']);
$functionObject->setLines((string) $function['startLine']);
$functionObject->setCrap($function['crap']);
$functionObject->setTotals((string) $function['executableLines'], (string) $function['executedLines'], (string) $function['coverage']);
}
private function processTests(array $tests): void
{
$testsObject = $this->project->tests();
foreach ($tests as $test => $result) {
$testsObject->addTest($test, $result);
}
}
private function setTotals(AbstractNode $node, Totals $totals): void
{
$loc = $node->linesOfCode();
$totals->setNumLines(
$loc['linesOfCode'],
$loc['commentLinesOfCode'],
$loc['nonCommentLinesOfCode'],
$node->numberOfExecutableLines(),
$node->numberOfExecutedLines()
);
$totals->setNumClasses(
$node->numberOfClasses(),
$node->numberOfTestedClasses()
);
$totals->setNumTraits(
$node->numberOfTraits(),
$node->numberOfTestedTraits()
);
$totals->setNumMethods(
$node->numberOfMethods(),
$node->numberOfTestedMethods()
);
$totals->setNumFunctions(
$node->numberOfFunctions(),
$node->numberOfTestedFunctions()
);
}
private function targetDirectory(): string
{
return $this->target;
}
/**
* @throws XmlException
*/
private function saveDocument(DOMDocument $document, string $name): void
{
$filename = sprintf('%s/%s.xml', $this->targetDirectory(), $name);
$document->formatOutput = true;
$document->preserveWhiteSpace = false;
$this->initTargetDirectory(dirname($filename));
file_put_contents($filename, $this->documentAsString($document));
}
/**
* @throws XmlException
*
* @see https://bugs.php.net/bug.php?id=79191
*/
private function documentAsString(DOMDocument $document): string
{
$xmlErrorHandling = libxml_use_internal_errors(true);
$xml = $document->saveXML();
if ($xml === false) {
$message = 'Unable to generate the XML';
foreach (libxml_get_errors() as $error) {
$message .= PHP_EOL . $error->message;
}
throw new XmlException($message);
}
libxml_clear_errors();
libxml_use_internal_errors($xmlErrorHandling);
return $xml;
}
}
@@ -0,0 +1,87 @@
<?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 DOMDocument;
use DOMElement;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
class File
{
/**
* @var DOMDocument
*/
private $dom;
/**
* @var DOMElement
*/
private $contextNode;
public function __construct(DOMElement $context)
{
$this->dom = $context->ownerDocument;
$this->contextNode = $context;
}
public function totals(): Totals
{
$totalsContainer = $this->contextNode->firstChild;
if (!$totalsContainer) {
$totalsContainer = $this->contextNode->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'totals'
)
);
}
return new Totals($totalsContainer);
}
public function lineCoverage(string $line): Coverage
{
$coverage = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'coverage'
)->item(0);
if (!$coverage) {
$coverage = $this->contextNode->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'coverage'
)
);
}
$lineNode = $coverage->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'line'
)
);
return new Coverage($lineNode, $line);
}
protected function contextNode(): DOMElement
{
return $this->contextNode;
}
protected function dom(): DOMDocument
{
return $this->dom;
}
}
@@ -0,0 +1,61 @@
<?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 DOMElement;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Method
{
/**
* @var DOMElement
*/
private $contextNode;
public function __construct(DOMElement $context, string $name)
{
$this->contextNode = $context;
$this->setName($name);
}
public function setSignature(string $signature): void
{
$this->contextNode->setAttribute('signature', $signature);
}
public function setLines(string $start, ?string $end = null): void
{
$this->contextNode->setAttribute('start', $start);
if ($end !== null) {
$this->contextNode->setAttribute('end', $end);
}
}
public function setTotals(string $executable, string $executed, string $coverage): void
{
$this->contextNode->setAttribute('executable', $executable);
$this->contextNode->setAttribute('executed', $executed);
$this->contextNode->setAttribute('coverage', $coverage);
}
public function setCrap(string $crap): void
{
$this->contextNode->setAttribute('crap', $crap);
}
private function setName(string $name): void
{
$this->contextNode->setAttribute('name', $name);
}
}
@@ -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\Report\Xml;
use DOMDocument;
use DOMElement;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
abstract class Node
{
/**
* @var DOMDocument
*/
private $dom;
/**
* @var DOMElement
*/
private $contextNode;
public function __construct(DOMElement $context)
{
$this->setContextNode($context);
}
public function dom(): DOMDocument
{
return $this->dom;
}
public function totals(): Totals
{
$totalsContainer = $this->contextNode()->firstChild;
if (!$totalsContainer) {
$totalsContainer = $this->contextNode()->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'totals'
)
);
}
return new Totals($totalsContainer);
}
public function addDirectory(string $name): Directory
{
$dirNode = $this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'directory'
);
$dirNode->setAttribute('name', $name);
$this->contextNode()->appendChild($dirNode);
return new Directory($dirNode);
}
public function addFile(string $name, string $href): File
{
$fileNode = $this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'file'
);
$fileNode->setAttribute('name', $name);
$fileNode->setAttribute('href', $href);
$this->contextNode()->appendChild($fileNode);
return new File($fileNode);
}
protected function setContextNode(DOMElement $context): void
{
$this->dom = $context->ownerDocument;
$this->contextNode = $context;
}
protected function contextNode(): DOMElement
{
return $this->contextNode;
}
}
@@ -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\Report\Xml;
use DOMDocument;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Project extends Node
{
public function __construct(string $directory)
{
$this->init();
$this->setProjectSourceDirectory($directory);
}
public function projectSourceDirectory(): string
{
return $this->contextNode()->getAttribute('source');
}
public function buildInformation(): BuildInformation
{
$buildNode = $this->dom()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'build'
)->item(0);
if (!$buildNode) {
$buildNode = $this->dom()->documentElement->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'build'
)
);
}
return new BuildInformation($buildNode);
}
public function tests(): Tests
{
$testsNode = $this->contextNode()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'tests'
)->item(0);
if (!$testsNode) {
$testsNode = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'tests'
)
);
}
return new Tests($testsNode);
}
public function asDom(): DOMDocument
{
return $this->dom();
}
private function init(): void
{
$dom = new DOMDocument;
$dom->loadXML('<?xml version="1.0" ?><phpunit xmlns="https://schema.phpunit.de/coverage/1.0"><build/><project/></phpunit>');
$this->setContextNode(
$dom->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'project'
)->item(0)
);
}
private function setProjectSourceDirectory(string $name): void
{
$this->contextNode()->setAttribute('source', $name);
}
}
@@ -0,0 +1,99 @@
<?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;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Report extends File
{
public function __construct(string $name)
{
$dom = new DOMDocument();
$dom->loadXML('<?xml version="1.0" ?><phpunit xmlns="https://schema.phpunit.de/coverage/1.0"><file /></phpunit>');
$contextNode = $dom->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'file'
)->item(0);
parent::__construct($contextNode);
$this->setName($name);
}
public function asDom(): DOMDocument
{
return $this->dom();
}
public function functionObject($name): Method
{
$node = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'function'
)
);
return new Method($node, $name);
}
public function classObject($name): Unit
{
return $this->unitObject('class', $name);
}
public function traitObject($name): Unit
{
return $this->unitObject('trait', $name);
}
public function source(): Source
{
$source = $this->contextNode()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'source'
)->item(0);
if (!$source) {
$source = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'source'
)
);
}
return new Source($source);
}
private function setName(string $name): void
{
$this->contextNode()->setAttribute('name', basename($name));
$this->contextNode()->setAttribute('path', dirname($name));
}
private function unitObject(string $tagName, $name): Unit
{
$node = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
$tagName
)
);
return new Unit($node, $name);
}
}
@@ -0,0 +1,42 @@
<?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 DOMElement;
use TheSeer\Tokenizer\NamespaceUri;
use TheSeer\Tokenizer\Tokenizer;
use TheSeer\Tokenizer\XMLSerializer;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Source
{
/** @var DOMElement */
private $context;
public function __construct(DOMElement $context)
{
$this->context = $context;
}
public function setSourceCode(string $source): void
{
$context = $this->context;
$tokens = (new Tokenizer())->parse($source);
$srcDom = (new XMLSerializer(new NamespaceUri($context->namespaceURI)))->toDom($tokens);
$context->parentNode->replaceChild(
$context->ownerDocument->importNode($srcDom->documentElement, true),
$context
);
}
}
@@ -0,0 +1,51 @@
<?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 DOMElement;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Tests
{
private $contextNode;
private $codeMap = [
-1 => 'UNKNOWN', // PHPUnit_Runner_BaseTestRunner::STATUS_UNKNOWN
0 => 'PASSED', // PHPUnit_Runner_BaseTestRunner::STATUS_PASSED
1 => 'SKIPPED', // PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED
2 => 'INCOMPLETE', // PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE
3 => 'FAILURE', // PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE
4 => 'ERROR', // PHPUnit_Runner_BaseTestRunner::STATUS_ERROR
5 => 'RISKY', // PHPUnit_Runner_BaseTestRunner::STATUS_RISKY
6 => 'WARNING', // PHPUnit_Runner_BaseTestRunner::STATUS_WARNING
];
public function __construct(DOMElement $context)
{
$this->contextNode = $context;
}
public function addTest(string $test, array $result): void
{
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'test'
)
);
$node->setAttribute('name', $test);
$node->setAttribute('size', $result['size']);
$node->setAttribute('result', (string) $result['status']);
$node->setAttribute('status', $this->codeMap[(int) $result['status']]);
}
}
@@ -0,0 +1,146 @@
<?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 DOMElement;
use DOMNode;
use SebastianBergmann\CodeCoverage\Util\Percentage;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Totals
{
/**
* @var DOMNode
*/
private $container;
/**
* @var DOMElement
*/
private $linesNode;
/**
* @var DOMElement
*/
private $methodsNode;
/**
* @var DOMElement
*/
private $functionsNode;
/**
* @var DOMElement
*/
private $classesNode;
/**
* @var DOMElement
*/
private $traitsNode;
public function __construct(DOMElement $container)
{
$this->container = $container;
$dom = $container->ownerDocument;
$this->linesNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'lines'
);
$this->methodsNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'methods'
);
$this->functionsNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'functions'
);
$this->classesNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'classes'
);
$this->traitsNode = $dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'traits'
);
$container->appendChild($this->linesNode);
$container->appendChild($this->methodsNode);
$container->appendChild($this->functionsNode);
$container->appendChild($this->classesNode);
$container->appendChild($this->traitsNode);
}
public function container(): DOMNode
{
return $this->container;
}
public function setNumLines(int $loc, int $cloc, int $ncloc, int $executable, int $executed): void
{
$this->linesNode->setAttribute('total', (string) $loc);
$this->linesNode->setAttribute('comments', (string) $cloc);
$this->linesNode->setAttribute('code', (string) $ncloc);
$this->linesNode->setAttribute('executable', (string) $executable);
$this->linesNode->setAttribute('executed', (string) $executed);
$this->linesNode->setAttribute(
'percent',
$executable === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($executed, $executable)->asFloat())
);
}
public function setNumClasses(int $count, int $tested): void
{
$this->classesNode->setAttribute('count', (string) $count);
$this->classesNode->setAttribute('tested', (string) $tested);
$this->classesNode->setAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
);
}
public function setNumTraits(int $count, int $tested): void
{
$this->traitsNode->setAttribute('count', (string) $count);
$this->traitsNode->setAttribute('tested', (string) $tested);
$this->traitsNode->setAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
);
}
public function setNumMethods(int $count, int $tested): void
{
$this->methodsNode->setAttribute('count', (string) $count);
$this->methodsNode->setAttribute('tested', (string) $tested);
$this->methodsNode->setAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
);
}
public function setNumFunctions(int $count, int $tested): void
{
$this->functionsNode->setAttribute('count', (string) $count);
$this->functionsNode->setAttribute('tested', (string) $tested);
$this->functionsNode->setAttribute(
'percent',
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
);
}
}
@@ -0,0 +1,78 @@
<?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 DOMElement;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Unit
{
/**
* @var DOMElement
*/
private $contextNode;
public function __construct(DOMElement $context, string $name)
{
$this->contextNode = $context;
$this->setName($name);
}
public function setLines(int $start, int $executable, int $executed): void
{
$this->contextNode->setAttribute('start', (string) $start);
$this->contextNode->setAttribute('executable', (string) $executable);
$this->contextNode->setAttribute('executed', (string) $executed);
}
public function setCrap(float $crap): void
{
$this->contextNode->setAttribute('crap', (string) $crap);
}
public function setNamespace(string $namespace): void
{
$node = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'namespace'
)->item(0);
if (!$node) {
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'namespace'
)
);
}
$node->setAttribute('name', $namespace);
}
public function addMethod(string $name): Method
{
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'method'
)
);
return new Method($node, $name);
}
private function setName(string $name): void
{
$this->contextNode->setAttribute('name', $name);
}
}
@@ -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\StaticAnalysis;
use SebastianBergmann\CodeCoverage\Filter;
final class CacheWarmer
{
public function warmCache(string $cacheDirectory, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode, Filter $filter): void
{
$analyser = new CachingFileAnalyser(
$cacheDirectory,
new ParsingFileAnalyser(
$useAnnotationsForIgnoringCode,
$ignoreDeprecatedCode
)
);
foreach ($filter->files() as $file) {
$analyser->process($file);
}
}
}
@@ -0,0 +1,183 @@
<?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 assert;
use function crc32;
use function file_get_contents;
use function file_put_contents;
use function is_file;
use function serialize;
use GlobIterator;
use SebastianBergmann\CodeCoverage\Util\Filesystem;
use SplFileInfo;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class CachingFileAnalyser implements FileAnalyser
{
/**
* @var ?string
*/
private static $cacheVersion;
/**
* @var FileAnalyser
*/
private $analyser;
/**
* @var array
*/
private $cache = [];
/**
* @var string
*/
private $directory;
public function __construct(string $directory, FileAnalyser $analyser)
{
Filesystem::createDirectory($directory);
$this->analyser = $analyser;
$this->directory = $directory;
if (self::$cacheVersion === null) {
$this->calculateCacheVersion();
}
}
public function classesIn(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['classesIn'];
}
public function traitsIn(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['traitsIn'];
}
public function functionsIn(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['functionsIn'];
}
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
public function linesOfCodeFor(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['linesOfCodeFor'];
}
public function executableLinesIn(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['executableLinesIn'];
}
public function ignoredLinesFor(string $filename): array
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['ignoredLinesFor'];
}
public function process(string $filename): void
{
$cache = $this->read($filename);
if ($cache !== false) {
$this->cache[$filename] = $cache;
return;
}
$this->cache[$filename] = [
'classesIn' => $this->analyser->classesIn($filename),
'traitsIn' => $this->analyser->traitsIn($filename),
'functionsIn' => $this->analyser->functionsIn($filename),
'linesOfCodeFor' => $this->analyser->linesOfCodeFor($filename),
'ignoredLinesFor' => $this->analyser->ignoredLinesFor($filename),
'executableLinesIn' => $this->analyser->executableLinesIn($filename),
];
$this->write($filename, $this->cache[$filename]);
}
/**
* @return mixed
*/
private function read(string $filename)
{
$cacheFile = $this->cacheFile($filename);
if (!is_file($cacheFile)) {
return false;
}
return unserialize(
file_get_contents($cacheFile),
['allowed_classes' => false]
);
}
/**
* @param mixed $data
*/
private function write(string $filename, $data): void
{
file_put_contents(
$this->cacheFile($filename),
serialize($data)
);
}
private function cacheFile(string $filename): string
{
return $this->directory . DIRECTORY_SEPARATOR . hash('sha256', $filename . crc32(file_get_contents($filename)) . self::$cacheVersion);
}
private function calculateCacheVersion(): void
{
$buffer = '';
foreach (new GlobIterator(__DIR__ . '/*.php') as $file) {
assert($file instanceof SplFileInfo);
$buffer .= file_get_contents($file->getPathname());
}
self::$cacheVersion = (string) crc32($buffer);
}
}
@@ -0,0 +1,323 @@
<?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 implode;
use function rtrim;
use function trim;
use PhpParser\Node;
use PhpParser\Node\ComplexType;
use PhpParser\Node\Identifier;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\UnionType;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use SebastianBergmann\Complexity\CyclomaticComplexityCalculatingVisitor;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class CodeUnitFindingVisitor extends NodeVisitorAbstract
{
/**
* @psalm-var array<string,array{name: string, namespacedName: string, namespace: string, startLine: int, endLine: int, methods: array<string,array{methodName: string, signature: string, visibility: string, startLine: int, endLine: int, ccn: int}>}>
*/
private $classes = [];
/**
* @psalm-var array<string,array{name: string, namespacedName: string, namespace: string, startLine: int, endLine: int, methods: array<string,array{methodName: string, signature: string, visibility: string, startLine: int, endLine: int, ccn: int}>}>
*/
private $traits = [];
/**
* @psalm-var array<string,array{name: string, namespacedName: string, namespace: string, signature: string, startLine: int, endLine: int, ccn: int}>
*/
private $functions = [];
public function enterNode(Node $node): void
{
if ($node instanceof Class_) {
if ($node->isAnonymous()) {
return;
}
$this->processClass($node);
}
if ($node instanceof Trait_) {
$this->processTrait($node);
}
if (!$node instanceof ClassMethod && !$node instanceof Function_) {
return;
}
if ($node instanceof ClassMethod) {
$parentNode = $node->getAttribute('parent');
if ($parentNode instanceof Class_ && $parentNode->isAnonymous()) {
return;
}
$this->processMethod($node);
return;
}
$this->processFunction($node);
}
/**
* @psalm-return array<string,array{name: string, namespacedName: string, namespace: string, startLine: int, endLine: int, methods: array<string,array{methodName: string, signature: string, visibility: string, startLine: int, endLine: int, ccn: int}>}>
*/
public function classes(): array
{
return $this->classes;
}
/**
* @psalm-return array<string,array{name: string, namespacedName: string, namespace: string, startLine: int, endLine: int, methods: array<string,array{methodName: string, signature: string, visibility: string, startLine: int, endLine: int, ccn: int}>}>
*/
public function traits(): array
{
return $this->traits;
}
/**
* @psalm-return array<string,array{name: string, namespacedName: string, namespace: string, signature: string, startLine: int, endLine: int, ccn: int}>
*/
public function functions(): array
{
return $this->functions;
}
/**
* @psalm-param ClassMethod|Function_ $node
*/
private function cyclomaticComplexity(Node $node): int
{
assert($node instanceof ClassMethod || $node instanceof Function_);
$nodes = $node->getStmts();
if ($nodes === null) {
return 0;
}
$traverser = new NodeTraverser;
$cyclomaticComplexityCalculatingVisitor = new CyclomaticComplexityCalculatingVisitor;
$traverser->addVisitor($cyclomaticComplexityCalculatingVisitor);
/* @noinspection UnusedFunctionResultInspection */
$traverser->traverse($nodes);
return $cyclomaticComplexityCalculatingVisitor->cyclomaticComplexity();
}
/**
* @psalm-param ClassMethod|Function_ $node
*/
private function signature(Node $node): string
{
assert($node instanceof ClassMethod || $node instanceof Function_);
$signature = ($node->returnsByRef() ? '&' : '') . $node->name->toString() . '(';
$parameters = [];
foreach ($node->getParams() as $parameter) {
assert(isset($parameter->var->name));
$parameterAsString = '';
if ($parameter->type !== null) {
$parameterAsString = $this->type($parameter->type) . ' ';
}
$parameterAsString .= '$' . $parameter->var->name;
/* @todo Handle default values */
$parameters[] = $parameterAsString;
}
$signature .= implode(', ', $parameters) . ')';
$returnType = $node->getReturnType();
if ($returnType !== null) {
$signature .= ': ' . $this->type($returnType);
}
return $signature;
}
/**
* @psalm-param Identifier|Name|ComplexType $type
*/
private function type(Node $type): string
{
assert($type instanceof Identifier || $type instanceof Name || $type instanceof ComplexType);
if ($type instanceof NullableType) {
return '?' . $type->type;
}
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $this->unionOrIntersectionAsString($type);
}
return $type->toString();
}
private function visibility(ClassMethod $node): string
{
if ($node->isPrivate()) {
return 'private';
}
if ($node->isProtected()) {
return 'protected';
}
return 'public';
}
private function processClass(Class_ $node): void
{
$name = $node->name->toString();
$namespacedName = $node->namespacedName->toString();
$this->classes[$namespacedName] = [
'name' => $name,
'namespacedName' => $namespacedName,
'namespace' => $this->namespace($namespacedName, $name),
'startLine' => $node->getStartLine(),
'endLine' => $node->getEndLine(),
'methods' => [],
];
}
private function processTrait(Trait_ $node): void
{
$name = $node->name->toString();
$namespacedName = $node->namespacedName->toString();
$this->traits[$namespacedName] = [
'name' => $name,
'namespacedName' => $namespacedName,
'namespace' => $this->namespace($namespacedName, $name),
'startLine' => $node->getStartLine(),
'endLine' => $node->getEndLine(),
'methods' => [],
];
}
private function processMethod(ClassMethod $node): void
{
$parentNode = $node->getAttribute('parent');
if ($parentNode instanceof Interface_) {
return;
}
assert($parentNode instanceof Class_ || $parentNode instanceof Trait_ || $parentNode instanceof Enum_);
assert(isset($parentNode->name));
assert(isset($parentNode->namespacedName));
assert($parentNode->namespacedName instanceof Name);
$parentName = $parentNode->name->toString();
$parentNamespacedName = $parentNode->namespacedName->toString();
if ($parentNode instanceof Class_) {
$storage = &$this->classes;
} else {
$storage = &$this->traits;
}
if (!isset($storage[$parentNamespacedName])) {
$storage[$parentNamespacedName] = [
'name' => $parentName,
'namespacedName' => $parentNamespacedName,
'namespace' => $this->namespace($parentNamespacedName, $parentName),
'startLine' => $parentNode->getStartLine(),
'endLine' => $parentNode->getEndLine(),
'methods' => [],
];
}
$storage[$parentNamespacedName]['methods'][$node->name->toString()] = [
'methodName' => $node->name->toString(),
'signature' => $this->signature($node),
'visibility' => $this->visibility($node),
'startLine' => $node->getStartLine(),
'endLine' => $node->getEndLine(),
'ccn' => $this->cyclomaticComplexity($node),
];
}
private function processFunction(Function_ $node): void
{
assert(isset($node->name));
assert(isset($node->namespacedName));
assert($node->namespacedName instanceof Name);
$name = $node->name->toString();
$namespacedName = $node->namespacedName->toString();
$this->functions[$namespacedName] = [
'name' => $name,
'namespacedName' => $namespacedName,
'namespace' => $this->namespace($namespacedName, $name),
'signature' => $this->signature($node),
'startLine' => $node->getStartLine(),
'endLine' => $node->getEndLine(),
'ccn' => $this->cyclomaticComplexity($node),
];
}
private function namespace(string $namespacedName, string $name): string
{
return trim(rtrim($namespacedName, $name), '\\');
}
/**
* @psalm-param UnionType|IntersectionType $type
*/
private function unionOrIntersectionAsString(ComplexType $type): string
{
if ($type instanceof UnionType) {
$separator = '|';
} else {
$separator = '&';
}
$types = [];
foreach ($type->types as $_type) {
if ($_type instanceof Name) {
$types[] = $_type->toCodeString();
} else {
$types[] = $_type->toString();
}
}
return implode($separator, $types);
}
}
@@ -0,0 +1,222 @@
<?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 PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\CallLike;
use PhpParser\Node\Expr\Cast;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\Match_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\NullsafePropertyFetch;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Expr\Ternary;
use PhpParser\Node\MatchArm;
use PhpParser\Node\Scalar\Encapsed;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Case_;
use PhpParser\Node\Stmt\Catch_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Continue_;
use PhpParser\Node\Stmt\Do_;
use PhpParser\Node\Stmt\Echo_;
use PhpParser\Node\Stmt\Else_;
use PhpParser\Node\Stmt\ElseIf_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Finally_;
use PhpParser\Node\Stmt\For_;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\Node\Stmt\Goto_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Property;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Stmt\Switch_;
use PhpParser\Node\Stmt\Throw_;
use PhpParser\Node\Stmt\TryCatch;
use PhpParser\Node\Stmt\Unset_;
use PhpParser\Node\Stmt\While_;
use PhpParser\NodeVisitorAbstract;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class ExecutableLinesFindingVisitor extends NodeVisitorAbstract
{
/**
* @psalm-var array<int, int>
*/
private $executableLines = [];
/**
* @psalm-var array<int, int>
*/
private $propertyLines = [];
public function enterNode(Node $node): void
{
$this->savePropertyLines($node);
if (!$this->isExecutable($node)) {
return;
}
foreach ($this->getLines($node) as $line) {
if (isset($this->propertyLines[$line])) {
return;
}
$this->executableLines[$line] = $line;
}
}
/**
* @psalm-return array<int, int>
*/
public function executableLines(): array
{
sort($this->executableLines);
return $this->executableLines;
}
private function savePropertyLines(Node $node): void
{
if (!$node instanceof Property && !$node instanceof Node\Stmt\ClassConst) {
return;
}
foreach (range($node->getStartLine(), $node->getEndLine()) as $index) {
$this->propertyLines[$index] = $index;
}
}
/**
* @return int[]
*/
private function getLines(Node $node): array
{
if ($node instanceof Cast ||
$node instanceof PropertyFetch ||
$node instanceof NullsafePropertyFetch ||
$node instanceof StaticPropertyFetch) {
return [$node->getEndLine()];
}
if ($node instanceof ArrayDimFetch) {
if (null === $node->dim) {
return [];
}
return [$node->dim->getStartLine()];
}
if ($node instanceof ClassMethod) {
if ($node->name->name !== '__construct') {
return [];
}
$existsAPromotedProperty = false;
foreach ($node->getParams() as $param) {
if (0 !== ($param->flags & Class_::VISIBILITY_MODIFIER_MASK)) {
$existsAPromotedProperty = true;
break;
}
}
if ($existsAPromotedProperty) {
// Only the line with `function` keyword should be listed here
// but `nikic/php-parser` doesn't provide a way to fetch it
return range($node->getStartLine(), $node->name->getEndLine());
}
return [];
}
if ($node instanceof MethodCall) {
return [$node->name->getStartLine()];
}
if ($node instanceof Ternary) {
$lines = [$node->cond->getStartLine()];
if (null !== $node->if) {
$lines[] = $node->if->getStartLine();
}
$lines[] = $node->else->getStartLine();
return $lines;
}
if ($node instanceof Match_) {
return [$node->cond->getStartLine()];
}
if ($node instanceof MatchArm) {
return [$node->body->getStartLine()];
}
if ($node instanceof Expression && (
$node->expr instanceof Cast ||
$node->expr instanceof Match_ ||
$node->expr instanceof MethodCall
)) {
return [];
}
return [$node->getStartLine()];
}
private function isExecutable(Node $node): bool
{
return $node instanceof Assign ||
$node instanceof ArrayDimFetch ||
$node instanceof BinaryOp ||
$node instanceof Break_ ||
$node instanceof CallLike ||
$node instanceof Case_ ||
$node instanceof Cast ||
$node instanceof Catch_ ||
$node instanceof ClassMethod ||
$node instanceof Closure ||
$node instanceof Continue_ ||
$node instanceof Do_ ||
$node instanceof Echo_ ||
$node instanceof ElseIf_ ||
$node instanceof Else_ ||
$node instanceof Encapsed ||
$node instanceof Expression ||
$node instanceof Finally_ ||
$node instanceof For_ ||
$node instanceof Foreach_ ||
$node instanceof Goto_ ||
$node instanceof If_ ||
$node instanceof Match_ ||
$node instanceof MatchArm ||
$node instanceof MethodCall ||
$node instanceof NullsafePropertyFetch ||
$node instanceof PropertyFetch ||
$node instanceof Return_ ||
$node instanceof StaticPropertyFetch ||
$node instanceof Switch_ ||
$node instanceof Ternary ||
$node instanceof Throw_ ||
$node instanceof TryCatch ||
$node instanceof Unset_ ||
$node instanceof While_;
}
}
@@ -0,0 +1,31 @@
<?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;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
interface FileAnalyser
{
public function classesIn(string $filename): array;
public function traitsIn(string $filename): array;
public function functionsIn(string $filename): array;
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
public function linesOfCodeFor(string $filename): array;
public function executableLinesIn(string $filename): array;
public function ignoredLinesFor(string $filename): array;
}
@@ -0,0 +1,106 @@
<?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 array_merge;
use function range;
use function strpos;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\NodeVisitorAbstract;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class IgnoredLinesFindingVisitor extends NodeVisitorAbstract
{
/**
* @psalm-var list<int>
*/
private $ignoredLines = [];
/**
* @var bool
*/
private $useAnnotationsForIgnoringCode;
/**
* @var bool
*/
private $ignoreDeprecated;
public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecated)
{
$this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
$this->ignoreDeprecated = $ignoreDeprecated;
}
public function enterNode(Node $node): void
{
if (!$node instanceof Class_ &&
!$node instanceof Trait_ &&
!$node instanceof Interface_ &&
!$node instanceof ClassMethod &&
!$node instanceof Function_) {
return;
}
if ($node instanceof Class_ && $node->isAnonymous()) {
return;
}
// Workaround for https://bugs.xdebug.org/view.php?id=1798
if ($node instanceof Class_ ||
$node instanceof Trait_ ||
$node instanceof Interface_) {
$this->ignoredLines[] = $node->getStartLine();
}
if (!$this->useAnnotationsForIgnoringCode) {
return;
}
if ($node instanceof Interface_) {
return;
}
$docComment = $node->getDocComment();
if ($docComment === null) {
return;
}
if (strpos($docComment->getText(), '@codeCoverageIgnore') !== false) {
$this->ignoredLines = array_merge(
$this->ignoredLines,
range($node->getStartLine(), $node->getEndLine())
);
}
if ($this->ignoreDeprecated && strpos($docComment->getText(), '@deprecated') !== false) {
$this->ignoredLines = array_merge(
$this->ignoredLines,
range($node->getStartLine(), $node->getEndLine())
);
}
}
/**
* @psalm-return list<int>
*/
public function ignoredLines(): array
{
return $this->ignoredLines;
}
}
@@ -0,0 +1,250 @@
<?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 array_unique;
use function assert;
use function file_get_contents;
use function is_array;
use function sprintf;
use function substr_count;
use function token_get_all;
use function trim;
use PhpParser\Error;
use PhpParser\Lexer;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\NodeVisitor\ParentConnectingVisitor;
use PhpParser\ParserFactory;
use SebastianBergmann\CodeCoverage\ParserException;
use SebastianBergmann\LinesOfCode\LineCountingVisitor;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class ParsingFileAnalyser implements FileAnalyser
{
/**
* @var array
*/
private $classes = [];
/**
* @var array
*/
private $traits = [];
/**
* @var array
*/
private $functions = [];
/**
* @var array<string,array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}>
*/
private $linesOfCode = [];
/**
* @var array
*/
private $ignoredLines = [];
/**
* @var array
*/
private $executableLines = [];
/**
* @var bool
*/
private $useAnnotationsForIgnoringCode;
/**
* @var bool
*/
private $ignoreDeprecatedCode;
public function __construct(bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode)
{
$this->useAnnotationsForIgnoringCode = $useAnnotationsForIgnoringCode;
$this->ignoreDeprecatedCode = $ignoreDeprecatedCode;
}
public function classesIn(string $filename): array
{
$this->analyse($filename);
return $this->classes[$filename];
}
public function traitsIn(string $filename): array
{
$this->analyse($filename);
return $this->traits[$filename];
}
public function functionsIn(string $filename): array
{
$this->analyse($filename);
return $this->functions[$filename];
}
/**
* @psalm-return array{linesOfCode: int, commentLinesOfCode: int, nonCommentLinesOfCode: int}
*/
public function linesOfCodeFor(string $filename): array
{
$this->analyse($filename);
return $this->linesOfCode[$filename];
}
public function executableLinesIn(string $filename): array
{
$this->analyse($filename);
return $this->executableLines[$filename];
}
public function ignoredLinesFor(string $filename): array
{
$this->analyse($filename);
return $this->ignoredLines[$filename];
}
/**
* @throws ParserException
*/
private function analyse(string $filename): void
{
if (isset($this->classes[$filename])) {
return;
}
$source = file_get_contents($filename);
$linesOfCode = substr_count($source, "\n");
if ($linesOfCode === 0 && !empty($source)) {
$linesOfCode = 1;
}
$parser = (new ParserFactory)->create(
ParserFactory::PREFER_PHP7,
new Lexer
);
try {
$nodes = $parser->parse($source);
assert($nodes !== null);
$traverser = new NodeTraverser;
$codeUnitFindingVisitor = new CodeUnitFindingVisitor;
$lineCountingVisitor = new LineCountingVisitor($linesOfCode);
$ignoredLinesFindingVisitor = new IgnoredLinesFindingVisitor($this->useAnnotationsForIgnoringCode, $this->ignoreDeprecatedCode);
$executableLinesFindingVisitor = new ExecutableLinesFindingVisitor;
$traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ParentConnectingVisitor);
$traverser->addVisitor($codeUnitFindingVisitor);
$traverser->addVisitor($lineCountingVisitor);
$traverser->addVisitor($ignoredLinesFindingVisitor);
$traverser->addVisitor($executableLinesFindingVisitor);
/* @noinspection UnusedFunctionResultInspection */
$traverser->traverse($nodes);
// @codeCoverageIgnoreStart
} catch (Error $error) {
throw new ParserException(
sprintf(
'Cannot parse %s: %s',
$filename,
$error->getMessage()
),
(int) $error->getCode(),
$error
);
}
// @codeCoverageIgnoreEnd
$this->classes[$filename] = $codeUnitFindingVisitor->classes();
$this->traits[$filename] = $codeUnitFindingVisitor->traits();
$this->functions[$filename] = $codeUnitFindingVisitor->functions();
$this->executableLines[$filename] = $executableLinesFindingVisitor->executableLines();
$this->ignoredLines[$filename] = [];
$this->findLinesIgnoredByLineBasedAnnotations($filename, $source, $this->useAnnotationsForIgnoringCode);
$this->ignoredLines[$filename] = array_unique(
array_merge(
$this->ignoredLines[$filename],
$ignoredLinesFindingVisitor->ignoredLines()
)
);
sort($this->ignoredLines[$filename]);
$result = $lineCountingVisitor->result();
$this->linesOfCode[$filename] = [
'linesOfCode' => $result->linesOfCode(),
'commentLinesOfCode' => $result->commentLinesOfCode(),
'nonCommentLinesOfCode' => $result->nonCommentLinesOfCode(),
];
}
private function findLinesIgnoredByLineBasedAnnotations(string $filename, string $source, bool $useAnnotationsForIgnoringCode): void
{
$ignore = false;
$stop = false;
foreach (token_get_all($source) as $token) {
if (!is_array($token)) {
continue;
}
switch ($token[0]) {
case T_COMMENT:
case T_DOC_COMMENT:
if (!$useAnnotationsForIgnoringCode) {
break;
}
$comment = trim($token[1]);
if ($comment === '// @codeCoverageIgnore' ||
$comment === '//@codeCoverageIgnore') {
$ignore = true;
$stop = true;
} elseif ($comment === '// @codeCoverageIgnoreStart' ||
$comment === '//@codeCoverageIgnoreStart') {
$ignore = true;
} elseif ($comment === '// @codeCoverageIgnoreEnd' ||
$comment === '//@codeCoverageIgnoreEnd') {
$stop = true;
}
break;
}
if ($ignore) {
$this->ignoredLines[$filename][] = $token[2];
if ($stop) {
$ignore = false;
$stop = false;
}
}
}
}
}
@@ -0,0 +1,37 @@
<?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 function is_dir;
use function mkdir;
use function sprintf;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Filesystem
{
/**
* @throws DirectoryCouldNotBeCreatedException
*/
public static function createDirectory(string $directory): void
{
$success = !(!is_dir($directory) && !@mkdir($directory, 0777, true) && !is_dir($directory));
if (!$success) {
throw new DirectoryCouldNotBeCreatedException(
sprintf(
'Directory "%s" could not be created',
$directory
)
);
}
}
}
Loaded 100 of 1222 files, more files were not shown because too many files have changed in this diff. Show more