1<?php 2/* 3 * This file is part of the php-code-coverage package. 4 * 5 * (c) Sebastian Bergmann <sebastian@phpunit.de> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11namespace SebastianBergmann\CodeCoverage; 12 13use SebastianBergmann\CodeCoverage\Driver\Driver; 14use SebastianBergmann\CodeCoverage\Driver\Xdebug; 15use SebastianBergmann\CodeCoverage\Driver\HHVM; 16use SebastianBergmann\CodeCoverage\Driver\PHPDBG; 17use SebastianBergmann\CodeCoverage\Node\Builder; 18use SebastianBergmann\CodeCoverage\Node\Directory; 19use SebastianBergmann\CodeUnitReverseLookup\Wizard; 20use SebastianBergmann\Environment\Runtime; 21 22/** 23 * Provides collection functionality for PHP code coverage information. 24 */ 25class CodeCoverage 26{ 27 /** 28 * @var Driver 29 */ 30 private $driver; 31 32 /** 33 * @var Filter 34 */ 35 private $filter; 36 37 /** 38 * @var Wizard 39 */ 40 private $wizard; 41 42 /** 43 * @var bool 44 */ 45 private $cacheTokens = false; 46 47 /** 48 * @var bool 49 */ 50 private $checkForUnintentionallyCoveredCode = false; 51 52 /** 53 * @var bool 54 */ 55 private $forceCoversAnnotation = false; 56 57 /** 58 * @var bool 59 */ 60 private $checkForUnexecutedCoveredCode = false; 61 62 /** 63 * @var bool 64 */ 65 private $checkForMissingCoversAnnotation = false; 66 67 /** 68 * @var bool 69 */ 70 private $addUncoveredFilesFromWhitelist = true; 71 72 /** 73 * @var bool 74 */ 75 private $processUncoveredFilesFromWhitelist = false; 76 77 /** 78 * @var bool 79 */ 80 private $ignoreDeprecatedCode = false; 81 82 /** 83 * @var mixed 84 */ 85 private $currentId; 86 87 /** 88 * Code coverage data. 89 * 90 * @var array 91 */ 92 private $data = []; 93 94 /** 95 * @var array 96 */ 97 private $ignoredLines = []; 98 99 /** 100 * @var bool 101 */ 102 private $disableIgnoredLines = false; 103 104 /** 105 * Test data. 106 * 107 * @var array 108 */ 109 private $tests = []; 110 111 /** 112 * @var string[] 113 */ 114 private $unintentionallyCoveredSubclassesWhitelist = []; 115 116 /** 117 * Determine if the data has been initialized or not 118 * 119 * @var bool 120 */ 121 private $isInitialized = false; 122 123 /** 124 * Determine whether we need to check for dead and unused code on each test 125 * 126 * @var bool 127 */ 128 private $shouldCheckForDeadAndUnused = true; 129 130 /** 131 * Constructor. 132 * 133 * @param Driver $driver 134 * @param Filter $filter 135 * 136 * @throws RuntimeException 137 */ 138 public function __construct(Driver $driver = null, Filter $filter = null) 139 { 140 if ($driver === null) { 141 $driver = $this->selectDriver(); 142 } 143 144 if ($filter === null) { 145 $filter = new Filter; 146 } 147 148 $this->driver = $driver; 149 $this->filter = $filter; 150 151 $this->wizard = new Wizard; 152 } 153 154 /** 155 * Returns the code coverage information as a graph of node objects. 156 * 157 * @return Directory 158 */ 159 public function getReport() 160 { 161 $builder = new Builder; 162 163 return $builder->build($this); 164 } 165 166 /** 167 * Clears collected code coverage data. 168 */ 169 public function clear() 170 { 171 $this->isInitialized = false; 172 $this->currentId = null; 173 $this->data = []; 174 $this->tests = []; 175 } 176 177 /** 178 * Returns the filter object used. 179 * 180 * @return Filter 181 */ 182 public function filter() 183 { 184 return $this->filter; 185 } 186 187 /** 188 * Returns the collected code coverage data. 189 * Set $raw = true to bypass all filters. 190 * 191 * @param bool $raw 192 * 193 * @return array 194 */ 195 public function getData($raw = false) 196 { 197 if (!$raw && $this->addUncoveredFilesFromWhitelist) { 198 $this->addUncoveredFilesFromWhitelist(); 199 } 200 201 return $this->data; 202 } 203 204 /** 205 * Sets the coverage data. 206 * 207 * @param array $data 208 */ 209 public function setData(array $data) 210 { 211 $this->data = $data; 212 } 213 214 /** 215 * Returns the test data. 216 * 217 * @return array 218 */ 219 public function getTests() 220 { 221 return $this->tests; 222 } 223 224 /** 225 * Sets the test data. 226 * 227 * @param array $tests 228 */ 229 public function setTests(array $tests) 230 { 231 $this->tests = $tests; 232 } 233 234 /** 235 * Start collection of code coverage information. 236 * 237 * @param mixed $id 238 * @param bool $clear 239 * 240 * @throws InvalidArgumentException 241 */ 242 public function start($id, $clear = false) 243 { 244 if (!is_bool($clear)) { 245 throw InvalidArgumentException::create( 246 1, 247 'boolean' 248 ); 249 } 250 251 if ($clear) { 252 $this->clear(); 253 } 254 255 if ($this->isInitialized === false) { 256 $this->initializeData(); 257 } 258 259 $this->currentId = $id; 260 261 $this->driver->start($this->shouldCheckForDeadAndUnused); 262 } 263 264 /** 265 * Stop collection of code coverage information. 266 * 267 * @param bool $append 268 * @param mixed $linesToBeCovered 269 * @param array $linesToBeUsed 270 * 271 * @return array 272 * 273 * @throws InvalidArgumentException 274 */ 275 public function stop($append = true, $linesToBeCovered = [], array $linesToBeUsed = []) 276 { 277 if (!is_bool($append)) { 278 throw InvalidArgumentException::create( 279 1, 280 'boolean' 281 ); 282 } 283 284 if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) { 285 throw InvalidArgumentException::create( 286 2, 287 'array or false' 288 ); 289 } 290 291 $data = $this->driver->stop(); 292 $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed); 293 294 $this->currentId = null; 295 296 return $data; 297 } 298 299 /** 300 * Appends code coverage data. 301 * 302 * @param array $data 303 * @param mixed $id 304 * @param bool $append 305 * @param mixed $linesToBeCovered 306 * @param array $linesToBeUsed 307 * 308 * @throws RuntimeException 309 */ 310 public function append(array $data, $id = null, $append = true, $linesToBeCovered = [], array $linesToBeUsed = []) 311 { 312 if ($id === null) { 313 $id = $this->currentId; 314 } 315 316 if ($id === null) { 317 throw new RuntimeException; 318 } 319 320 $this->applyListsFilter($data); 321 $this->applyIgnoredLinesFilter($data); 322 $this->initializeFilesThatAreSeenTheFirstTime($data); 323 324 if (!$append) { 325 return; 326 } 327 328 if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') { 329 $this->applyCoversAnnotationFilter( 330 $data, 331 $linesToBeCovered, 332 $linesToBeUsed 333 ); 334 } 335 336 if (empty($data)) { 337 return; 338 } 339 340 $size = 'unknown'; 341 $status = null; 342 343 if ($id instanceof \PHPUnit_Framework_TestCase) { 344 $_size = $id->getSize(); 345 346 if ($_size == \PHPUnit_Util_Test::SMALL) { 347 $size = 'small'; 348 } elseif ($_size == \PHPUnit_Util_Test::MEDIUM) { 349 $size = 'medium'; 350 } elseif ($_size == \PHPUnit_Util_Test::LARGE) { 351 $size = 'large'; 352 } 353 354 $status = $id->getStatus(); 355 $id = get_class($id) . '::' . $id->getName(); 356 } elseif ($id instanceof \PHPUnit_Extensions_PhptTestCase) { 357 $size = 'large'; 358 $id = $id->getName(); 359 } 360 361 $this->tests[$id] = ['size' => $size, 'status' => $status]; 362 363 foreach ($data as $file => $lines) { 364 if (!$this->filter->isFile($file)) { 365 continue; 366 } 367 368 foreach ($lines as $k => $v) { 369 if ($v == Driver::LINE_EXECUTED) { 370 if (empty($this->data[$file][$k]) || !in_array($id, $this->data[$file][$k])) { 371 $this->data[$file][$k][] = $id; 372 } 373 } 374 } 375 } 376 } 377 378 /** 379 * Merges the data from another instance. 380 * 381 * @param CodeCoverage $that 382 */ 383 public function merge(CodeCoverage $that) 384 { 385 $this->filter->setWhitelistedFiles( 386 array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles()) 387 ); 388 389 foreach ($that->data as $file => $lines) { 390 if (!isset($this->data[$file])) { 391 if (!$this->filter->isFiltered($file)) { 392 $this->data[$file] = $lines; 393 } 394 395 continue; 396 } 397 398 foreach ($lines as $line => $data) { 399 if ($data !== null) { 400 if (!isset($this->data[$file][$line])) { 401 $this->data[$file][$line] = $data; 402 } else { 403 $this->data[$file][$line] = array_unique( 404 array_merge($this->data[$file][$line], $data) 405 ); 406 } 407 } 408 } 409 } 410 411 $this->tests = array_merge($this->tests, $that->getTests()); 412 } 413 414 /** 415 * @param bool $flag 416 * 417 * @throws InvalidArgumentException 418 */ 419 public function setCacheTokens($flag) 420 { 421 if (!is_bool($flag)) { 422 throw InvalidArgumentException::create( 423 1, 424 'boolean' 425 ); 426 } 427 428 $this->cacheTokens = $flag; 429 } 430 431 /** 432 * @return bool 433 */ 434 public function getCacheTokens() 435 { 436 return $this->cacheTokens; 437 } 438 439 /** 440 * @param bool $flag 441 * 442 * @throws InvalidArgumentException 443 */ 444 public function setCheckForUnintentionallyCoveredCode($flag) 445 { 446 if (!is_bool($flag)) { 447 throw InvalidArgumentException::create( 448 1, 449 'boolean' 450 ); 451 } 452 453 $this->checkForUnintentionallyCoveredCode = $flag; 454 } 455 456 /** 457 * @param bool $flag 458 * 459 * @throws InvalidArgumentException 460 */ 461 public function setForceCoversAnnotation($flag) 462 { 463 if (!is_bool($flag)) { 464 throw InvalidArgumentException::create( 465 1, 466 'boolean' 467 ); 468 } 469 470 $this->forceCoversAnnotation = $flag; 471 } 472 473 /** 474 * @param bool $flag 475 * 476 * @throws InvalidArgumentException 477 */ 478 public function setCheckForMissingCoversAnnotation($flag) 479 { 480 if (!is_bool($flag)) { 481 throw InvalidArgumentException::create( 482 1, 483 'boolean' 484 ); 485 } 486 487 $this->checkForMissingCoversAnnotation = $flag; 488 } 489 490 /** 491 * @param bool $flag 492 * 493 * @throws InvalidArgumentException 494 */ 495 public function setCheckForUnexecutedCoveredCode($flag) 496 { 497 if (!is_bool($flag)) { 498 throw InvalidArgumentException::create( 499 1, 500 'boolean' 501 ); 502 } 503 504 $this->checkForUnexecutedCoveredCode = $flag; 505 } 506 507 /** 508 * @deprecated 509 * 510 * @param bool $flag 511 * 512 * @throws InvalidArgumentException 513 */ 514 public function setMapTestClassNameToCoveredClassName($flag) 515 { 516 } 517 518 /** 519 * @param bool $flag 520 * 521 * @throws InvalidArgumentException 522 */ 523 public function setAddUncoveredFilesFromWhitelist($flag) 524 { 525 if (!is_bool($flag)) { 526 throw InvalidArgumentException::create( 527 1, 528 'boolean' 529 ); 530 } 531 532 $this->addUncoveredFilesFromWhitelist = $flag; 533 } 534 535 /** 536 * @param bool $flag 537 * 538 * @throws InvalidArgumentException 539 */ 540 public function setProcessUncoveredFilesFromWhitelist($flag) 541 { 542 if (!is_bool($flag)) { 543 throw InvalidArgumentException::create( 544 1, 545 'boolean' 546 ); 547 } 548 549 $this->processUncoveredFilesFromWhitelist = $flag; 550 } 551 552 /** 553 * @param bool $flag 554 * 555 * @throws InvalidArgumentException 556 */ 557 public function setDisableIgnoredLines($flag) 558 { 559 if (!is_bool($flag)) { 560 throw InvalidArgumentException::create( 561 1, 562 'boolean' 563 ); 564 } 565 566 $this->disableIgnoredLines = $flag; 567 } 568 569 /** 570 * @param bool $flag 571 * 572 * @throws InvalidArgumentException 573 */ 574 public function setIgnoreDeprecatedCode($flag) 575 { 576 if (!is_bool($flag)) { 577 throw InvalidArgumentException::create( 578 1, 579 'boolean' 580 ); 581 } 582 583 $this->ignoreDeprecatedCode = $flag; 584 } 585 586 /** 587 * @param array $whitelist 588 */ 589 public function setUnintentionallyCoveredSubclassesWhitelist(array $whitelist) 590 { 591 $this->unintentionallyCoveredSubclassesWhitelist = $whitelist; 592 } 593 594 /** 595 * Applies the @covers annotation filtering. 596 * 597 * @param array $data 598 * @param mixed $linesToBeCovered 599 * @param array $linesToBeUsed 600 * 601 * @throws MissingCoversAnnotationException 602 * @throws UnintentionallyCoveredCodeException 603 */ 604 private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed) 605 { 606 if ($linesToBeCovered === false || 607 ($this->forceCoversAnnotation && empty($linesToBeCovered))) { 608 if ($this->checkForMissingCoversAnnotation) { 609 throw new MissingCoversAnnotationException; 610 } 611 612 $data = []; 613 614 return; 615 } 616 617 if (empty($linesToBeCovered)) { 618 return; 619 } 620 621 if ($this->checkForUnintentionallyCoveredCode && 622 (!$this->currentId instanceof \PHPUnit_Framework_TestCase || 623 (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) { 624 $this->performUnintentionallyCoveredCodeCheck( 625 $data, 626 $linesToBeCovered, 627 $linesToBeUsed 628 ); 629 } 630 631 if ($this->checkForUnexecutedCoveredCode) { 632 $this->performUnexecutedCoveredCodeCheck($data, $linesToBeCovered, $linesToBeUsed); 633 } 634 635 $data = array_intersect_key($data, $linesToBeCovered); 636 637 foreach (array_keys($data) as $filename) { 638 $_linesToBeCovered = array_flip($linesToBeCovered[$filename]); 639 640 $data[$filename] = array_intersect_key( 641 $data[$filename], 642 $_linesToBeCovered 643 ); 644 } 645 } 646 647 /** 648 * Applies the whitelist filtering. 649 * 650 * @param array $data 651 */ 652 private function applyListsFilter(array &$data) 653 { 654 foreach (array_keys($data) as $filename) { 655 if ($this->filter->isFiltered($filename)) { 656 unset($data[$filename]); 657 } 658 } 659 } 660 661 /** 662 * Applies the "ignored lines" filtering. 663 * 664 * @param array $data 665 */ 666 private function applyIgnoredLinesFilter(array &$data) 667 { 668 foreach (array_keys($data) as $filename) { 669 if (!$this->filter->isFile($filename)) { 670 continue; 671 } 672 673 foreach ($this->getLinesToBeIgnored($filename) as $line) { 674 unset($data[$filename][$line]); 675 } 676 } 677 } 678 679 /** 680 * @param array $data 681 */ 682 private function initializeFilesThatAreSeenTheFirstTime(array $data) 683 { 684 foreach ($data as $file => $lines) { 685 if ($this->filter->isFile($file) && !isset($this->data[$file])) { 686 $this->data[$file] = []; 687 688 foreach ($lines as $k => $v) { 689 $this->data[$file][$k] = $v == -2 ? null : []; 690 } 691 } 692 } 693 } 694 695 /** 696 * Processes whitelisted files that are not covered. 697 */ 698 private function addUncoveredFilesFromWhitelist() 699 { 700 $data = []; 701 $uncoveredFiles = array_diff( 702 $this->filter->getWhitelist(), 703 array_keys($this->data) 704 ); 705 706 foreach ($uncoveredFiles as $uncoveredFile) { 707 if (!file_exists($uncoveredFile)) { 708 continue; 709 } 710 711 if (!$this->processUncoveredFilesFromWhitelist) { 712 $data[$uncoveredFile] = []; 713 714 $lines = count(file($uncoveredFile)); 715 716 for ($i = 1; $i <= $lines; $i++) { 717 $data[$uncoveredFile][$i] = Driver::LINE_NOT_EXECUTED; 718 } 719 } 720 } 721 722 $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); 723 } 724 725 /** 726 * Returns the lines of a source file that should be ignored. 727 * 728 * @param string $filename 729 * 730 * @return array 731 * 732 * @throws InvalidArgumentException 733 */ 734 private function getLinesToBeIgnored($filename) 735 { 736 if (!is_string($filename)) { 737 throw InvalidArgumentException::create( 738 1, 739 'string' 740 ); 741 } 742 743 if (!isset($this->ignoredLines[$filename])) { 744 $this->ignoredLines[$filename] = []; 745 746 if ($this->disableIgnoredLines) { 747 return $this->ignoredLines[$filename]; 748 } 749 750 $ignore = false; 751 $stop = false; 752 $lines = file($filename); 753 $numLines = count($lines); 754 755 foreach ($lines as $index => $line) { 756 if (!trim($line)) { 757 $this->ignoredLines[$filename][] = $index + 1; 758 } 759 } 760 761 if ($this->cacheTokens) { 762 $tokens = \PHP_Token_Stream_CachingFactory::get($filename); 763 } else { 764 $tokens = new \PHP_Token_Stream($filename); 765 } 766 767 $classes = array_merge($tokens->getClasses(), $tokens->getTraits()); 768 $tokens = $tokens->tokens(); 769 770 foreach ($tokens as $token) { 771 switch (get_class($token)) { 772 case 'PHP_Token_COMMENT': 773 case 'PHP_Token_DOC_COMMENT': 774 $_token = trim($token); 775 $_line = trim($lines[$token->getLine() - 1]); 776 777 if ($_token == '// @codeCoverageIgnore' || 778 $_token == '//@codeCoverageIgnore') { 779 $ignore = true; 780 $stop = true; 781 } elseif ($_token == '// @codeCoverageIgnoreStart' || 782 $_token == '//@codeCoverageIgnoreStart') { 783 $ignore = true; 784 } elseif ($_token == '// @codeCoverageIgnoreEnd' || 785 $_token == '//@codeCoverageIgnoreEnd') { 786 $stop = true; 787 } 788 789 if (!$ignore) { 790 $start = $token->getLine(); 791 $end = $start + substr_count($token, "\n"); 792 793 // Do not ignore the first line when there is a token 794 // before the comment 795 if (0 !== strpos($_token, $_line)) { 796 $start++; 797 } 798 799 for ($i = $start; $i < $end; $i++) { 800 $this->ignoredLines[$filename][] = $i; 801 } 802 803 // A DOC_COMMENT token or a COMMENT token starting with "/*" 804 // does not contain the final \n character in its text 805 if (isset($lines[$i-1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i-1]), -2)) { 806 $this->ignoredLines[$filename][] = $i; 807 } 808 } 809 break; 810 811 case 'PHP_Token_INTERFACE': 812 case 'PHP_Token_TRAIT': 813 case 'PHP_Token_CLASS': 814 case 'PHP_Token_FUNCTION': 815 /* @var \PHP_Token_Interface $token */ 816 817 $docblock = $token->getDocblock(); 818 819 $this->ignoredLines[$filename][] = $token->getLine(); 820 821 if (strpos($docblock, '@codeCoverageIgnore') || ($this->ignoreDeprecatedCode && strpos($docblock, '@deprecated'))) { 822 $endLine = $token->getEndLine(); 823 824 for ($i = $token->getLine(); $i <= $endLine; $i++) { 825 $this->ignoredLines[$filename][] = $i; 826 } 827 } elseif ($token instanceof \PHP_Token_INTERFACE || 828 $token instanceof \PHP_Token_TRAIT || 829 $token instanceof \PHP_Token_CLASS) { 830 if (empty($classes[$token->getName()]['methods'])) { 831 for ($i = $token->getLine(); 832 $i <= $token->getEndLine(); 833 $i++) { 834 $this->ignoredLines[$filename][] = $i; 835 } 836 } else { 837 $firstMethod = array_shift( 838 $classes[$token->getName()]['methods'] 839 ); 840 841 do { 842 $lastMethod = array_pop( 843 $classes[$token->getName()]['methods'] 844 ); 845 } while ($lastMethod !== null && 846 substr($lastMethod['signature'], 0, 18) == 'anonymous function'); 847 848 if ($lastMethod === null) { 849 $lastMethod = $firstMethod; 850 } 851 852 for ($i = $token->getLine(); 853 $i < $firstMethod['startLine']; 854 $i++) { 855 $this->ignoredLines[$filename][] = $i; 856 } 857 858 for ($i = $token->getEndLine(); 859 $i > $lastMethod['endLine']; 860 $i--) { 861 $this->ignoredLines[$filename][] = $i; 862 } 863 } 864 } 865 break; 866 867 case 'PHP_Token_NAMESPACE': 868 $this->ignoredLines[$filename][] = $token->getEndLine(); 869 870 // Intentional fallthrough 871 case 'PHP_Token_DECLARE': 872 case 'PHP_Token_OPEN_TAG': 873 case 'PHP_Token_CLOSE_TAG': 874 case 'PHP_Token_USE': 875 $this->ignoredLines[$filename][] = $token->getLine(); 876 break; 877 } 878 879 if ($ignore) { 880 $this->ignoredLines[$filename][] = $token->getLine(); 881 882 if ($stop) { 883 $ignore = false; 884 $stop = false; 885 } 886 } 887 } 888 889 $this->ignoredLines[$filename][] = $numLines + 1; 890 891 $this->ignoredLines[$filename] = array_unique( 892 $this->ignoredLines[$filename] 893 ); 894 895 sort($this->ignoredLines[$filename]); 896 } 897 898 return $this->ignoredLines[$filename]; 899 } 900 901 /** 902 * @param array $data 903 * @param array $linesToBeCovered 904 * @param array $linesToBeUsed 905 * 906 * @throws UnintentionallyCoveredCodeException 907 */ 908 private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) 909 { 910 $allowedLines = $this->getAllowedLines( 911 $linesToBeCovered, 912 $linesToBeUsed 913 ); 914 915 $unintentionallyCoveredUnits = []; 916 917 foreach ($data as $file => $_data) { 918 foreach ($_data as $line => $flag) { 919 if ($flag == 1 && !isset($allowedLines[$file][$line])) { 920 $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line); 921 } 922 } 923 } 924 925 $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits); 926 927 if (!empty($unintentionallyCoveredUnits)) { 928 throw new UnintentionallyCoveredCodeException( 929 $unintentionallyCoveredUnits 930 ); 931 } 932 } 933 934 /** 935 * @param array $data 936 * @param array $linesToBeCovered 937 * @param array $linesToBeUsed 938 * 939 * @throws CoveredCodeNotExecutedException 940 */ 941 private function performUnexecutedCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) 942 { 943 $expectedLines = $this->getAllowedLines( 944 $linesToBeCovered, 945 $linesToBeUsed 946 ); 947 948 foreach ($data as $file => $_data) { 949 foreach (array_keys($_data) as $line) { 950 if (!isset($expectedLines[$file][$line])) { 951 continue; 952 } 953 954 unset($expectedLines[$file][$line]); 955 } 956 } 957 958 $message = ''; 959 960 foreach ($expectedLines as $file => $lines) { 961 if (empty($lines)) { 962 continue; 963 } 964 965 foreach (array_keys($lines) as $line) { 966 $message .= sprintf('- %s:%d' . PHP_EOL, $file, $line); 967 } 968 } 969 970 if (!empty($message)) { 971 throw new CoveredCodeNotExecutedException($message); 972 } 973 } 974 975 /** 976 * @param array $linesToBeCovered 977 * @param array $linesToBeUsed 978 * 979 * @return array 980 */ 981 private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed) 982 { 983 $allowedLines = []; 984 985 foreach (array_keys($linesToBeCovered) as $file) { 986 if (!isset($allowedLines[$file])) { 987 $allowedLines[$file] = []; 988 } 989 990 $allowedLines[$file] = array_merge( 991 $allowedLines[$file], 992 $linesToBeCovered[$file] 993 ); 994 } 995 996 foreach (array_keys($linesToBeUsed) as $file) { 997 if (!isset($allowedLines[$file])) { 998 $allowedLines[$file] = []; 999 } 1000 1001 $allowedLines[$file] = array_merge( 1002 $allowedLines[$file], 1003 $linesToBeUsed[$file] 1004 ); 1005 } 1006 1007 foreach (array_keys($allowedLines) as $file) { 1008 $allowedLines[$file] = array_flip( 1009 array_unique($allowedLines[$file]) 1010 ); 1011 } 1012 1013 return $allowedLines; 1014 } 1015 1016 /** 1017 * @return Driver 1018 * 1019 * @throws RuntimeException 1020 */ 1021 private function selectDriver() 1022 { 1023 $runtime = new Runtime; 1024 1025 if (!$runtime->canCollectCodeCoverage()) { 1026 throw new RuntimeException('No code coverage driver available'); 1027 } 1028 1029 if ($runtime->isHHVM()) { 1030 return new HHVM; 1031 } elseif ($runtime->isPHPDBG()) { 1032 return new PHPDBG; 1033 } else { 1034 return new Xdebug; 1035 } 1036 } 1037 1038 /** 1039 * @param array $unintentionallyCoveredUnits 1040 * 1041 * @return array 1042 */ 1043 private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits) 1044 { 1045 $unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits); 1046 sort($unintentionallyCoveredUnits); 1047 1048 foreach (array_keys($unintentionallyCoveredUnits) as $k => $v) { 1049 $unit = explode('::', $unintentionallyCoveredUnits[$k]); 1050 1051 if (count($unit) != 2) { 1052 continue; 1053 } 1054 1055 $class = new \ReflectionClass($unit[0]); 1056 1057 foreach ($this->unintentionallyCoveredSubclassesWhitelist as $whitelisted) { 1058 if ($class->isSubclassOf($whitelisted)) { 1059 unset($unintentionallyCoveredUnits[$k]); 1060 break; 1061 } 1062 } 1063 } 1064 1065 return array_values($unintentionallyCoveredUnits); 1066 } 1067 1068 /** 1069 * If we are processing uncovered files from whitelist, 1070 * we can initialize the data before we start to speed up the tests 1071 */ 1072 protected function initializeData() 1073 { 1074 $this->isInitialized = true; 1075 1076 if ($this->processUncoveredFilesFromWhitelist) { 1077 $this->shouldCheckForDeadAndUnused = false; 1078 1079 $this->driver->start(true); 1080 1081 foreach ($this->filter->getWhitelist() as $file) { 1082 if ($this->filter->isFile($file)) { 1083 include_once($file); 1084 } 1085 } 1086 1087 $data = []; 1088 $coverage = $this->driver->stop(); 1089 1090 foreach ($coverage as $file => $fileCoverage) { 1091 if ($this->filter->isFiltered($file)) { 1092 continue; 1093 } 1094 1095 foreach (array_keys($fileCoverage) as $key) { 1096 if ($fileCoverage[$key] == Driver::LINE_EXECUTED) { 1097 $fileCoverage[$key] = Driver::LINE_NOT_EXECUTED; 1098 } 1099 } 1100 1101 $data[$file] = $fileCoverage; 1102 } 1103 1104 $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); 1105 } 1106 } 1107} 1108