3 * This file is part of the PHP_CodeCoverage package.
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
11 use SebastianBergmann\Environment\Runtime;
14 * Provides collection functionality for PHP code coverage information.
16 * @since Class available since Release 1.0.0
18 class PHP_CodeCoverage
21 * @var PHP_CodeCoverage_Driver
26 * @var PHP_CodeCoverage_Filter
33 private $cacheTokens = false;
38 private $checkForUnintentionallyCoveredCode = false;
43 private $forceCoversAnnotation = false;
48 private $mapTestClassNameToCoveredClassName = false;
53 private $addUncoveredFilesFromWhitelist = true;
58 private $processUncoveredFilesFromWhitelist = false;
70 private $data = array();
75 private $ignoredLines = array();
80 private $disableIgnoredLines = false;
87 private $tests = array();
92 * @param PHP_CodeCoverage_Driver $driver
93 * @param PHP_CodeCoverage_Filter $filter
94 * @throws PHP_CodeCoverage_Exception
96 public function __construct(PHP_CodeCoverage_Driver $driver = null, PHP_CodeCoverage_Filter $filter = null)
98 if ($driver === null) {
99 $driver = $this->selectDriver();
102 if ($filter === null) {
103 $filter = new PHP_CodeCoverage_Filter;
106 $this->driver = $driver;
107 $this->filter = $filter;
111 * Returns the PHP_CodeCoverage_Report_Node_* object graph
112 * for this PHP_CodeCoverage object.
114 * @return PHP_CodeCoverage_Report_Node_Directory
115 * @since Method available since Release 1.1.0
117 public function getReport()
119 $factory = new PHP_CodeCoverage_Report_Factory;
121 return $factory->create($this);
125 * Clears collected code coverage data.
127 public function clear()
129 $this->currentId = null;
130 $this->data = array();
131 $this->tests = array();
135 * Returns the PHP_CodeCoverage_Filter used.
137 * @return PHP_CodeCoverage_Filter
139 public function filter()
141 return $this->filter;
145 * Returns the collected code coverage data.
146 * Set $raw = true to bypass all filters.
150 * @since Method available since Release 1.1.0
152 public function getData($raw = false)
154 if (!$raw && $this->addUncoveredFilesFromWhitelist) {
155 $this->addUncoveredFilesFromWhitelist();
158 // We need to apply the blacklist filter a second time
159 // when no whitelist is used.
160 if (!$raw && !$this->filter->hasWhitelist()) {
161 $this->applyListsFilter($this->data);
168 * Sets the coverage data.
171 * @since Method available since Release 2.0.0
173 public function setData(array $data)
179 * Returns the test data.
182 * @since Method available since Release 1.1.0
184 public function getTests()
190 * Sets the test data.
192 * @param array $tests
193 * @since Method available since Release 2.0.0
195 public function setTests(array $tests)
197 $this->tests = $tests;
201 * Start collection of code coverage information.
205 * @throws PHP_CodeCoverage_Exception
207 public function start($id, $clear = false)
209 if (!is_bool($clear)) {
210 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
220 $this->currentId = $id;
222 $this->driver->start();
226 * Stop collection of code coverage information.
228 * @param bool $append
229 * @param mixed $linesToBeCovered
230 * @param array $linesToBeUsed
232 * @throws PHP_CodeCoverage_Exception
234 public function stop($append = true, $linesToBeCovered = array(), array $linesToBeUsed = array())
236 if (!is_bool($append)) {
237 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
243 if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {
244 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
250 $data = $this->driver->stop();
251 $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);
253 $this->currentId = null;
259 * Appends code coverage data.
263 * @param bool $append
264 * @param mixed $linesToBeCovered
265 * @param array $linesToBeUsed
266 * @throws PHP_CodeCoverage_Exception
268 public function append(array $data, $id = null, $append = true, $linesToBeCovered = array(), array $linesToBeUsed = array())
271 $id = $this->currentId;
275 throw new PHP_CodeCoverage_Exception;
278 $this->applyListsFilter($data);
279 $this->applyIgnoredLinesFilter($data);
280 $this->initializeFilesThatAreSeenTheFirstTime($data);
286 if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') {
287 $this->applyCoversAnnotationFilter(
301 if ($id instanceof PHPUnit_Framework_TestCase) {
302 $_size = $id->getSize();
304 if ($_size == PHPUnit_Util_Test::SMALL) {
306 } elseif ($_size == PHPUnit_Util_Test::MEDIUM) {
308 } elseif ($_size == PHPUnit_Util_Test::LARGE) {
312 $status = $id->getStatus();
313 $id = get_class($id) . '::' . $id->getName();
314 } elseif ($id instanceof PHPUnit_Extensions_PhptTestCase) {
316 $id = $id->getName();
319 $this->tests[$id] = array('size' => $size, 'status' => $status);
321 foreach ($data as $file => $lines) {
322 if (!$this->filter->isFile($file)) {
326 foreach ($lines as $k => $v) {
327 if ($v == PHP_CodeCoverage_Driver::LINE_EXECUTED) {
328 if (empty($this->data[$file][$k]) || !in_array($id, $this->data[$file][$k])) {
329 $this->data[$file][$k][] = $id;
337 * Merges the data from another instance of PHP_CodeCoverage.
339 * @param PHP_CodeCoverage $that
341 public function merge(PHP_CodeCoverage $that)
343 $this->filter->setBlacklistedFiles(
344 array_merge($this->filter->getBlacklistedFiles(), $that->filter()->getBlacklistedFiles())
347 $this->filter->setWhitelistedFiles(
348 array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles())
351 foreach ($that->data as $file => $lines) {
352 if (!isset($this->data[$file])) {
353 if (!$this->filter->isFiltered($file)) {
354 $this->data[$file] = $lines;
360 foreach ($lines as $line => $data) {
361 if ($data !== null) {
362 if (!isset($this->data[$file][$line])) {
363 $this->data[$file][$line] = $data;
365 $this->data[$file][$line] = array_unique(
366 array_merge($this->data[$file][$line], $data)
373 $this->tests = array_merge($this->tests, $that->getTests());
379 * @throws PHP_CodeCoverage_Exception
380 * @since Method available since Release 1.1.0
382 public function setCacheTokens($flag)
384 if (!is_bool($flag)) {
385 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
391 $this->cacheTokens = $flag;
395 * @since Method available since Release 1.1.0
397 public function getCacheTokens()
399 return $this->cacheTokens;
404 * @throws PHP_CodeCoverage_Exception
405 * @since Method available since Release 2.0.0
407 public function setCheckForUnintentionallyCoveredCode($flag)
409 if (!is_bool($flag)) {
410 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
416 $this->checkForUnintentionallyCoveredCode = $flag;
421 * @throws PHP_CodeCoverage_Exception
423 public function setForceCoversAnnotation($flag)
425 if (!is_bool($flag)) {
426 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
432 $this->forceCoversAnnotation = $flag;
437 * @throws PHP_CodeCoverage_Exception
439 public function setMapTestClassNameToCoveredClassName($flag)
441 if (!is_bool($flag)) {
442 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
448 $this->mapTestClassNameToCoveredClassName = $flag;
453 * @throws PHP_CodeCoverage_Exception
455 public function setAddUncoveredFilesFromWhitelist($flag)
457 if (!is_bool($flag)) {
458 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
464 $this->addUncoveredFilesFromWhitelist = $flag;
469 * @throws PHP_CodeCoverage_Exception
471 public function setProcessUncoveredFilesFromWhitelist($flag)
473 if (!is_bool($flag)) {
474 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
480 $this->processUncoveredFilesFromWhitelist = $flag;
485 * @throws PHP_CodeCoverage_Exception
487 public function setDisableIgnoredLines($flag)
489 if (!is_bool($flag)) {
490 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
496 $this->disableIgnoredLines = $flag;
500 * Applies the @covers annotation filtering.
503 * @param mixed $linesToBeCovered
504 * @param array $linesToBeUsed
505 * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode
507 private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed)
509 if ($linesToBeCovered === false ||
510 ($this->forceCoversAnnotation && empty($linesToBeCovered))) {
516 if (empty($linesToBeCovered)) {
520 if ($this->checkForUnintentionallyCoveredCode) {
521 $this->performUnintentionallyCoveredCodeCheck(
528 $data = array_intersect_key($data, $linesToBeCovered);
530 foreach (array_keys($data) as $filename) {
531 $_linesToBeCovered = array_flip($linesToBeCovered[$filename]);
533 $data[$filename] = array_intersect_key(
541 * Applies the blacklist/whitelist filtering.
545 private function applyListsFilter(array &$data)
547 foreach (array_keys($data) as $filename) {
548 if ($this->filter->isFiltered($filename)) {
549 unset($data[$filename]);
555 * Applies the "ignored lines" filtering.
559 private function applyIgnoredLinesFilter(array &$data)
561 foreach (array_keys($data) as $filename) {
562 if (!$this->filter->isFile($filename)) {
566 foreach ($this->getLinesToBeIgnored($filename) as $line) {
567 unset($data[$filename][$line]);
574 * @since Method available since Release 1.1.0
576 private function initializeFilesThatAreSeenTheFirstTime(array $data)
578 foreach ($data as $file => $lines) {
579 if ($this->filter->isFile($file) && !isset($this->data[$file])) {
580 $this->data[$file] = array();
582 foreach ($lines as $k => $v) {
583 $this->data[$file][$k] = $v == -2 ? null : array();
590 * Processes whitelisted files that are not covered.
592 private function addUncoveredFilesFromWhitelist()
595 $uncoveredFiles = array_diff(
596 $this->filter->getWhitelist(),
597 array_keys($this->data)
600 foreach ($uncoveredFiles as $uncoveredFile) {
601 if (!file_exists($uncoveredFile)) {
605 if ($this->processUncoveredFilesFromWhitelist) {
606 $this->processUncoveredFileFromWhitelist(
612 $data[$uncoveredFile] = array();
614 $lines = count(file($uncoveredFile));
616 for ($i = 1; $i <= $lines; $i++) {
617 $data[$uncoveredFile][$i] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED;
622 $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST');
626 * @param string $uncoveredFile
628 * @param array $uncoveredFiles
630 private function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, array $uncoveredFiles)
632 $this->driver->start();
633 include_once $uncoveredFile;
634 $coverage = $this->driver->stop();
636 foreach ($coverage as $file => $fileCoverage) {
637 if (!isset($data[$file]) &&
638 in_array($file, $uncoveredFiles)) {
639 foreach (array_keys($fileCoverage) as $key) {
640 if ($fileCoverage[$key] == PHP_CodeCoverage_Driver::LINE_EXECUTED) {
641 $fileCoverage[$key] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED;
645 $data[$file] = $fileCoverage;
651 * Returns the lines of a source file that should be ignored.
653 * @param string $filename
655 * @throws PHP_CodeCoverage_Exception
656 * @since Method available since Release 2.0.0
658 private function getLinesToBeIgnored($filename)
660 if (!is_string($filename)) {
661 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory(
667 if (!isset($this->ignoredLines[$filename])) {
668 $this->ignoredLines[$filename] = array();
670 if ($this->disableIgnoredLines) {
671 return $this->ignoredLines[$filename];
676 $lines = file($filename);
677 $numLines = count($lines);
679 foreach ($lines as $index => $line) {
681 $this->ignoredLines[$filename][] = $index + 1;
685 if ($this->cacheTokens) {
686 $tokens = PHP_Token_Stream_CachingFactory::get($filename);
688 $tokens = new PHP_Token_Stream($filename);
691 $classes = array_merge($tokens->getClasses(), $tokens->getTraits());
692 $tokens = $tokens->tokens();
694 foreach ($tokens as $token) {
695 switch (get_class($token)) {
696 case 'PHP_Token_COMMENT':
697 case 'PHP_Token_DOC_COMMENT':
698 $_token = trim($token);
699 $_line = trim($lines[$token->getLine() - 1]);
701 if ($_token == '// @codeCoverageIgnore' ||
702 $_token == '//@codeCoverageIgnore') {
705 } elseif ($_token == '// @codeCoverageIgnoreStart' ||
706 $_token == '//@codeCoverageIgnoreStart') {
708 } elseif ($_token == '// @codeCoverageIgnoreEnd' ||
709 $_token == '//@codeCoverageIgnoreEnd') {
714 $start = $token->getLine();
715 $end = $start + substr_count($token, "\n");
717 // Do not ignore the first line when there is a token
718 // before the comment
719 if (0 !== strpos($_token, $_line)) {
723 for ($i = $start; $i < $end; $i++) {
724 $this->ignoredLines[$filename][] = $i;
727 // A DOC_COMMENT token or a COMMENT token starting with "/*"
728 // does not contain the final \n character in its text
729 if (isset($lines[$i-1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i-1]), -2)) {
730 $this->ignoredLines[$filename][] = $i;
735 case 'PHP_Token_INTERFACE':
736 case 'PHP_Token_TRAIT':
737 case 'PHP_Token_CLASS':
738 case 'PHP_Token_FUNCTION':
739 $docblock = $token->getDocblock();
741 $this->ignoredLines[$filename][] = $token->getLine();
743 if (strpos($docblock, '@codeCoverageIgnore') || strpos($docblock, '@deprecated')) {
744 $endLine = $token->getEndLine();
746 for ($i = $token->getLine(); $i <= $endLine; $i++) {
747 $this->ignoredLines[$filename][] = $i;
749 } elseif ($token instanceof PHP_Token_INTERFACE ||
750 $token instanceof PHP_Token_TRAIT ||
751 $token instanceof PHP_Token_CLASS) {
752 if (empty($classes[$token->getName()]['methods'])) {
753 for ($i = $token->getLine();
754 $i <= $token->getEndLine();
756 $this->ignoredLines[$filename][] = $i;
759 $firstMethod = array_shift(
760 $classes[$token->getName()]['methods']
764 $lastMethod = array_pop(
765 $classes[$token->getName()]['methods']
767 } while ($lastMethod !== null &&
768 substr($lastMethod['signature'], 0, 18) == 'anonymous function');
770 if ($lastMethod === null) {
771 $lastMethod = $firstMethod;
774 for ($i = $token->getLine();
775 $i < $firstMethod['startLine'];
777 $this->ignoredLines[$filename][] = $i;
780 for ($i = $token->getEndLine();
781 $i > $lastMethod['endLine'];
783 $this->ignoredLines[$filename][] = $i;
789 case 'PHP_Token_NAMESPACE':
790 $this->ignoredLines[$filename][] = $token->getEndLine();
792 // Intentional fallthrough
793 case 'PHP_Token_OPEN_TAG':
794 case 'PHP_Token_CLOSE_TAG':
795 case 'PHP_Token_USE':
796 $this->ignoredLines[$filename][] = $token->getLine();
801 $this->ignoredLines[$filename][] = $token->getLine();
810 $this->ignoredLines[$filename][] = $numLines + 1;
812 $this->ignoredLines[$filename] = array_unique(
813 $this->ignoredLines[$filename]
816 sort($this->ignoredLines[$filename]);
819 return $this->ignoredLines[$filename];
824 * @param array $linesToBeCovered
825 * @param array $linesToBeUsed
826 * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode
827 * @since Method available since Release 2.0.0
829 private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed)
831 $allowedLines = $this->getAllowedLines(
838 foreach ($data as $file => $_data) {
839 foreach ($_data as $line => $flag) {
841 (!isset($allowedLines[$file]) ||
842 !isset($allowedLines[$file][$line]))) {
852 if (!empty($message)) {
853 throw new PHP_CodeCoverage_Exception_UnintentionallyCoveredCode(
860 * @param array $linesToBeCovered
861 * @param array $linesToBeUsed
863 * @since Method available since Release 2.0.0
865 private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed)
867 $allowedLines = array();
869 foreach (array_keys($linesToBeCovered) as $file) {
870 if (!isset($allowedLines[$file])) {
871 $allowedLines[$file] = array();
874 $allowedLines[$file] = array_merge(
875 $allowedLines[$file],
876 $linesToBeCovered[$file]
880 foreach (array_keys($linesToBeUsed) as $file) {
881 if (!isset($allowedLines[$file])) {
882 $allowedLines[$file] = array();
885 $allowedLines[$file] = array_merge(
886 $allowedLines[$file],
887 $linesToBeUsed[$file]
891 foreach (array_keys($allowedLines) as $file) {
892 $allowedLines[$file] = array_flip(
893 array_unique($allowedLines[$file])
897 return $allowedLines;
901 * @return PHP_CodeCoverage_Driver
902 * @throws PHP_CodeCoverage_Exception
904 private function selectDriver()
906 $runtime = new Runtime;
908 if (!$runtime->canCollectCodeCoverage()) {
909 throw new PHP_CodeCoverage_Exception('No code coverage driver available');
912 if ($runtime->isHHVM()) {
913 return new PHP_CodeCoverage_Driver_HHVM;
914 } elseif ($runtime->isPHPDBG()) {
915 return new PHP_CodeCoverage_Driver_PHPDBG;
917 return new PHP_CodeCoverage_Driver_Xdebug;