Blame | Letzte Änderung | Log anzeigen | RSS feed
<?php declare(strict_types=1);/** This file is part of phpunit/php-code-coverage.** (c) Sebastian Bergmann <sebastian@phpunit.de>** For the full copyright and license information, please view the LICENSE* file that was distributed with this source code.*/namespace SebastianBergmann\CodeCoverage;use function array_diff;use function array_diff_key;use function array_flip;use function array_keys;use function array_merge;use function array_unique;use function array_values;use function count;use function explode;use function get_class;use function is_array;use function sort;use PHPUnit\Framework\TestCase;use PHPUnit\Runner\PhptTestCase;use PHPUnit\Util\Test;use ReflectionClass;use SebastianBergmann\CodeCoverage\Driver\Driver;use SebastianBergmann\CodeCoverage\Node\Builder;use SebastianBergmann\CodeCoverage\Node\Directory;use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingFileAnalyser;use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;use SebastianBergmann\CodeUnitReverseLookup\Wizard;/*** Provides collection functionality for PHP code coverage information.*/final class CodeCoverage{private const UNCOVERED_FILES = 'UNCOVERED_FILES';/*** @var Driver*/private $driver;/*** @var Filter*/private $filter;/*** @var Wizard*/private $wizard;/*** @var bool*/private $checkForUnintentionallyCoveredCode = false;/*** @var bool*/private $includeUncoveredFiles = true;/*** @var bool*/private $processUncoveredFiles = false;/*** @var bool*/private $ignoreDeprecatedCode = false;/*** @var null|PhptTestCase|string|TestCase*/private $currentId;/*** Code coverage data.** @var ProcessedCodeCoverageData*/private $data;/*** @var bool*/private $useAnnotationsForIgnoringCode = true;/*** Test data.** @var array*/private $tests = [];/*** @psalm-var list<class-string>*/private $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];/*** @var ?FileAnalyser*/private $analyser;/*** @var ?string*/private $cacheDirectory;public function __construct(Driver $driver, Filter $filter){$this->driver = $driver;$this->filter = $filter;$this->data = new ProcessedCodeCoverageData;$this->wizard = new Wizard;}/*** Returns the code coverage information as a graph of node objects.*/public function getReport(): Directory{return (new Builder($this->analyser()))->build($this);}/*** Clears collected code coverage data.*/public function clear(): void{$this->currentId = null;$this->data = new ProcessedCodeCoverageData;$this->tests = [];}/*** Returns the filter object used.*/public function filter(): Filter{return $this->filter;}/*** Returns the collected code coverage data.*/public function getData(bool $raw = false): ProcessedCodeCoverageData{if (!$raw) {if ($this->processUncoveredFiles) {$this->processUncoveredFilesFromFilter();} elseif ($this->includeUncoveredFiles) {$this->addUncoveredFilesFromFilter();}}return $this->data;}/*** Sets the coverage data.*/public function setData(ProcessedCodeCoverageData $data): void{$this->data = $data;}/*** Returns the test data.*/public function getTests(): array{return $this->tests;}/*** Sets the test data.*/public function setTests(array $tests): void{$this->tests = $tests;}/*** Start collection of code coverage information.** @param PhptTestCase|string|TestCase $id*/public function start($id, bool $clear = false): void{if ($clear) {$this->clear();}$this->currentId = $id;$this->driver->start();}/*** Stop collection of code coverage information.** @param array|false $linesToBeCovered*/public function stop(bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): RawCodeCoverageData{if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {throw new InvalidArgumentException('$linesToBeCovered must be an array or false');}$data = $this->driver->stop();$this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);$this->currentId = null;return $data;}/*** Appends code coverage data.** @param PhptTestCase|string|TestCase $id* @param array|false $linesToBeCovered** @throws ReflectionException* @throws TestIdMissingException* @throws UnintentionallyCoveredCodeException*/public function append(RawCodeCoverageData $rawData, $id = null, bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): void{if ($id === null) {$id = $this->currentId;}if ($id === null) {throw new TestIdMissingException;}$this->applyFilter($rawData);$this->applyExecutableLinesFilter($rawData);if ($this->useAnnotationsForIgnoringCode) {$this->applyIgnoredLinesFilter($rawData);}$this->data->initializeUnseenData($rawData);if (!$append) {return;}if ($id !== self::UNCOVERED_FILES) {$this->applyCoversAnnotationFilter($rawData,$linesToBeCovered,$linesToBeUsed);if (empty($rawData->lineCoverage())) {return;}$size = 'unknown';$status = -1;$fromTestcase = false;if ($id instanceof TestCase) {$fromTestcase = true;$_size = $id->getSize();if ($_size === Test::SMALL) {$size = 'small';} elseif ($_size === Test::MEDIUM) {$size = 'medium';} elseif ($_size === Test::LARGE) {$size = 'large';}$status = $id->getStatus();$id = get_class($id) . '::' . $id->getName();} elseif ($id instanceof PhptTestCase) {$fromTestcase = true;$size = 'large';$id = $id->getName();}$this->tests[$id] = ['size' => $size, 'status' => $status, 'fromTestcase' => $fromTestcase];$this->data->markCodeAsExecutedByTestCase($id, $rawData);}}/*** Merges the data from another instance.*/public function merge(self $that): void{$this->filter->includeFiles($that->filter()->files());$this->data->merge($that->data);$this->tests = array_merge($this->tests, $that->getTests());}public function enableCheckForUnintentionallyCoveredCode(): void{$this->checkForUnintentionallyCoveredCode = true;}public function disableCheckForUnintentionallyCoveredCode(): void{$this->checkForUnintentionallyCoveredCode = false;}public function includeUncoveredFiles(): void{$this->includeUncoveredFiles = true;}public function excludeUncoveredFiles(): void{$this->includeUncoveredFiles = false;}public function processUncoveredFiles(): void{$this->processUncoveredFiles = true;}public function doNotProcessUncoveredFiles(): void{$this->processUncoveredFiles = false;}public function enableAnnotationsForIgnoringCode(): void{$this->useAnnotationsForIgnoringCode = true;}public function disableAnnotationsForIgnoringCode(): void{$this->useAnnotationsForIgnoringCode = false;}public function ignoreDeprecatedCode(): void{$this->ignoreDeprecatedCode = true;}public function doNotIgnoreDeprecatedCode(): void{$this->ignoreDeprecatedCode = false;}/*** @psalm-assert-if-true !null $this->cacheDirectory*/public function cachesStaticAnalysis(): bool{return $this->cacheDirectory !== null;}public function cacheStaticAnalysis(string $directory): void{$this->cacheDirectory = $directory;}public function doNotCacheStaticAnalysis(): void{$this->cacheDirectory = null;}/*** @throws StaticAnalysisCacheNotConfiguredException*/public function cacheDirectory(): string{if (!$this->cachesStaticAnalysis()) {throw new StaticAnalysisCacheNotConfiguredException('The static analysis cache is not configured');}return $this->cacheDirectory;}/*** @psalm-param class-string $className*/public function excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck(string $className): void{$this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck[] = $className;}public function enableBranchAndPathCoverage(): void{$this->driver->enableBranchAndPathCoverage();}public function disableBranchAndPathCoverage(): void{$this->driver->disableBranchAndPathCoverage();}public function collectsBranchAndPathCoverage(): bool{return $this->driver->collectsBranchAndPathCoverage();}public function detectsDeadCode(): bool{return $this->driver->detectsDeadCode();}/*** Applies the @covers annotation filtering.** @param array|false $linesToBeCovered** @throws ReflectionException* @throws UnintentionallyCoveredCodeException*/private function applyCoversAnnotationFilter(RawCodeCoverageData $rawData, $linesToBeCovered, array $linesToBeUsed): void{if ($linesToBeCovered === false) {$rawData->clear();return;}if (empty($linesToBeCovered)) {return;}if ($this->checkForUnintentionallyCoveredCode &&(!$this->currentId instanceof TestCase ||(!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {$this->performUnintentionallyCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed);}$rawLineData = $rawData->lineCoverage();$filesWithNoCoverage = array_diff_key($rawLineData, $linesToBeCovered);foreach (array_keys($filesWithNoCoverage) as $fileWithNoCoverage) {$rawData->removeCoverageDataForFile($fileWithNoCoverage);}if (is_array($linesToBeCovered)) {foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) {$rawData->keepLineCoverageDataOnlyForLines($fileToBeCovered, $includedLines);$rawData->keepFunctionCoverageDataOnlyForLines($fileToBeCovered, $includedLines);}}}private function applyFilter(RawCodeCoverageData $data): void{if ($this->filter->isEmpty()) {return;}foreach (array_keys($data->lineCoverage()) as $filename) {if ($this->filter->isExcluded($filename)) {$data->removeCoverageDataForFile($filename);}}}private function applyExecutableLinesFilter(RawCodeCoverageData $data): void{foreach (array_keys($data->lineCoverage()) as $filename) {if (!$this->filter->isFile($filename)) {continue;}$linesToBranchMap = $this->analyser()->executableLinesIn($filename);$data->keepLineCoverageDataOnlyForLines($filename,array_keys($linesToBranchMap));$data->markExecutableLineByBranch($filename,$linesToBranchMap);}}private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void{foreach (array_keys($data->lineCoverage()) as $filename) {if (!$this->filter->isFile($filename)) {continue;}$data->removeCoverageDataForLines($filename,$this->analyser()->ignoredLinesFor($filename));}}/*** @throws UnintentionallyCoveredCodeException*/private function addUncoveredFilesFromFilter(): void{$uncoveredFiles = array_diff($this->filter->files(),$this->data->coveredFiles());foreach ($uncoveredFiles as $uncoveredFile) {if ($this->filter->isFile($uncoveredFile)) {$this->append(RawCodeCoverageData::fromUncoveredFile($uncoveredFile,$this->analyser()),self::UNCOVERED_FILES);}}}/*** @throws UnintentionallyCoveredCodeException*/private function processUncoveredFilesFromFilter(): void{$uncoveredFiles = array_diff($this->filter->files(),$this->data->coveredFiles());$this->driver->start();foreach ($uncoveredFiles as $uncoveredFile) {if ($this->filter->isFile($uncoveredFile)) {include_once $uncoveredFile;}}$this->append($this->driver->stop(), self::UNCOVERED_FILES);}/*** @throws ReflectionException* @throws UnintentionallyCoveredCodeException*/private function performUnintentionallyCoveredCodeCheck(RawCodeCoverageData $data, array $linesToBeCovered, array $linesToBeUsed): void{$allowedLines = $this->getAllowedLines($linesToBeCovered,$linesToBeUsed);$unintentionallyCoveredUnits = [];foreach ($data->lineCoverage() as $file => $_data) {foreach ($_data as $line => $flag) {if ($flag === 1 && !isset($allowedLines[$file][$line])) {$unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line);}}}$unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);if (!empty($unintentionallyCoveredUnits)) {throw new UnintentionallyCoveredCodeException($unintentionallyCoveredUnits);}}private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed): array{$allowedLines = [];foreach (array_keys($linesToBeCovered) as $file) {if (!isset($allowedLines[$file])) {$allowedLines[$file] = [];}$allowedLines[$file] = array_merge($allowedLines[$file],$linesToBeCovered[$file]);}foreach (array_keys($linesToBeUsed) as $file) {if (!isset($allowedLines[$file])) {$allowedLines[$file] = [];}$allowedLines[$file] = array_merge($allowedLines[$file],$linesToBeUsed[$file]);}foreach (array_keys($allowedLines) as $file) {$allowedLines[$file] = array_flip(array_unique($allowedLines[$file]));}return $allowedLines;}/*** @throws ReflectionException*/private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array{$unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits);sort($unintentionallyCoveredUnits);foreach (array_keys($unintentionallyCoveredUnits) as $k => $v) {$unit = explode('::', $unintentionallyCoveredUnits[$k]);if (count($unit) !== 2) {continue;}try {$class = new ReflectionClass($unit[0]);foreach ($this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck as $parentClass) {if ($class->isSubclassOf($parentClass)) {unset($unintentionallyCoveredUnits[$k]);break;}}} catch (\ReflectionException $e) {throw new ReflectionException($e->getMessage(),$e->getCode(),$e);}}return array_values($unintentionallyCoveredUnits);}private function analyser(): FileAnalyser{if ($this->analyser !== null) {return $this->analyser;}$this->analyser = new ParsingFileAnalyser($this->useAnnotationsForIgnoringCode,$this->ignoreDeprecatedCode);if ($this->cachesStaticAnalysis()) {$this->analyser = new CachingFileAnalyser($this->cacheDirectory,$this->analyser);}return $this->analyser;}}