upgrade to laravel 8.x

This commit is contained in:
2021-05-30 08:20:41 +00:00
parent 41b78a0599
commit 3ad6ed8bfa
2333 changed files with 113904 additions and 59058 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
use function sprintf;
/**
* @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 static function fromCyclomaticComplexityAndCoveragePercentage(int $cyclomaticComplexity, float $codeCoverage): self
{
return new self($cyclomaticComplexity, $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
);
}
}

View File

@@ -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;
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 Directory
{
/**
* @throws DirectoryCouldNotBeCreatedException
*/
public static function create(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
)
);
}
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,17 +9,25 @@
*/
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;
/**
* Interface for code coverage drivers.
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
interface Driver
abstract class Driver
{
/**
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public const LINE_EXECUTED = 1;
public const LINE_NOT_EXECUTABLE = -2;
/**
* @var int
@@ -33,15 +41,127 @@ interface Driver
*
* @see http://xdebug.org/docs/code_coverage
*/
public const LINE_NOT_EXECUTABLE = -2;
public const LINE_EXECUTED = 1;
/**
* Start collection of code coverage information.
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public function start(bool $determineUnusedAndDead = true): void;
public const BRANCH_NOT_HIT = 0;
/**
* Stop collection of code coverage information.
* @var int
*
* @see http://xdebug.org/docs/code_coverage
*/
public function stop(): array;
public const BRANCH_HIT = 1;
/**
* @var bool
*/
private $collectBranchAndPathCoverage = false;
/**
* @var bool
*/
private $detectDeadCode = false;
/**
* @throws NoCodeCoverageDriverAvailableException
* @throws PcovNotAvailableException
* @throws PhpdbgNotAvailableException
* @throws XdebugNotAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
*
* @deprecated Use DriverSelector::forLineCoverage() instead
*/
public static function forLineCoverage(Filter $filter): self
{
return (new Selector)->forLineCoverage($filter);
}
/**
* @throws NoCodeCoverageDriverWithPathCoverageSupportAvailableException
* @throws XdebugNotAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
*
* @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;
}

View File

@@ -1,45 +0,0 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (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;
/**
* Driver for PCOV code coverage functionality.
*
* @codeCoverageIgnore
*/
final class PCOV implements Driver
{
/**
* Start collection of code coverage information.
*/
public function start(bool $determineUnusedAndDead = true): void
{
\pcov\start();
}
/**
* Stop collection of code coverage information.
*/
public function stop(): array
{
\pcov\stop();
$waiting = \pcov\waiting();
$collect = [];
if ($waiting) {
$collect = \pcov\collect(\pcov\inclusive, $waiting);
\pcov\clear();
}
return $collect;
}
}

View File

@@ -0,0 +1,62 @@
<?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 extension_loaded;
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
{
\pcov\start();
}
public function stop(): RawCodeCoverageData
{
\pcov\stop();
$collect = \pcov\collect(
\pcov\inclusive,
!$this->filter->isEmpty() ? $this->filter->files() : \pcov\waiting()
);
\pcov\clear();
return RawCodeCoverageData::fromXdebugWithoutPathCoverage($collect);
}
public function nameAndVersion(): string
{
return 'PCOV ' . phpversion('pcov');
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,54 +9,47 @@
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use SebastianBergmann\CodeCoverage\RuntimeException;
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;
/**
* Driver for PHPDBG's code coverage functionality.
*
* @codeCoverageIgnore
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class PHPDBG implements Driver
final class PhpdbgDriver extends Driver
{
/**
* @throws RuntimeException
* @throws PhpdbgNotAvailableException
*/
public function __construct()
{
if (\PHP_SAPI !== 'phpdbg') {
throw new RuntimeException(
'This driver requires the PHPDBG SAPI'
);
}
if (!\function_exists('phpdbg_start_oplog')) {
throw new RuntimeException(
'This build of PHPDBG does not support code coverage'
);
if (PHP_SAPI !== 'phpdbg') {
throw new PhpdbgNotAvailableException;
}
}
/**
* Start collection of code coverage information.
*/
public function start(bool $determineUnusedAndDead = true): void
public function start(): void
{
\phpdbg_start_oplog();
phpdbg_start_oplog();
}
/**
* Stop collection of code coverage information.
*/
public function stop(): array
public function stop(): RawCodeCoverageData
{
static $fetchedLines = [];
$dbgData = \phpdbg_end_oplog();
$dbgData = phpdbg_end_oplog();
if ($fetchedLines == []) {
$sourceLines = \phpdbg_get_executable();
if ($fetchedLines === []) {
$sourceLines = phpdbg_get_executable();
} else {
$newFiles = \array_diff(\get_included_files(), \array_keys($fetchedLines));
$newFiles = array_diff(get_included_files(), array_keys($fetchedLines));
$sourceLines = [];
@@ -71,14 +64,18 @@ final class PHPDBG implements Driver
}
}
$fetchedLines = \array_merge($fetchedLines, $sourceLines);
$fetchedLines = array_merge($fetchedLines, $sourceLines);
return $this->detectExecutedLines($fetchedLines, $dbgData);
return RawCodeCoverageData::fromXdebugWithoutPathCoverage(
$this->detectExecutedLines($fetchedLines, $dbgData)
);
}
public function nameAndVersion(): string
{
return 'PHPDBG ' . PHP_VERSION;
}
/**
* Convert phpdbg based data into the format CodeCoverage expects
*/
private function detectExecutedLines(array $sourceLines, array $dbgData): array
{
foreach ($dbgData as $file => $coveredLines) {

View File

@@ -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 XdebugNotAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
*/
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 XdebugNotAvailableException
* @throws Xdebug2NotEnabledException
* @throws Xdebug3NotEnabledException
*/
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;
}
}

View File

@@ -1,123 +0,0 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\Driver;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\RuntimeException;
/**
* Driver for Xdebug's code coverage functionality.
*
* @codeCoverageIgnore
*/
final class Xdebug implements Driver
{
/**
* @var array
*/
private $cacheNumLines = [];
/**
* @var Filter
*/
private $filter;
/**
* @throws RuntimeException
*/
public function __construct(Filter $filter = null)
{
if (!\extension_loaded('xdebug')) {
throw new RuntimeException('This driver requires Xdebug');
}
if (\version_compare(\phpversion('xdebug'), '3', '>=')) {
$mode = \getenv('XDEBUG_MODE');
if ($mode === false) {
$mode = \ini_get('xdebug.mode');
}
if ($mode === false ||
!\in_array('coverage', \explode(',', $mode), true)) {
throw new RuntimeException('XDEBUG_MODE=coverage or xdebug.mode=coverage has to be set');
}
} elseif (!\ini_get('xdebug.coverage_enable')) {
throw new RuntimeException('xdebug.coverage_enable=On has to be set');
}
if ($filter === null) {
$filter = new Filter;
}
$this->filter = $filter;
}
/**
* Start collection of code coverage information.
*/
public function start(bool $determineUnusedAndDead = true): void
{
if ($determineUnusedAndDead) {
\xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
} else {
\xdebug_start_code_coverage();
}
}
/**
* Stop collection of code coverage information.
*/
public function stop(): array
{
$data = \xdebug_get_code_coverage();
\xdebug_stop_code_coverage();
return $this->cleanup($data);
}
private function cleanup(array $data): array
{
foreach (\array_keys($data) as $file) {
unset($data[$file][0]);
if (!$this->filter->isFile($file)) {
continue;
}
$numLines = $this->getNumberOfLinesInFile($file);
foreach (\array_keys($data[$file]) as $line) {
if ($line > $numLines) {
unset($data[$file][$line]);
}
}
}
return $data;
}
private function getNumberOfLinesInFile(string $fileName): int
{
if (!isset($this->cacheNumLines[$fileName])) {
$buffer = \file_get_contents($fileName);
$lines = \substr_count($buffer, "\n");
if (\substr($buffer, -1) !== "\n") {
$lines++;
}
$this->cacheNumLines[$fileName] = $lines;
}
return $this->cacheNumLines[$fileName];
}
}

View File

@@ -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 XdebugNotAvailableException
* @throws WrongXdebugVersionException
* @throws Xdebug2NotEnabledException
*/
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');
}
}

View File

@@ -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 XdebugNotAvailableException
* @throws WrongXdebugVersionException
* @throws Xdebug3NotEnabledException
*/
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 = 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');
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,9 +9,8 @@
*/
namespace SebastianBergmann\CodeCoverage;
/**
* Exception that is raised when covered code is not executed.
*/
final class CoveredCodeNotExecutedException extends RuntimeException
use RuntimeException;
final class BranchAndPathCoverageNotSupportedException extends RuntimeException implements Exception
{
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,9 +9,8 @@
*/
namespace SebastianBergmann\CodeCoverage;
/**
* Exception that is raised when @covers must be used but is not.
*/
final class MissingCoversAnnotationException extends RuntimeException
use RuntimeException;
final class DeadCodeDetectionNotSupportedException extends RuntimeException implements Exception
{
}

View File

@@ -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 DirectoryCouldNotBeCreatedException extends RuntimeException implements Exception
{
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,9 +9,8 @@
*/
namespace SebastianBergmann\CodeCoverage;
/**
* Exception interface for php-code-coverage component.
*/
interface Exception
use Throwable;
interface Exception extends Throwable
{
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -11,26 +11,4 @@ namespace SebastianBergmann\CodeCoverage;
final class InvalidArgumentException extends \InvalidArgumentException implements Exception
{
/**
* @param int $argument
* @param string $type
* @param null|mixed $value
*
* @return InvalidArgumentException
*/
public static function create($argument, $type, $value = null): self
{
$stack = \debug_backtrace(0);
return new self(
\sprintf(
'Argument #%d%sof %s::%s() must be a %s',
$argument,
$value !== null ? ' (' . \gettype($value) . '#' . $value . ')' : ' (No Value) ',
$stack[1]['class'],
$stack[1]['function'],
$type
)
);
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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
{
}

View File

@@ -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));
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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
{
}

View File

@@ -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');
}
}

View File

@@ -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
{
}

View File

@@ -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');
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,15 +9,14 @@
*/
namespace SebastianBergmann\CodeCoverage;
/**
* Exception that is raised when code is unintentionally covered.
*/
final class UnintentionallyCoveredCodeException extends RuntimeException
use RuntimeException;
final class UnintentionallyCoveredCodeException extends RuntimeException implements Exception
{
/**
* @var array
*/
private $unintentionallyCoveredUnits = [];
private $unintentionallyCoveredUnits;
public function __construct(array $unintentionallyCoveredUnits)
{

View File

@@ -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));
}
}

View File

