Provides collection functionality for PHP code coverage information.
@category PHP @package CodeCoverage @author Sebastian Bergmann <sebastian@phpunit.de> @copyright 2009-2013 Sebastian Bergmann <sebastian@phpunit.de> @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License @link http://github.com/sebastianbergmann/php-code-coverage @since Class available since Release 1.0.0
Expanded class hierarchy of PHP_CodeCoverage
class PHP_CodeCoverage {
/**
* @var PHP_CodeCoverage_Driver
*/
protected $driver;
/**
* @var PHP_CodeCoverage_Filter
*/
protected $filter;
/**
* @var boolean
*/
protected $cacheTokens = FALSE;
/**
* @var boolean
*/
protected $forceCoversAnnotation = FALSE;
/**
* @var boolean
*/
protected $mapTestClassNameToCoveredClassName = FALSE;
/**
* @var boolean
*/
protected $addUncoveredFilesFromWhitelist = TRUE;
/**
* @var boolean
*/
protected $processUncoveredFilesFromWhitelist = FALSE;
/**
* @var mixed
*/
protected $currentId;
/**
* Code coverage data.
*
* @var array
*/
protected $data = array();
/**
* Test data.
*
* @var array
*/
protected $tests = array();
/**
* Constructor.
*
* @param PHP_CodeCoverage_Driver $driver
* @param PHP_CodeCoverage_Filter $filter
*/
public function __construct(PHP_CodeCoverage_Driver $driver = NULL, PHP_CodeCoverage_Filter $filter = NULL) {
if ($driver === NULL) {
$driver = new PHP_CodeCoverage_Driver_Xdebug();
}
if ($filter === NULL) {
$filter = new PHP_CodeCoverage_Filter();
}
$this->driver = $driver;
$this->filter = $filter;
}
/**
* Returns the PHP_CodeCoverage_Report_Node_* object graph
* for this PHP_CodeCoverage object.
*
* @return PHP_CodeCoverage_Report_Node_Directory
* @since Method available since Release 1.1.0
*/
public function getReport() {
$factory = new PHP_CodeCoverage_Report_Factory();
return $factory
->create($this);
}
/**
* Clears collected code coverage data.
*/
public function clear() {
$this->currentId = NULL;
$this->data = array();
$this->tests = array();
}
/**
* Returns the PHP_CodeCoverage_Filter used.
*
* @return PHP_CodeCoverage_Filter
*/
public function filter() {
return $this->filter;
}
/**
* Returns the collected code coverage data.
*
* @return array
* @since Method available since Release 1.1.0
*/
public function getData() {
if ($this->addUncoveredFilesFromWhitelist) {
$this
->addUncoveredFilesFromWhitelist();
}
// We need to apply the blacklist filter a second time
// when no whitelist is used.
if (!$this->filter
->hasWhitelist()) {
$this
->applyListsFilter($this->data);
}
return $this->data;
}
/**
* Returns the test data.
*
* @return array
* @since Method available since Release 1.1.0
*/
public function getTests() {
return $this->tests;
}
/**
* Start collection of code coverage information.
*
* @param mixed $id
* @param boolean $clear
* @throws PHP_CodeCoverage_Exception
*/
public function start($id, $clear = FALSE) {
if (!is_bool($clear)) {
throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
}
if ($clear) {
$this
->clear();
}
$this->currentId = $id;
$this->driver
->start();
}
/**
* Stop collection of code coverage information.
*
* @param boolean $append
* @return array
* @throws PHP_CodeCoverage_Exception
*/
public function stop($append = TRUE) {
if (!is_bool($append)) {
throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
}
$data = $this->driver
->stop();
$this
->append($data, NULL, $append);
$this->currentId = NULL;
return $data;
}
/**
* Appends code coverage data.
*
* @param array $data
* @param mixed $id
* @param boolean $append
*/
public function append(array $data, $id = NULL, $append = TRUE) {
if ($id === NULL) {
$id = $this->currentId;
}
if ($id === NULL) {
throw new PHP_CodeCoverage_Exception();
}
$this
->applyListsFilter($data);
$this
->initializeFilesThatAreSeenTheFirstTime($data);
if (!$append) {
return;
}
if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') {
$this
->applyCoversAnnotationFilter($data, $id);
}
if (empty($data)) {
return;
}
$status = NULL;
if ($id instanceof PHPUnit_Framework_TestCase) {
$status = $id
->getStatus();
$id = get_class($id) . '::' . $id
->getName();
}
else {
if ($id instanceof PHPUnit_Extensions_PhptTestCase) {
$id = $id
->getName();
}
}
$this->tests[$id] = $status;
foreach ($data as $file => $lines) {
if (!$this->filter
->isFile($file)) {
continue;
}
foreach ($lines as $k => $v) {
if ($v == 1) {
$this->data[$file][$k][] = $id;
}
}
}
}
/**
* Merges the data from another instance of PHP_CodeCoverage.
*
* @param PHP_CodeCoverage $that
*/
public function merge(PHP_CodeCoverage $that) {
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());
}
/**
* @param boolean $flag
* @throws PHP_CodeCoverage_Exception
* @since Method available since Release 1.1.0
*/
public function setCacheTokens($flag) {
if (!is_bool($flag)) {
throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
}
$this->cacheTokens = $flag;
}
/**
* @param boolean $flag
* @since Method available since Release 1.1.0
*/
public function getCacheTokens() {
return $this->cacheTokens;
}
/**
* @param boolean $flag
* @throws PHP_CodeCoverage_Exception
*/
public function setForceCoversAnnotation($flag) {
if (!is_bool($flag)) {
throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
}
$this->forceCoversAnnotation = $flag;
}
/**
* @param boolean $flag
* @throws PHP_CodeCoverage_Exception
*/
public function setMapTestClassNameToCoveredClassName($flag) {
if (!is_bool($flag)) {
throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
}
$this->mapTestClassNameToCoveredClassName = $flag;
}
/**
* @param boolean $flag
* @throws PHP_CodeCoverage_Exception
*/
public function setAddUncoveredFilesFromWhitelist($flag) {
if (!is_bool($flag)) {
throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
}
$this->addUncoveredFilesFromWhitelist = $flag;
}
/**
* @param boolean $flag
* @throws PHP_CodeCoverage_Exception
*/
public function setProcessUncoveredFilesFromWhitelist($flag) {
if (!is_bool($flag)) {
throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(1, 'boolean');
}
$this->processUncoveredFilesFromWhitelist = $flag;
}
/**
* Applies the @covers annotation filtering.
*
* @param array $data
* @param mixed $id
*/
protected function applyCoversAnnotationFilter(&$data, $id) {
if ($id instanceof PHPUnit_Framework_TestCase) {
$testClassName = get_class($id);
$linesToBeCovered = $this
->getLinesToBeCovered($testClassName, $id
->getName());
if ($linesToBeCovered === FALSE) {
$data = array();
return;
}
if ($this->mapTestClassNameToCoveredClassName && empty($linesToBeCovered)) {
$testedClass = substr($testClassName, 0, -4);
if (class_exists($testedClass)) {
$class = new ReflectionClass($testedClass);
$linesToBeCovered = array(
$class
->getFileName() => range($class
->getStartLine(), $class
->getEndLine()),
);
}
}
}
else {
$linesToBeCovered = array();
}
if (!empty($linesToBeCovered)) {
$data = array_intersect_key($data, $linesToBeCovered);
foreach (array_keys($data) as $filename) {
$data[$filename] = array_intersect_key($data[$filename], array_flip($linesToBeCovered[$filename]));
}
}
else {
if ($this->forceCoversAnnotation) {
$data = array();
}
}
}
/**
* Applies the blacklist/whitelist filtering.
*
* @param array $data
*/
protected function applyListsFilter(&$data) {
foreach (array_keys($data) as $filename) {
if ($this->filter
->isFiltered($filename)) {
unset($data[$filename]);
}
}
}
/**
* @since Method available since Release 1.1.0
*/
protected function initializeFilesThatAreSeenTheFirstTime($data) {
foreach ($data as $file => $lines) {
if ($this->filter
->isFile($file) && !isset($this->data[$file])) {
$this->data[$file] = array();
foreach ($lines as $k => $v) {
$this->data[$file][$k] = $v == -2 ? NULL : array();
}
}
}
}
/**
* Processes whitelisted files that are not covered.
*/
protected function addUncoveredFilesFromWhitelist() {
$data = array();
$uncoveredFiles = array_diff($this->filter
->getWhitelist(), array_keys($this->data));
foreach ($uncoveredFiles as $uncoveredFile) {
if (!file_exists($uncoveredFile)) {
continue;
}
if ($this->processUncoveredFilesFromWhitelist) {
$this
->processUncoveredFileFromWhitelist($uncoveredFile, $data, $uncoveredFiles);
}
else {
$data[$uncoveredFile] = array();
$lines = count(file($uncoveredFile));
for ($i = 1; $i <= $lines; $i++) {
$data[$uncoveredFile][$i] = -1;
}
}
}
$this
->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
}
/**
* @param string $uncoveredFile
* @param array $data
* @param array $uncoveredFiles
*/
protected function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, array $uncoveredFiles) {
$this->driver
->start();
include_once $uncoveredFile;
$coverage = $this->driver
->stop();
foreach ($coverage as $file => $fileCoverage) {
if (!isset($data[$file]) && in_array($file, $uncoveredFiles)) {
foreach (array_keys($fileCoverage) as $key) {
if ($fileCoverage[$key] == 1) {
$fileCoverage[$key] = -1;
}
}
$data[$file] = $fileCoverage;
}
}
}
/**
* Returns the files and lines a test method wants to cover.
*
* @param string $className
* @param string $methodName
* @return array
* @since Method available since Release 1.2.0
*/
protected function getLinesToBeCovered($className, $methodName) {
$codeToCoverList = array();
$result = array();
// @codeCoverageIgnoreStart
if (($pos = strpos($methodName, ' ')) !== FALSE) {
$methodName = substr($methodName, 0, $pos);
}
// @codeCoverageIgnoreEnd
$class = new ReflectionClass($className);
try {
$method = new ReflectionMethod($className, $methodName);
} catch (ReflectionException $e) {
return array();
}
$docComment = substr($class
->getDocComment(), 3, -2) . PHP_EOL . substr($method
->getDocComment(), 3, -2);
$templateMethods = array(
'setUp',
'assertPreConditions',
'assertPostConditions',
'tearDown',
);
foreach ($templateMethods as $templateMethod) {
if ($class
->hasMethod($templateMethod)) {
$reflector = $class
->getMethod($templateMethod);
$docComment .= PHP_EOL . substr($reflector
->getDocComment(), 3, -2);
unset($reflector);
}
}
if (strpos($docComment, '@coversNothing') !== FALSE) {
return FALSE;
}
$classShortcut = preg_match_all('(@coversDefaultClass\\s+(?P<coveredClass>[^\\s]++)\\s*$)m', $class
->getDocComment(), $matches);
if ($classShortcut) {
if ($classShortcut > 1) {
throw new PHP_CodeCoverage_Exception(sprintf('More than one @coversClass annotation in class or interface "%s".', $className));
}
$classShortcut = $matches['coveredClass'][0];
}
$match = preg_match_all('(@covers\\s+(?P<coveredElement>[^\\s()]++)[\\s()]*$)m', $docComment, $matches);
if ($match) {
foreach ($matches['coveredElement'] as $coveredElement) {
if ($classShortcut && strncmp($coveredElement, '::', 2) === 0) {
$coveredElement = $classShortcut . $coveredElement;
}
$codeToCoverList = array_merge($codeToCoverList, $this
->resolveCoversToReflectionObjects($coveredElement));
}
foreach ($codeToCoverList as $codeToCover) {
$fileName = $codeToCover
->getFileName();
if (!isset($result[$fileName])) {
$result[$fileName] = array();
}
$result[$fileName] = array_unique(array_merge($result[$fileName], range($codeToCover
->getStartLine(), $codeToCover
->getEndLine())));
}
}
return $result;
}
/**
* @param string $coveredElement
* @return array
* @since Method available since Release 1.2.0
*/
protected function resolveCoversToReflectionObjects($coveredElement) {
$codeToCoverList = array();
if (strpos($coveredElement, '::') !== FALSE) {
list($className, $methodName) = explode('::', $coveredElement);
if (isset($methodName[0]) && $methodName[0] == '<') {
$classes = array(
$className,
);
foreach ($classes as $className) {
if (!class_exists($className) && !interface_exists($className)) {
throw new PHP_CodeCoverage_Exception(sprintf('Trying to @cover not existing class or ' . 'interface "%s".', $className));
}
$class = new ReflectionClass($className);
$methods = $class
->getMethods();
$inverse = isset($methodName[1]) && $methodName[1] == '!';
if (strpos($methodName, 'protected')) {
$visibility = 'isProtected';
}
else {
if (strpos($methodName, 'private')) {
$visibility = 'isPrivate';
}
else {
if (strpos($methodName, 'public')) {
$visibility = 'isPublic';
}
}
}
foreach ($methods as $method) {
if ($inverse && !$method
->{$visibility}()) {
$codeToCoverList[] = $method;
}
else {
if (!$inverse && $method
->{$visibility}()) {
$codeToCoverList[] = $method;
}
}
}
}
}
else {
$classes = array(
$className,
);
foreach ($classes as $className) {
if ($className == '' && function_exists($methodName)) {
$codeToCoverList[] = new ReflectionFunction($methodName);
}
else {
if (!((class_exists($className) || interface_exists($className) || trait_exists($className)) && method_exists($className, $methodName))) {
throw new PHP_CodeCoverage_Exception(sprintf('Trying to @cover not existing method "%s::%s".', $className, $methodName));
}
$codeToCoverList[] = new ReflectionMethod($className, $methodName);
}
}
}
}
else {
$extended = FALSE;
if (strpos($coveredElement, '<extended>') !== FALSE) {
$coveredElement = str_replace('<extended>', '', $coveredElement);
$extended = TRUE;
}
$classes = array(
$coveredElement,
);
if ($extended) {
$classes = array_merge($classes, class_implements($coveredElement), class_parents($coveredElement));
}
foreach ($classes as $className) {
if (!class_exists($className) && !interface_exists($className) && !trait_exists($className)) {
throw new PHP_CodeCoverage_Exception(sprintf('Trying to @cover not existing class or ' . 'interface "%s".', $className));
}
$codeToCoverList[] = new ReflectionClass($className);
}
}
return $codeToCoverList;
}
}
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
PHP_CodeCoverage:: |
protected | property | ||
PHP_CodeCoverage:: |
protected | property | ||
PHP_CodeCoverage:: |
protected | property | ||
PHP_CodeCoverage:: |
protected | property | Code coverage data. | |
PHP_CodeCoverage:: |
protected | property | ||
PHP_CodeCoverage:: |
protected | property | ||
PHP_CodeCoverage:: |
protected | property | ||
PHP_CodeCoverage:: |
protected | property | ||
PHP_CodeCoverage:: |
protected | property | ||
PHP_CodeCoverage:: |
protected | property | Test data. | |
PHP_CodeCoverage:: |
protected | function | Processes whitelisted files that are not covered. | |
PHP_CodeCoverage:: |
public | function | Appends code coverage data. | |
PHP_CodeCoverage:: |
protected | function | Applies the @covers annotation filtering. | |
PHP_CodeCoverage:: |
protected | function | Applies the blacklist/whitelist filtering. | |
PHP_CodeCoverage:: |
public | function | Clears collected code coverage data. | |
PHP_CodeCoverage:: |
public | function | Returns the PHP_CodeCoverage_Filter used. | |
PHP_CodeCoverage:: |
public | function | @since Method available since Release 1.1.0 | |
PHP_CodeCoverage:: |
public | function | Returns the collected code coverage data. | |
PHP_CodeCoverage:: |
protected | function | Returns the files and lines a test method wants to cover. | |
PHP_CodeCoverage:: |
public | function | Returns the PHP_CodeCoverage_Report_Node_* object graph for this PHP_CodeCoverage object. | |
PHP_CodeCoverage:: |
public | function | Returns the test data. | |
PHP_CodeCoverage:: |
protected | function | @since Method available since Release 1.1.0 | |
PHP_CodeCoverage:: |
public | function | Merges the data from another instance of PHP_CodeCoverage. | |
PHP_CodeCoverage:: |
protected | function | ||
PHP_CodeCoverage:: |
protected | function | @since Method available since Release 1.2.0 | |
PHP_CodeCoverage:: |
public | function | ||
PHP_CodeCoverage:: |
public | function | @since Method available since Release 1.1.0 | |
PHP_CodeCoverage:: |
public | function | ||
PHP_CodeCoverage:: |
public | function | ||
PHP_CodeCoverage:: |
public | function | ||
PHP_CodeCoverage:: |
public | function | Start collection of code coverage information. | |
PHP_CodeCoverage:: |
public | function | Stop collection of code coverage information. | |
PHP_CodeCoverage:: |
public | function | Constructor. |