. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * * Neither the name of Sebastian Bergmann nor the names of his * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * @category PHP * @package CodeCoverage * @author Sebastian Bergmann * @copyright 2009-2011 Sebastian Bergmann * @license http://www.opensource.org/licenses/bsd-license.php BSD License * @link http://github.com/sebastianbergmann/php-code-coverage * @since File available since Release 1.0.0 */ require_once 'PHP/CodeCoverage/Driver/Xdebug.php'; require_once 'PHP/CodeCoverage/Filter.php'; require_once 'PHP/CodeCoverage/Util.php'; /** * Provides collection functionality for PHP code coverage information. * * @category PHP * @package CodeCoverage * @author Sebastian Bergmann * @copyright 2009-2011 Sebastian Bergmann * @license http://www.opensource.org/licenses/bsd-license.php BSD License * @version Release: 1.0.4 * @link http://github.com/sebastianbergmann/php-code-coverage * @since Class available since Release 1.0.0 */ class PHP_CodeCoverage { /** * @var PHP_CodeCoverage_Driver */ protected $driver; /** * @var PHP_CodeCoverage_Filter */ protected $filter; /** * @var boolean */ protected $forceCoversAnnotation = FALSE; /** * @var boolean */ protected $mapTestClassNameToCoveredClassName = FALSE; /** * @var boolean */ protected $processUncoveredFilesFromWhitelist = TRUE; /** * @var mixed */ protected $currentId; /** * List of covered files. * * @var array */ protected $coveredFiles = array(); /** * Raw code coverage data. * * @var array */ protected $data = array(); /** * Summarized code coverage data. * * @var array */ protected $summary = array(); /** * Test data. * * @var array */ protected $tests = array(); /** * @var boolean */ protected $isCodeCoverageTestSuite = FALSE; /** * @var boolean */ protected $isFileIteratorTestSuite = FALSE; /** * @var boolean */ protected $isTimerTestSuite = FALSE; /** * @var boolean */ protected $isTokenStreamTestSuite = FALSE; /** * Default PHP_CodeCoverage object. * * @var PHP_CodeCoverage */ protected static $instance; /** * Constructor. * * @param PHP_CodeCoverage_Driver $driver * @param PHP_CodeCoverage_Filter $filter * @throws InvalidArgumentException */ 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 = PHP_CodeCoverage_Filter::getInstance(); } $this->driver = $driver; $this->filter = $filter; if (defined('PHP_CODECOVERAGE_TESTSUITE')) { $this->isCodeCoverageTestSuite = TRUE; } if (defined('FILE_ITERATOR_TESTSUITE')) { $this->isFileIteratorTestSuite = TRUE; } if (defined('PHP_TIMER_TESTSUITE')) { $this->isTimerTestSuite = TRUE; } if (defined('PHP_TOKENSTREAM_TESTSUITE')) { $this->isTokenStreamTestSuite = TRUE; } } /** * Returns the default instance. * * @return PHP_CodeCoverage */ public static function getInstance() { if (self::$instance === NULL) { // @codeCoverageIgnoreStart self::$instance = new PHP_CodeCoverage; } // @codeCoverageIgnoreEnd return self::$instance; } /** * Start collection of code coverage information. * * @param mixed $id * @param boolean $clear * @throws InvalidArgumentException */ public function start($id, $clear = FALSE) { if (!is_bool($clear)) { throw new InvalidArgumentException; } if ($clear) { $this->clear(); } $this->currentId = $id; $this->driver->start(); } /** * Stop collection of code coverage information. * * @param boolean $append * @return array * @throws InvalidArgumentException */ public function stop($append = TRUE) { if (!is_bool($append)) { throw new InvalidArgumentException; } $data = $this->driver->stop(); if ($append) { $this->append($data); } $this->currentId = NULL; return $data; } /** * Appends code coverage data. * * @param array $data * @param mixed $id * @param array $filterGroups */ public function append(array $data, $id = NULL, array $filterGroups = array('DEFAULT')) { if ($id === NULL) { $id = $this->currentId; } if ($id === NULL) { throw new InvalidArgumentException; } $this->applySelfFilter($data); $this->applyListsFilter($data, $filterGroups); $raw = $data; $this->applyCoversAnnotationFilter($data, $id); if (!empty($data)) { if ($id instanceof PHPUnit_Framework_TestCase) { $status = $id->getStatus(); $id = get_class($id) . '::' . $id->getName(); $this->tests[$id] = $status; } else if ($id instanceof PHPUnit_Extensions_PhptTestCase) { $id = $id->getName(); } $this->coveredFiles = array_unique( array_merge($this->coveredFiles, array_keys($data)) ); $this->data[$id] = array('filtered' => $data, 'raw' => $raw); $this->summary = array(); } } /** * Merges the data from another instance of PHP_CodeCoverage. * * @param PHP_CodeCoverage $that */ public function merge(PHP_CodeCoverage $that) { foreach ($that->data as $id => $data) { if (!isset($this->data[$id])) { $this->data[$id] = $data; } else { foreach (array('filtered', 'raw') as $type) { foreach ($data[$type] as $file => $lines) { if (!isset($this->data[$id][$type][$file])) { $this->data[$id][$type][$file] = $lines; } else { foreach ($lines as $line => $flag) { if (!isset($this->data[$id][$type][$file][$line]) || $flag > $this->data[$id][$type][$file][$line]) { $this->data[$id][$type][$file][$line] = $flag; } } } } } } } foreach ($that->tests as $id => $status) { if (!isset($this->tests[$id]) || $status > $this->tests[$id]) { $this->tests[$id] = $status; } } $this->coveredFiles = array_unique( array_merge($this->coveredFiles, $that->coveredFiles) ); $this->summary = array(); } /** * Returns summarized code coverage data. * * Format of the result array: * * * array( * "/tested/code.php" => array( * linenumber => array(tests that executed the line) * ) * ) * * * @return array */ public function getSummary() { if (empty($this->summary)) { if ($this->processUncoveredFilesFromWhitelist) { $this->processUncoveredFilesFromWhitelist(); } foreach ($this->data as $test => $coverage) { foreach ($coverage['filtered'] as $file => $lines) { foreach ($lines as $line => $flag) { if ($flag == 1) { if (!isset($this->summary[$file][$line][0])) { $this->summary[$file][$line] = array(); } if (isset($this->tests[$test])) { $status = $this->tests[$test]; } else { $status = NULL; } $this->summary[$file][$line][] = array( 'id' => $test, 'status' => $status ); } } } foreach ($coverage['raw'] as $file => $lines) { foreach ($lines as $line => $flag) { if ($flag != 1 && !isset($this->summary[$file][$line][0])) { $this->summary[$file][$line] = $flag; } } } } foreach ($this->summary as &$file) { ksort($file); } ksort($this->summary); } return $this->summary; } /** * Clears collected code coverage data. */ public function clear() { $this->data = array(); $this->coveredFiles = array(); $this->summary = array(); $this->currentId = NULL; } /** * Returns the PHP_CodeCoverage_Filter used. * * @return PHP_CodeCoverage_Filter */ public function filter() { return $this->filter; } /** * @param boolean $flag * @throws InvalidArgumentException */ public function setForceCoversAnnotation($flag) { if (!is_bool($flag)) { throw new InvalidArgumentException; } $this->forceCoversAnnotation = $flag; } /** * @param boolean $flag * @throws InvalidArgumentException */ public function setMapTestClassNameToCoveredClassName($flag) { if (!is_bool($flag)) { throw new InvalidArgumentException; } $this->mapTestClassNameToCoveredClassName = $flag; } /** * @param boolean $flag * @throws InvalidArgumentException */ public function setProcessUncoveredFilesFromWhitelist($flag) { if (!is_bool($flag)) { throw new InvalidArgumentException; } $this->processUncoveredFilesFromWhitelist = $flag; } /** * Filters sourcecode files from PHP_CodeCoverage, PHP_TokenStream, * Text_Template, and File_Iterator. * * @param array $data */ protected function applySelfFilter(&$data) { foreach (array_keys($data) as $filename) { if (!$this->filter->isFile($filename)) { unset($data[$filename]); continue; } if (!$this->isCodeCoverageTestSuite && strpos($filename, dirname(__FILE__)) === 0) { unset($data[$filename]); continue; } if (!$this->isFileIteratorTestSuite && (substr($filename, -17) == 'File/Iterator.php' || substr($filename, -25) == 'File/Iterator/Factory.php')) { unset($data[$filename]); continue; } if (!$this->isTimerTestSuite && (substr($filename, -13) == 'PHP/Timer.php')) { unset($data[$filename]); continue; } if (!$this->isTokenStreamTestSuite && (substr($filename, -13) == 'PHP/Token.php' || substr($filename, -20) == 'PHP/Token/Stream.php' || substr($filename, -35) == 'PHP/Token/Stream/CachingFactory.php')) { unset($data[$filename]); continue; } if (substr($filename, -17) == 'Text/Template.php') { unset($data[$filename]); } } } /** * Applies the blacklist/whitelist filtering. * * @param array $data * @param array $filterGroups */ protected function applyListsFilter(&$data, $filterGroups) { foreach (array_keys($data) as $filename) { if ($this->filter->isFiltered($filename, $filterGroups)) { unset($data[$filename]); } } } /** * 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 = PHP_CodeCoverage_Util::getLinesToBeCovered( $testClassName, $id->getName() ); 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(); } } /** * Processes whitelisted files that are not covered. */ protected function processUncoveredFilesFromWhitelist() { $data = array(); $includedFiles = array_flip(get_included_files()); $uncoveredFiles = array_diff( $this->filter->getWhitelist(), $this->coveredFiles ); foreach ($uncoveredFiles as $uncoveredFile) { if (isset($includedFiles[$uncoveredFile])) { foreach (array_keys($this->data) as $test) { if (isset($this->data[$test]['raw'][$uncoveredFile])) { $coverage = $this->data[$test]['raw'][$uncoveredFile]; foreach (array_keys($coverage) as $key) { if ($coverage[$key] == 1) { $coverage[$key] = -1; } } $data[$uncoveredFile] = $coverage; break; } } } else { $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; $includedFiles[$file] = TRUE; } } } } $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); } }