5 * Copyright (c) 2009-2011, Sebastian Bergmann <sb@sebastian-bergmann.de>.
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
12 * * Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
15 * * Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in
17 * the documentation and/or other materials provided with the
20 * * Neither the name of Sebastian Bergmann nor the names of his
21 * contributors may be used to endorse or promote products derived
22 * from this software without specific prior written permission.
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
27 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
28 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
29 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
30 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
33 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
34 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35 * POSSIBILITY OF SUCH DAMAGE.
38 * @package CodeCoverage
39 * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
40 * @copyright 2009-2011 Sebastian Bergmann <sb@sebastian-bergmann.de>
41 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
42 * @link http://github.com/sebastianbergmann/php-code-coverage
43 * @since File available since Release 1.0.0
46 require_once 'PHP/CodeCoverage/Driver/Xdebug.php';
47 require_once 'PHP/CodeCoverage/Filter.php';
48 require_once 'PHP/CodeCoverage/Util.php';
51 * Provides collection functionality for PHP code coverage information.
54 * @package CodeCoverage
55 * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
56 * @copyright 2009-2011 Sebastian Bergmann <sb@sebastian-bergmann.de>
57 * @license http://www.opensource.org/licenses/bsd-license.php BSD License
58 * @version Release: 1.0.4
59 * @link http://github.com/sebastianbergmann/php-code-coverage
60 * @since Class available since Release 1.0.0
62 class PHP_CodeCoverage
65 * @var PHP_CodeCoverage_Driver
70 * @var PHP_CodeCoverage_Filter
77 protected $forceCoversAnnotation = FALSE;
82 protected $mapTestClassNameToCoveredClassName = FALSE;
87 protected $processUncoveredFilesFromWhitelist = TRUE;
95 * List of covered files.
99 protected $coveredFiles = array();
102 * Raw code coverage data.
106 protected $data = array();
109 * Summarized code coverage data.
113 protected $summary = array();
120 protected $tests = array();
125 protected $isCodeCoverageTestSuite = FALSE;
130 protected $isFileIteratorTestSuite = FALSE;
135 protected $isTimerTestSuite = FALSE;
140 protected $isTokenStreamTestSuite = FALSE;
143 * Default PHP_CodeCoverage object.
145 * @var PHP_CodeCoverage
147 protected static $instance;
152 * @param PHP_CodeCoverage_Driver $driver
153 * @param PHP_CodeCoverage_Filter $filter
154 * @throws InvalidArgumentException
156 public function __construct(PHP_CodeCoverage_Driver $driver = NULL, PHP_CodeCoverage_Filter $filter = NULL)
158 if ($driver === NULL) {
159 $driver = new PHP_CodeCoverage_Driver_Xdebug;
162 if ($filter === NULL) {
163 $filter = PHP_CodeCoverage_Filter::getInstance();
166 $this->driver = $driver;
167 $this->filter = $filter;
169 if (defined('PHP_CODECOVERAGE_TESTSUITE')) {
170 $this->isCodeCoverageTestSuite = TRUE;
173 if (defined('FILE_ITERATOR_TESTSUITE')) {
174 $this->isFileIteratorTestSuite = TRUE;
177 if (defined('PHP_TIMER_TESTSUITE')) {
178 $this->isTimerTestSuite = TRUE;
181 if (defined('PHP_TOKENSTREAM_TESTSUITE')) {
182 $this->isTokenStreamTestSuite = TRUE;
187 * Returns the default instance.
189 * @return PHP_CodeCoverage
191 public static function getInstance()
193 if (self::$instance === NULL) {
194 // @codeCoverageIgnoreStart
195 self::$instance = new PHP_CodeCoverage;
197 // @codeCoverageIgnoreEnd
198 return self::$instance;
202 * Start collection of code coverage information.
205 * @param boolean $clear
206 * @throws InvalidArgumentException
208 public function start($id, $clear = FALSE)
210 if (!is_bool($clear)) {
211 throw new InvalidArgumentException;
218 $this->currentId = $id;
220 $this->driver->start();
224 * Stop collection of code coverage information.
226 * @param boolean $append
228 * @throws InvalidArgumentException
230 public function stop($append = TRUE)
232 if (!is_bool($append)) {
233 throw new InvalidArgumentException;
236 $data = $this->driver->stop();
239 $this->append($data);
242 $this->currentId = NULL;
248 * Appends code coverage data.
252 * @param array $filterGroups
254 public function append(array $data, $id = NULL, array $filterGroups = array('DEFAULT'))
257 $id = $this->currentId;
261 throw new InvalidArgumentException;
264 $this->applySelfFilter($data);
265 $this->applyListsFilter($data, $filterGroups);
267 $this->applyCoversAnnotationFilter($data, $id);
270 if ($id instanceof PHPUnit_Framework_TestCase) {
271 $status = $id->getStatus();
272 $id = get_class($id) . '::' . $id->getName();
273 $this->tests[$id] = $status;
276 else if ($id instanceof PHPUnit_Extensions_PhptTestCase) {
277 $id = $id->getName();
280 $this->coveredFiles = array_unique(
281 array_merge($this->coveredFiles, array_keys($data))
284 $this->data[$id] = array('filtered' => $data, 'raw' => $raw);
285 $this->summary = array();
290 * Merges the data from another instance of PHP_CodeCoverage.
292 * @param PHP_CodeCoverage $that
294 public function merge(PHP_CodeCoverage $that)
296 foreach ($that->data as $id => $data) {
297 if (!isset($this->data[$id])) {
298 $this->data[$id] = $data;
300 foreach (array('filtered', 'raw') as $type) {
301 foreach ($data[$type] as $file => $lines) {
302 if (!isset($this->data[$id][$type][$file])) {
303 $this->data[$id][$type][$file] = $lines;
305 foreach ($lines as $line => $flag) {
306 if (!isset($this->data[$id][$type][$file][$line]) ||
307 $flag > $this->data[$id][$type][$file][$line]) {
308 $this->data[$id][$type][$file][$line] = $flag;
317 foreach ($that->tests as $id => $status) {
318 if (!isset($this->tests[$id]) || $status > $this->tests[$id]) {
319 $this->tests[$id] = $status;
323 $this->coveredFiles = array_unique(
324 array_merge($this->coveredFiles, $that->coveredFiles)
327 $this->summary = array();
331 * Returns summarized code coverage data.
333 * Format of the result array:
337 * "/tested/code.php" => array(
338 * linenumber => array(tests that executed the line)
345 public function getSummary()
347 if (empty($this->summary)) {
348 if ($this->processUncoveredFilesFromWhitelist) {
349 $this->processUncoveredFilesFromWhitelist();
352 foreach ($this->data as $test => $coverage) {
353 foreach ($coverage['filtered'] as $file => $lines) {
354 foreach ($lines as $line => $flag) {
356 if (!isset($this->summary[$file][$line][0])) {
357 $this->summary[$file][$line] = array();
360 if (isset($this->tests[$test])) {
361 $status = $this->tests[$test];
366 $this->summary[$file][$line][] = array(
367 'id' => $test, 'status' => $status
373 foreach ($coverage['raw'] as $file => $lines) {
374 foreach ($lines as $line => $flag) {
376 !isset($this->summary[$file][$line][0])) {
377 $this->summary[$file][$line] = $flag;
383 foreach ($this->summary as &$file) {
387 ksort($this->summary);
390 return $this->summary;
394 * Clears collected code coverage data.
396 public function clear()
398 $this->data = array();
399 $this->coveredFiles = array();
400 $this->summary = array();
401 $this->currentId = NULL;
405 * Returns the PHP_CodeCoverage_Filter used.
407 * @return PHP_CodeCoverage_Filter
409 public function filter()
411 return $this->filter;
415 * @param boolean $flag
416 * @throws InvalidArgumentException
418 public function setForceCoversAnnotation($flag)
420 if (!is_bool($flag)) {
421 throw new InvalidArgumentException;
424 $this->forceCoversAnnotation = $flag;
428 * @param boolean $flag
429 * @throws InvalidArgumentException
431 public function setMapTestClassNameToCoveredClassName($flag)
433 if (!is_bool($flag)) {
434 throw new InvalidArgumentException;
437 $this->mapTestClassNameToCoveredClassName = $flag;
441 * @param boolean $flag
442 * @throws InvalidArgumentException
444 public function setProcessUncoveredFilesFromWhitelist($flag)
446 if (!is_bool($flag)) {
447 throw new InvalidArgumentException;
450 $this->processUncoveredFilesFromWhitelist = $flag;
454 * Filters sourcecode files from PHP_CodeCoverage, PHP_TokenStream,
455 * Text_Template, and File_Iterator.
459 protected function applySelfFilter(&$data)
461 foreach (array_keys($data) as $filename) {
462 if (!$this->filter->isFile($filename)) {
463 unset($data[$filename]);
467 if (!$this->isCodeCoverageTestSuite &&
468 strpos($filename, dirname(__FILE__)) === 0) {
469 unset($data[$filename]);
473 if (!$this->isFileIteratorTestSuite &&
474 (substr($filename, -17) == 'File/Iterator.php' ||
475 substr($filename, -25) == 'File/Iterator/Factory.php')) {
476 unset($data[$filename]);
480 if (!$this->isTimerTestSuite &&
481 (substr($filename, -13) == 'PHP/Timer.php')) {
482 unset($data[$filename]);
486 if (!$this->isTokenStreamTestSuite &&
487 (substr($filename, -13) == 'PHP/Token.php' ||
488 substr($filename, -20) == 'PHP/Token/Stream.php' ||
489 substr($filename, -35) == 'PHP/Token/Stream/CachingFactory.php')) {
490 unset($data[$filename]);
494 if (substr($filename, -17) == 'Text/Template.php') {
495 unset($data[$filename]);
501 * Applies the blacklist/whitelist filtering.
504 * @param array $filterGroups
506 protected function applyListsFilter(&$data, $filterGroups)
508 foreach (array_keys($data) as $filename) {
509 if ($this->filter->isFiltered($filename, $filterGroups)) {
510 unset($data[$filename]);
516 * Applies the @covers annotation filtering.
521 protected function applyCoversAnnotationFilter(&$data, $id)
523 if ($id instanceof PHPUnit_Framework_TestCase) {
524 $testClassName = get_class($id);
525 $linesToBeCovered = PHP_CodeCoverage_Util::getLinesToBeCovered(
526 $testClassName, $id->getName()
529 if ($this->mapTestClassNameToCoveredClassName &&
530 empty($linesToBeCovered)) {
531 $testedClass = substr($testClassName, 0, -4);
533 if (class_exists($testedClass)) {
534 $class = new ReflectionClass($testedClass);
536 $linesToBeCovered = array(
537 $class->getFileName() => range(
538 $class->getStartLine(), $class->getEndLine()
544 $linesToBeCovered = array();
547 if (!empty($linesToBeCovered)) {
548 $data = array_intersect_key($data, $linesToBeCovered);
550 foreach (array_keys($data) as $filename) {
551 $data[$filename] = array_intersect_key(
552 $data[$filename], array_flip($linesToBeCovered[$filename])
557 else if ($this->forceCoversAnnotation) {
563 * Processes whitelisted files that are not covered.
565 protected function processUncoveredFilesFromWhitelist()
568 $includedFiles = array_flip(get_included_files());
569 $uncoveredFiles = array_diff(
570 $this->filter->getWhitelist(), $this->coveredFiles
573 foreach ($uncoveredFiles as $uncoveredFile) {
574 if (isset($includedFiles[$uncoveredFile])) {
575 foreach (array_keys($this->data) as $test) {
576 if (isset($this->data[$test]['raw'][$uncoveredFile])) {
577 $coverage = $this->data[$test]['raw'][$uncoveredFile];
579 foreach (array_keys($coverage) as $key) {
580 if ($coverage[$key] == 1) {
581 $coverage[$key] = -1;
585 $data[$uncoveredFile] = $coverage;
591 $this->driver->start();
592 include_once $uncoveredFile;
593 $coverage = $this->driver->stop();
595 foreach ($coverage as $file => $fileCoverage) {
596 if (!isset($data[$file]) &&
597 in_array($file, $uncoveredFiles)) {
598 foreach (array_keys($fileCoverage) as $key) {
599 if ($fileCoverage[$key] == 1) {
600 $fileCoverage[$key] = -1;
604 $data[$file] = $fileCoverage;
605 $includedFiles[$file] = TRUE;
611 $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');