nginxsonarrradarrplexorganizrdashboardnzbgetbookmarkapplication-dashboardmuximuxlandingpagestartpagelandinghtpcserverhomepagesabnzbdheimdallembycouchpotato
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1185 lines
31 KiB
1185 lines
31 KiB
7 years ago
|
<?php
|
||
|
/*
|
||
|
* 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;
|
||
|
|
||
|
use PHPUnit\Framework\TestCase;
|
||
|
use PHPUnit\Runner\PhptTestCase;
|
||
|
use SebastianBergmann\CodeCoverage\Driver\Driver;
|
||
|
use SebastianBergmann\CodeCoverage\Driver\HHVM;
|
||
|
use SebastianBergmann\CodeCoverage\Driver\PHPDBG;
|
||
|
use SebastianBergmann\CodeCoverage\Driver\Xdebug;
|
||
|
use SebastianBergmann\CodeCoverage\Node\Builder;
|
||
|
use SebastianBergmann\CodeCoverage\Node\Directory;
|
||
|
use SebastianBergmann\CodeUnitReverseLookup\Wizard;
|
||
|
use SebastianBergmann\Environment\Runtime;
|
||
|
|
||
|
/**
|
||
|
* Provides collection functionality for PHP code coverage information.
|
||
|
*/
|
||
|
class CodeCoverage
|
||
|
{
|
||
|
/**
|
||
|
* @var Driver
|
||
|
*/
|
||
|
private $driver;
|
||
|
|
||
|
/**
|
||
|
* @var Filter
|
||
|
*/
|
||
|
private $filter;
|
||
|
|
||
|
/**
|
||
|
* @var Wizard
|
||
|
*/
|
||
|
private $wizard;
|
||
|
|
||
|
/**
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $cacheTokens = false;
|
||
|
|
||
|
/**
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $checkForUnintentionallyCoveredCode = false;
|
||
|
|
||
|
/**
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $forceCoversAnnotation = false;
|
||
|
|
||
|
/**
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $checkForUnexecutedCoveredCode = false;
|
||
|
|
||
|
/**
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $checkForMissingCoversAnnotation = false;
|
||
|
|
||
|
/**
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $addUncoveredFilesFromWhitelist = true;
|
||
|
|
||
|
/**
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $processUncoveredFilesFromWhitelist = false;
|
||
|
|
||
|
/**
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $ignoreDeprecatedCode = false;
|
||
|
|
||
|
/**
|
||
|
* @var mixed
|
||
|
*/
|
||
|
private $currentId;
|
||
|
|
||
|
/**
|
||
|
* Code coverage data.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
private $data = [];
|
||
|
|
||
|
/**
|
||
|
* @var array
|
||
|
*/
|
||
|
private $ignoredLines = [];
|
||
|
|
||
|
/**
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $disableIgnoredLines = false;
|
||
|
|
||
|
/**
|
||
|
* Test data.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
private $tests = [];
|
||
|
|
||
|
/**
|
||
|
* @var string[]
|
||
|
*/
|
||
|
private $unintentionallyCoveredSubclassesWhitelist = [];
|
||
|
|
||
|
/**
|
||
|
* Determine if the data has been initialized or not
|
||
|
*
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $isInitialized = false;
|
||
|
|
||
|
/**
|
||
|
* Determine whether we need to check for dead and unused code on each test
|
||
|
*
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $shouldCheckForDeadAndUnused = true;
|
||
|
|
||
|
/**
|
||
|
* @var Directory
|
||
|
*/
|
||
|
private $report;
|
||
|
|
||
|
/**
|
||
|
* Constructor.
|
||
|
*
|
||
|
* @param Driver $driver
|
||
|
* @param Filter $filter
|
||
|
*
|
||
|
* @throws RuntimeException
|
||
|
*/
|
||
|
public function __construct(Driver $driver = null, Filter $filter = null)
|
||
|
{
|
||
|
if ($driver === null) {
|
||
|
$driver = $this->selectDriver();
|
||
|
}
|
||
|
|
||
|
if ($filter === null) {
|
||
|
$filter = new Filter;
|
||
|
}
|
||
|
|
||
|
$this->driver = $driver;
|
||
|
$this->filter = $filter;
|
||
|
|
||
|
$this->wizard = new Wizard;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the code coverage information as a graph of node objects.
|
||
|
*
|
||
|
* @return Directory
|
||
|
*/
|
||
|
public function getReport()
|
||
|
{
|
||
|
if ($this->report === null) {
|
||
|
$builder = new Builder;
|
||
|
|
||
|
$this->report = $builder->build($this);
|
||
|
}
|
||
|
|
||
|
return $this->report;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clears collected code coverage data.
|
||
|
*/
|
||
|
public function clear()
|
||
|
{
|
||
|
$this->isInitialized = false;
|
||
|
$this->currentId = null;
|
||
|
$this->data = [];
|
||
|
$this->tests = [];
|
||
|
$this->report = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the filter object used.
|
||
|
*
|
||
|
* @return Filter
|
||
|
*/
|
||
|
public function filter()
|
||
|
{
|
||
|
return $this->filter;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the collected code coverage data.
|
||
|
* Set $raw = true to bypass all filters.
|
||
|
*
|
||
|
* @param bool $raw
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
public function getData($raw = false)
|
||
|
{
|
||
|
if (!$raw && $this->addUncoveredFilesFromWhitelist) {
|
||
|
$this->addUncoveredFilesFromWhitelist();
|
||
|
}
|
||
|
|
||
|
return $this->data;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the coverage data.
|
||
|
*
|
||
|
* @param array $data
|
||
|
*/
|
||
|
public function setData(array $data)
|
||
|
{
|
||
|
$this->data = $data;
|
||
|
$this->report = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the test data.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
public function getTests()
|
||
|
{
|
||
|
return $this->tests;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the test data.
|
||
|
*
|
||
|
* @param array $tests
|
||
|
*/
|
||
|
public function setTests(array $tests)
|
||
|
{
|
||
|
$this->tests = $tests;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Start collection of code coverage information.
|
||
|
*
|
||
|
* @param mixed $id
|
||
|
* @param bool $clear
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function start($id, $clear = false)
|
||
|
{
|
||
|
if (!\is_bool($clear)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'boolean'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if ($clear) {
|
||
|
$this->clear();
|
||
|
}
|
||
|
|
||
|
if ($this->isInitialized === false) {
|
||
|
$this->initializeData();
|
||
|
}
|
||
|
|
||
|
$this->currentId = $id;
|
||
|
|
||
|
$this->driver->start($this->shouldCheckForDeadAndUnused);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stop collection of code coverage information.
|
||
|
*
|
||
|
* @param bool $append
|
||
|
* @param mixed $linesToBeCovered
|
||
|
* @param array $linesToBeUsed
|
||
|
* @param bool $ignoreForceCoversAnnotation
|
||
|
*
|
||
|
* @return array
|
||
|
*
|
||
|
* @throws \SebastianBergmann\CodeCoverage\RuntimeException
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function stop($append = true, $linesToBeCovered = [], array $linesToBeUsed = [], $ignoreForceCoversAnnotation = false)
|
||
|
{
|
||
|
if (!\is_bool($append)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'boolean'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (!\is_array($linesToBeCovered) && $linesToBeCovered !== false) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
2,
|
||
|
'array or false'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$data = $this->driver->stop();
|
||
|
$this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed, $ignoreForceCoversAnnotation);
|
||
|
|
||
|
$this->currentId = null;
|
||
|
|
||
|
return $data;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Appends code coverage data.
|
||
|
*
|
||
|
* @param array $data
|
||
|
* @param mixed $id
|
||
|
* @param bool $append
|
||
|
* @param mixed $linesToBeCovered
|
||
|
* @param array $linesToBeUsed
|
||
|
* @param bool $ignoreForceCoversAnnotation
|
||
|
*
|
||
|
* @throws \SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException
|
||
|
* @throws \SebastianBergmann\CodeCoverage\MissingCoversAnnotationException
|
||
|
* @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
|
||
|
* @throws \ReflectionException
|
||
|
* @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
|
||
|
* @throws RuntimeException
|
||
|
*/
|
||
|
public function append(array $data, $id = null, $append = true, $linesToBeCovered = [], array $linesToBeUsed = [], $ignoreForceCoversAnnotation = false)
|
||
|
{
|
||
|
if ($id === null) {
|
||
|
$id = $this->currentId;
|
||
|
}
|
||
|
|
||
|
if ($id === null) {
|
||
|
throw new RuntimeException;
|
||
|
}
|
||
|
|
||
|
$this->applyListsFilter($data);
|
||
|
$this->applyIgnoredLinesFilter($data);
|
||
|
$this->initializeFilesThatAreSeenTheFirstTime($data);
|
||
|
|
||
|
if (!$append) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ($id !== 'UNCOVERED_FILES_FROM_WHITELIST') {
|
||
|
$this->applyCoversAnnotationFilter(
|
||
|
$data,
|
||
|
$linesToBeCovered,
|
||
|
$linesToBeUsed,
|
||
|
$ignoreForceCoversAnnotation
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (empty($data)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$size = 'unknown';
|
||
|
$status = null;
|
||
|
|
||
|
if ($id instanceof TestCase) {
|
||
|
$_size = $id->getSize();
|
||
|
|
||
|
if ($_size === \PHPUnit\Util\Test::SMALL) {
|
||
|
$size = 'small';
|
||
|
} elseif ($_size === \PHPUnit\Util\Test::MEDIUM) {
|
||
|
$size = 'medium';
|
||
|
} elseif ($_size === \PHPUnit\Util\Test::LARGE) {
|
||
|
$size = 'large';
|
||
|
}
|
||
|
|
||
|
$status = $id->getStatus();
|
||
|
$id = \get_class($id) . '::' . $id->getName();
|
||
|
} elseif ($id instanceof PhptTestCase) {
|
||
|
$size = 'large';
|
||
|
$id = $id->getName();
|
||
|
}
|
||
|
|
||
|
$this->tests[$id] = ['size' => $size, 'status' => $status];
|
||
|
|
||
|
foreach ($data as $file => $lines) {
|
||
|
if (!$this->filter->isFile($file)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
foreach ($lines as $k => $v) {
|
||
|
if ($v === Driver::LINE_EXECUTED) {
|
||
|
if (empty($this->data[$file][$k]) || !\in_array($id, $this->data[$file][$k])) {
|
||
|
$this->data[$file][$k][] = $id;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->report = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Merges the data from another instance.
|
||
|
*
|
||
|
* @param CodeCoverage $that
|
||
|
*/
|
||
|
public function merge(self $that)
|
||
|
{
|
||
|
$this->filter->setWhitelistedFiles(
|
||
|
\array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles())
|
||
|
);
|
||
|
|
||
|
foreach ($that->data as $file => $lines) {
|
||
|
if (!isset($this->data[$file])) {
|
||
|
if (!$this->filter->isFiltered($file)) {
|
||
|
$this->data[$file] = $lines;
|
||
|
}
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
foreach ($lines as $line => $data) {
|
||
|
if ($data !== null) {
|
||
|
if (!isset($this->data[$file][$line])) {
|
||
|
$this->data[$file][$line] = $data;
|
||
|
} else {
|
||
|
$this->data[$file][$line] = \array_unique(
|
||
|
\array_merge($this->data[$file][$line], $data)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->tests = \array_merge($this->tests, $that->getTests());
|
||
|
$this->report = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param bool $flag
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function setCacheTokens($flag)
|
||
|
{
|
||
|
if (!\is_bool($flag)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'boolean'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->cacheTokens = $flag;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function getCacheTokens()
|
||
|
{
|
||
|
return $this->cacheTokens;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param bool $flag
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function setCheckForUnintentionallyCoveredCode($flag)
|
||
|
{
|
||
|
if (!\is_bool($flag)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'boolean'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->checkForUnintentionallyCoveredCode = $flag;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param bool $flag
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function setForceCoversAnnotation($flag)
|
||
|
{
|
||
|
if (!\is_bool($flag)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'boolean'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->forceCoversAnnotation = $flag;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param bool $flag
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function setCheckForMissingCoversAnnotation($flag)
|
||
|
{
|
||
|
if (!\is_bool($flag)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'boolean'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->checkForMissingCoversAnnotation = $flag;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param bool $flag
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function setCheckForUnexecutedCoveredCode($flag)
|
||
|
{
|
||
|
if (!\is_bool($flag)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'boolean'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->checkForUnexecutedCoveredCode = $flag;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @deprecated
|
||
|
*
|
||
|
* @param bool $flag
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function setMapTestClassNameToCoveredClassName($flag)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param bool $flag
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function setAddUncoveredFilesFromWhitelist($flag)
|
||
|
{
|
||
|
if (!\is_bool($flag)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'boolean'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->addUncoveredFilesFromWhitelist = $flag;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param bool $flag
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function setProcessUncoveredFilesFromWhitelist($flag)
|
||
|
{
|
||
|
if (!\is_bool($flag)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'boolean'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->processUncoveredFilesFromWhitelist = $flag;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param bool $flag
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function setDisableIgnoredLines($flag)
|
||
|
{
|
||
|
if (!\is_bool($flag)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'boolean'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->disableIgnoredLines = $flag;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param bool $flag
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
public function setIgnoreDeprecatedCode($flag)
|
||
|
{
|
||
|
if (!\is_bool($flag)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'boolean'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->ignoreDeprecatedCode = $flag;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array $whitelist
|
||
|
*/
|
||
|
public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist)
|
||
|
{
|
||
|
$this->unintentionallyCoveredSubclassesWhitelist = $whitelist;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Applies the @covers annotation filtering.
|
||
|
*
|
||
|
* @param array $data
|
||
|
* @param mixed $linesToBeCovered
|
||
|
* @param array $linesToBeUsed
|
||
|
* @param bool $ignoreForceCoversAnnotation
|
||
|
*
|
||
|
* @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
|
||
|
* @throws \ReflectionException
|
||
|
* @throws MissingCoversAnnotationException
|
||
|
* @throws UnintentionallyCoveredCodeException
|
||
|
*/
|
||
|
private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed, $ignoreForceCoversAnnotation)
|
||
|
{
|
||
|
if ($linesToBeCovered === false ||
|
||
|
($this->forceCoversAnnotation && empty($linesToBeCovered) && !$ignoreForceCoversAnnotation)) {
|
||
|
if ($this->checkForMissingCoversAnnotation) {
|
||
|
throw new MissingCoversAnnotationException;
|
||
|
}
|
||
|
|
||
|
$data = [];
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (empty($linesToBeCovered)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if ($this->checkForUnintentionallyCoveredCode &&
|
||
|
(!$this->currentId instanceof TestCase ||
|
||
|
(!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {
|
||
|
$this->performUnintentionallyCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
|
||
|
}
|
||
|
|
||
|
if ($this->checkForUnexecutedCoveredCode) {
|
||
|
$this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed);
|
||
|
}
|
||
|
|
||
|
$data = \array_intersect_key($data, $linesToBeCovered);
|
||
|
|
||
|
foreach (\array_keys($data) as $filename) {
|
||
|
$_linesToBeCovered = \array_flip($linesToBeCovered[$filename]);
|
||
|
$data[$filename] = \array_intersect_key($data[$filename], $_linesToBeCovered);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Applies the whitelist filtering.
|
||
|
*
|
||
|
* @param array $data
|
||
|
*/
|
||
|
private function applyListsFilter(array &$data)
|
||
|
{
|
||
|
foreach (\array_keys($data) as $filename) {
|
||
|
if ($this->filter->isFiltered($filename)) {
|
||
|
unset($data[$filename]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Applies the "ignored lines" filtering.
|
||
|
*
|
||
|
* @param array $data
|
||
|
*
|
||
|
* @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
|
||
|
*/
|
||
|
private function applyIgnoredLinesFilter(array &$data)
|
||
|
{
|
||
|
foreach (\array_keys($data) as $filename) {
|
||
|
if (!$this->filter->isFile($filename)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
foreach ($this->getLinesToBeIgnored($filename) as $line) {
|
||
|
unset($data[$filename][$line]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array $data
|
||
|
*/
|
||
|
private function initializeFilesThatAreSeenTheFirstTime(array $data)
|
||
|
{
|
||
|
foreach ($data as $file => $lines) {
|
||
|
if (!isset($this->data[$file]) && $this->filter->isFile($file)) {
|
||
|
$this->data[$file] = [];
|
||
|
|
||
|
foreach ($lines as $k => $v) {
|
||
|
$this->data[$file][$k] = $v === -2 ? null : [];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Processes whitelisted files that are not covered.
|
||
|
*/
|
||
|
private function addUncoveredFilesFromWhitelist()
|
||
|
{
|
||
|
$data = [];
|
||
|
$uncoveredFiles = \array_diff(
|
||
|
$this->filter->getWhitelist(),
|
||
|
\array_keys($this->data)
|
||
|
);
|
||
|
|
||
|
foreach ($uncoveredFiles as $uncoveredFile) {
|
||
|
if (!\file_exists($uncoveredFile)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$data[$uncoveredFile] = [];
|
||
|
|
||
|
$lines = \count(\file($uncoveredFile));
|
||
|
|
||
|
for ($i = 1; $i <= $lines; $i++) {
|
||
|
$data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the lines of a source file that should be ignored.
|
||
|
*
|
||
|
* @param string $filename
|
||
|
*
|
||
|
* @return array
|
||
|
*
|
||
|
* @throws InvalidArgumentException
|
||
|
*/
|
||
|
private function getLinesToBeIgnored($filename)
|
||
|
{
|
||
|
if (!\is_string($filename)) {
|
||
|
throw InvalidArgumentException::create(
|
||
|
1,
|
||
|
'string'
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (isset($this->ignoredLines[$filename])) {
|
||
|
return $this->ignoredLines[$filename];
|
||
|
}
|
||
|
|
||
|
$this->ignoredLines[$filename] = [];
|
||
|
|
||
|
$lines = \file($filename);
|
||
|
|
||
|
foreach ($lines as $index => $line) {
|
||
|
if (!\trim($line)) {
|
||
|
$this->ignoredLines[$filename][] = $index + 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($this->cacheTokens) {
|
||
|
$tokens = \PHP_Token_Stream_CachingFactory::get($filename);
|
||
|
} else {
|
||
|
$tokens = new \PHP_Token_Stream($filename);
|
||
|
}
|
||
|
|
||
|
foreach ($tokens->getInterfaces() as $interface) {
|
||
|
$interfaceStartLine = $interface['startLine'];
|
||
|
$interfaceEndLine = $interface['endLine'];
|
||
|
|
||
|
foreach (\range($interfaceStartLine, $interfaceEndLine) as $line) {
|
||
|
$this->ignoredLines[$filename][] = $line;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
foreach (\array_merge($tokens->getClasses(), $tokens->getTraits()) as $classOrTrait) {
|
||
|
$classOrTraitStartLine = $classOrTrait['startLine'];
|
||
|
$classOrTraitEndLine = $classOrTrait['endLine'];
|
||
|
|
||
|
if (empty($classOrTrait['methods'])) {
|
||
|
foreach (\range($classOrTraitStartLine, $classOrTraitEndLine) as $line) {
|
||
|
$this->ignoredLines[$filename][] = $line;
|
||
|
}
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$firstMethod = \array_shift($classOrTrait['methods']);
|
||
|
$firstMethodStartLine = $firstMethod['startLine'];
|
||
|
$firstMethodEndLine = $firstMethod['endLine'];
|
||
|
$lastMethodEndLine = $firstMethodEndLine;
|
||
|
|
||
|
do {
|
||
|
$lastMethod = \array_pop($classOrTrait['methods']);
|
||
|
} while ($lastMethod !== null && 0 === \strpos($lastMethod['signature'], 'anonymousFunction'));
|
||
|
|
||
|
if ($lastMethod !== null) {
|
||
|
$lastMethodEndLine = $lastMethod['endLine'];
|
||
|
}
|
||
|
|
||
|
foreach (\range($classOrTraitStartLine, $firstMethodStartLine) as $line) {
|
||
|
$this->ignoredLines[$filename][] = $line;
|
||
|
}
|
||
|
|
||
|
foreach (\range($lastMethodEndLine + 1, $classOrTraitEndLine) as $line) {
|
||
|
$this->ignoredLines[$filename][] = $line;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($this->disableIgnoredLines) {
|
||
|
$this->ignoredLines[$filename] = array_unique($this->ignoredLines[$filename]);
|
||
|
\sort($this->ignoredLines[$filename]);
|
||
|
|
||
|
return $this->ignoredLines[$filename];
|
||
|
}
|
||
|
|
||
|
$ignore = false;
|
||
|
$stop = false;
|
||
|
|
||
|
foreach ($tokens->tokens() as $token) {
|
||
|
switch (\get_class($token)) {
|
||
|
case \PHP_Token_COMMENT::class:
|
||
|
case \PHP_Token_DOC_COMMENT::class:
|
||
|
$_token = \trim($token);
|
||
|
$_line = \trim($lines[$token->getLine() - 1]);
|
||
|
|
||
|
if ($_token === '// @codeCoverageIgnore' ||
|
||
|
$_token === '//@codeCoverageIgnore') {
|
||
|
$ignore = true;
|
||
|
$stop = true;
|
||
|
} elseif ($_token === '// @codeCoverageIgnoreStart' ||
|
||
|
$_token === '//@codeCoverageIgnoreStart') {
|
||
|
$ignore = true;
|
||
|
} elseif ($_token === '// @codeCoverageIgnoreEnd' ||
|
||
|
$_token === '//@codeCoverageIgnoreEnd') {
|
||
|
$stop = true;
|
||
|
}
|
||
|
|
||
|
if (!$ignore) {
|
||
|
$start = $token->getLine();
|
||
|
$end = $start + \substr_count($token, "\n");
|
||
|
|
||
|
// Do not ignore the first line when there is a token
|
||
|
// before the comment
|
||
|
if (0 !== \strpos($_token, $_line)) {
|
||
|
$start++;
|
||
|
}
|
||
|
|
||
|
for ($i = $start; $i < $end; $i++) {
|
||
|
$this->ignoredLines[$filename][] = $i;
|
||
|
}
|
||
|
|
||
|
// A DOC_COMMENT token or a COMMENT token starting with "/*"
|
||
|
// does not contain the final \n character in its text
|
||
|
if (isset($lines[$i - 1]) && 0 === \strpos($_token, '/*') && '*/' === \substr(\trim($lines[$i - 1]), -2)) {
|
||
|
$this->ignoredLines[$filename][] = $i;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
case \PHP_Token_INTERFACE::class:
|
||
|
case \PHP_Token_TRAIT::class:
|
||
|
case \PHP_Token_CLASS::class:
|
||
|
case \PHP_Token_FUNCTION::class:
|
||
|
/* @var \PHP_Token_Interface $token */
|
||
|
|
||
|
$docblock = $token->getDocblock();
|
||
|
|
||
|
$this->ignoredLines[$filename][] = $token->getLine();
|
||
|
|
||
|
if (\strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && \strpos($docblock, '@deprecated'))) {
|
||
|
$endLine = $token->getEndLine();
|
||
|
|
||
|
for ($i = $token->getLine(); $i <= $endLine; $i++) {
|
||
|
$this->ignoredLines[$filename][] = $i;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
case \PHP_Token_ENUM::class:
|
||
|
$this->ignoredLines[$filename][] = $token->getLine();
|
||
|
|
||
|
break;
|
||
|
|
||
|
case \PHP_Token_NAMESPACE::class:
|
||
|
$this->ignoredLines[$filename][] = $token->getEndLine();
|
||
|
|
||
|
// Intentional fallthrough
|
||
|
case \PHP_Token_DECLARE::class:
|
||
|
case \PHP_Token_OPEN_TAG::class:
|
||
|
case \PHP_Token_CLOSE_TAG::class:
|
||
|
case \PHP_Token_USE::class:
|
||
|
$this->ignoredLines[$filename][] = $token->getLine();
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if ($ignore) {
|
||
|
$this->ignoredLines[$filename][] = $token->getLine();
|
||
|
|
||
|
if ($stop) {
|
||
|
$ignore = false;
|
||
|
$stop = false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->ignoredLines[$filename][] = \count($lines) + 1;
|
||
|
|
||
|
$this->ignoredLines[$filename] = \array_unique(
|
||
|
$this->ignoredLines[$filename]
|
||
|
);
|
||
|
|
||
|
$this->ignoredLines[$filename] = array_unique($this->ignoredLines[$filename]);
|
||
|
\sort($this->ignoredLines[$filename]);
|
||
|
|
||
|
return $this->ignoredLines[$filename];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array $data
|
||
|
* @param array $linesToBeCovered
|
||
|
* @param array $linesToBeUsed
|
||
|
*
|
||
|
* @throws \ReflectionException
|
||
|
* @throws UnintentionallyCoveredCodeException
|
||
|
*/
|
||
|
private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
|
||
|
{
|
||
|
$allowedLines = $this->getAllowedLines(
|
||
|
$linesToBeCovered,
|
||
|
$linesToBeUsed
|
||
|
);
|
||
|
|
||
|
$unintentionallyCoveredUnits = [];
|
||
|
|
||
|
foreach ($data as $file => $_data) {
|
||
|
foreach ($_data as $line => $flag) {
|
||
|
if ($flag === 1 && !isset($allowedLines[$file][$line])) {
|
||
|
$unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
|
||
|
|
||
|
if (!empty($unintentionallyCoveredUnits)) {
|
||
|
throw new UnintentionallyCoveredCodeException(
|
||
|
$unintentionallyCoveredUnits
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array $data
|
||
|
* @param array $linesToBeCovered
|
||
|
* @param array $linesToBeUsed
|
||
|
*
|
||
|
* @throws CoveredCodeNotExecutedException
|
||
|
*/
|
||
|
private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
|
||
|
{
|
||
|
$executedCodeUnits = $this->coverageToCodeUnits($data);
|
||
|
$message = '';
|
||
|
|
||
|
foreach ($this->linesToCodeUnits($linesToBeCovered) as $codeUnit) {
|
||
|
if (!\in_array($codeUnit, $executedCodeUnits)) {
|
||
|
$message .= \sprintf(
|
||
|
'- %s is expected to be executed (@covers) but was not executed' . "\n",
|
||
|
$codeUnit
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
foreach ($this->linesToCodeUnits($linesToBeUsed) as $codeUnit) {
|
||
|
if (!\in_array($codeUnit, $executedCodeUnits)) {
|
||
|
$message .= \sprintf(
|
||
|
'- %s is expected to be executed (@uses) but was not executed' . "\n",
|
||
|
$codeUnit
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!empty($message)) {
|
||
|
throw new CoveredCodeNotExecutedException($message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array $linesToBeCovered
|
||
|
* @param array $linesToBeUsed
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed)
|
||
|
{
|
||
|
$allowedLines = [];
|
||
|
|
||
|
foreach (\array_keys($linesToBeCovered) as $file) {
|
||
|
if (!isset($allowedLines[$file])) {
|
||
|
$allowedLines[$file] = [];
|
||
|
}
|
||
|
|
||
|
$allowedLines[$file] = \array_merge(
|
||
|
$allowedLines[$file],
|
||
|
$linesToBeCovered[$file]
|
||
|
);
|
||
|
}
|
||
|
|
||
|
foreach (\array_keys($linesToBeUsed) as $file) {
|
||
|
if (!isset($allowedLines[$file])) {
|
||
|
$allowedLines[$file] = [];
|
||
|
}
|
||
|
|
||
|
$allowedLines[$file] = \array_merge(
|
||
|
$allowedLines[$file],
|
||
|
$linesToBeUsed[$file]
|
||
|
);
|
||
|
}
|
||
|
|
||
|
foreach (\array_keys($allowedLines) as $file) {
|
||
|
$allowedLines[$file] = \array_flip(
|
||
|
\array_unique($allowedLines[$file])
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return $allowedLines;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return Driver
|
||
|
*
|
||
|
* @throws RuntimeException
|
||
|
*/
|
||
|
private function selectDriver()
|
||
|
{
|
||
|
$runtime = new Runtime;
|
||
|
|
||
|
if (!$runtime->canCollectCodeCoverage()) {
|
||
|
throw new RuntimeException('No code coverage driver available');
|
||
|
}
|
||
|
|
||
|
if ($runtime->isHHVM()) {
|
||
|
return new HHVM;
|
||
|
}
|
||
|
|
||
|
if ($runtime->isPHPDBG()) {
|
||
|
return new PHPDBG;
|
||
|
}
|
||
|
|
||
|
return new Xdebug;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array $unintentionallyCoveredUnits
|
||
|
*
|
||
|
* @return array
|
||
|
*
|
||
|
* @throws \ReflectionException
|
||
|
*/
|
||
|
private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits)
|
||
|
{
|
||
|
$unintentionallyCoveredUnits = \array_unique($unintentionallyCoveredUnits);
|
||
|
\sort($unintentionallyCoveredUnits);
|
||
|
|
||
|
foreach (\array_keys($unintentionallyCoveredUnits) as $k => $v) {
|
||
|
$unit = \explode('::', $unintentionallyCoveredUnits[$k]);
|
||
|
|
||
|
if (\count($unit) !== 2) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$class = new \ReflectionClass($unit[0]);
|
||
|
|
||
|
foreach ($this->unintentionallyCoveredSubclassesWhitelist as $whitelisted) {
|
||
|
if ($class->isSubclassOf($whitelisted)) {
|
||
|
unset($unintentionallyCoveredUnits[$k]);
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return \array_values($unintentionallyCoveredUnits);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* If we are processing uncovered files from whitelist,
|
||
|
* we can initialize the data before we start to speed up the tests
|
||
|
*
|
||
|
* @throws \SebastianBergmann\CodeCoverage\RuntimeException
|
||
|
*/
|
||
|
protected function initializeData()
|
||
|
{
|
||
|
$this->isInitialized = true;
|
||
|
|
||
|
if ($this->processUncoveredFilesFromWhitelist) {
|
||
|
$this->shouldCheckForDeadAndUnused = false;
|
||
|
|
||
|
$this->driver->start(true);
|
||
|
|
||
|
foreach ($this->filter->getWhitelist() as $file) {
|
||
|
if ($this->filter->isFile($file)) {
|
||
|
include_once($file);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$data = [];
|
||
|
$coverage = $this->driver->stop();
|
||
|
|
||
|
foreach ($coverage as $file => $fileCoverage) {
|
||
|
if ($this->filter->isFiltered($file)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
foreach (\array_keys($fileCoverage) as $key) {
|
||
|
if ($fileCoverage[$key] === Driver::LINE_EXECUTED) {
|
||
|
$fileCoverage[$key] = Driver::LINE_NOT_EXECUTED;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$data[$file] = $fileCoverage;
|
||
|
}
|
||
|
|
||
|
$this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array $data
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
private function coverageToCodeUnits(array $data)
|
||
|
{
|
||
|
$codeUnits = [];
|
||
|
|
||
|
foreach ($data as $filename => $lines) {
|
||
|
foreach ($lines as $line => $flag) {
|
||
|
if ($flag === 1) {
|
||
|
$codeUnits[] = $this->wizard->lookup($filename, $line);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return \array_unique($codeUnits);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array $data
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
private function linesToCodeUnits(array $data)
|
||
|
{
|
||
|
$codeUnits = [];
|
||
|
|
||
|
foreach ($data as $filename => $lines) {
|
||
|
foreach ($lines as $line) {
|
||
|
$codeUnits[] = $this->wizard->lookup($filename, $line);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return \array_unique($codeUnits);
|
||
|
}
|
||
|
}
|