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