Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of phpunit/php-code-coverage.
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
namespace SebastianBergmann\CodeCoverage;
11
 
12
use function array_diff;
13
use function array_diff_key;
14
use function array_flip;
15
use function array_keys;
16
use function array_merge;
17
use function array_unique;
18
use function array_values;
19
use function count;
20
use function explode;
21
use function get_class;
22
use function is_array;
23
use function sort;
24
use PHPUnit\Framework\TestCase;
25
use PHPUnit\Runner\PhptTestCase;
26
use PHPUnit\Util\Test;
27
use ReflectionClass;
28
use SebastianBergmann\CodeCoverage\Driver\Driver;
29
use SebastianBergmann\CodeCoverage\Node\Builder;
30
use SebastianBergmann\CodeCoverage\Node\Directory;
31
use SebastianBergmann\CodeCoverage\StaticAnalysis\CachingFileAnalyser;
32
use SebastianBergmann\CodeCoverage\StaticAnalysis\FileAnalyser;
33
use SebastianBergmann\CodeCoverage\StaticAnalysis\ParsingFileAnalyser;
34
use SebastianBergmann\CodeUnitReverseLookup\Wizard;
35
 
36
/**
37
 * Provides collection functionality for PHP code coverage information.
38
 */
39
final class CodeCoverage
40
{
41
    private const UNCOVERED_FILES = 'UNCOVERED_FILES';
42
 
43
    /**
44
     * @var Driver
45
     */
46
    private $driver;
47
 
48
    /**
49
     * @var Filter
50
     */
51
    private $filter;
52
 
53
    /**
54
     * @var Wizard
55
     */
56
    private $wizard;
57
 
58
    /**
59
     * @var bool
60
     */
61
    private $checkForUnintentionallyCoveredCode = false;
62
 
63
    /**
64
     * @var bool
65
     */
66
    private $includeUncoveredFiles = true;
67
 
68
    /**
69
     * @var bool
70
     */
71
    private $processUncoveredFiles = false;
72
 
73
    /**
74
     * @var bool
75
     */
76
    private $ignoreDeprecatedCode = false;
77
 
78
    /**
79
     * @var null|PhptTestCase|string|TestCase
80
     */
81
    private $currentId;
82
 
83
    /**
84
     * Code coverage data.
85
     *
86
     * @var ProcessedCodeCoverageData
87
     */
88
    private $data;
89
 
90
    /**
91
     * @var bool
92
     */
93
    private $useAnnotationsForIgnoringCode = true;
94
 
95
    /**
96
     * Test data.
97
     *
98
     * @var array
99
     */
100
    private $tests = [];
101
 
102
    /**
103
     * @psalm-var list<class-string>
104
     */
105
    private $parentClassesExcludedFromUnintentionallyCoveredCodeCheck = [];
106
 
107
    /**
108
     * @var ?FileAnalyser
109
     */
110
    private $analyser;
111
 
112
    /**
113
     * @var ?string
114
     */
115
    private $cacheDirectory;
116
 
117
    public function __construct(Driver $driver, Filter $filter)
118
    {
119
        $this->driver = $driver;
120
        $this->filter = $filter;
121
        $this->data   = new ProcessedCodeCoverageData;
122
        $this->wizard = new Wizard;
123
    }
124
 
125
    /**
126
     * Returns the code coverage information as a graph of node objects.
127
     */
128
    public function getReport(): Directory
129
    {
130
        return (new Builder($this->analyser()))->build($this);
131
    }
132
 
133
    /**
134
     * Clears collected code coverage data.
135
     */
136
    public function clear(): void
137
    {
138
        $this->currentId = null;
139
        $this->data      = new ProcessedCodeCoverageData;
140
        $this->tests     = [];
141
    }
142
 
143
    /**
144
     * Returns the filter object used.
145
     */
146
    public function filter(): Filter
147
    {
148
        return $this->filter;
149
    }
150
 
151
    /**
152
     * Returns the collected code coverage data.
153
     */
154
    public function getData(bool $raw = false): ProcessedCodeCoverageData
155
    {
156
        if (!$raw) {
157
            if ($this->processUncoveredFiles) {
158
                $this->processUncoveredFilesFromFilter();
159
            } elseif ($this->includeUncoveredFiles) {
160
                $this->addUncoveredFilesFromFilter();
161
            }
162
        }
163
 
164
        return $this->data;
165
    }
166
 
167
    /**
168
     * Sets the coverage data.
169
     */
170
    public function setData(ProcessedCodeCoverageData $data): void
171
    {
172
        $this->data = $data;
173
    }
174
 
175
    /**
176
     * Returns the test data.
177
     */
178
    public function getTests(): array
179
    {
180
        return $this->tests;
181
    }
182
 
183
    /**
184
     * Sets the test data.
185
     */
186
    public function setTests(array $tests): void
187
    {
188
        $this->tests = $tests;
189
    }
190
 
191
    /**
192
     * Start collection of code coverage information.
193
     *
194
     * @param PhptTestCase|string|TestCase $id
195
     */
196
    public function start($id, bool $clear = false): void
197
    {
198
        if ($clear) {
199
            $this->clear();
200
        }
201
 
202
        $this->currentId = $id;
203
 
204
        $this->driver->start();
205
    }
206
 
207
    /**
208
     * Stop collection of code coverage information.
209
     *
210
     * @param array|false $linesToBeCovered
211
     */
212
    public function stop(bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): RawCodeCoverageData
213
    {
214
        if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) {
215
            throw new InvalidArgumentException(
216
                '$linesToBeCovered must be an array or false'
217
            );
218
        }
219
 
220
        $data = $this->driver->stop();
221
        $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed);
