CodeCoverage.php (22826B)
1 <?php 2 /** 3 * PHP_CodeCoverage 4 * 5 * Copyright (c) 2009-2014, Sebastian Bergmann <sebastian@phpunit.de>. 6 * All rights reserved. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 12 * * Redistributions of source code must retain the above copyright 13 * notice, this list of conditions and the following disclaimer. 14 * 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 18 * distribution. 19 * 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. 23 * 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. 36 * 37 * @category PHP 38 * @package CodeCoverage 39 * @author Sebastian Bergmann <sebastian@phpunit.de> 40 * @copyright 2009-2014 Sebastian Bergmann <sebastian@phpunit.de> 41 * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License 42 * @link http://github.com/sebastianbergmann/php-code-coverage 43 * @since File available since Release 1.0.0 44 */ 45 46 // @codeCoverageIgnoreStart 47 // @codingStandardsIgnoreStart 48 /** 49 * @SuppressWarnings(PHPMD) 50 */ 51 if (!function_exists('trait_exists')) { 52 function trait_exists($name) 53 { 54 return FALSE; 55 } 56 } 57 // @codingStandardsIgnoreEnd 58 // @codeCoverageIgnoreEnd 59 60 /** 61 * Provides collection functionality for PHP code coverage information. 62 * 63 * @category PHP 64 * @package CodeCoverage 65 * @author Sebastian Bergmann <sebastian@phpunit.de> 66 * @copyright 2009-2014 Sebastian Bergmann <sebastian@phpunit.de> 67 * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License 68 * @link http://github.com/sebastianbergmann/php-code-coverage 69 * @since Class available since Release 1.0.0 70 */ 71 class PHP_CodeCoverage 72 { 73 /** 74 * @var PHP_CodeCoverage_Driver 75 */ 76 protected $driver; 77 78 /** 79 * @var PHP_CodeCoverage_Filter 80 */ 81 protected $filter; 82 83 /** 84 * @var boolean 85 */ 86 protected $cacheTokens = FALSE; 87 88 /** 89 * @var boolean 90 */ 91 protected $forceCoversAnnotation = FALSE; 92 93 /** 94 * @var boolean 95 */ 96 protected $mapTestClassNameToCoveredClassName = FALSE; 97 98 /** 99 * @var boolean 100 */ 101 protected $addUncoveredFilesFromWhitelist = TRUE; 102 103 /** 104 * @var boolean 105 */ 106 protected $processUncoveredFilesFromWhitelist = FALSE; 107 108 /** 109 * @var mixed 110 */ 111 protected $currentId; 112 113 /** 114 * Code coverage data. 115 * 116 * @var array 117 */ 118 protected $data = array(); 119 120 /** 121 * Test data. 122 * 123 * @var array 124 */ 125 protected $tests = array(); 126 127 /** 128 * Constructor. 129 * 130 * @param PHP_CodeCoverage_Driver $driver 131 * @param PHP_CodeCoverage_Filter $filter 132 */ 133 public function __construct(PHP_CodeCoverage_Driver $driver = NULL, PHP_CodeCoverage_Filter $filter = NULL) 134 { 135 if ($driver === NULL) { 136 $driver = new PHP_CodeCoverage_Driver_Xdebug; 137 } 138 139 if ($filter === NULL) { 140 $filter = new PHP_CodeCoverage_Filter; 141 } 142 143 $this->driver = $driver; 144 $this->filter = $filter; 145 } 146 147 /** 148 * Returns the PHP_CodeCoverage_Report_Node_* object graph 149 * for this PHP_CodeCoverage object. 150 * 151 * @return PHP_CodeCoverage_Report_Node_Directory 152 * @since Method available since Release 1.1.0 153 */ 154 public function getReport() 155 { 156 $factory = new PHP_CodeCoverage_Report_Factory; 157 158 return $factory->create($this); 159 } 160 161 /** 162 * Clears collected code coverage data. 163 */ 164 public function clear() 165 { 166 $this->currentId = NULL; 167 $this->data = array(); 168 $this->tests = array(); 169 } 170 171 /** 172 * Returns the PHP_CodeCoverage_Filter used. 173 * 174 * @return PHP_CodeCoverage_Filter 175 */ 176 public function filter() 177 { 178 return $this->filter; 179 } 180 181 /** 182 * Returns the collected code coverage data. 183 * 184 * @return array 185 * @since Method available since Release 1.1.0 186 */ 187 public function getData() 188 { 189 if ($this->addUncoveredFilesFromWhitelist) { 190 $this->addUncoveredFilesFromWhitelist(); 191 } 192 193 // We need to apply the blacklist filter a second time 194 // when no whitelist is used. 195 if (!$this->filter->hasWhitelist()) { 196 $this->applyListsFilter($this->data); 197 } 198 199 return $this->data; 200 } 201 202 /** 203 * Returns the test data. 204 * 205 * @return array 206 * @since Method available since Release 1.1.0 207 */ 208 public function getTests() 209 { 210 return $this->tests; 211 } 212 213 /** 214 * Start collection of code coverage information. 215 * 216 * @param mixed $id 217 * @param boolean $clear 218 * @throws PHP_CodeCoverage_Exception 219 */ 220 public function start($id, $clear = FALSE) 221 { 222 if (!is_bool($clear)) { 223 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 224 1, 'boolean' 225 ); 226 } 227 228 if ($clear) { 229 $this->clear(); 230 } 231 232 $this->currentId = $id; 233 234 $this->driver->start(); 235 } 236 237 /** 238 * Stop collection of code coverage information. 239 * 240 * @param boolean $append 241 * @return array 242 * @throws PHP_CodeCoverage_Exception 243 */ 244 public function stop($append = TRUE) 245 { 246 if (!is_bool($append)) { 247 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 248 1, 'boolean' 249 ); 250 } 251 252 $data = $this->driver->stop(); 253 $this->append($data, NULL, $append); 254 255 $this->currentId = NULL; 256 257 return $data; 258 } 259 260 /** 261 * Appends code coverage data. 262 * 263 * @param array $data 264 * @param mixed $id 265 * @param boolean $append 266 */ 267 public function append(array $data, $id = NULL, $append = TRUE) 268 { 269 if ($id === NULL) { 270 $id = $this->currentId; 271 } 272 273 if ($id === NULL) { 274 throw new PHP_CodeCoverage_Exception; 275 } 276 277 $this->applyListsFilter($data); 278 $this->initializeFilesThatAreSeenTheFirstTime($data); 279 280 if (!$append) { 281 return; 282 } 283 284 if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') { 285 $this->applyCoversAnnotationFilter($data, $id); 286 } 287 288 if (empty($data)) { 289 return; 290 } 291 292 $status = NULL; 293 294 if ($id instanceof PHPUnit_Framework_TestCase) { 295 $status = $id->getStatus(); 296 $id = get_class($id) . '::' . $id->getName(); 297 } 298 299 else if ($id instanceof PHPUnit_Extensions_PhptTestCase) { 300 $id = $id->getName(); 301 } 302 303 $this->tests[$id] = $status; 304 305 foreach ($data as $file => $lines) { 306 if (!$this->filter->isFile($file)) { 307 continue; 308 } 309 310 foreach ($lines as $k => $v) { 311 if ($v == 1) { 312 $this->data[$file][$k][] = $id; 313 } 314 } 315 } 316 } 317 318 /** 319 * Merges the data from another instance of PHP_CodeCoverage. 320 * 321 * @param PHP_CodeCoverage $that 322 */ 323 public function merge(PHP_CodeCoverage $that) 324 { 325 foreach ($that->data as $file => $lines) { 326 if (!isset($this->data[$file])) { 327 if (!$this->filter->isFiltered($file)) { 328 $this->data[$file] = $lines; 329 } 330 331 continue; 332 } 333 334 foreach ($lines as $line => $data) { 335 if ($data !== NULL) { 336 if (!isset($this->data[$file][$line])) { 337 $this->data[$file][$line] = $data; 338 } else { 339 $this->data[$file][$line] = array_unique( 340 array_merge($this->data[$file][$line], $data) 341 ); 342 } 343 } 344 } 345 } 346 347 $this->tests = array_merge($this->tests, $that->getTests()); 348 } 349 350 /** 351 * @param boolean $flag 352 * @throws PHP_CodeCoverage_Exception 353 * @since Method available since Release 1.1.0 354 */ 355 public function setCacheTokens($flag) 356 { 357 if (!is_bool($flag)) { 358 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 359 1, 'boolean' 360 ); 361 } 362 363 $this->cacheTokens = $flag; 364 } 365 366 /** 367 * @param boolean $flag 368 * @since Method available since Release 1.1.0 369 */ 370 public function getCacheTokens() 371 { 372 return $this->cacheTokens; 373 } 374 375 /** 376 * @param boolean $flag 377 * @throws PHP_CodeCoverage_Exception 378 */ 379 public function setForceCoversAnnotation($flag) 380 { 381 if (!is_bool($flag)) { 382 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 383 1, 'boolean' 384 ); 385 } 386 387 $this->forceCoversAnnotation = $flag; 388 } 389 390 /** 391 * @param boolean $flag 392 * @throws PHP_CodeCoverage_Exception 393 */ 394 public function setMapTestClassNameToCoveredClassName($flag) 395 { 396 if (!is_bool($flag)) { 397 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 398 1, 'boolean' 399 ); 400 } 401 402 $this->mapTestClassNameToCoveredClassName = $flag; 403 } 404 405 /** 406 * @param boolean $flag 407 * @throws PHP_CodeCoverage_Exception 408 */ 409 public function setAddUncoveredFilesFromWhitelist($flag) 410 { 411 if (!is_bool($flag)) { 412 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 413 1, 'boolean' 414 ); 415 } 416 417 $this->addUncoveredFilesFromWhitelist = $flag; 418 } 419 420 /** 421 * @param boolean $flag 422 * @throws PHP_CodeCoverage_Exception 423 */ 424 public function setProcessUncoveredFilesFromWhitelist($flag) 425 { 426 if (!is_bool($flag)) { 427 throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 428 1, 'boolean' 429 ); 430 } 431 432 $this->processUncoveredFilesFromWhitelist = $flag; 433 } 434 435 /** 436 * Applies the @covers annotation filtering. 437 * 438 * @param array $data 439 * @param mixed $id 440 */ 441 protected function applyCoversAnnotationFilter(&$data, $id) 442 { 443 if ($id instanceof PHPUnit_Framework_TestCase) { 444 $testClassName = get_class($id); 445 $linesToBeCovered = $this->getLinesToBeCovered( 446 $testClassName, $id->getName() 447 ); 448 449 if ($linesToBeCovered === FALSE) { 450 $data = array(); 451 return; 452 } 453 454 if ($this->mapTestClassNameToCoveredClassName && 455 empty($linesToBeCovered)) { 456 $testedClass = substr($testClassName, 0, -4); 457 458 if (class_exists($testedClass)) { 459 $class = new ReflectionClass($testedClass); 460 461 $linesToBeCovered = array( 462 $class->getFileName() => range( 463 $class->getStartLine(), $class->getEndLine() 464 ) 465 ); 466 } 467 } 468 } else { 469 $linesToBeCovered = array(); 470 } 471 472 if (!empty($linesToBeCovered)) { 473 $data = array_intersect_key($data, $linesToBeCovered); 474 475 foreach (array_keys($data) as $filename) { 476 $data[$filename] = array_intersect_key( 477 $data[$filename], array_flip($linesToBeCovered[$filename]) 478 ); 479 } 480 } 481 482 else if ($this->forceCoversAnnotation) { 483 $data = array(); 484 } 485 } 486 487 /** 488 * Applies the blacklist/whitelist filtering. 489 * 490 * @param array $data 491 */ 492 protected function applyListsFilter(&$data) 493 { 494 foreach (array_keys($data) as $filename) { 495 if ($this->filter->isFiltered($filename)) { 496 unset($data[$filename]); 497 } 498 } 499 } 500 501 /** 502 * @since Method available since Release 1.1.0 503 */ 504 protected function initializeFilesThatAreSeenTheFirstTime($data) 505 { 506 foreach ($data as $file => $lines) { 507 if ($this->filter->isFile($file) && !isset($this->data[$file])) { 508 $this->data[$file] = array(); 509 510 foreach ($lines as $k => $v) { 511 $this->data[$file][$k] = $v == -2 ? NULL : array(); 512 } 513 } 514 } 515 } 516 517 /** 518 * Processes whitelisted files that are not covered. 519 */ 520 protected function addUncoveredFilesFromWhitelist() 521 { 522 $data = array(); 523 $uncoveredFiles = array_diff( 524 $this->filter->getWhitelist(), array_keys($this->data) 525 ); 526 527 foreach ($uncoveredFiles as $uncoveredFile) { 528 if (!file_exists($uncoveredFile)) { 529 continue; 530 } 531 532 if ($this->processUncoveredFilesFromWhitelist) { 533 $this->processUncoveredFileFromWhitelist( 534 $uncoveredFile, $data, $uncoveredFiles 535 ); 536 } else { 537 $data[$uncoveredFile] = array(); 538 539 $lines = count(file($uncoveredFile)); 540 541 for ($i = 1; $i <= $lines; $i++) { 542 $data[$uncoveredFile][$i] = -1; 543 } 544 } 545 } 546 547 $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); 548 } 549 550 /** 551 * @param string $uncoveredFile 552 * @param array $data 553 * @param array $uncoveredFiles 554 */ 555 protected function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, array $uncoveredFiles) 556 { 557 $this->driver->start(); 558 include_once $uncoveredFile; 559 $coverage = $this->driver->stop(); 560 561 foreach ($coverage as $file => $fileCoverage) { 562 if (!isset($data[$file]) && 563 in_array($file, $uncoveredFiles)) { 564 foreach (array_keys($fileCoverage) as $key) { 565 if ($fileCoverage[$key] == 1) { 566 $fileCoverage[$key] = -1; 567 } 568 } 569 570 $data[$file] = $fileCoverage; 571 } 572 } 573 } 574 575 /** 576 * Returns the files and lines a test method wants to cover. 577 * 578 * @param string $className 579 * @param string $methodName 580 * @return array 581 * @since Method available since Release 1.2.0 582 */ 583 protected function getLinesToBeCovered($className, $methodName) 584 { 585 $codeToCoverList = array(); 586 $result = array(); 587 588 // @codeCoverageIgnoreStart 589 if (($pos = strpos($methodName, ' ')) !== FALSE) { 590 $methodName = substr($methodName, 0, $pos); 591 } 592 // @codeCoverageIgnoreEnd 593 594 $class = new ReflectionClass($className); 595 596 try { 597 $method = new ReflectionMethod($className, $methodName); 598 } 599 600 catch (ReflectionException $e) { 601 return array(); 602 } 603 604 $docComment = substr($class->getDocComment(), 3, -2) . PHP_EOL . substr($method->getDocComment(), 3, -2); 605 606 $templateMethods = array( 607 'setUp', 'assertPreConditions', 'assertPostConditions', 'tearDown' 608 ); 609 610 foreach ($templateMethods as $templateMethod) { 611 if ($class->hasMethod($templateMethod)) { 612 $reflector = $class->getMethod($templateMethod); 613 $docComment .= PHP_EOL . substr($reflector->getDocComment(), 3, -2); 614 unset($reflector); 615 } 616 } 617 618 if (strpos($docComment, '@coversNothing') !== FALSE) { 619 return FALSE; 620 } 621 622 $classShortcut = preg_match_all( 623 '(@coversDefaultClass\s+(?P<coveredClass>[^\s]++)\s*$)m', 624 $class->getDocComment(), 625 $matches 626 ); 627 628 if ($classShortcut) { 629 if ($classShortcut > 1) { 630 throw new PHP_CodeCoverage_Exception( 631 sprintf( 632 'More than one @coversClass annotation in class or interface "%s".', 633 $className 634 ) 635 ); 636 } 637 638 $classShortcut = $matches['coveredClass'][0]; 639 } 640 641 $match = preg_match_all( 642 '(@covers\s+(?P<coveredElement>[^\s()]++)[\s()]*$)m', 643 $docComment, 644 $matches 645 ); 646 647 if ($match) { 648 foreach ($matches['coveredElement'] as $coveredElement) { 649 if ($classShortcut && strncmp($coveredElement, '::', 2) === 0) { 650 $coveredElement = $classShortcut . $coveredElement; 651 } 652 653 $codeToCoverList = array_merge( 654 $codeToCoverList, 655 $this->resolveCoversToReflectionObjects($coveredElement) 656 ); 657 } 658 659 foreach ($codeToCoverList as $codeToCover) { 660 $fileName = $codeToCover->getFileName(); 661 662 if (!isset($result[$fileName])) { 663 $result[$fileName] = array(); 664 } 665 666 $result[$fileName] = array_unique( 667 array_merge( 668 $result[$fileName], 669 range( 670 $codeToCover->getStartLine(), $codeToCover->getEndLine() 671 ) 672 ) 673 ); 674 } 675 } 676 677 return $result; 678 } 679 680 /** 681 * @param string $coveredElement 682 * @return array 683 * @since Method available since Release 1.2.0 684 */ 685 protected function resolveCoversToReflectionObjects($coveredElement) 686 { 687 $codeToCoverList = array(); 688 689 if (strpos($coveredElement, '::') !== FALSE) { 690 list($className, $methodName) = explode('::', $coveredElement); 691 692 if (isset($methodName[0]) && $methodName[0] == '<') { 693 $classes = array($className); 694 695 foreach ($classes as $className) { 696 if (!class_exists($className) && 697 !interface_exists($className)) { 698 throw new PHP_CodeCoverage_Exception( 699 sprintf( 700 'Trying to @cover not existing class or ' . 701 'interface "%s".', 702 $className 703 ) 704 ); 705 } 706 707 $class = new ReflectionClass($className); 708 $methods = $class->getMethods(); 709 $inverse = isset($methodName[1]) && $methodName[1] == '!'; 710 711 if (strpos($methodName, 'protected')) { 712 $visibility = 'isProtected'; 713 } 714 715 else if (strpos($methodName, 'private')) { 716 $visibility = 'isPrivate'; 717 } 718 719 else if (strpos($methodName, 'public')) { 720 $visibility = 'isPublic'; 721 } 722 723 foreach ($methods as $method) { 724 if ($inverse && !$method->$visibility()) { 725 $codeToCoverList[] = $method; 726 } 727 728 else if (!$inverse && $method->$visibility()) { 729 $codeToCoverList[] = $method; 730 } 731 } 732 } 733 } else { 734 $classes = array($className); 735 736 foreach ($classes as $className) { 737 if ($className == '' && function_exists($methodName)) { 738 $codeToCoverList[] = new ReflectionFunction( 739 $methodName 740 ); 741 } else { 742 if (!((class_exists($className) || 743 interface_exists($className) || 744 trait_exists($className)) && 745 method_exists($className, $methodName))) { 746 throw new PHP_CodeCoverage_Exception( 747 sprintf( 748 'Trying to @cover not existing method "%s::%s".', 749 $className, 750 $methodName 751 ) 752 ); 753 } 754 755 $codeToCoverList[] = new ReflectionMethod( 756 $className, $methodName 757 ); 758 } 759 } 760 } 761 } else { 762 $extended = FALSE; 763 764 if (strpos($coveredElement, '<extended>') !== FALSE) { 765 $coveredElement = str_replace( 766 '<extended>', '', $coveredElement 767 ); 768 769 $extended = TRUE; 770 } 771 772 $classes = array($coveredElement); 773 774 if ($extended) { 775 $classes = array_merge( 776 $classes, 777 class_implements($coveredElement), 778 class_parents($coveredElement) 779 ); 780 } 781 782 foreach ($classes as $className) { 783 if (!class_exists($className) && 784 !interface_exists($className) && 785 !trait_exists($className)) { 786 throw new PHP_CodeCoverage_Exception( 787 sprintf( 788 'Trying to @cover not existing class or ' . 789 'interface "%s".', 790 $className 791 ) 792 ); 793 } 794 795 $codeToCoverList[] = new ReflectionClass($className); 796 } 797 } 798 799 return $codeToCoverList; 800 } 801 }