@@ -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
{
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,6 +9,8 @@
*/
namespace SebastianBergmann\CodeCoverage;
class RuntimeException extends \RuntimeException implements Exception
use RuntimeException;
final class XmlException extends RuntimeException implements Exception
{
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,166 +9,114 @@
*/
namespace SebastianBergmann\CodeCoverage;
use function array_keys;
use function is_file;
use function realpath;
use function strpos;
use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
/**
* Filter for whitelisting of code coverage information.
*/
final class Filter
{
/**
* Source files that are whitelisted.
*
* @var array
* @psalm-var array<string,true>
*/
private $whitelistedFiles = [];
private $files = [];
/**
* Remembers the result of the `is_file()` calls.
*
* @var bool[]
* @psalm-var array<string,bool>
*/
private $isFileCallsCache = [];
private $isFileCache = [];
/**
* Adds a directory to the whitelist (recursively).
*/
public function addDirectoryToWhitelist(string $directory, string $suffix = '.php', string $prefix = ''): void
public function includeDirectory(string $directory, string $suffix = '.php', string $prefix = ''): void
{
$facade = new FileIteratorFacade;
$files = $facade->getFilesAsArray($directory, $suffix, $prefix);
foreach ($files as $file) {
$this->addFileToWhitelist($file);
foreach ((new FileIteratorFacade)->getFilesAsArray($directory, $suffix, $prefix) as $file) {
$this->includeFile($file);
}
}
/**
* Adds a file to the whitelist.
* @psalm-param list<string> $files
*/
public function addFileToWhitelist(string $filename): void
public function includeFiles(array $filenames): void
{
$filename = \realpath($filename);
foreach ($filenames as $filename) {
$this->includeFile($filename);
}
}
public function includeFile(string $filename): void
{
$filename = realpath($filename);
if (!$filename) {
return;
}
$this->whitelistedFiles[$filename] = true;
$this->files[$filename] = true;
}
/**
* Adds files to the whitelist.
*
* @param string[] $files
*/
public function addFilesToWhitelist(array $files): void
public function excludeDirectory(string $directory, string $suffix = '.php', string $prefix = ''): void
{
foreach ($files as $file) {
$this->addFileToWhitelist($file);
foreach ((new FileIteratorFacade)->getFilesAsArray($directory, $suffix, $prefix) as $file) {
$this->excludeFile($file);
}
}
/**
* Removes a directory from the whitelist (recursively).
*/
public function removeDirectoryFromWhitelist(string $directory, string $suffix = '.php', string $prefix = ''): void
public function excludeFile(string $filename): void
{
$facade = new FileIteratorFacade;
$files = $facade->getFilesAsArray($directory, $suffix, $prefix);
$filename = realpath($filename);
foreach ($files as $file) {
$this->removeFileFromWhitelist($file);
}
}
/**
* Removes a file from the whitelist.
*/
public function removeFileFromWhitelist(string $filename): void
{
$filename = \realpath($filename);
if (!$filename || !isset($this->whitelistedFiles[$filename])) {
if (!$filename || !isset($this->files[$filename])) {
return;
}
unset($this->whitelistedFiles[$filename]);
unset($this->files[$filename]);
}
/**
* Checks whether a filename is a real filename.
*/
public function isFile(string $filename): bool
{
if (isset($this->isFileCallsCache[$filename])) {
return $this->isFileCallsCache[$filename];
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) {
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 = \file_exists($filename);
$isFile = is_file($filename);
}
$this->isFileCallsCache[$filename] = $isFile;
$this->isFileCache[$filename] = $isFile;
return $isFile;
}
/**
* Checks whether or not a file is filtered.
*/
public function isFiltered(string $filename): bool
public function isExcluded(string $filename): bool
{
if (!$this->isFile($filename)) {
return true;
}
return !isset($this->whitelistedFiles[$filename]);
return !isset($this->files[$filename]);
}
/**
* Returns the list of whitelisted files.
*
* @return string[]
* @psalm-return list<string>
*/
public function getWhitelist(): array
public function files(): array
{
return \array_keys($this->whitelistedFiles);
return array_keys($this->files);
}
/**
* Returns whether this filter has a whitelist.
*/
public function hasWhitelist(): bool
public function isEmpty(): bool
{
return !empty($this->whitelistedFiles);
}
/**
* Returns the whitelisted files.
*
* @return string[]
*/
public function getWhitelistedFiles(): array
{
return $this->whitelistedFiles;
}
/**
* Sets the whitelisted files.
*/
public function setWhitelistedFiles(array $whitelistedFiles): void
{
$this->whitelistedFiles = $whitelistedFiles;
return empty($this->files);
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,12 +9,18 @@
*/
namespace SebastianBergmann\CodeCoverage\Node;
use SebastianBergmann\CodeCoverage\Util;
use const DIRECTORY_SEPARATOR;
use function array_merge;
use function str_replace;
use function substr;
use Countable;
use SebastianBergmann\CodeCoverage\Percentage;
use SebastianBergmann\LinesOfCode\LinesOfCode;
/**
* Base class for nodes in the code coverage information tree.
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
abstract class AbstractNode implements \Countable
abstract class AbstractNode implements Countable
{
/**
* @var string
@@ -24,12 +30,12 @@ abstract class AbstractNode implements \Countable
/**
* @var string
*/
private $path;
private $pathAsString;
/**
* @var array
*/
private $pathArray;
private $pathAsArray;
/**
* @var AbstractNode
@@ -43,31 +49,31 @@ abstract class AbstractNode implements \Countable
public function __construct(string $name, self $parent = null)
{
if (\substr($name, -1) == \DIRECTORY_SEPARATOR) {
$name = \substr($name, 0, -1);
if (substr($name, -1) === DIRECTORY_SEPARATOR) {
$name = substr($name, 0, -1);
}
$this->name = $name;
$this->parent = $parent;
}
public function getName(): string
public function name(): string
{
return $this->name;
}
public function getId(): string
public function id(): string
{
if ($this->id === null) {
$parent = $this->getParent();
$parent = $this->parent();
if ($parent === null) {
$this->id = 'index';
} else {
$parentId = $parent->getId();
$parentId = $parent->id();
if ($parentId === 'index') {
$this->id = \str_replace(':', '_', $this->name);
$this->id = str_replace(':', '_', $this->name);
} else {
$this->id = $parentId . '/' . $this->name;
}
@@ -77,252 +83,169 @@ abstract class AbstractNode implements \Countable
return $this->id;
}
public function getPath(): string
public function pathAsString(): string
{
if ($this->path === null) {
if ($this->parent === null || $this->parent->getPath() === null || $this->parent->getPath() === false) {
$this->path = $this->name;
} else {
$this->path = $this->parent->getPath() . \DIRECTORY_SEPARATOR . $this->name;
}
}
return $this->path;
}
public function getPathAsArray(): array
{
if ($this->pathArray === null) {
if ($this->pathAsString === null) {
if ($this->parent === null) {
$this->pathArray = [];
$this->pathAsString = $this->name;
} else {
$this->pathArray = $this->parent->getPathAsArray();
$this->pathAsString = $this->parent->pathAsString() . DIRECTORY_SEPARATOR . $this->name;
}
$this->pathArray[] = $this;
}
return $this->pathArray;
return $this->pathAsString;
}
public function getParent(): ?self
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;
}
/**
* Returns the percentage of classes that has been tested.
*
* @return int|string
*/
public function getTestedClassesPercent(bool $asString = true)
public function percentageOfTestedClasses(): Percentage
{
return Util::percent(
$this->getNumTestedClasses(),
$this->getNumClasses(),
$asString
return Percentage::fromFractionAndTotal(
$this->numberOfTestedClasses(),
$this->numberOfClasses(),
);
}
/**
* Returns the percentage of traits that has been tested.
*
* @return int|string
*/
public function getTestedTraitsPercent(bool $asString = true)
public function percentageOfTestedTraits(): Percentage
{
return Util::percent(
$this->getNumTestedTraits(),
$this->getNumTraits(),
$asString
return Percentage::fromFractionAndTotal(
$this->numberOfTestedTraits(),
$this->numberOfTraits(),
);
}
/**
* Returns the percentage of classes and traits that has been tested.
*
* @return int|string
*/
public function getTestedClassesAndTraitsPercent(bool $asString = true)
public function percentageOfTestedClassesAndTraits(): Percentage
{
return Util::percent(
$this->getNumTestedClassesAndTraits(),
$this->getNumClassesAndTraits(),
$asString
return Percentage::fromFractionAndTotal(
$this->numberOfTestedClassesAndTraits(),
$this->numberOfClassesAndTraits(),
);
}
/**
* Returns the percentage of functions that has been tested.
*
* @return int|string
*/
public function getTestedFunctionsPercent(bool $asString = true)
public function percentageOfTestedFunctions(): Percentage
{
return Util::percent(
$this->getNumTestedFunctions(),
$this->getNumFunctions(),
$asString
return Percentage::fromFractionAndTotal(
$this->numberOfTestedFunctions(),
$this->numberOfFunctions(),
);
}
/**
* Returns the percentage of methods that has been tested.
*
* @return int|string
*/
public function getTestedMethodsPercent(bool $asString = true)
public function percentageOfTestedMethods(): Percentage
{
return Util::percent(
$this->getNumTestedMethods(),
$this->getNumMethods(),
$asString
return Percentage::fromFractionAndTotal(
$this->numberOfTestedMethods(),
$this->numberOfMethods(),
);
}
/**
* Returns the percentage of functions and methods that has been tested.
*
* @return int|string
*/
public function getTestedFunctionsAndMethodsPercent(bool $asString = true)
public function percentageOfTestedFunctionsAndMethods(): Percentage
{
return Util::percent(
$this->getNumTestedFunctionsAndMethods(),
$this->getNumFunctionsAndMethods(),
$asString
return Percentage::fromFractionAndTotal(
$this->numberOfTestedFunctionsAndMethods(),
$this->numberOfFunctionsAndMethods(),
);
}
/**
* Returns the percentage of executed lines.
*
* @return int|string
*/
public function getLineExecutedPercent(bool $asString = true)
public function percentageOfExecutedLines(): Percentage
{
return Util::percent(
$this->getNumExecutedLines(),
$this->getNumExecutableLines(),
$asString
return Percentage::fromFractionAndTotal(
$this->numberOfExecutedLines(),
$this->numberOfExecutableLines(),
);
}
/**
* Returns the number of classes and traits.
*/
public function getNumClassesAndTraits(): int
public function percentageOfExecutedBranches(): Percentage
{
return $this->getNumClasses() + $this->getNumTraits();
return Percentage::fromFractionAndTotal(
$this->numberOfExecutedBranches(),
$this->numberOfExecutableBranches()
);
}
/**
* Returns the number of tested classes and traits.
*/
public function getNumTestedClassesAndTraits(): int
public function percentageOfExecutedPaths(): Percentage
{
return $this->getNumTestedClasses() + $this->getNumTestedTraits();
return Percentage::fromFractionAndTotal(
$this->numberOfExecutedPaths(),
$this->numberOfExecutablePaths()
);
}
/**
* Returns the classes and traits of this node.
*/
public function getClassesAndTraits(): array
public function numberOfClassesAndTraits(): int
{
return \array_merge($this->getClasses(), $this->getTraits());
return $this->numberOfClasses() + $this->numberOfTraits();
}
/**
* Returns the number of functions and methods.
*/
public function getNumFunctionsAndMethods(): int
public function numberOfTestedClassesAndTraits(): int
{
return $this->getNumFunctions() + $this->getNumMethods();
return $this->numberOfTestedClasses() + $this->numberOfTestedTraits();
}
/**
* Returns the number of tested functions and methods.
*/
public function getNumTestedFunctionsAndMethods(): int
public function classesAndTraits(): array
{
return $this->getNumTestedFunctions() + $this->getNumTestedMethods();
return array_merge($this->classes(), $this->traits());
}
/**
* Returns the functions and methods of this node.
*/
public function getFunctionsAndMethods(): array
public function numberOfFunctionsAndMethods(): int
{
return \array_merge($this->getFunctions(), $this->getMethods());
return $this->numberOfFunctions() + $this->numberOfMethods();
}
/**
* Returns the classes of this node.
*/
abstract public function getClasses(): array;
public function numberOfTestedFunctionsAndMethods(): int
{
return $this->numberOfTestedFunctions() + $this->numberOfTestedMethods();
}
/**
* Returns the traits of this node.
*/
abstract public function getTraits(): array;
abstract public function classes(): array;
/**
* Returns the functions of this node.
*/
abstract public function getFunctions(): array;
abstract public function traits(): array;
/**
* Returns the LOC/CLOC/NCLOC of this node.
*/
abstract public function getLinesOfCode(): array;
abstract public function functions(): array;
/**
* Returns the number of executable lines.
*/
abstract public function getNumExecutableLines(): int;
abstract public function linesOfCode(): LinesOfCode;
/**
* Returns the number of executed lines.
*/
abstract public function getNumExecutedLines(): int;
abstract public function numberOfExecutableLines(): int;
/**
* Returns the number of classes.
*/
abstract public function getNumClasses(): int;
abstract public function numberOfExecutedLines(): int;
/**
* Returns the number of tested classes.
*/
abstract public function getNumTestedClasses(): int;
abstract public function numberOfExecutableBranches(): int;
/**
* Returns the number of traits.
*/
abstract public function getNumTraits(): int;
abstract public function numberOfExecutedBranches(): int;
/**
* Returns the number of tested traits.
*/
abstract public function getNumTestedTraits(): int;
abstract public function numberOfExecutablePaths(): int;
/**
* Returns the number of methods.
*/
abstract public function getNumMethods(): int;
abstract public function numberOfExecutedPaths(): int;
/**
* Returns the number of tested methods.
*/
abstract public function getNumTestedMethods(): int;
abstract public function numberOfClasses(): int;
/**
* Returns the number of functions.
*/
abstract public function getNumFunctions(): int;
abstract public function numberOfTestedClasses(): int;
/**
* Returns the number of tested functions.
*/
abstract public function getNumTestedFunctions(): 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;
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,14 +9,40 @@
*/
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\CoveredFileAnalyser;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Builder
{
/**
* @var CoveredFileAnalyser
*/
private $coveredFileAnalyser;
public function __construct(CoveredFileAnalyser $coveredFileAnalyser)
{
$this->coveredFileAnalyser = $coveredFileAnalyser;
}
public function build(CodeCoverage $coverage): Directory
{
$files = $coverage->getData();
$commonPath = $this->reducePaths($files);
$data = clone $coverage->getData(); // clone because path munging is destructive to the original data
$commonPath = $this->reducePaths($data);
$root = new Directory(
$commonPath,
null
@@ -24,28 +50,41 @@ final class Builder
$this->addItems(
$root,
$this->buildDirectoryStructure($files),
$coverage->getTests(),
$coverage->getCacheTokens()
$this->buildDirectoryStructure($data),
$coverage->getTests()
);
return $root;
}
private function addItems(Directory $root, array $items, array $tests, bool $cacheTokens): void
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);
if (substr($key, -2) === '/f') {
$key = substr($key, 0, -2);
$filename = $root->pathAsString() . DIRECTORY_SEPARATOR . $key;
if (\file_exists($root->getPath() . \DIRECTORY_SEPARATOR . $key)) {
$root->addFile($key, $value, $tests, $cacheTokens);
if (is_file($filename)) {
$root->addFile(
new File(
$key,
$root,
$value['lineCoverage'],
$value['functionCoverage'],
$tests,
$this->coveredFileAnalyser->classesIn($filename),
$this->coveredFileAnalyser->traitsIn($filename),
$this->coveredFileAnalyser->functionsIn($filename),
$this->coveredFileAnalyser->linesOfCodeFor($filename)
)
);
}
} else {
$child = $root->addDirectory($key);
$this->addItems($child, $value, $tests, $cacheTokens);
$this->addItems($child, $value, $tests);
}
}
}
@@ -90,14 +129,14 @@ final class Builder
* )
* </code>
*/
private function buildDirectoryStructure(array $files): array
private function buildDirectoryStructure(ProcessedCodeCoverageData $data): array
{
$result = [];
foreach ($files as $path => $file) {
$path = \explode(\DIRECTORY_SEPARATOR, $path);
foreach ($data->coveredFiles() as $originalPath) {
$path = explode(DIRECTORY_SEPARATOR, $originalPath);
$pointer = &$result;
$max = \count($path);
$max = count($path);
for ($i = 0; $i < $max; $i++) {
$type = '';
@@ -109,7 +148,10 @@ final class Builder
$pointer = &$pointer[$path[$i] . $type];
}
$pointer = $file;
$pointer = [
'lineCoverage' => $data->lineCoverage()[$originalPath] ?? [],
'functionCoverage' => $data->functionCoverage()[$originalPath] ?? [],
];
}
return $result;
@@ -152,41 +194,39 @@ final class Builder
* )
* </code>
*/
private function reducePaths(array &$files): string
private function reducePaths(ProcessedCodeCoverageData $coverage): string
{
if (empty($files)) {
if (empty($coverage->coveredFiles())) {
return '.';
}
$commonPath = '';
$paths = \array_keys($files);
$paths = $coverage->coveredFiles();
if (\count($files) === 1) {
$commonPath = \dirname($paths[0]) . \DIRECTORY_SEPARATOR;
$files[\basename($paths[0])] = $files[$paths[0]];
unset($files[$paths[0]]);
if (count($paths) === 1) {
$commonPath = dirname($paths[0]) . DIRECTORY_SEPARATOR;
$coverage->renameFile($paths[0], basename($paths[0]));
return $commonPath;
}
$max = \count($paths);
$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]);
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]);
$paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]);
if (empty($paths[$i][0])) {
$paths[$i][0] = \DIRECTORY_SEPARATOR;
$paths[$i][0] = DIRECTORY_SEPARATOR;
}
}
$done = false;
$max = \count($paths);
$max = count($paths);
while (!$done) {
for ($i = 0; $i < $max - 1; $i++) {
@@ -202,26 +242,23 @@ final class Builder
if (!$done) {
$commonPath .= $paths[0][0];
if ($paths[0][0] !== \DIRECTORY_SEPARATOR) {
$commonPath .= \DIRECTORY_SEPARATOR;
if ($paths[0][0] !== DIRECTORY_SEPARATOR) {
$commonPath .= DIRECTORY_SEPARATOR;
}
for ($i = 0; $i < $max; $i++) {
\array_shift($paths[$i]);
array_shift($paths[$i]);
}
}
}
$original = \array_keys($files);
$max = \count($original);
$original = $coverage->coveredFiles();
$max = count($original);
for ($i = 0; $i < $max; $i++) {
$files[\implode(\DIRECTORY_SEPARATOR, $paths[$i])] = $files[$original[$i]];
unset($files[$original[$i]]);
$coverage->renameFile($original[$i], implode(DIRECTORY_SEPARATOR, $paths[$i]));
}
\ksort($files);
return \substr($commonPath, 0, -1);
return substr($commonPath, 0, -1);
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,12 +9,16 @@
*/
namespace SebastianBergmann\CodeCoverage\Node;
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
use function array_merge;
use function count;
use IteratorAggregate;
use RecursiveIteratorIterator;
use SebastianBergmann\LinesOfCode\LinesOfCode;
/**
* Represents a directory in the code coverage information tree.
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Directory extends AbstractNode implements \IteratorAggregate
final class Directory extends AbstractNode implements IteratorAggregate
{
/**
* @var AbstractNode[]
@@ -47,7 +51,7 @@ final class Directory extends AbstractNode implements \IteratorAggregate
private $functions;
/**
* @var array
* @var LinesOfCode
*/
private $linesOfCode;
@@ -66,6 +70,26 @@ final class Directory extends AbstractNode implements \IteratorAggregate
*/
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
*/
@@ -106,100 +130,70 @@ final class Directory extends AbstractNode implements \IteratorAggregate
*/
private $numTestedFunctions = -1;
/**
* Returns the number of files in/under this node.
*/
public function count(): int
{
if ($this->numFiles === -1) {
$this->numFiles = 0;
foreach ($this->children as $child) {
$this->numFiles += \count($child);
$this->numFiles += count($child);
}
}
return $this->numFiles;
}
/**
* Returns an iterator for this node.
*/
public function getIterator(): \RecursiveIteratorIterator
public function getIterator(): RecursiveIteratorIterator
{
return new \RecursiveIteratorIterator(
return new RecursiveIteratorIterator(
new Iterator($this),
\RecursiveIteratorIterator::SELF_FIRST
RecursiveIteratorIterator::SELF_FIRST
);
}
/**
* Adds a new directory.
*/
public function addDirectory(string $name): self
{
$directory = new self($name, $this);
$this->children[] = $directory;
$this->directories[] = &$this->children[\count($this->children) - 1];
$this->directories[] = &$this->children[count($this->children) - 1];
return $directory;
}
/**
* Adds a new file.
*
* @throws InvalidArgumentException
*/
public function addFile(string $name, array $coverageData, array $testData, bool $cacheTokens): File
public function addFile(File $file): void
{
$file = new File($name, $this, $coverageData, $testData, $cacheTokens);
$this->children[] = $file;
$this->files[] = &$this->children[\count($this->children) - 1];
$this->files[] = &$this->children[count($this->children) - 1];
$this->numExecutableLines = -1;
$this->numExecutedLines = -1;
return $file;
}
/**
* Returns the directories in this directory.
*/
public function getDirectories(): array
public function directories(): array
{
return $this->directories;
}
/**
* Returns the files in this directory.
*/
public function getFiles(): array
public function files(): array
{
return $this->files;
}
/**
* Returns the child nodes of this node.
*/
public function getChildNodes(): array
public function children(): array
{
return $this->children;
}
/**
* Returns the classes of this node.
*/
public function getClasses(): array
public function classes(): array
{
if ($this->classes === null) {
$this->classes = [];
foreach ($this->children as $child) {
$this->classes = \array_merge(
$this->classes = array_merge(
$this->classes,
$child->getClasses()
$child->classes()
);
}
}
@@ -207,18 +201,15 @@ final class Directory extends AbstractNode implements \IteratorAggregate
return $this->classes;
}
/**
* Returns the traits of this node.
*/
public function getTraits(): array
public function traits(): array
{
if ($this->traits === null) {
$this->traits = [];
foreach ($this->children as $child) {
$this->traits = \array_merge(
$this->traits = array_merge(
$this->traits,
$child->getTraits()
$child->traits()
);
}
}
@@ -226,18 +217,15 @@ final class Directory extends AbstractNode implements \IteratorAggregate
return $this->traits;
}
/**
* Returns the functions of this node.
*/
public function getFunctions(): array
public function functions(): array
{
if ($this->functions === null) {
$this->functions = [];
foreach ($this->children as $child) {
$this->functions = \array_merge(
$this->functions = array_merge(
$this->functions,
$child->getFunctions()
$child->functions()
);
}
}
@@ -245,180 +233,195 @@ final class Directory extends AbstractNode implements \IteratorAggregate
return $this->functions;
}
/**
* Returns the LOC/CLOC/NCLOC of this node.
*/
public function getLinesOfCode(): array
public function linesOfCode(): LinesOfCode
{
if ($this->linesOfCode === null) {
$this->linesOfCode = ['loc' => 0, 'cloc' => 0, 'ncloc' => 0];
$this->linesOfCode = new LinesOfCode(0, 0, 0, 0);
foreach ($this->children as $child) {
$linesOfCode = $child->getLinesOfCode();
$this->linesOfCode['loc'] += $linesOfCode['loc'];
$this->linesOfCode['cloc'] += $linesOfCode['cloc'];
$this->linesOfCode['ncloc'] += $linesOfCode['ncloc'];
$this->linesOfCode = $this->linesOfCode->plus($child->linesOfCode());
}
}
return $this->linesOfCode;
}
/**
* Returns the number of executable lines.
*/
public function getNumExecutableLines(): int
public function numberOfExecutableLines(): int
{
if ($this->numExecutableLines === -1) {
$this->numExecutableLines = 0;
foreach ($this->children as $child) {
$this->numExecutableLines += $child->getNumExecutableLines();
$this->numExecutableLines += $child->numberOfExecutableLines();
}
}
return $this->numExecutableLines;
}
/**
* Returns the number of executed lines.
*/
public function getNumExecutedLines(): int
public function numberOfExecutedLines(): int
{
if ($this->numExecutedLines === -1) {
$this->numExecutedLines = 0;
foreach ($this->children as $child) {
$this->numExecutedLines += $child->getNumExecutedLines();
$this->numExecutedLines += $child->numberOfExecutedLines();
}
}
return $this->numExecutedLines;
}
/**
* Returns the number of classes.
*/
public function getNumClasses(): int
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->getNumClasses();
$this->numClasses += $child->numberOfClasses();
}
}
return $this->numClasses;
}
/**
* Returns the number of tested classes.
*/
public function getNumTestedClasses(): int
public function numberOfTestedClasses(): int
{
if ($this->numTestedClasses === -1) {
$this->numTestedClasses = 0;
foreach ($this->children as $child) {
$this->numTestedClasses += $child->getNumTestedClasses();
$this->numTestedClasses += $child->numberOfTestedClasses();
}
}
return $this->numTestedClasses;
}
/**
* Returns the number of traits.
*/
public function getNumTraits(): int
public function numberOfTraits(): int
{
if ($this->numTraits === -1) {
$this->numTraits = 0;
foreach ($this->children as $child) {
$this->numTraits += $child->getNumTraits();
$this->numTraits += $child->numberOfTraits();
}
}
return $this->numTraits;
}
/**
* Returns the number of tested traits.
*/
public function getNumTestedTraits(): int
public function numberOfTestedTraits(): int
{
if ($this->numTestedTraits === -1) {
$this->numTestedTraits = 0;
foreach ($this->children as $child) {
$this->numTestedTraits += $child->getNumTestedTraits();
$this->numTestedTraits += $child->numberOfTestedTraits();
}
}
return $this->numTestedTraits;
}
/**
* Returns the number of methods.
*/
public function getNumMethods(): int
public function numberOfMethods(): int
{
if ($this->numMethods === -1) {
$this->numMethods = 0;
foreach ($this->children as $child) {
$this->numMethods += $child->getNumMethods();
$this->numMethods += $child->numberOfMethods();
}
}
return $this->numMethods;
}
/**
* Returns the number of tested methods.
*/
public function getNumTestedMethods(): int
public function numberOfTestedMethods(): int
{
if ($this->numTestedMethods === -1) {
$this->numTestedMethods = 0;
foreach ($this->children as $child) {
$this->numTestedMethods += $child->getNumTestedMethods();
$this->numTestedMethods += $child->numberOfTestedMethods();
}
}
return $this->numTestedMethods;
}
/**
* Returns the number of functions.
*/
public function getNumFunctions(): int
public function numberOfFunctions(): int
{
if ($this->numFunctions === -1) {
$this->numFunctions = 0;
foreach ($this->children as $child) {
$this->numFunctions += $child->getNumFunctions();
$this->numFunctions += $child->numberOfFunctions();
}
}
return $this->numFunctions;
}
/**
* Returns the number of tested functions.
*/
public function getNumTestedFunctions(): int
public function numberOfTestedFunctions(): int
{
if ($this->numTestedFunctions === -1) {
$this->numTestedFunctions = 0;
foreach ($this->children as $child) {
$this->numTestedFunctions += $child->getNumTestedFunctions();
$this->numTestedFunctions += $child->numberOfTestedFunctions();
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,15 +9,26 @@
*/
namespace SebastianBergmann\CodeCoverage\Node;
use function array_filter;
use function count;
use function range;
use SebastianBergmann\CodeCoverage\CrapIndex;
use SebastianBergmann\LinesOfCode\LinesOfCode;
/**
* Represents a file in the code coverage information tree.
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class File extends AbstractNode
{
/**
* @var array
*/
private $coverageData;
private $lineCoverageData;
/**
* @var array
*/
private $functionCoverageData;
/**
* @var array
@@ -34,6 +45,26 @@ final class File extends AbstractNode
*/
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
*/
@@ -50,9 +81,9 @@ final class File extends AbstractNode
private $functions = [];
/**
* @var array
* @var LinesOfCode
*/
private $linesOfCode = [];
private $linesOfCode;
/**
* @var int
@@ -89,103 +120,94 @@ final class File extends AbstractNode
*/
private $numTestedFunctions;
/**
* @var bool
*/
private $cacheTokens;
/**
* @var array
*/
private $codeUnitsByLine = [];
public function __construct(string $name, AbstractNode $parent, array $coverageData, array $testData, bool $cacheTokens)
public function __construct(string $name, AbstractNode $parent, array $lineCoverageData, array $functionCoverageData, array $testData, array $classes, array $traits, array $functions, LinesOfCode $linesOfCode)
{
parent::__construct($name, $parent);
$this->coverageData = $coverageData;
$this->testData = $testData;
$this->cacheTokens = $cacheTokens;
$this->lineCoverageData = $lineCoverageData;
$this->functionCoverageData = $functionCoverageData;
$this->testData = $testData;
$this->linesOfCode = $linesOfCode;
$this->calculateStatistics();
$this->calculateStatistics($classes, $traits, $functions);
}
/**
* Returns the number of files in/under this node.
*/
public function count(): int
{
return 1;
}
/**
* Returns the code coverage data of this node.
*/
public function getCoverageData(): array
public function lineCoverageData(): array
{
return $this->coverageData;
return $this->lineCoverageData;
}
/**
* Returns the test data of this node.
*/
public function getTestData(): array
public function functionCoverageData(): array
{
return $this->functionCoverageData;
}
public function testData(): array
{
return $this->testData;
}
/**
* Returns the classes of this node.
*/
public function getClasses(): array
public function classes(): array
{
return $this->classes;
}
/**
* Returns the traits of this node.
*/
public function getTraits(): array
public function traits(): array
{
return $this->traits;
}
/**
* Returns the functions of this node.
*/
public function getFunctions(): array
public function functions(): array
{
return $this->functions;
}
/**
* Returns the LOC/CLOC/NCLOC of this node.
*/
public function getLinesOfCode(): array
public function linesOfCode(): LinesOfCode
{
return $this->linesOfCode;
}
/**
* Returns the number of executable lines.
*/
public function getNumExecutableLines(): int
public function numberOfExecutableLines(): int
{
return $this->numExecutableLines;
}
/**
* Returns the number of executed lines.
*/
public function getNumExecutedLines(): int
public function numberOfExecutedLines(): int
{
return $this->numExecutedLines;
}
/**
* Returns the number of classes.
*/
public function getNumClasses(): int
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;
@@ -204,18 +226,12 @@ final class File extends AbstractNode
return $this->numClasses;
}
/**
* Returns the number of tested classes.
*/
public function getNumTestedClasses(): int
public function numberOfTestedClasses(): int
{
return $this->numTestedClasses;
}
/**
* Returns the number of traits.
*/
public function getNumTraits(): int
public function numberOfTraits(): int
{
if ($this->numTraits === null) {
$this->numTraits = 0;
@@ -234,18 +250,12 @@ final class File extends AbstractNode
return $this->numTraits;
}
/**
* Returns the number of tested traits.
*/
public function getNumTestedTraits(): int
public function numberOfTestedTraits(): int
{
return $this->numTestedTraits;
}
/**
* Returns the number of methods.
*/
public function getNumMethods(): int
public function numberOfMethods(): int
{
if ($this->numMethods === null) {
$this->numMethods = 0;
@@ -270,10 +280,7 @@ final class File extends AbstractNode
return $this->numMethods;
}
/**
* Returns the number of tested methods.
*/
public function getNumTestedMethods(): int
public function numberOfTestedMethods(): int
{
if ($this->numTestedMethods === null) {
$this->numTestedMethods = 0;
@@ -300,18 +307,12 @@ final class File extends AbstractNode
return $this->numTestedMethods;
}
/**
* Returns the number of functions.
*/
public function getNumFunctions(): int
public function numberOfFunctions(): int
{
return \count($this->functions);
return count($this->functions);
}
/**
* Returns the number of tested functions.
*/
public function getNumTestedFunctions(): int
public function numberOfTestedFunctions(): int
{
if ($this->numTestedFunctions === null) {
$this->numTestedFunctions = 0;
@@ -327,32 +328,18 @@ final class File extends AbstractNode
return $this->numTestedFunctions;
}
private function calculateStatistics(): void
private function calculateStatistics(array $classes, array $traits, array $functions): void
{
if ($this->cacheTokens) {
$tokens = \PHP_Token_Stream_CachingFactory::get($this->getPath());
} else {
$tokens = new \PHP_Token_Stream($this->getPath());
}
$this->linesOfCode = $tokens->getLinesOfCode();
foreach (\range(1, $this->linesOfCode['loc']) as $lineNumber) {
foreach (range(1, $this->linesOfCode->linesOfCode()) as $lineNumber) {
$this->codeUnitsByLine[$lineNumber] = [];
}
try {
$this->processClasses($tokens);
$this->processTraits($tokens);
$this->processFunctions($tokens);
} catch (\OutOfBoundsException $e) {
// This can happen with PHP_Token_Stream if the file is syntactically invalid,
// and probably affects a file that wasn't executed.
}
unset($tokens);
$this->processClasses($classes);
$this->processTraits($traits);
$this->processFunctions($functions);
foreach (\range(1, $this->linesOfCode['loc']) as $lineNumber) {
if (isset($this->coverageData[$lineNumber])) {
foreach (range(1, $this->linesOfCode->linesOfCode()) as $lineNumber) {
if (isset($this->lineCoverageData[$lineNumber])) {
foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) {
$codeUnit['executableLines']++;
}
@@ -361,7 +348,7 @@ final class File extends AbstractNode
$this->numExecutableLines++;
if (\count($this->coverageData[$lineNumber]) > 0) {
if (count($this->lineCoverageData[$lineNumber]) > 0) {
foreach ($this->codeUnitsByLine[$lineNumber] as &$codeUnit) {
$codeUnit['executedLines']++;
}
@@ -375,134 +362,111 @@ final class File extends AbstractNode
foreach ($this->traits as &$trait) {
foreach ($trait['methods'] as &$method) {
if ($method['executableLines'] > 0) {
$method['coverage'] = ($method['executedLines'] /
$method['executableLines']) * 100;
} else {
$method['coverage'] = 100;
}
$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['crap'] = $this->crap(
$method['ccn'],
$method['coverage']
);
$method['coverage'] = $methodBranchCoverage ?: $methodLineCoverage;
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage ?: $methodLineCoverage))->asString();
$trait['ccn'] += $method['ccn'];
}
unset($method);
if ($trait['executableLines'] > 0) {
$trait['coverage'] = ($trait['executedLines'] /
$trait['executableLines']) * 100;
$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;
if ($trait['coverage'] === 100) {
$this->numTestedClasses++;
}
} else {
$trait['coverage'] = 100;
$trait['coverage'] = $traitBranchCoverage ?: $traitLineCoverage;
$trait['crap'] = (new CrapIndex($trait['ccn'], $traitPathCoverage ?: $traitLineCoverage))->asString();
if ($trait['executableLines'] > 0 && $trait['coverage'] === 100) {
$this->numTestedClasses++;
}
$trait['crap'] = $this->crap(
$trait['ccn'],
$trait['coverage']
);
}
unset($trait);
foreach ($this->classes as &$class) {
foreach ($class['methods'] as &$method) {
if ($method['executableLines'] > 0) {
$method['coverage'] = ($method['executedLines'] /
$method['executableLines']) * 100;
} else {
$method['coverage'] = 100;
}
$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['crap'] = $this->crap(
$method['ccn'],
$method['coverage']
);
$method['coverage'] = $methodBranchCoverage ?: $methodLineCoverage;
$method['crap'] = (new CrapIndex($method['ccn'], $methodPathCoverage ?: $methodLineCoverage))->asString();
$class['ccn'] += $method['ccn'];
}
unset($method);
if ($class['executableLines'] > 0) {
$class['coverage'] = ($class['executedLines'] /
$class['executableLines']) * 100;
$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;
if ($class['coverage'] === 100) {
$this->numTestedClasses++;
}
} else {
$class['coverage'] = 100;
$class['coverage'] = $classBranchCoverage ?: $classLineCoverage;
$class['crap'] = (new CrapIndex($class['ccn'], $classPathCoverage ?: $classLineCoverage))->asString();
if ($class['executableLines'] > 0 && $class['coverage'] === 100) {
$this->numTestedClasses++;
}
$class['crap'] = $this->crap(
$class['ccn'],
$class['coverage']
);
}
unset($class);
foreach ($this->functions as &$function) {
if ($function['executableLines'] > 0) {
$function['coverage'] = ($function['executedLines'] /
$function['executableLines']) * 100;
} else {
$function['coverage'] = 100;
}
$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++;
}
$function['crap'] = $this->crap(
$function['ccn'],
$function['coverage']
);
}
}
private function processClasses(\PHP_Token_Stream $tokens): void
private function processClasses(array $classes): void
{
$classes = $tokens->getClasses();
$link = $this->getId() . '.html#';
$link = $this->id() . '.html#';
foreach ($classes as $className => $class) {
if (\strpos($className, 'anonymous') === 0) {
continue;
}
if (!empty($class['package']['namespace'])) {
$className = $class['package']['namespace'] . '\\' . $className;
}
$this->classes[$className] = [
'className' => $className,
'methods' => [],
'startLine' => $class['startLine'],
'executableLines' => 0,
'executedLines' => 0,
'ccn' => 0,
'coverage' => 0,
'crap' => 0,
'package' => $class['package'],
'link' => $link . $class['startLine'],
'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) {
if (\strpos($methodName, 'anonymous') === 0) {
continue;
}
$methodData = $this->newMethod($className, $methodName, $method, $link);
$this->classes[$className]['methods'][$methodName] = $methodData;
$this->classes[$className]['methods'][$methodName] = $this->newMethod($methodName, $method, $link);
$this->classes[$className]['executableBranches'] += $methodData['executableBranches'];
$this->classes[$className]['executedBranches'] += $methodData['executedBranches'];
$this->classes[$className]['executablePaths'] += $methodData['executablePaths'];
$this->classes[$className]['executedPaths'] += $methodData['executedPaths'];
foreach (\range($method['startLine'], $method['endLine']) as $lineNumber) {
$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],
@@ -512,33 +476,43 @@ final class File extends AbstractNode
}
}
private function processTraits(\PHP_Token_Stream $tokens): void
private function processTraits(array $traits): void
{
$traits = $tokens->getTraits();
$link = $this->getId() . '.html#';
$link = $this->id() . '.html#';
foreach ($traits as $traitName => $trait) {
$this->traits[$traitName] = [
'traitName' => $traitName,
'methods' => [],
'startLine' => $trait['startLine'],
'executableLines' => 0,
'executedLines' => 0,
'ccn' => 0,
'coverage' => 0,
'crap' => 0,
'package' => $trait['package'],
'link' => $link . $trait['startLine'],
'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) {
if (\strpos($methodName, 'anonymous') === 0) {
continue;
}
$methodData = $this->newMethod($traitName, $methodName, $method, $link);
$this->traits[$traitName]['methods'][$methodName] = $methodData;
$this->traits[$traitName]['methods'][$methodName] = $this->newMethod($methodName, $method, $link);
$this->traits[$traitName]['executableBranches'] += $methodData['executableBranches'];
$this->traits[$traitName]['executedBranches'] += $methodData['executedBranches'];
$this->traits[$traitName]['executablePaths'] += $methodData['executablePaths'];
$this->traits[$traitName]['executedPaths'] += $methodData['executedPaths'];
foreach (\range($method['startLine'], $method['endLine']) as $lineNumber) {
$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],
@@ -548,64 +522,122 @@ final class File extends AbstractNode
}
}
private function processFunctions(\PHP_Token_Stream $tokens): void
private function processFunctions(array $functions): void
{
$functions = $tokens->getFunctions();
$link = $this->getId() . '.html#';
$link = $this->id() . '.html#';
foreach ($functions as $functionName => $function) {
if (\strpos($functionName, 'anonymous') === 0) {
continue;
}
$this->functions[$functionName] = [
'functionName' => $functionName,
'signature' => $function['signature'],
'startLine' => $function['startLine'],
'executableLines' => 0,
'executedLines' => 0,
'ccn' => $function['ccn'],
'coverage' => 0,
'crap' => 0,
'link' => $link . $function['startLine'],
'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) {
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 crap(int $ccn, float $coverage): string
private function newMethod(string $className, string $methodName, array $method, string $link): array
{
if ($coverage === 0.0) {
return (string) ($ccn ** 2 + $ccn);
}
if ($coverage >= 95) {
return (string) $ccn;
}
return \sprintf(
'%01.2F',
$ccn ** 2 * (1 - $coverage / 100) ** 3 + $ccn
);
}
private function newMethod(string $methodName, array $method, string $link): array
{
return [
'methodName' => $methodName,
'visibility' => $method['visibility'],
'signature' => $method['signature'],
'startLine' => $method['startLine'],
'endLine' => $method['endLine'],
'executableLines' => 0,
'executedLines' => 0,
'ccn' => $method['ccn'],
'coverage' => 0,
'crap' => 0,
'link' => $link . $method['startLine'],
$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;
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,10 +9,13 @@
*/
namespace SebastianBergmann\CodeCoverage\Node;
use function count;
use RecursiveIterator;
/**
* Recursive iterator for node object graphs.
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Iterator implements \RecursiveIterator
final class Iterator implements RecursiveIterator
{
/**
* @var int
@@ -26,7 +29,7 @@ final class Iterator implements \RecursiveIterator
public function __construct(Directory $node)
{
$this->nodes = $node->getChildNodes();
$this->nodes = $node->children();
}
/**
@@ -42,7 +45,7 @@ final class Iterator implements \RecursiveIterator
*/
public function valid(): bool
{
return $this->position < \count($this->nodes);
return $this->position < count($this->nodes);
}
/**
@@ -56,7 +59,7 @@ final class Iterator implements \RecursiveIterator
/**
* Returns the current element.
*/
public function current(): AbstractNode
public function current(): ?AbstractNode
{
return $this->valid() ? $this->nodes[$this->position] : null;
}

View File

@@ -0,0 +1,66 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
use function sprintf;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Percentage
{
/**
* @var float
*/
private $fraction;
/**
* @var float
*/
private $total;
public static function fromFractionAndTotal(float $fraction, float $total): self
{
return new self($fraction, $total);
}
private function __construct(float $fraction, float $total)
{
$this->fraction = $fraction;
$this->total = $total;
}
public function asFloat(): float
{
if ($this->total > 0) {
return ($this->fraction / $this->total) * 100;
}
return 100.0;
}
public function asString(): string
{
if ($this->total > 0) {
return sprintf('%01.2F%%', $this->asFloat());
}
return '';
}
public function asFixedWidthString(): string
{
if ($this->total > 0) {
return sprintf('%6.2F%%', $this->asFloat());
}
return '';
}
}

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;
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'] = [];
}
}
}
}

View File

@@ -0,0 +1,227 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage;
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 file;
use function in_array;
use function range;
use SebastianBergmann\CodeCoverage\Driver\Driver;
use SebastianBergmann\CodeCoverage\StaticAnalysis\UncoveredFileAnalyser;
/**
* @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, UncoveredFileAnalyser $uncoveredFileAnalyser): self
{
$lineCoverage = [];
foreach ($uncoveredFileAnalyser->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 keepCoverageDataOnlyForLines(string $filename, array $lines): void
{
if (!isset($this->lineCoverage[$filename])) {
return;
}
$this->lineCoverage[$filename] = array_intersect_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_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];
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,31 +9,40 @@
*/
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\Directory;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\RuntimeException;
/**
* Generates a Clover XML logfile from a code coverage object.
*/
final class Clover
{
/**
* @throws \RuntimeException
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
{
$xmlDocument = new \DOMDocument('1.0', 'UTF-8');
$time = (string) time();
$xmlDocument = new DOMDocument('1.0', 'UTF-8');
$xmlDocument->formatOutput = true;
$xmlCoverage = $xmlDocument->createElement('coverage');
$xmlCoverage->setAttribute('generated', (string) $_SERVER['REQUEST_TIME']);
$xmlCoverage->setAttribute('generated', $time);
$xmlDocument->appendChild($xmlCoverage);
$xmlProject = $xmlDocument->createElement('project');
$xmlProject->setAttribute('timestamp', (string) $_SERVER['REQUEST_TIME']);
$xmlProject->setAttribute('timestamp', $time);
if (\is_string($name)) {
if (is_string($name)) {
$xmlProject->setAttribute('name', $name);
}
@@ -50,10 +59,10 @@ final class Clover
/* @var File $item */
$xmlFile = $xmlDocument->createElement('file');
$xmlFile->setAttribute('name', $item->getPath());
$xmlFile->setAttribute('name', $item->pathAsString());
$classes = $item->getClassesAndTraits();
$coverageData = $item->getCoverageData();
$classes = $item->classesAndTraits();
$coverageData = $item->lineCoverageData();
$lines = [];
$namespace = 'global';
@@ -78,19 +87,19 @@ final class Clover
$methodCount = 0;
foreach (\range($method['startLine'], $method['endLine']) as $line) {
foreach (range($method['startLine'], $method['endLine']) as $line) {
if (isset($coverageData[$line]) && ($coverageData[$line] !== null)) {
$methodCount = \max($methodCount, \count($coverageData[$line]));
$methodCount = max($methodCount, count($coverageData[$line]));
}
}
$lines[$method['startLine']] = [
'ccn' => $method['ccn'],
'count' => $methodCount,
'crap' => $method['crap'],
'type' => 'method',
'visibility' => $method['visibility'],
'name' => $methodName,
'ccn' => $method['ccn'],
'count' => $methodCount,
'crap' => $method['crap'],
'type' => 'method',
'visibility' => $method['visibility'],
'name' => $methodName,
];
}
@@ -136,12 +145,12 @@ final class Clover
$xmlMetrics->setAttribute('complexity', (string) $class['ccn']);
$xmlMetrics->setAttribute('methods', (string) $classMethods);
$xmlMetrics->setAttribute('coveredmethods', (string) $coveredMethods);
$xmlMetrics->setAttribute('conditionals', '0');
$xmlMetrics->setAttribute('coveredconditionals', '0');
$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 /* + conditionals */));
$xmlMetrics->setAttribute('coveredelements', (string) ($coveredMethods + $coveredClassStatements /* + coveredconditionals */));
$xmlMetrics->setAttribute('elements', (string) ($classMethods + $classStatements + $class['executableBranches']));
$xmlMetrics->setAttribute('coveredelements', (string) ($coveredMethods + $coveredClassStatements + $class['executedBranches']));
$xmlClass->appendChild($xmlMetrics);
}
@@ -151,11 +160,11 @@ final class Clover
}
$lines[$line] = [
'count' => \count($data), 'type' => 'stmt',
'count' => count($data), 'type' => 'stmt',
];
}
\ksort($lines);
ksort($lines);
foreach ($lines as $line => $data) {
$xmlLine = $xmlDocument->createElement('line');
@@ -182,20 +191,20 @@ final class Clover
$xmlFile->appendChild($xmlLine);
}
$linesOfCode = $item->getLinesOfCode();
$linesOfCode = $item->linesOfCode();
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('loc', (string) $linesOfCode['loc']);
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode['ncloc']);
$xmlMetrics->setAttribute('classes', (string) $item->getNumClassesAndTraits());
$xmlMetrics->setAttribute('methods', (string) $item->getNumMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $item->getNumTestedMethods());
$xmlMetrics->setAttribute('conditionals', '0');
$xmlMetrics->setAttribute('coveredconditionals', '0');
$xmlMetrics->setAttribute('statements', (string) $item->getNumExecutableLines());
$xmlMetrics->setAttribute('coveredstatements', (string) $item->getNumExecutedLines());
$xmlMetrics->setAttribute('elements', (string) ($item->getNumMethods() + $item->getNumExecutableLines() /* + conditionals */));
$xmlMetrics->setAttribute('coveredelements', (string) ($item->getNumTestedMethods() + $item->getNumExecutedLines() /* + coveredconditionals */));
$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') {
@@ -214,45 +223,33 @@ final class Clover
}
}
$linesOfCode = $report->getLinesOfCode();
$linesOfCode = $report->linesOfCode();
$xmlMetrics = $xmlDocument->createElement('metrics');
$xmlMetrics->setAttribute('files', (string) \count($report));
$xmlMetrics->setAttribute('loc', (string) $linesOfCode['loc']);
$xmlMetrics->setAttribute('ncloc', (string) $linesOfCode['ncloc']);
$xmlMetrics->setAttribute('classes', (string) $report->getNumClassesAndTraits());
$xmlMetrics->setAttribute('methods', (string) $report->getNumMethods());
$xmlMetrics->setAttribute('coveredmethods', (string) $report->getNumTestedMethods());
$xmlMetrics->setAttribute('conditionals', '0');
$xmlMetrics->setAttribute('coveredconditionals', '0');
$xmlMetrics->setAttribute('statements', (string) $report->getNumExecutableLines());
$xmlMetrics->setAttribute('coveredstatements', (string) $report->getNumExecutedLines());
$xmlMetrics->setAttribute('elements', (string) ($report->getNumMethods() + $report->getNumExecutableLines() /* + conditionals */));
$xmlMetrics->setAttribute('coveredelements', (string) ($report->getNumTestedMethods() + $report->getNumExecutedLines() /* + coveredconditionals */));
$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) {
if (!$this->createDirectory(\dirname($target))) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', \dirname($target)));
}
Directory::create(dirname($target));
if (@\file_put_contents($target, $buffer) === false) {
throw new RuntimeException(
\sprintf(
'Could not write to "%s',
$target
)
);
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
}
return $buffer;
}
private function createDirectory(string $directory): bool
{
return !(!\is_dir($directory) && !@\mkdir($directory, 0777, true) && !\is_dir($directory));
}
}

View File

@@ -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\Directory;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Node\File;
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) {
Directory::create(dirname($target));
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
}
return $buffer;
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,9 +9,17 @@
*/
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\Directory;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\CodeCoverage\RuntimeException;
final class Crap4j
{
@@ -26,19 +34,19 @@ final class Crap4j
}
/**
* @throws \RuntimeException
* @throws WriteOperationFailedException
*/
public function process(CodeCoverage $coverage, ?string $target = null, ?string $name = null): string
{
$document = new \DOMDocument('1.0', 'UTF-8');
$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 : '');
$project = $document->createElement('project', is_string($name) ? $name : '');
$root->appendChild($project);
$root->appendChild($document->createElement('timestamp', \date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME'])));
$root->appendChild($document->createElement('timestamp', date('Y-m-d H:i:s')));
$stats = $document->createElement('stats');
$methodsNode = $document->createElement('methods');
@@ -59,13 +67,13 @@ final class Crap4j
}
$file = $document->createElement('file');
$file->setAttribute('name', $item->getPath());
$file->setAttribute('name', $item->pathAsString());
$classes = $item->getClassesAndTraits();
$classes = $item->classesAndTraits();
foreach ($classes as $className => $class) {
foreach ($class['methods'] as $methodName => $method) {
$crapLoad = $this->getCrapLoad($method['crap'], $method['ccn'], $method['coverage']);
$crapLoad = $this->crapLoad((float) $method['crap'], $method['ccn'], $method['coverage']);
$fullCrap += $method['crap'];
$fullCrapLoad += $crapLoad;
@@ -77,19 +85,19 @@ final class Crap4j
$methodNode = $document->createElement('method');
if (!empty($class['package']['namespace'])) {
$namespace = $class['package']['namespace'];
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($method['crap'])));
$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)));
$methodNode->appendChild($document->createElement('crapLoad', (string) round($crapLoad)));
$methodsNode->appendChild($methodNode);
}
@@ -99,7 +107,7 @@ final class Crap4j
$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('crapLoad', (string) round($fullCrapLoad)));
$stats->appendChild($document->createElement('totalCrap', (string) $fullCrap));
$crapMethodPercent = 0;
@@ -116,29 +124,17 @@ final class Crap4j
$buffer = $document->saveXML();
if ($target !== null) {
if (!$this->createDirectory(\dirname($target))) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', \dirname($target)));
}
Directory::create(dirname($target));
if (@\file_put_contents($target, $buffer) === false) {
throw new RuntimeException(
\sprintf(
'Could not write to "%s',
$target
)
);
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
}
return $buffer;
}
/**
* @param float $crapValue
* @param int $cyclomaticComplexity
* @param float $coveragePercent
*/
private function getCrapLoad($crapValue, $cyclomaticComplexity, $coveragePercent): float
private function crapLoad(float $crapValue, int $cyclomaticComplexity, float $coveragePercent): float
{
$crapLoad = 0;
@@ -150,16 +146,8 @@ final class Crap4j
return $crapLoad;
}
/**
* @param float $value
*/
private function roundValue($value): float
private function roundValue(float $value): float
{
return \round($value, 2);
}
private function createDirectory(string $directory): bool
{
return !(!\is_dir($directory) && !@\mkdir($directory, 0777, true) && !\is_dir($directory));
return round($value, 2);
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,13 +9,15 @@
*/
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\Directory as DirectoryUtil;
use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
use SebastianBergmann\CodeCoverage\RuntimeException;
/**
* Generates an HTML report from a code coverage object.
*/
final class Facade
{
/**
@@ -46,28 +48,19 @@ final class Facade
$this->templatePath = __DIR__ . '/Renderer/Template/';
}
/**
* @throws RuntimeException
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
public function process(CodeCoverage $coverage, string $target): void
{
$target = $this->getDirectory($target);
$target = $this->directory($target);
$report = $coverage->getReport();
if (!isset($_SERVER['REQUEST_TIME'])) {
$_SERVER['REQUEST_TIME'] = \time();
}
$date = \date('D M j G:i:s T Y', $_SERVER['REQUEST_TIME']);
$date = date('D M j G:i:s T Y');
$dashboard = new Dashboard(
$this->templatePath,
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound
$this->highLowerBound,
$coverage->collectsBranchAndPathCoverage()
);
$directory = new Directory(
@@ -75,7 +68,8 @@ final class Facade
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound
$this->highLowerBound,
$coverage->collectsBranchAndPathCoverage()
);
$file = new File(
@@ -83,85 +77,64 @@ final class Facade
$this->generator,
$date,
$this->lowUpperBound,
$this->highLowerBound
$this->highLowerBound,
$coverage->collectsBranchAndPathCoverage()
);
$directory->render($report, $target . 'index.html');
$dashboard->render($report, $target . 'dashboard.html');
foreach ($report as $node) {
$id = $node->getId();
$id = $node->id();
if ($node instanceof DirectoryNode) {
if (!$this->createDirectory($target . $id)) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', $target . $id));
}
DirectoryUtil::create($target . $id);
$directory->render($node, $target . $id . '/index.html');
$dashboard->render($node, $target . $id . '/dashboard.html');
} else {
$dir = \dirname($target . $id);
$dir = dirname($target . $id);
if (!$this->createDirectory($dir)) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', $dir));
}
DirectoryUtil::create($dir);
$file->render($node, $target . $id . '.html');
$file->render($node, $target . $id);
}
}
$this->copyFiles($target);
}
/**
* @throws RuntimeException
*/
private function copyFiles(string $target): void
{
$dir = $this->getDirectory($target . '_css');
$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');
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->getDirectory($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 . '_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->getDirectory($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');
$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');
}
/**
* @throws RuntimeException
*/
private function getDirectory(string $directory): string
private function directory(string $directory): string
{
if (\substr($directory, -1, 1) != \DIRECTORY_SEPARATOR) {
$directory .= \DIRECTORY_SEPARATOR;
if (substr($directory, -1, 1) != DIRECTORY_SEPARATOR) {
$directory .= DIRECTORY_SEPARATOR;
}
if (!$this->createDirectory($directory)) {
throw new RuntimeException(
\sprintf(
'Directory "%s" does not exist.',
$directory
)
);
}
DirectoryUtil::create($directory);
return $directory;
}
private function createDirectory(string $directory): bool
{
return !(!\is_dir($directory) && !@\mkdir($directory, 0777, true) && !\is_dir($directory));
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,14 +9,21 @@
*/
namespace SebastianBergmann\CodeCoverage\Report\Html;
use function array_pop;
use function count;
use function phpversion;
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;
/**
* Base class for node renderers.
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
abstract class Renderer
{
@@ -45,32 +52,38 @@ abstract class Renderer
*/
protected $highLowerBound;
/**
* @var bool
*/
protected $hasBranchCoverage;
/**
* @var string
*/
protected $version;
public function __construct(string $templatePath, string $generator, string $date, int $lowUpperBound, int $highLowerBound)
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->templatePath = $templatePath;
$this->generator = $generator;
$this->date = $date;
$this->lowUpperBound = $lowUpperBound;
$this->highLowerBound = $highLowerBound;
$this->version = Version::id();
$this->hasBranchCoverage = $hasBranchCoverage;
}
protected function renderItemTemplate(\Text_Template $template, array $data): string
protected function renderItemTemplate(Template $template, array $data): string
{
$numSeparator = '&nbsp;/&nbsp;';
$numSeparator = '&nbsp;/&nbsp;';
if (isset($data['numClasses']) && $data['numClasses'] > 0) {
$classesLevel = $this->getColorLevel($data['testedClassesPercent']);
$classesLevel = $this->colorLevel($data['testedClassesPercent']);
$classesNumber = $data['numTestedClasses'] . $numSeparator .
$data['numClasses'];
$classesBar = $this->getCoverageBar(
$classesBar = $this->coverageBar(
$data['testedClassesPercent']
);
} else {
@@ -81,12 +94,12 @@ abstract class Renderer
}
if ($data['numMethods'] > 0) {
$methodsLevel = $this->getColorLevel($data['testedMethodsPercent']);
$methodsLevel = $this->colorLevel($data['testedMethodsPercent']);
$methodsNumber = $data['numTestedMethods'] . $numSeparator .
$data['numMethods'];
$methodsBar = $this->getCoverageBar(
$methodsBar = $this->coverageBar(
$data['testedMethodsPercent']
);
} else {
@@ -97,12 +110,12 @@ abstract class Renderer
}
if ($data['numExecutableLines'] > 0) {
$linesLevel = $this->getColorLevel($data['linesExecutedPercent']);
$linesLevel = $this->colorLevel($data['linesExecutedPercent']);
$linesNumber = $data['numExecutedLines'] . $numSeparator .
$data['numExecutableLines'];
$linesBar = $this->getCoverageBar(
$linesBar = $this->coverageBar(
$data['linesExecutedPercent']
);
} else {
@@ -112,40 +125,80 @@ abstract class Renderer
$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,
'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,
'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(\Text_Template $template, AbstractNode $node): void
protected function setCommonTemplateVariables(Template $template, AbstractNode $node): void
{
$template->setVar(
[
'id' => $node->getId(),
'full_path' => $node->getPath(),
'path_to_root' => $this->getPathToRoot($node),
'breadcrumbs' => $this->getBreadcrumbs($node),
'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->getRuntimeString(),
'runtime' => $this->runtimeString(),
'generator' => $this->generator,
'low_upper_bound' => $this->lowUpperBound,
'high_lower_bound' => $this->highLowerBound,
@@ -153,40 +206,40 @@ abstract class Renderer
);
}
protected function getBreadcrumbs(AbstractNode $node): string
protected function breadcrumbs(AbstractNode $node): string
{
$breadcrumbs = '';
$path = $node->getPathAsArray();
$path = $node->pathAsArray();
$pathToRoot = [];
$max = \count($path);
$max = count($path);
if ($node instanceof FileNode) {
$max--;
}
for ($i = 0; $i < $max; $i++) {
$pathToRoot[] = \str_repeat('../', $i);
$pathToRoot[] = str_repeat('../', $i);
}
foreach ($path as $step) {
if ($step !== $node) {
$breadcrumbs .= $this->getInactiveBreadcrumb(
$breadcrumbs .= $this->inactiveBreadcrumb(
$step,
\array_pop($pathToRoot)
array_pop($pathToRoot)
);
} else {
$breadcrumbs .= $this->getActiveBreadcrumb($step);
$breadcrumbs .= $this->activeBreadcrumb($step);
}
}
return $breadcrumbs;
}
protected function getActiveBreadcrumb(AbstractNode $node): string
protected function activeBreadcrumb(AbstractNode $node): string
{
$buffer = \sprintf(
$buffer = sprintf(
' <li class="breadcrumb-item active">%s</li>' . "\n",
$node->getName()
$node->name()
);
if ($node instanceof DirectoryNode) {
@@ -196,44 +249,45 @@ abstract class Renderer
return $buffer;
}
protected function getInactiveBreadcrumb(AbstractNode $node, string $pathToRoot): string
protected function inactiveBreadcrumb(AbstractNode $node, string $pathToRoot): string
{
return \sprintf(
return sprintf(
' <li class="breadcrumb-item"><a href="%sindex.html">%s</a></li>' . "\n",
$pathToRoot,
$node->getName()
$node->name()
);
}
protected function getPathToRoot(AbstractNode $node): string
protected function pathToRoot(AbstractNode $node): string
{
$id = $node->getId();
$depth = \substr_count($id, '/');
$id = $node->id();
$depth = substr_count($id, '/');
if ($id !== 'index' &&
$node instanceof DirectoryNode) {
$depth++;
}
return \str_repeat('../', $depth);
return str_repeat('../', $depth);
}
protected function getCoverageBar(float $percent): string
protected function coverageBar(float $percent): string
{
$level = $this->getColorLevel($percent);
$level = $this->colorLevel($percent);
$template = new \Text_Template(
$this->templatePath . 'coverage_bar.html',
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'coverage_bar_branch.html' : 'coverage_bar.html');
$template = new Template(
$templateName,
'{{',
'}}'
);
$template->setVar(['level' => $level, 'percent' => \sprintf('%.2F', $percent)]);
$template->setVar(['level' => $level, 'percent' => sprintf('%.2F', $percent)]);
return $template->render();
}
protected function getColorLevel(float $percent): string
protected function colorLevel(float $percent): string
{
if ($percent <= $this->lowUpperBound) {
return 'danger';
@@ -247,28 +301,30 @@ abstract class Renderer
return 'success';
}
private function getRuntimeString(): string
private function runtimeString(): string
{
$runtime = new Runtime;
$buffer = \sprintf(
$buffer = sprintf(
'<a href="%s" target="_top">%s %s</a>',
$runtime->getVendorUrl(),
$runtime->getName(),
$runtime->getVersion()
);
if ($runtime->hasXdebug() && !$runtime->hasPHPDBGCodeCoverage()) {
$buffer .= \sprintf(
' with <a href="https://xdebug.org/">Xdebug %s</a>',
\phpversion('xdebug')
);
if ($runtime->hasPHPDBGCodeCoverage()) {
return $buffer;
}
if ($runtime->hasPCOV() && !$runtime->hasPHPDBGCodeCoverage()) {
$buffer .= \sprintf(
if ($runtime->hasPCOV()) {
$buffer .= sprintf(
' with <a href="https://github.com/krakjoe/pcov">PCOV %s</a>',
\phpversion('pcov')
phpversion('pcov')
);
} elseif ($runtime->hasXdebug()) {
$buffer .= sprintf(
' with <a href="https://xdebug.org/">Xdebug %s</a>',
phpversion('xdebug')
);
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,30 +9,37 @@
*/
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;
/**
* Renders the dashboard for a directory node.
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Dashboard extends Renderer
{
/**
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
public function render(DirectoryNode $node, string $file): void
{
$classes = $node->getClassesAndTraits();
$template = new \Text_Template(
$this->templatePath . 'dashboard.html',
$classes = $node->classesAndTraits();
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'dashboard_branch.html' : 'dashboard.html');
$template = new Template(
$templateName,
'{{',
'}}'
);
$this->setCommonTemplateVariables($template, $node);
$baseLink = $node->getId() . '/';
$baseLink = $node->id() . '/';
$complexity = $this->complexity($classes, $baseLink);
$coverageDistribution = $this->coverageDistribution($classes);
$insufficientCoverage = $this->insufficientCoverage($classes, $baseLink);
@@ -54,10 +61,19 @@ final class Dashboard extends Renderer
$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.
*/
protected function complexity(array $classes, string $baseLink): array
private function complexity(array $classes, string $baseLink): array
{
$result = ['class' => [], 'method' => []];
@@ -70,9 +86,9 @@ final class Dashboard extends Renderer
$result['method'][] = [
$method['coverage'],
$method['ccn'],
\sprintf(
sprintf(
'<a href="%s">%s</a>',
\str_replace($baseLink, '', $method['link']),
str_replace($baseLink, '', $method['link']),
$methodName
),
];
@@ -81,24 +97,24 @@ final class Dashboard extends Renderer
$result['class'][] = [
$class['coverage'],
$class['ccn'],
\sprintf(
sprintf(
'<a href="%s">%s</a>',
\str_replace($baseLink, '', $class['link']),
str_replace($baseLink, '', $class['link']),
$className
),
];
}
return [
'class' => \json_encode($result['class']),
'method' => \json_encode($result['method']),
'class' => json_encode($result['class']),
'method' => json_encode($result['method']),
];
}
/**
* Returns the data for the Class / Method Coverage Distribution chart.
*/
protected function coverageDistribution(array $classes): array
private function coverageDistribution(array $classes): array
{
$result = [
'class' => [
@@ -138,7 +154,7 @@ final class Dashboard extends Renderer
} elseif ($method['coverage'] === 100) {
$result['method']['100%']++;
} else {
$key = \floor($method['coverage'] / 10) * 10;
$key = floor($method['coverage'] / 10) * 10;
$key = $key . '-' . ($key + 10) . '%';
$result['method'][$key]++;
}
@@ -149,22 +165,22 @@ final class Dashboard extends Renderer
} elseif ($class['coverage'] === 100) {
$result['class']['100%']++;
} else {
$key = \floor($class['coverage'] / 10) * 10;
$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'])),
'class' => json_encode(array_values($result['class'])),
'method' => json_encode(array_values($result['method'])),
];
}
/**
* Returns the classes / methods with insufficient coverage.
*/
protected function insufficientCoverage(array $classes, string $baseLink): array
private function insufficientCoverage(array $classes, string $baseLink): array
{
$leastTestedClasses = [];
$leastTestedMethods = [];
@@ -188,24 +204,24 @@ final class Dashboard extends Renderer
}
}
\asort($leastTestedClasses);
\asort($leastTestedMethods);
asort($leastTestedClasses);
asort($leastTestedMethods);
foreach ($leastTestedClasses as $className => $coverage) {
$result['class'] .= \sprintf(
$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']),
str_replace($baseLink, '', $classes[$className]['link']),
$className,
$coverage
);
}
foreach ($leastTestedMethods as $methodName => $coverage) {
[$class, $method] = \explode('::', $methodName);
[$class, $method] = explode('::', $methodName);
$result['method'] .= \sprintf(
$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']),
str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']),
$methodName,
$method,
$coverage
@@ -218,7 +234,7 @@ final class Dashboard extends Renderer
/**
* Returns the project risks according to the CRAP index.
*/
protected function projectRisks(array $classes, string $baseLink): array
private function projectRisks(array $classes, string $baseLink): array
{
$classRisks = [];
$methodRisks = [];
@@ -238,29 +254,29 @@ final class Dashboard extends Renderer
}
if ($class['coverage'] < $this->highLowerBound &&
$class['ccn'] > \count($class['methods'])) {
$class['ccn'] > count($class['methods'])) {
$classRisks[$className] = $class['crap'];
}
}
\arsort($classRisks);
\arsort($methodRisks);
arsort($classRisks);
arsort($methodRisks);
foreach ($classRisks as $className => $crap) {
$result['class'] .= \sprintf(
$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']),
str_replace($baseLink, '', $classes[$className]['link']),
$className,
$crap
);
}
foreach ($methodRisks as $methodName => $crap) {
[$class, $method] = \explode('::', $methodName);
[$class, $method] = explode('::', $methodName);
$result['method'] .= \sprintf(
$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']),
str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']),
$methodName,
$method,
$crap
@@ -269,13 +285,4 @@ final class Dashboard extends Renderer
return $result;
}
protected function getActiveBreadcrumb(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->getName()
);
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,37 +9,38 @@
*/
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;
/**
* Renders a directory node.
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Directory extends Renderer
{
/**
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
public function render(DirectoryNode $node, string $file): void
{
$template = new \Text_Template($this->templatePath . 'directory.html', '{{', '}}');
$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->getDirectories() as $item) {
foreach ($node->directories() as $item) {
$items .= $this->renderItem($item);
}
foreach ($node->getFiles() as $item) {
foreach ($node->files() as $item) {
$items .= $this->renderItem($item);
}
$template->setVar(
[
'id' => $node->getId(),
'id' => $node->id(),
'items' => $items,
]
);
@@ -47,51 +48,65 @@ final class Directory extends Renderer
$template->renderTo($file);
}
protected function renderItem(Node $node, bool $total = false): string
private function renderItem(Node $node, bool $total = false): string
{
$data = [
'numClasses' => $node->getNumClassesAndTraits(),
'numTestedClasses' => $node->getNumTestedClassesAndTraits(),
'numMethods' => $node->getNumFunctionsAndMethods(),
'numTestedMethods' => $node->getNumTestedFunctionsAndMethods(),
'linesExecutedPercent' => $node->getLineExecutedPercent(false),
'linesExecutedPercentAsString' => $node->getLineExecutedPercent(),
'numExecutedLines' => $node->getNumExecutedLines(),
'numExecutableLines' => $node->getNumExecutableLines(),
'testedMethodsPercent' => $node->getTestedFunctionsAndMethodsPercent(false),
'testedMethodsPercentAsString' => $node->getTestedFunctionsAndMethodsPercent(),
'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false),
'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(),
'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(
$data['name'] = sprintf(
'<a href="%s/index.html">%s</a>',
$node->getName(),
$node->getName()
$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()
);
$up = \str_repeat('../', \count($node->getPathAsArray()) - 2);
$data['icon'] = \sprintf('<img src="%s_icons/file-directory.svg" class="octicon" />', $up);
} else {
$data['name'] = \sprintf(
$data['name'] = sprintf(
'<a href="%s.html">%s</a>',
$node->getName(),
$node->getName()
$node->name(),
$node->name()
);
$up = \str_repeat('../', \count($node->getPathAsArray()) - 2);
$data['icon'] = \sprintf('<img src="%s_icons/file-code.svg" class="octicon" />', $up);
}
}
$templateName = $this->templatePath . ($this->hasBranchCoverage ? 'directory_item_branch.html' : 'directory_item.html');
return $this->renderItemTemplate(
new \Text_Template($this->templatePath . 'directory_item.html', '{{', '}}'),
new Template($templateName, '{{', '}}'),
$data
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -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}}

View File

@@ -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>

File diff suppressed because one or more lines are too long

View File

@@ -48,7 +48,7 @@ body {
background-color: #f2dede;
}
.table tbody td.warning, li.warning, span.warning {
.table tbody tr.warning, .table tbody td.warning, li.warning, span.warning {
background-color: #fcf8e3;
}
@@ -65,7 +65,7 @@ td.small {
td.codeLine {
font-family: "Source Code Pro", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
white-space: pre;
white-space: pre-wrap;
}
td span.comment {
@@ -120,3 +120,8 @@ svg text {
overflow-x:hidden;
overflow-y:scroll;
}
table + .structure-heading {
border-top: 1px solid lightgrey;
padding-top: 0.5em;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -43,19 +43,12 @@
</tbody>
</table>
</div>
<table id="code" class="table table-borderless table-condensed">
<tbody>
{{lines}}
</tbody>
</table>
{{structure}}
<footer>
<hr/>
<h4>Legend</h4>
<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>
{{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>

View File

@@ -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>Classes and Traits</strong></div></td>
<td colspan="4"><div align="center"><strong>Functions and Methods</strong></div></td>
<td colspan="3"><div align="center"><strong>Paths</strong></div></td>
<td colspan="3"><div align="center"><strong>Branches</strong></div></td>
<td colspan="3"><div align="center"><strong>Lines</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>

View File

@@ -0,0 +1,20 @@
<tr>
<td class="{{classes_level}}">{{name}}</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>
<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="{{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="{{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="{{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>
</tr>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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>

View File

@@ -0,0 +1,5 @@
<table id="code" class="table table-borderless table-condensed">
<tbody>
{{lines}}
</tbody>
</table>

View File

@@ -0,0 +1,17 @@
<tr>
<td class="{{methods_level}}" colspan="4">{{name}}</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="{{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="{{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="{{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>
</tr>

View File

@@ -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}}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,56 +9,35 @@
*/
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\RuntimeException;
use SebastianBergmann\CodeCoverage\Directory;
use SebastianBergmann\CodeCoverage\Driver\WriteOperationFailedException;
/**
* Uses var_export() to write a SebastianBergmann\CodeCoverage\CodeCoverage object to a file.
*/
final class PHP
{
/**
* @throws \SebastianBergmann\CodeCoverage\RuntimeException
*/
public function process(CodeCoverage $coverage, ?string $target = null): string
{
$filter = $coverage->filter();
$buffer = \sprintf(
'<?php
$coverage = new SebastianBergmann\CodeCoverage\CodeCoverage;
$coverage->setData(%s);
$coverage->setTests(%s);
$filter = $coverage->filter();
$filter->setWhitelistedFiles(%s);
return $coverage;',
\var_export($coverage->getData(true), true),
\var_export($coverage->getTests(), true),
\var_export($filter->getWhitelistedFiles(), true)
$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) {
if (!$this->createDirectory(\dirname($target))) {
throw new \RuntimeException(\sprintf('Directory "%s" was not created', \dirname($target)));
}
Directory::create(dirname($target));
if (@\file_put_contents($target, $buffer) === false) {
throw new RuntimeException(
\sprintf(
'Could not write to "%s',
$target
)
);
if (@file_put_contents($target, $buffer) === false) {
throw new WriteOperationFailedException($target);
}
}
return $buffer;
}
private function createDirectory(string $directory): bool
{
return !(!\is_dir($directory) && !@\mkdir($directory, 0777, true) && !\is_dir($directory));
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,15 +9,18 @@
*/
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;
use SebastianBergmann\CodeCoverage\Percentage;
/**
* Generates human readable output from a code coverage object.
*
* The output gets put into a text file our written to the CLI.
*/
final class Text
{
/**
@@ -80,32 +83,46 @@ final class Text
public function process(CodeCoverage $coverage, bool $showColors = false): string
{
$output = \PHP_EOL . \PHP_EOL;
$hasBranchCoverage = !empty($coverage->getData(true)->functionCoverage());
$output = PHP_EOL . PHP_EOL;
$report = $coverage->getReport();
$colors = [
'header' => '',
'classes' => '',
'methods' => '',
'lines' => '',
'reset' => '',
'eol' => '',
'header' => '',
'classes' => '',
'methods' => '',
'lines' => '',
'branches' => '',
'paths' => '',
'reset' => '',
'eol' => '',
];
if ($showColors) {
$colors['classes'] = $this->getCoverageColor(
$report->getNumTestedClassesAndTraits(),
$report->getNumClassesAndTraits()
$colors['classes'] = $this->coverageColor(
$report->numberOfTestedClassesAndTraits(),
$report->numberOfClassesAndTraits()
);
$colors['methods'] = $this->getCoverageColor(
$report->getNumTestedMethods(),
$report->getNumMethods()
$colors['methods'] = $this->coverageColor(
$report->numberOfTestedMethods(),
$report->numberOfMethods()
);
$colors['lines'] = $this->getCoverageColor(
$report->getNumExecutedLines(),
$report->getNumExecutableLines()
$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;
@@ -113,48 +130,70 @@ final class Text
$colors['eol'] = self::COLOR_EOL;
}
$classes = \sprintf(
$classes = sprintf(
' Classes: %6s (%d/%d)',
Util::percent(
$report->getNumTestedClassesAndTraits(),
$report->getNumClassesAndTraits(),
true
),
$report->getNumTestedClassesAndTraits(),
$report->getNumClassesAndTraits()
Percentage::fromFractionAndTotal(
$report->numberOfTestedClassesAndTraits(),
$report->numberOfClassesAndTraits()
)->asString(),
$report->numberOfTestedClassesAndTraits(),
$report->numberOfClassesAndTraits()
);
$methods = \sprintf(
$methods = sprintf(
' Methods: %6s (%d/%d)',
Util::percent(
$report->getNumTestedMethods(),
$report->getNumMethods(),
true
),
$report->getNumTestedMethods(),
$report->getNumMethods()
Percentage::fromFractionAndTotal(
$report->numberOfTestedMethods(),
$report->numberOfMethods(),
)->asString(),
$report->numberOfTestedMethods(),
$report->numberOfMethods()
);
$lines = \sprintf(
$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)',
Util::percent(
$report->getNumExecutedLines(),
$report->getNumExecutableLines(),
true
),
$report->getNumExecutedLines(),
$report->getNumExecutableLines()
Percentage::fromFractionAndTotal(
$report->numberOfExecutedLines(),
$report->numberOfExecutableLines(),
)->asString(),
$report->numberOfExecutedLines(),
$report->numberOfExecutableLines()
);
$padding = \max(\array_map('strlen', [$classes, $methods, $lines]));
$padding = max(array_map('strlen', [$classes, $methods, $lines]));
if ($this->showOnlySummary) {
$title = 'Code Coverage Report Summary:';
$padding = \max($padding, \strlen($title));
$padding = max($padding, strlen($title));
$output .= $this->format($colors['header'], $padding, $title);
} else {
$date = \date(' Y-m-d H:i:s', $_SERVER['REQUEST_TIME']);
$date = date(' Y-m-d H:i:s');
$title = 'Code Coverage Report:';
$output .= $this->format($colors['header'], $padding, $title);
@@ -165,10 +204,15 @@ final class Text
$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;
return $output . PHP_EOL;
}
$classCoverage = [];
@@ -178,13 +222,17 @@ final class Text
continue;
}
$classes = $item->getClassesAndTraits();
$classes = $item->classesAndTraits();
foreach ($classes as $className => $class) {
$classStatements = 0;
$coveredClassStatements = 0;
$coveredMethods = 0;
$classMethods = 0;
$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) {
@@ -192,68 +240,77 @@ final class Text
}
$classMethods++;
$classStatements += $method['executableLines'];
$coveredClassStatements += $method['executedLines'];
$classExecutableLines += $method['executableLines'];
$classExecutedLines += $method['executedLines'];
$classExecutableBranches += $method['executableBranches'];
$classExecutedBranches += $method['executedBranches'];
$classExecutablePaths += $method['executablePaths'];
$classExecutedPaths += $method['executedPaths'];
if ($method['coverage'] == 100) {
$coveredMethods++;
}
}
$namespace = '';
if (!empty($class['package']['namespace'])) {
$namespace = '\\' . $class['package']['namespace'] . '::';
} elseif (!empty($class['package']['fullPackage'])) {
$namespace = '@' . $class['package']['fullPackage'] . '::';
}
$classCoverage[$namespace . $className] = [
'namespace' => $namespace,
'className ' => $className,
$classCoverage[$className] = [
'namespace' => $class['namespace'],
'className' => $className,
'methodsCovered' => $coveredMethods,
'methodCount' => $classMethods,
'statementsCovered' => $coveredClassStatements,
'statementCount' => $classStatements,
'statementsCovered' => $classExecutedLines,
'statementCount' => $classExecutableLines,
'branchesCovered' => $classExecutedBranches,
'branchesCount' => $classExecutableBranches,
'pathsCovered' => $classExecutedPaths,
'pathsCount' => $classExecutablePaths,
];
}
}
\ksort($classCoverage);
ksort($classCoverage);
$methodColor = '';
$linesColor = '';
$resetColor = '';
$methodColor = '';
$pathsColor = '';
$branchesColor = '';
$linesColor = '';
$resetColor = '';
foreach ($classCoverage as $fullQualifiedPath => $classInfo) {
if ($this->showUncoveredFiles || $classInfo['statementsCovered'] != 0) {
if ($showColors) {
$methodColor = $this->getCoverageColor($classInfo['methodsCovered'], $classInfo['methodCount']);
$linesColor = $this->getCoverageColor($classInfo['statementsCovered'], $classInfo['statementCount']);
$resetColor = $colors['reset'];
$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 . ' '
. ' ' . $linesColor . 'Lines: ' . $this->printCoverageCounts($classInfo['statementsCovered'], $classInfo['statementCount'], 3) . $resetColor;
$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;
return $output . PHP_EOL;
}
private function getCoverageColor(int $numberOfCoveredElements, int $totalNumberOfElements): string
private function coverageColor(int $numberOfCoveredElements, int $totalNumberOfElements): string
{
$coverage = Util::percent(
$coverage = Percentage::fromFractionAndTotal(
$numberOfCoveredElements,
$totalNumberOfElements
);
if ($coverage >= $this->highLowerBound) {
if ($coverage->asFloat() >= $this->highLowerBound) {
return self::COLOR_GREEN;
}
if ($coverage > $this->lowUpperBound) {
if ($coverage->asFloat() > $this->lowUpperBound) {
return self::COLOR_YELLOW;
}
@@ -264,20 +321,21 @@ final class Text
{
$format = '%' . $precision . 's';
return Util::percent(
return Percentage::fromFractionAndTotal(
$numberOfCoveredElements,
$totalNumberOfElements,
true,
true
) .
' (' . \sprintf($format, $numberOfCoveredElements) . '/' .
\sprintf($format, $totalNumberOfElements) . ')';
$totalNumberOfElements
)->asFixedWidthString() .
' (' . sprintf($format, $numberOfCoveredElements) . '/' .
sprintf($format, $totalNumberOfElements) . ')';
}
private function format($color, $padding, $string): string
/**
* @param false|string $string
*/
private function format(string $color, int $padding, $string): string
{
$reset = $color ? self::COLOR_RESET : '';
return $color . \str_pad($string, $padding) . $reset . \PHP_EOL;
return $color . str_pad((string) $string, $padding) . $reset . PHP_EOL;
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,47 +9,54 @@
*/
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
* @var DOMElement
*/
private $contextNode;
public function __construct(\DOMElement $contextNode)
public function __construct(DOMElement $contextNode)
{
$this->contextNode = $contextNode;
}
public function setRuntimeInformation(Runtime $runtime): void
{
$runtimeNode = $this->getNodeByName('runtime');
$runtimeNode = $this->nodeByName('runtime');
$runtimeNode->setAttribute('name', $runtime->getName());
$runtimeNode->setAttribute('version', $runtime->getVersion());
$runtimeNode->setAttribute('url', $runtime->getVendorUrl());
$driverNode = $this->getNodeByName('driver');
$driverNode = $this->nodeByName('driver');
if ($runtime->hasPHPDBGCodeCoverage()) {
$driverNode->setAttribute('name', 'phpdbg');
$driverNode->setAttribute('version', \constant('PHPDBG_VERSION'));
$driverNode->setAttribute('version', constant('PHPDBG_VERSION'));
}
if ($runtime->hasXdebug()) {
$driverNode->setAttribute('name', 'xdebug');
$driverNode->setAttribute('version', \phpversion('xdebug'));
$driverNode->setAttribute('version', phpversion('xdebug'));
}
if ($runtime->hasPCOV()) {
$driverNode->setAttribute('name', 'pcov');
$driverNode->setAttribute('version', \phpversion('pcov'));
$driverNode->setAttribute('version', phpversion('pcov'));
}
}
public function setBuildTime(\DateTime $date): void
public function setBuildTime(DateTimeImmutable $date): void
{
$this->contextNode->setAttribute('time', $date->format('D M j G:i:s T Y'));
}
@@ -60,7 +67,7 @@ final class BuildInformation
$this->contextNode->setAttribute('coverage', $coverageVersion);
}
private function getNodeByName(string $name): \DOMElement
private function nodeByName(string $name): DOMElement
{
$node = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,17 +9,22 @@
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use SebastianBergmann\CodeCoverage\RuntimeException;
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
* @var XMLWriter
*/
private $writer;
/**
* @var \DOMElement
* @var DOMElement
*/
private $contextNode;
@@ -28,23 +33,23 @@ final class Coverage
*/
private $finalized = false;
public function __construct(\DOMElement $context, string $line)
public function __construct(DOMElement $context, string $line)
{
$this->contextNode = $context;
$this->writer = new \XMLWriter();
$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 RuntimeException
* @throws ReportAlreadyFinalizedException
*/
public function addTest(string $test): void
{
if ($this->finalized) {
throw new RuntimeException('Coverage Report already finalized');
throw new ReportAlreadyFinalizedException;
}
$this->writer->startElement('covered');

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,6 +9,9 @@
*/
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
{
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,12 +9,33 @@
*/
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\Directory as DirectoryUtil;
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\RuntimeException;
use SebastianBergmann\CodeCoverage\Version;
use SebastianBergmann\CodeCoverage\XmlException;
use SebastianBergmann\Environment\Runtime;
final class Facade
@@ -40,12 +61,12 @@ final class Facade
}
/**
* @throws RuntimeException
* @throws XmlException
*/
public function process(CodeCoverage $coverage, string $target): void
{
if (\substr($target, -1, 1) !== \DIRECTORY_SEPARATOR) {
$target .= \DIRECTORY_SEPARATOR;
if (substr($target, -1, 1) !== DIRECTORY_SEPARATOR) {
$target .= DIRECTORY_SEPARATOR;
}
$this->target = $target;
@@ -54,7 +75,7 @@ final class Facade
$report = $coverage->getReport();
$this->project = new Project(
$coverage->getReport()->getName()
$coverage->getReport()->name()
);
$this->setBuildInformation();
@@ -66,92 +87,90 @@ final class Facade
private function setBuildInformation(): void
{
$buildNode = $this->project->getBuildInformation();
$buildNode->setRuntimeInformation(new Runtime());
$buildNode->setBuildTime(\DateTime::createFromFormat('U', (string) $_SERVER['REQUEST_TIME']));
$buildNode = $this->project->buildInformation();
$buildNode->setRuntimeInformation(new Runtime);
$buildNode->setBuildTime(new DateTimeImmutable);
$buildNode->setGeneratorVersions($this->phpUnitVersion, Version::id());
}
/**
* @throws RuntimeException
* @throws PathExistsButIsNotDirectoryException
* @throws WriteOperationFailedException
*/
private function initTargetDirectory(string $directory): void
{
if (\file_exists($directory)) {
if (!\is_dir($directory)) {
throw new RuntimeException(
"'$directory' exists but is not a directory."
);
if (is_file($directory)) {
if (!is_dir($directory)) {
throw new PathExistsButIsNotDirectoryException($directory);
}
if (!\is_writable($directory)) {
throw new RuntimeException(
"'$directory' exists but is not writable."
);
if (!is_writable($directory)) {
throw new WriteOperationFailedException($directory);
}
} elseif (!$this->createDirectory($directory)) {
throw new RuntimeException(
"'$directory' could not be created."
);
}
DirectoryUtil::create($directory);
}
/**
* @throws XmlException
*/
private function processDirectory(DirectoryNode $directory, Node $context): void
{
$directoryName = $directory->getName();
$directoryName = $directory->name();
if ($this->project->getProjectSourceDirectory() === $directoryName) {
if ($this->project->projectSourceDirectory() === $directoryName) {
$directoryName = '/';
}
$directoryObject = $context->addDirectory($directoryName);
$this->setTotals($directory, $directoryObject->getTotals());
$this->setTotals($directory, $directoryObject->totals());
foreach ($directory->getDirectories() as $node) {
foreach ($directory->directories() as $node) {
$this->processDirectory($node, $directoryObject);
}
foreach ($directory->getFiles() as $node) {
foreach ($directory->files() as $node) {
$this->processFile($node, $directoryObject);
}
}
/**
* @throws RuntimeException
* @throws XmlException
*/
private function processFile(FileNode $file, Directory $context): void
{
$fileObject = $context->addFile(
$file->getName(),
$file->getId() . '.xml'
$file->name(),
$file->id() . '.xml'
);
$this->setTotals($file, $fileObject->getTotals());
$this->setTotals($file, $fileObject->totals());
$path = \substr(
$file->getPath(),
\strlen($this->project->getProjectSourceDirectory())
$path = substr(
$file->pathAsString(),
strlen($this->project->projectSourceDirectory())
);
$fileReport = new Report($path);
$this->setTotals($file, $fileReport->getTotals());
$this->setTotals($file, $fileReport->totals());
foreach ($file->getClassesAndTraits() as $unit) {
foreach ($file->classesAndTraits() as $unit) {
$this->processUnit($unit, $fileReport);
}
foreach ($file->getFunctions() as $function) {
foreach ($file->functions() as $function) {
$this->processFunction($function, $fileReport);
}
foreach ($file->getCoverageData() as $line => $tests) {
if (!\is_array($tests) || \count($tests) === 0) {
foreach ($file->lineCoverageData() as $line => $tests) {
if (!is_array($tests) || count($tests) === 0) {
continue;
}
$coverage = $fileReport->getLineCoverage((string) $line);
$coverage = $fileReport->lineCoverage((string) $line);
foreach ($tests as $test) {
$coverage->addTest($test);
@@ -160,19 +179,19 @@ final class Facade
$coverage->finalize();
}
$fileReport->getSource()->setSourceCode(
\file_get_contents($file->getPath())
$fileReport->source()->setSourceCode(
file_get_contents($file->pathAsString())
);
$this->saveDocument($fileReport->asDom(), $file->getId());
$this->saveDocument($fileReport->asDom(), $file->id());
}
private function processUnit(array $unit, Report $report): void
{
if (isset($unit['className'])) {
$unitObject = $report->getClassObject($unit['className']);
$unitObject = $report->classObject($unit['className']);
} else {
$unitObject = $report->getTraitObject($unit['traitName']);
$unitObject = $report->traitObject($unit['traitName']);
}
$unitObject->setLines(
@@ -182,15 +201,7 @@ final class Facade
);
$unitObject->setCrap((float) $unit['crap']);
$unitObject->setPackage(
$unit['package']['fullPackage'],
$unit['package']['package'],
$unit['package']['subpackage'],
$unit['package']['category']
);
$unitObject->setNamespace($unit['package']['namespace']);
$unitObject->setNamespace($unit['namespace']);
foreach ($unit['methods'] as $method) {
$methodObject = $unitObject->addMethod($method['methodName']);
@@ -207,7 +218,7 @@ final class Facade
private function processFunction(array $function, Report $report): void
{
$functionObject = $report->getFunctionObject($function['functionName']);
$functionObject = $report->functionObject($function['functionName']);
$functionObject->setSignature($function['signature']);
$functionObject->setLines((string) $function['startLine']);
@@ -217,71 +228,88 @@ final class Facade
private function processTests(array $tests): void
{
$testsObject = $this->project->getTests();
$testsObject = $this->project->tests();
foreach ($tests as $test => $result) {
if ($test === 'UNCOVERED_FILES_FROM_WHITELIST') {
continue;
}
$testsObject->addTest($test, $result);
}
}
private function setTotals(AbstractNode $node, Totals $totals): void
{
$loc = $node->getLinesOfCode();
$loc = $node->linesOfCode();
$totals->setNumLines(
$loc['loc'],
$loc['cloc'],
$loc['ncloc'],
$node->getNumExecutableLines(),
$node->getNumExecutedLines()
$loc->linesOfCode(),
$loc->commentLinesOfCode(),
$loc->nonCommentLinesOfCode(),
$node->numberOfExecutableLines(),
$node->numberOfExecutedLines()
);
$totals->setNumClasses(
$node->getNumClasses(),
$node->getNumTestedClasses()
$node->numberOfClasses(),
$node->numberOfTestedClasses()
);
$totals->setNumTraits(
$node->getNumTraits(),
$node->getNumTestedTraits()
$node->numberOfTraits(),
$node->numberOfTestedTraits()
);
$totals->setNumMethods(
$node->getNumMethods(),
$node->getNumTestedMethods()
$node->numberOfMethods(),
$node->numberOfTestedMethods()
);
$totals->setNumFunctions(
$node->getNumFunctions(),
$node->getNumTestedFunctions()
$node->numberOfFunctions(),
$node->numberOfTestedFunctions()
);
}
private function getTargetDirectory(): string
private function targetDirectory(): string
{
return $this->target;
}
/**
* @throws RuntimeException
* @throws XmlException
*/
private function saveDocument(\DOMDocument $document, string $name): void
private function saveDocument(DOMDocument $document, string $name): void
{
$filename = \sprintf('%s/%s.xml', $this->getTargetDirectory(), $name);
$filename = sprintf('%s/%s.xml', $this->targetDirectory(), $name);
$document->formatOutput = true;
$document->preserveWhiteSpace = false;
$this->initTargetDirectory(\dirname($filename));
$this->initTargetDirectory(dirname($filename));
$document->save($filename);
file_put_contents($filename, $this->documentAsString($document));
}
private function createDirectory(string $directory): bool
/**
* @throws XmlException
*
* @see https://bugs.php.net/bug.php?id=79191
*/
private function documentAsString(DOMDocument $document): string
{
return !(!\is_dir($directory) && !@\mkdir($directory, 0777, true) && !\is_dir($directory));
$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;
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,25 +9,31 @@
*/
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
* @var DOMDocument
*/
private $dom;
/**
* @var \DOMElement
* @var DOMElement
*/
private $contextNode;
public function __construct(\DOMElement $context)
public function __construct(DOMElement $context)
{
$this->dom = $context->ownerDocument;
$this->contextNode = $context;
}
public function getTotals(): Totals
public function totals(): Totals
{
$totalsContainer = $this->contextNode->firstChild;
@@ -43,7 +49,7 @@ class File
return new Totals($totalsContainer);
}
public function getLineCoverage(string $line): Coverage
public function lineCoverage(string $line): Coverage
{
$coverage = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
@@ -69,12 +75,12 @@ class File
return new Coverage($lineNode, $line);
}
protected function getContextNode(): \DOMElement
protected function contextNode(): DOMElement
{
return $this->contextNode;
}
protected function getDomDocument(): \DOMDocument
protected function dom(): DOMDocument
{
return $this->dom;
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,14 +9,19 @@
*/
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
* @var DOMElement
*/
private $contextNode;
public function __construct(\DOMElement $context, string $name)
public function __construct(DOMElement $context, string $name)
{
$this->contextNode = $context;

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,34 +9,40 @@
*/
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
* @var DOMDocument
*/
private $dom;
/**
* @var \DOMElement
* @var DOMElement
*/
private $contextNode;
public function __construct(\DOMElement $context)
public function __construct(DOMElement $context)
{
$this->setContextNode($context);
}
public function getDom(): \DOMDocument
public function dom(): DOMDocument
{
return $this->dom;
}
public function getTotals(): Totals
public function totals(): Totals
{
$totalsContainer = $this->getContextNode()->firstChild;
$totalsContainer = $this->contextNode()->firstChild;
if (!$totalsContainer) {
$totalsContainer = $this->getContextNode()->appendChild(
$totalsContainer = $this->contextNode()->appendChild(
$this->dom->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'totals'
@@ -49,38 +55,38 @@ abstract class Node
public function addDirectory(string $name): Directory
{
$dirNode = $this->getDom()->createElementNS(
$dirNode = $this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'directory'
);
$dirNode->setAttribute('name', $name);
$this->getContextNode()->appendChild($dirNode);
$this->contextNode()->appendChild($dirNode);
return new Directory($dirNode);
}
public function addFile(string $name, string $href): File
{
$fileNode = $this->getDom()->createElementNS(
$fileNode = $this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'file'
);
$fileNode->setAttribute('name', $name);
$fileNode->setAttribute('href', $href);
$this->getContextNode()->appendChild($fileNode);
$this->contextNode()->appendChild($fileNode);
return new File($fileNode);
}
protected function setContextNode(\DOMElement $context): void
protected function setContextNode(DOMElement $context): void
{
$this->dom = $context->ownerDocument;
$this->contextNode = $context;
}
protected function getContextNode(): \DOMElement
protected function contextNode(): DOMElement
{
return $this->contextNode;
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,6 +9,11 @@
*/
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)
@@ -17,21 +22,21 @@ final class Project extends Node
$this->setProjectSourceDirectory($directory);
}
public function getProjectSourceDirectory(): string
public function projectSourceDirectory(): string
{
return $this->getContextNode()->getAttribute('source');
return $this->contextNode()->getAttribute('source');
}
public function getBuildInformation(): BuildInformation
public function buildInformation(): BuildInformation
{
$buildNode = $this->getDom()->getElementsByTagNameNS(
$buildNode = $this->dom()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'build'
)->item(0);
if (!$buildNode) {
$buildNode = $this->getDom()->documentElement->appendChild(
$this->getDom()->createElementNS(
$buildNode = $this->dom()->documentElement->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'build'
)
@@ -41,16 +46,16 @@ final class Project extends Node
return new BuildInformation($buildNode);
}
public function getTests(): Tests
public function tests(): Tests
{
$testsNode = $this->getContextNode()->getElementsByTagNameNS(
$testsNode = $this->contextNode()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'tests'
)->item(0);
if (!$testsNode) {
$testsNode = $this->getContextNode()->appendChild(
$this->getDom()->createElementNS(
$testsNode = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'tests'
)
@@ -60,14 +65,14 @@ final class Project extends Node
return new Tests($testsNode);
}
public function asDom(): \DOMDocument
public function asDom(): DOMDocument
{
return $this->getDom();
return $this->dom();
}
private function init(): void
{
$dom = new \DOMDocument;
$dom = new DOMDocument;
$dom->loadXML('<?xml version="1.0" ?><phpunit xmlns="https://schema.phpunit.de/coverage/1.0"><build/><project/></phpunit>');
$this->setContextNode(
@@ -80,6 +85,6 @@ final class Project extends Node
private function setProjectSourceDirectory(string $name): void
{
$this->getContextNode()->setAttribute('source', $name);
$this->contextNode()->setAttribute('source', $name);
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,11 +9,18 @@
*/
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 = new DOMDocument();
$dom->loadXML('<?xml version="1.0" ?><phpunit xmlns="https://schema.phpunit.de/coverage/1.0"><file /></phpunit>');
$contextNode = $dom->getElementsByTagNameNS(
@@ -26,15 +33,15 @@ final class Report extends File
$this->setName($name);
}
public function asDom(): \DOMDocument
public function asDom(): DOMDocument
{
return $this->getDomDocument();
return $this->dom();
}
public function getFunctionObject($name): Method
public function functionObject($name): Method
{
$node = $this->getContextNode()->appendChild(
$this->getDomDocument()->createElementNS(
$node = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'function'
)
@@ -43,26 +50,26 @@ final class Report extends File
return new Method($node, $name);
}
public function getClassObject($name): Unit
public function classObject($name): Unit
{
return $this->getUnitObject('class', $name);
return $this->unitObject('class', $name);
}
public function getTraitObject($name): Unit
public function traitObject($name): Unit
{
return $this->getUnitObject('trait', $name);
return $this->unitObject('trait', $name);
}
public function getSource(): Source
public function source(): Source
{
$source = $this->getContextNode()->getElementsByTagNameNS(
$source = $this->contextNode()->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'source'
)->item(0);
if (!$source) {
$source = $this->getContextNode()->appendChild(
$this->getDomDocument()->createElementNS(
$source = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'source'
)
@@ -72,16 +79,16 @@ final class Report extends File
return new Source($source);
}
private function setName($name): void
private function setName(string $name): void
{
$this->getContextNode()->setAttribute('name', \basename($name));
$this->getContextNode()->setAttribute('path', \dirname($name));
$this->contextNode()->setAttribute('name', basename($name));
$this->contextNode()->setAttribute('path', dirname($name));
}
private function getUnitObject($tagName, $name): Unit
private function unitObject(string $tagName, $name): Unit
{
$node = $this->getContextNode()->appendChild(
$this->getDomDocument()->createElementNS(
$node = $this->contextNode()->appendChild(
$this->dom()->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
$tagName
)

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,16 +9,20 @@
*/
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 */
/** @var DOMElement */
private $context;
public function __construct(\DOMElement $context)
public function __construct(DOMElement $context)
{
$this->context = $context;
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,6 +9,11 @@
*/
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;
@@ -24,7 +29,7 @@ final class Tests
6 => 'WARNING', // PHPUnit_Runner_BaseTestRunner::STATUS_WARNING
];
public function __construct(\DOMElement $context)
public function __construct(DOMElement $context)
{
$this->contextNode = $context;
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,41 +9,47 @@
*/
namespace SebastianBergmann\CodeCoverage\Report\Xml;
use SebastianBergmann\CodeCoverage\Util;
use function sprintf;
use DOMElement;
use DOMNode;
use SebastianBergmann\CodeCoverage\Percentage;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class Totals
{
/**
* @var \DOMNode
* @var DOMNode
*/
private $container;
/**
* @var \DOMElement
* @var DOMElement
*/
private $linesNode;
/**
* @var \DOMElement
* @var DOMElement
*/
private $methodsNode;
/**
* @var \DOMElement
* @var DOMElement
*/
private $functionsNode;
/**
* @var \DOMElement
* @var DOMElement
*/
private $classesNode;
/**
* @var \DOMElement
* @var DOMElement
*/
private $traitsNode;
public function __construct(\DOMElement $container)
public function __construct(DOMElement $container)
{
$this->container = $container;
$dom = $container->ownerDocument;
@@ -80,7 +86,7 @@ final class Totals
$container->appendChild($this->traitsNode);
}
public function getContainer(): \DOMNode
public function container(): DOMNode
{
return $this->container;
}
@@ -94,7 +100,7 @@ final class Totals
$this->linesNode->setAttribute('executed', (string) $executed);
$this->linesNode->setAttribute(
'percent',
$executable === 0 ? '0' : \sprintf('%01.2F', Util::percent($executed, $executable))
$executable === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($executed, $executable)->asFloat())
);
}
@@ -104,7 +110,7 @@ final class Totals
$this->classesNode->setAttribute('tested', (string) $tested);
$this->classesNode->setAttribute(
'percent',
$count === 0 ? '0' : \sprintf('%01.2F', Util::percent($tested, $count))
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
);
}
@@ -114,7 +120,7 @@ final class Totals
$this->traitsNode->setAttribute('tested', (string) $tested);
$this->traitsNode->setAttribute(
'percent',
$count === 0 ? '0' : \sprintf('%01.2F', Util::percent($tested, $count))
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
);
}
@@ -124,7 +130,7 @@ final class Totals
$this->methodsNode->setAttribute('tested', (string) $tested);
$this->methodsNode->setAttribute(
'percent',
$count === 0 ? '0' : \sprintf('%01.2F', Util::percent($tested, $count))
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
);
}
@@ -134,7 +140,7 @@ final class Totals
$this->functionsNode->setAttribute('tested', (string) $tested);
$this->functionsNode->setAttribute(
'percent',
$count === 0 ? '0' : \sprintf('%01.2F', Util::percent($tested, $count))
$count === 0 ? '0' : sprintf('%01.2F', Percentage::fromFractionAndTotal($tested, $count)->asFloat())
);
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,14 +9,19 @@
*/
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
* @var DOMElement
*/
private $contextNode;
public function __construct(\DOMElement $context, string $name)
public function __construct(DOMElement $context, string $name)
{
$this->contextNode = $context;
@@ -35,28 +40,6 @@ final class Unit
$this->contextNode->setAttribute('crap', (string) $crap);
}
public function setPackage(string $full, string $package, string $sub, string $category): void
{
$node = $this->contextNode->getElementsByTagNameNS(
'https://schema.phpunit.de/coverage/1.0',
'package'
)->item(0);
if (!$node) {
$node = $this->contextNode->appendChild(
$this->contextNode->ownerDocument->createElementNS(
'https://schema.phpunit.de/coverage/1.0',
'package'
)
);
}
$node->setAttribute('full', $full);
$node->setAttribute('name', $package);
$node->setAttribute('sub', $sub);
$node->setAttribute('category', $category);
}
public function setNamespace(string $namespace): void
{
$node = $this->contextNode->getElementsByTagNameNS(

View File

@@ -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\StaticAnalysis;
use const DIRECTORY_SEPARATOR;
use function file_get_contents;
use function file_put_contents;
use function filemtime;
use function hash;
use function is_file;
use function serialize;
use function unserialize;
use SebastianBergmann\CodeCoverage\Directory;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
abstract class Cache
{
/**
* @var string
*/
private $directory;
public function __construct(string $directory)
{
Directory::create($directory);
$this->directory = $directory;
}
protected function has(string $filename, string $key): bool
{
$cacheFile = $this->cacheFile($filename, $key);
if (!is_file($cacheFile)) {
return false;
}
if (filemtime($cacheFile) < filemtime($filename)) {
return false;
}
return true;
}
/**
* @psalm-param list<class-string> $allowedClasses
*
* @return mixed
*/
protected function read(string $filename, string $key, array $allowedClasses = [])
{
$options = ['allowed_classes' => false];
if (!empty($allowedClasses)) {
$options = ['allowed_classes' => $allowedClasses];
}
return unserialize(
file_get_contents(
$this->cacheFile($filename, $key)
),
$options
);
}
/**
* @param mixed $data
*/
protected function write(string $filename, string $key, $data): void
{
file_put_contents(
$this->cacheFile($filename, $key),
serialize($data)
);
}
private function cacheFile(string $filename, string $key): string
{
return $this->directory . DIRECTORY_SEPARATOR . hash('sha256', $filename . $key);
}
}

View File

@@ -0,0 +1,38 @@
<?php declare(strict_types=1);
/*
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\CodeCoverage\StaticAnalysis;
use SebastianBergmann\CodeCoverage\Filter;
final class CacheWarmer
{
public function warmCache(string $cacheDirectory, bool $useAnnotationsForIgnoringCode, bool $ignoreDeprecatedCode, Filter $filter): void
{
$coveredFileAnalyser = new CachingCoveredFileAnalyser(
$cacheDirectory,
new ParsingCoveredFileAnalyser(
$useAnnotationsForIgnoringCode,
$ignoreDeprecatedCode
)
);
$uncoveredFileAnalyser = new CachingUncoveredFileAnalyser(
$cacheDirectory,
new ParsingUncoveredFileAnalyser
);
foreach ($filter->files() as $file) {
$coveredFileAnalyser->process($file);
/* @noinspection UnusedFunctionResultInspection */
$uncoveredFileAnalyser->executableLinesIn($file);
}
}
}

View File

@@ -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\StaticAnalysis;
use SebastianBergmann\LinesOfCode\LinesOfCode;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class CachingCoveredFileAnalyser extends Cache implements CoveredFileAnalyser
{
/**
* @var CoveredFileAnalyser
*/
private $coveredFileAnalyser;
/**
* @var array
*/
private $cache = [];
public function __construct(string $directory, CoveredFileAnalyser $coveredFileAnalyser)
{
parent::__construct($directory);
$this->coveredFileAnalyser = $coveredFileAnalyser;
}
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'];
}
public function linesOfCodeFor(string $filename): LinesOfCode
{
if (!isset($this->cache[$filename])) {
$this->process($filename);
}
return $this->cache[$filename]['linesOfCodeFor'];
}
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
{
if ($this->has($filename, __CLASS__)) {
$this->cache[$filename] = $this->read($filename, __CLASS__, [LinesOfCode::class]);
return;
}
$this->cache[$filename] = [
'classesIn' => $this->coveredFileAnalyser->classesIn($filename),
'traitsIn' => $this->coveredFileAnalyser->traitsIn($filename),
'functionsIn' => $this->coveredFileAnalyser->functionsIn($filename),
'linesOfCodeFor' => $this->coveredFileAnalyser->linesOfCodeFor($filename),
'ignoredLinesFor' => $this->coveredFileAnalyser->ignoredLinesFor($filename),
];
$this->write($filename, __CLASS__, $this->cache[$filename]);
}
}

View File

@@ -0,0 +1,41 @@
<?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
*/
final class CachingUncoveredFileAnalyser extends Cache implements UncoveredFileAnalyser
{
/**
* @var UncoveredFileAnalyser
*/
private $uncoveredFileAnalyser;
public function __construct(string $directory, UncoveredFileAnalyser $uncoveredFileAnalyser)
{
parent::__construct($directory);
$this->uncoveredFileAnalyser = $uncoveredFileAnalyser;
}
public function executableLinesIn(string $filename): array
{
if ($this->has($filename, __METHOD__)) {
return $this->read($filename, __METHOD__);
}
$data = $this->uncoveredFileAnalyser->executableLinesIn($filename);
$this->write($filename, __METHOD__, $data);
return $data;
}
}

View File

@@ -0,0 +1,293 @@
<?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\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
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\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
{
/**
* @var array
*/
private $classes = [];
/**
* @var array
*/
private $traits = [];
/**
* @var array
*/
private $functions = [];
public function enterNode(Node $node)
{
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 null;
}
if ($node instanceof ClassMethod) {
$parentNode = $node->getAttribute('parent');
if ($parentNode instanceof Class_ && $parentNode->isAnonymous()) {
return;
}
$this->processMethod($node);
return;
}
$this->processFunction($node);
}
public function classes(): array
{
return $this->classes;
}
public function traits(): array
{
return $this->traits;
}
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|NullableType|UnionType $type
*/
private function type(Node $type): string
{
assert($type instanceof Identifier || $type instanceof Name || $type instanceof NullableType || $type instanceof UnionType);
if ($type instanceof NullableType) {
return '?' . $type->type;
}
if ($type instanceof UnionType) {
$types = [];
foreach ($type->types as $_type) {
$types[] = $_type->toString();
}
return implode('|', $types);
}
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_);
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), '\\');
}
}

View File

@@ -0,0 +1,28 @@
<?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\LinesOfCode\LinesOfCode;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
interface CoveredFileAnalyser
{
public function classesIn(string $filename): array;
public function traitsIn(string $filename): array;
public function functionsIn(string $filename): array;
public function linesOfCodeFor(string $filename): LinesOfCode;
public function ignoredLinesFor(string $filename): array;
}

View File

@@ -0,0 +1,91 @@
<?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 sort;
use PhpParser\Node;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Case_;
use PhpParser\Node\Stmt\Catch_;
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\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 list<int>
*/
private $executableLines = [];
public function enterNode(Node $node): void
{
if (!$this->isExecutable($node)) {
return;
}
$this->executableLines[] = $node->getStartLine();
}
/**
* @psalm-return list<int>
*/
public function executableLines(): array
{
$executableLines = array_unique($this->executableLines);
sort($executableLines);
return $executableLines;
}
private function isExecutable(Node $node): bool
{
return $node instanceof Break_ ||
$node instanceof Case_ ||
$node instanceof Catch_ ||
$node instanceof Continue_ ||
$node instanceof Do_ ||
$node instanceof Echo_ ||
$node instanceof ElseIf_ ||
$node instanceof Else_ ||
$node instanceof Expression ||
$node instanceof Finally_ ||
$node instanceof Foreach_ ||
$node instanceof For_ ||
$node instanceof Goto_ ||
$node instanceof If_ ||
$node instanceof Return_ ||
$node instanceof Switch_ ||
$node instanceof Throw_ ||
$node instanceof TryCatch ||
$node instanceof Unset_ ||
$node instanceof While_;
}
}

View File

@@ -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\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\NodeTraverser;
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): ?int
{
if (!$node instanceof Class_ &&
!$node instanceof Trait_ &&
!$node instanceof Interface_ &&
!$node instanceof ClassMethod &&
!$node instanceof Function_) {
return null;
}
if ($node instanceof Class_ && $node->isAnonymous()) {
return null;
}
// 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 null;
}
if ($node instanceof Interface_) {
return null;
}
$docComment = $node->getDocComment();
if ($docComment === null) {
return null;
}
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())
);
}
if ($node instanceof ClassMethod || $node instanceof Function_) {
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
return null;
}
/**
* @psalm-return list<int>
*/
public function ignoredLines(): array
{
return $this->ignoredLines;
}
}

View File

@@ -0,0 +1,226 @@
<?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;
use SebastianBergmann\LinesOfCode\LinesOfCode;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class ParsingCoveredFileAnalyser implements CoveredFileAnalyser
{
/**
* @var array
*/
private $classes = [];
/**
* @var array
*/
private $traits = [];
/**
* @var array
*/
private $functions = [];
/**
* @var LinesOfCode[]
*/
private $linesOfCode = [];
/**
* @var array
*/
private $ignoredLines = [];
/**
* @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];
}
public function linesOfCodeFor(string $filename): LinesOfCode
{
$this->analyse($filename);
return $this->linesOfCode[$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);
$traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ParentConnectingVisitor);
$traverser->addVisitor($codeUnitFindingVisitor);
$traverser->addVisitor($lineCountingVisitor);
$traverser->addVisitor($ignoredLinesFindingVisitor);
/* @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->linesOfCode[$filename] = $lineCountingVisitor->result();
$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]);
}
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;
}
}
}
}
}

View File

@@ -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\StaticAnalysis;
use PhpParser\Error;
use PhpParser\Lexer;
use PhpParser\NodeTraverser;
use PhpParser\ParserFactory;
/**
* @internal This class is not covered by the backward compatibility promise for phpunit/php-code-coverage
*/
final class ParsingUncoveredFileAnalyser implements UncoveredFileAnalyser
{
public function executableLinesIn(string $filename): array
{
$parser = (new ParserFactory)->create(
ParserFactory::PREFER_PHP7,
new Lexer
);
try {
$nodes = $parser->parse(file_get_contents($filename));
assert($nodes !== null);
$traverser = new NodeTraverser;
$visitor = new ExecutableLinesFindingVisitor;
$traverser->addVisitor($visitor);
/* @noinspection UnusedFunctionResultInspection */
$traverser->traverse($nodes);
return $visitor->executableLines();
// @codeCoverageIgnoreStart
} catch (Error $error) {
}
// @codeCoverageIgnoreEnd
return [];
}
}

View File

@@ -0,0 +1,18 @@
<?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 UncoveredFileAnalyser
{
public function executableLinesIn(string $filename): array;
}

View File

@@ -1,40 +0,0 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
*
* (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;
/**
* Utility methods.
*/
final class Util
{
/**
* @return float|int|string
*/
public static function percent(float $a, float $b, bool $asString = false, bool $fixedWidth = false)
{
if ($asString && $b == 0) {
return '';
}
$percent = 100;
if ($b > 0) {
$percent = ($a / $b) * 100;
}
if ($asString) {
$format = $fixedWidth ? '%6.2F%%' : '%01.2F%%';
return \sprintf($format, $percent);
}
return $percent;
}
}

View File

@@ -1,6 +1,6 @@
<?php declare(strict_types=1);
/*
* This file is part of the php-code-coverage package.
* This file is part of phpunit/php-code-coverage.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
@@ -9,6 +9,7 @@
*/
namespace SebastianBergmann\CodeCoverage;
use function dirname;
use SebastianBergmann\Version as VersionId;
final class Version
@@ -21,8 +22,7 @@ final class Version
public static function id(): string
{
if (self::$version === null) {
$version = new VersionId('7.0.14', \dirname(__DIR__));
self::$version = $version->getVersion();
self::$version = (new VersionId('9.2.6', dirname(__DIR__)))->getVersion();
}
return self::$version;