222
 
223
        $this->currentId = null;
224
 
225
        return $data;
226
    }
227
 
228
    /**
229
     * Appends code coverage data.
230
     *
231
     * @param PhptTestCase|string|TestCase $id
232
     * @param array|false                  $linesToBeCovered
233
     *
234
     * @throws ReflectionException
235
     * @throws TestIdMissingException
236
     * @throws UnintentionallyCoveredCodeException
237
     */
238
    public function append(RawCodeCoverageData $rawData, $id = null, bool $append = true, $linesToBeCovered = [], array $linesToBeUsed = []): void
239
    {
240
        if ($id === null) {
241
            $id = $this->currentId;
242
        }
243
 
244
        if ($id === null) {
245
            throw new TestIdMissingException;
246
        }
247
 
248
        $this->applyFilter($rawData);
249
 
250
        $this->applyExecutableLinesFilter($rawData);
251
 
252
        if ($this->useAnnotationsForIgnoringCode) {
253
            $this->applyIgnoredLinesFilter($rawData);
254
        }
255
 
256
        $this->data->initializeUnseenData($rawData);
257
 
258
        if (!$append) {
259
            return;
260
        }
261
 
262
        if ($id !== self::UNCOVERED_FILES) {
263
            $this->applyCoversAnnotationFilter(
264
                $rawData,
265
                $linesToBeCovered,
266
                $linesToBeUsed
267
            );
268
 
269
            if (empty($rawData->lineCoverage())) {
270
                return;
271
            }
272
 
273
            $size         = 'unknown';
274
            $status       = -1;
275
            $fromTestcase = false;
276
 
277
            if ($id instanceof TestCase) {
278
                $fromTestcase = true;
279
                $_size        = $id->getSize();
280
 
281
                if ($_size === Test::SMALL) {
282
                    $size = 'small';
283
                } elseif ($_size === Test::MEDIUM) {
284
                    $size = 'medium';
285
                } elseif ($_size === Test::LARGE) {
286
                    $size = 'large';
287
                }
288
 
289
                $status = $id->getStatus();
290
                $id     = get_class($id) . '::' . $id->getName();
291
            } elseif ($id instanceof PhptTestCase) {
292
                $fromTestcase = true;
293
                $size         = 'large';
294
                $id           = $id->getName();
295
            }
296
 
297
            $this->tests[$id] = ['size' => $size, 'status' => $status, 'fromTestcase' => $fromTestcase];
298
 
299
            $this->data->markCodeAsExecutedByTestCase($id, $rawData);
300
        }
301
    }
302
 
303
    /**
304
     * Merges the data from another instance.
305
     */
306
    public function merge(self $that): void
307
    {
308
        $this->filter->includeFiles(
309
            $that->filter()->files()
310
        );
311
 
312
        $this->data->merge($that->data);
313
 
314
        $this->tests = array_merge($this->tests, $that->getTests());
315
    }
316
 
317
    public function enableCheckForUnintentionallyCoveredCode(): void
318
    {
319
        $this->checkForUnintentionallyCoveredCode = true;
320
    }
321
 
322
    public function disableCheckForUnintentionallyCoveredCode(): void
323
    {
324
        $this->checkForUnintentionallyCoveredCode = false;
325
    }
326
 
327
    public function includeUncoveredFiles(): void
328
    {
329
        $this->includeUncoveredFiles = true;
330
    }
331
 
332
    public function excludeUncoveredFiles(): void
333
    {
334
        $this->includeUncoveredFiles = false;
335
    }
336
 
337
    public function processUncoveredFiles(): void
338
    {
339
        $this->processUncoveredFiles = true;
340
    }
341
 
342
    public function doNotProcessUncoveredFiles(): void
343
    {
344
        $this->processUncoveredFiles = false;
345
    }
346
 
347
    public function enableAnnotationsForIgnoringCode(): void
348
    {
349
        $this->useAnnotationsForIgnoringCode = true;
350
    }
351
 
352
    public function disableAnnotationsForIgnoringCode(): void
353
    {
354
        $this->useAnnotationsForIgnoringCode = false;
355
    }
356
 
357
    public function ignoreDeprecatedCode(): void
358
    {
359
        $this->ignoreDeprecatedCode = true;
360
    }
361
 
362
    public function doNotIgnoreDeprecatedCode(): void
363
    {
364
        $this->ignoreDeprecatedCode = false;
365
    }
366
 
367
    /**
368
     * @psalm-assert-if-true !null $this->cacheDirectory
369
     */
370
    public function cachesStaticAnalysis(): bool
371
    {
372
        return $this->cacheDirectory !== null;
373
    }
374
 
375
    public function cacheStaticAnalysis(string $directory): void
376
    {
377
        $this->cacheDirectory = $directory;
378
    }
379
 
380
    public function doNotCacheStaticAnalysis(): void
381
    {
382
        $this->cacheDirectory = null;
383
    }
384
 
385
    /**
386
     * @throws StaticAnalysisCacheNotConfiguredException
387
     */
388
    public function cacheDirectory(): string
389
    {
390
        if (!$this->cachesStaticAnalysis()) {
391
            throw new StaticAnalysisCacheNotConfiguredException(
392
                'The static analysis cache is not configured'
393
            );
394
        }
395
 
396
        return $this->cacheDirectory;
397
    }
398
 
399
    /**
400
     * @psalm-param class-string $className
401
     */
402
    public function excludeSubclassesOfThisClassFromUnintentionallyCoveredCodeCheck(string $className): void
403
    {
404
        $this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck[] = $className;
405
    }
406
 
407
    public function enableBranchAndPathCoverage(): void
408
    {
409
        $this->driver->enableBranchAndPathCoverage();
410
    }
411
 
412
    public function disableBranchAndPathCoverage(): void
413
    {
414
        $this->driver->disableBranchAndPathCoverage();
415
    }
416
 
417
    public function collectsBranchAndPathCoverage(): bool
418
    {
419
        return $this->driver->collectsBranchAndPathCoverage();
420
    }
421
 
422
    public function detectsDeadCode(): bool
423
    {
424
        return $this->driver->detectsDeadCode();
425
    }
426
 
427
    /**
428
     * Applies the @covers annotation filtering.
429
     *
430
     * @param array|false $linesToBeCovered
431
     *
432
     * @throws ReflectionException
433
     * @throws UnintentionallyCoveredCodeException
434
     */
435
    private function applyCoversAnnotationFilter(RawCodeCoverageData $rawData, $linesToBeCovered, array $linesToBeUsed): void
436
    {
437
        if ($linesToBeCovered === false) {
438
            $rawData->clear();
439
 
440
            return;
441
        }
442
 
443
        if (empty($linesToBeCovered)) {
444
            return;
445
        }
446
 
447
        if ($this->checkForUnintentionallyCoveredCode &&
448
            (!$this->currentId instanceof TestCase ||
449
            (!$this->currentId->isMedium() && !$this->currentId->isLarge()))) {
450
            $this->performUnintentionallyCoveredCodeCheck($rawData, $linesToBeCovered, $linesToBeUsed);
451
        }
452
 
453
        $rawLineData         = $rawData->lineCoverage();
454
        $filesWithNoCoverage = array_diff_key($rawLineData, $linesToBeCovered);
455
 
456
        foreach (array_keys($filesWithNoCoverage) as $fileWithNoCoverage) {
457
            $rawData->removeCoverageDataForFile($fileWithNoCoverage);
458
        }
459
 
460
        if (is_array($linesToBeCovered)) {
461
            foreach ($linesToBeCovered as $fileToBeCovered => $includedLines) {
462
                $rawData->keepLineCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
463
                $rawData->keepFunctionCoverageDataOnlyForLines($fileToBeCovered, $includedLines);
464
            }
465
        }
466
    }
467
 
468
    private function applyFilter(RawCodeCoverageData $data): void
469
    {
470
        if ($this->filter->isEmpty()) {
471
            return;
472
        }
473
 
474
        foreach (array_keys($data->lineCoverage()) as $filename) {
475
            if ($this->filter->isExcluded($filename)) {
476
                $data->removeCoverageDataForFile($filename);
477
            }
478
        }
479
    }
480
 
481
    private function applyExecutableLinesFilter(RawCodeCoverageData $data): void
482
    {
483
        foreach (array_keys($data->lineCoverage()) as $filename) {
484
            if (!$this->filter->isFile($filename)) {
485
                continue;
486
            }
487
 
488
            $linesToBranchMap = $this->analyser()->executableLinesIn($filename);
489
 
490
            $data->keepLineCoverageDataOnlyForLines(
491
                $filename,
492
                array_keys($linesToBranchMap)
493
            );
494
 
495
            $data->markExecutableLineByBranch(
496
                $filename,
497
                $linesToBranchMap
498
            );
499
        }
500
    }
501
 
502
    private function applyIgnoredLinesFilter(RawCodeCoverageData $data): void
503
    {
504
        foreach (array_keys($data->lineCoverage()) as $filename) {
505
            if (!$this->filter->isFile($filename)) {
506
                continue;
507
            }
508
 
509
            $data->removeCoverageDataForLines(
510
                $filename,
511
                $this->analyser()->ignoredLinesFor($filename)
512
            );
513
        }
514
    }
515
 
516
    /**
517
     * @throws UnintentionallyCoveredCodeException
518
     */
519
    private function addUncoveredFilesFromFilter(): void
520
    {
521
        $uncoveredFiles = array_diff(
522
            $this->filter->files(),
523
            $this->data->coveredFiles()
524
        );
525
 
526
        foreach ($uncoveredFiles as $uncoveredFile) {
527
            if ($this->filter->isFile($uncoveredFile)) {
528
                $this->append(
529
                    RawCodeCoverageData::fromUncoveredFile(
530
                        $uncoveredFile,
531
                        $this->analyser()
532
                    ),
533
                    self::UNCOVERED_FILES
534
                );
535
            }
536
        }
537
    }
538
 
539
    /**
540
     * @throws UnintentionallyCoveredCodeException
541
     */
542
    private function processUncoveredFilesFromFilter(): void
543
    {
544
        $uncoveredFiles = array_diff(
545
            $this->filter->files(),
546
            $this->data->coveredFiles()
547
        );
548
 
549
        $this->driver->start();
550
 
551
        foreach ($uncoveredFiles as $uncoveredFile) {
552
            if ($this->filter->isFile($uncoveredFile)) {
553
                include_once $uncoveredFile;
554
            }
555
        }
556
 
557
        $this->append($this->driver->stop(), self::UNCOVERED_FILES);
558
    }
559
 
560
    /**
561
     * @throws ReflectionException
562
     * @throws UnintentionallyCoveredCodeException
563
     */
564
    private function performUnintentionallyCoveredCodeCheck(RawCodeCoverageData $data, array $linesToBeCovered, array $linesToBeUsed): void
565
    {
566
        $allowedLines = $this->getAllowedLines(
567
            $linesToBeCovered,
568
            $linesToBeUsed
569
        );
570
 
571
        $unintentionallyCoveredUnits = [];
572
 
573
        foreach ($data->lineCoverage() as $file => $_data) {
574
            foreach ($_data as $line => $flag) {
575
                if ($flag === 1 && !isset($allowedLines[$file][$line])) {
576
                    $unintentionallyCoveredUnits[] = $this->wizard->lookup($file, $line);
577
                }
578
            }
579
        }
580
 
581
        $unintentionallyCoveredUnits = $this->processUnintentionallyCoveredUnits($unintentionallyCoveredUnits);
582
 
583
        if (!empty($unintentionallyCoveredUnits)) {
584
            throw new UnintentionallyCoveredCodeException(
585
                $unintentionallyCoveredUnits
586
            );
587
        }
588
    }
589
 
590
    private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed): array
591
    {
592
        $allowedLines = [];
593
 
594
        foreach (array_keys($linesToBeCovered) as $file) {
595
            if (!isset($allowedLines[$file])) {
596
                $allowedLines[$file] = [];
597
            }
598
 
599
            $allowedLines[$file] = array_merge(
600
                $allowedLines[$file],
601
                $linesToBeCovered[$file]
602
            );
603
        }
604
 
605
        foreach (array_keys($linesToBeUsed) as $file) {
606
            if (!isset($allowedLines[$file])) {
607
                $allowedLines[$file] = [];
608
            }
609
 
610
            $allowedLines[$file] = array_merge(
611
                $allowedLines[$file],
612
                $linesToBeUsed[$file]
613
            );
614
        }
615
 
616
        foreach (array_keys($allowedLines) as $file) {
617
            $allowedLines[$file] = array_flip(
618
                array_unique($allowedLines[$file])
619
            );
620
        }
621
 
622
        return $allowedLines;
623
    }
624
 
625
    /**
626
     * @throws ReflectionException
627
     */
628
    private function processUnintentionallyCoveredUnits(array $unintentionallyCoveredUnits): array
629
    {
630
        $unintentionallyCoveredUnits = array_unique($unintentionallyCoveredUnits);
631
        sort($unintentionallyCoveredUnits);
632
 
633
        foreach (array_keys($unintentionallyCoveredUnits) as $k => $v) {
634
            $unit = explode('::', $unintentionallyCoveredUnits[$k]);
635
 
636
            if (count($unit) !== 2) {
637
                continue;
638
            }
639
 
640
            try {
641
                $class = new ReflectionClass($unit[0]);
642
 
643
                foreach ($this->parentClassesExcludedFromUnintentionallyCoveredCodeCheck as $parentClass) {
644
                    if ($class->isSubclassOf($parentClass)) {
645
                        unset($unintentionallyCoveredUnits[$k]);
646
 
647
                        break;
648
                    }
649
                }
650
            } catch (\ReflectionException $e) {
651
                throw new ReflectionException(
652
                    $e->getMessage(),
653
                    $e->getCode(),
654
                    $e
655
                );
656
            }
657
        }
658
 
659
        return array_values($unintentionallyCoveredUnits);
660
    }
661
 
662
    private function analyser(): FileAnalyser
663
    {
664
        if ($this->analyser !== null) {
665
            return $this->analyser;
666
        }
667
 
668
        $this->analyser = new ParsingFileAnalyser(
669
            $this->useAnnotationsForIgnoringCode,
670
            $this->ignoreDeprecatedCode
671
        );
672
 
673
        if ($this->cachesStaticAnalysis()) {
674
            $this->analyser = new CachingFileAnalyser(
675
                $this->cacheDirectory,
676
                $this->analyser
677
            );
678
        }
679
 
680
        return $this->analyser;
681
    }
682
}