Passed
Push — develop ( 30cf64...589229 )
by Guillaume
06:18 queued 04:10
created

Test::getMissingRequirements()   F

Complexity

Conditions 35
Paths 4800

Size

Total Lines 120
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 35
eloc 68
nc 4800
nop 2
dl 0
loc 120
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of PHPUnit.
4
 *
5
 * (c) Sebastian Bergmann <[email protected]>
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 PHPUnit\Util;
11
12
use PHPUnit\Framework\Assert;
13
use PHPUnit\Framework\CodeCoverageException;
14
use PHPUnit\Framework\InvalidCoversTargetException;
15
use PHPUnit\Framework\SelfDescribing;
16
use PHPUnit\Framework\TestCase;
17
use PHPUnit\Framework\Warning;
18
use PHPUnit\Runner\Version;
19
use PHPUnit\Util\Annotation\Registry;
20
use SebastianBergmann\CodeUnit\CodeUnitCollection;
21
use SebastianBergmann\CodeUnit\InvalidCodeUnitException;
22
use SebastianBergmann\CodeUnit\Mapper;
23
use SebastianBergmann\Environment\OperatingSystem;
24
25
/**
26
 * @internal This class is not covered by the backward compatibility promise for PHPUnit
27
 */
28
final class Test
29
{
30
    /**
31
     * @var int
32
     */
33
    public const UNKNOWN = -1;
34
35
    /**
36
     * @var int
37
     */
38
    public const SMALL = 0;
39
40
    /**
41
     * @var int
42
     */
43
    public const MEDIUM = 1;
44
45
    /**
46
     * @var int
47
     */
48
    public const LARGE = 2;
49
50
    /**
51
     * @var array
52
     */
53
    private static $hookMethods = [];
54
55
    /**
56
     * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
57
     */
58
    public static function describe(\PHPUnit\Framework\Test $test): array
59
    {
60
        if ($test instanceof TestCase) {
61
            return [\get_class($test), $test->getName()];
62
        }
63
64
        if ($test instanceof SelfDescribing) {
65
            return ['', $test->toString()];
66
        }
67
68
        return ['', \get_class($test)];
69
    }
70
71
    public static function describeAsString(\PHPUnit\Framework\Test $test): string
72
    {
73
        if ($test instanceof SelfDescribing) {
74
            return $test->toString();
75
        }
76
77
        return \get_class($test);
78
    }
79
80
    /**
81
     * @throws CodeCoverageException
82
     *
83
     * @return array|bool
84
     * @psalm-param class-string $className
85
     */
86
    public static function getLinesToBeCovered(string $className, string $methodName)
87
    {
88
        $annotations = self::parseTestMethodAnnotations(
89
            $className,
90
            $methodName
91
        );
92
93
        if (!self::shouldCoversAnnotationBeUsed($annotations)) {
94
            return false;
95
        }
96
97
        return self::getLinesToBeCoveredOrUsed($className, $methodName, 'covers');
98
    }
99
100
    /**
101
     * Returns lines of code specified with the @uses annotation.
102
     *
103
     * @throws CodeCoverageException
104
     * @psalm-param class-string $className
105
     */
106
    public static function getLinesToBeUsed(string $className, string $methodName): array
107
    {
108
        return self::getLinesToBeCoveredOrUsed($className, $methodName, 'uses');
109
    }
110
111
    public static function requiresCodeCoverageDataCollection(TestCase $test): bool
112
    {
113
        $annotations = $test->getAnnotations();
114
115
        // If there is no @covers annotation but a @coversNothing annotation on
116
        // the test method then code coverage data does not need to be collected
117
        if (isset($annotations['method']['coversNothing'])) {
118
            return false;
119
        }
120
121
        // If there is at least one @covers annotation then
122
        // code coverage data needs to be collected
123
        if (isset($annotations['method']['covers'])) {
124
            return true;
125
        }
126
127
        // If there is no @covers annotation but a @coversNothing annotation
128
        // then code coverage data does not need to be collected
129
        if (isset($annotations['class']['coversNothing'])) {
130
            return false;
131
        }
132
133
        // If there is no @coversNothing annotation then
134
        // code coverage data may be collected
135
        return true;
136
    }
137
138
    /**
139
     * @throws Exception
140
     * @psalm-param class-string $className
141
     */
142
    public static function getRequirements(string $className, string $methodName): array
143
    {
144
        return self::mergeArraysRecursively(
145
            Registry::getInstance()->forClassName($className)->requirements(),
146
            Registry::getInstance()->forMethod($className, $methodName)->requirements()
147
        );
148
    }
149
150
    /**
151
     * Returns the missing requirements for a test.
152
     *
153
     * @throws Exception
154
     * @throws Warning
155
     * @psalm-param class-string $className
156
     */
157
    public static function getMissingRequirements(string $className, string $methodName): array
158
    {
159
        $required = self::getRequirements($className, $methodName);
160
        $missing  = [];
161
        $hint     = null;
162
163
        if (!empty($required['PHP'])) {
164
            $operator = new VersionComparisonOperator(empty($required['PHP']['operator']) ? '>=' : $required['PHP']['operator']);
165
166
            if (!\version_compare(\PHP_VERSION, $required['PHP']['version'], $operator->asString())) {
167
                $missing[] = \sprintf('PHP %s %s is required.', $operator->asString(), $required['PHP']['version']);
168
                $hint      = 'PHP';
169
            }
170
        } elseif (!empty($required['PHP_constraint'])) {
171
            $version = new \PharIo\Version\Version(self::sanitizeVersionNumber(\PHP_VERSION));
172
173
            if (!$required['PHP_constraint']['constraint']->complies($version)) {
174
                $missing[] = \sprintf(
175
                    'PHP version does not match the required constraint %s.',
176
                    $required['PHP_constraint']['constraint']->asString()
177
                );
178
179
                $hint = 'PHP_constraint';
180
            }
181
        }
182
183
        if (!empty($required['PHPUnit'])) {
184
            $phpunitVersion = Version::id();
185
186
            $operator = new VersionComparisonOperator(empty($required['PHPUnit']['operator']) ? '>=' : $required['PHPUnit']['operator']);
187
188
            if (!\version_compare($phpunitVersion, $required['PHPUnit']['version'], $operator->asString())) {
189
                $missing[] = \sprintf('PHPUnit %s %s is required.', $operator->asString(), $required['PHPUnit']['version']);
190
                $hint      = $hint ?? 'PHPUnit';
191
            }
192
        } elseif (!empty($required['PHPUnit_constraint'])) {
193
            $phpunitVersion = new \PharIo\Version\Version(self::sanitizeVersionNumber(Version::id()));
194
195
            if (!$required['PHPUnit_constraint']['constraint']->complies($phpunitVersion)) {
196
                $missing[] = \sprintf(
197
                    'PHPUnit version does not match the required constraint %s.',
198
                    $required['PHPUnit_constraint']['constraint']->asString()
199
                );
200
201
                $hint = $hint ?? 'PHPUnit_constraint';
202
            }
203
        }
204
205
        if (!empty($required['OSFAMILY']) && $required['OSFAMILY'] !== (new OperatingSystem)->getFamily()) {
206
            $missing[] = \sprintf('Operating system %s is required.', $required['OSFAMILY']);
207
            $hint      = $hint ?? 'OSFAMILY';
208
        }
209
210
        if (!empty($required['OS'])) {
211
            $requiredOsPattern = \sprintf('/%s/i', \addcslashes($required['OS'], '/'));
212
213
            if (!\preg_match($requiredOsPattern, \PHP_OS)) {
214
                $missing[] = \sprintf('Operating system matching %s is required.', $requiredOsPattern);
215
                $hint      = $hint ?? 'OS';
216
            }
217
        }
218
219
        if (!empty($required['functions'])) {
220
            foreach ($required['functions'] as $function) {
221
                $pieces = \explode('::', $function);
222
223
                if (\count($pieces) === 2 && \class_exists($pieces[0]) && \method_exists($pieces[0], $pieces[1])) {
224
                    continue;
225
                }
226
227
                if (\function_exists($function)) {
228
                    continue;
229
                }
230
231
                $missing[] = \sprintf('Function %s is required.', $function);
232
                $hint      = $hint ?? 'function_' . $function;
233
            }
234
        }
235
236
        if (!empty($required['setting'])) {
237
            foreach ($required['setting'] as $setting => $value) {
238
                if (\ini_get($setting) !== $value) {
239
                    $missing[] = \sprintf('Setting "%s" must be "%s".', $setting, $value);
240
                    $hint      = $hint ?? '__SETTING_' . $setting;
241
                }
242
            }
243
        }
244
245
        if (!empty($required['extensions'])) {
246
            foreach ($required['extensions'] as $extension) {
247
                if (isset($required['extension_versions'][$extension])) {
248
                    continue;
249
                }
250
251
                if (!\extension_loaded($extension)) {
252
                    $missing[] = \sprintf('Extension %s is required.', $extension);
253
                    $hint      = $hint ?? 'extension_' . $extension;
254
                }
255
            }
256
        }
257
258
        if (!empty($required['extension_versions'])) {
259
            foreach ($required['extension_versions'] as $extension => $req) {
260
                $actualVersion = \phpversion($extension);
261
262
                $operator = new VersionComparisonOperator(empty($req['operator']) ? '>=' : $req['operator']);
263
264
                if ($actualVersion === false || !\version_compare($actualVersion, $req['version'], $operator->asString())) {
265
                    $missing[] = \sprintf('Extension %s %s %s is required.', $extension, $operator->asString(), $req['version']);
266
                    $hint      = $hint ?? 'extension_' . $extension;
267
                }
268
            }
269
        }
270
271
        if ($hint && isset($required['__OFFSET'])) {
272
            \array_unshift($missing, '__OFFSET_FILE=' . $required['__OFFSET']['__FILE']);
273
            \array_unshift($missing, '__OFFSET_LINE=' . ($required['__OFFSET'][$hint] ?? 1));
274
        }
275
276
        return $missing;
277
    }
278
279
    /**
280
     * Returns the provided data for a method.
281
     *
282
     * @throws Exception
283
     * @psalm-param class-string $className
284
     */
285
    public static function getProvidedData(string $className, string $methodName): ?array
286
    {
287
        return Registry::getInstance()->forMethod($className, $methodName)->getProvidedData();
288
    }
289
290
    /**
291
     * @psalm-param class-string $className
292
     */
293
    public static function parseTestMethodAnnotations(string $className, ?string $methodName = ''): array
294
    {
295
        $registry = Registry::getInstance();
296
297
        if ($methodName !== null) {
298
            try {
299
                return [
300
                    'method' => $registry->forMethod($className, $methodName)->symbolAnnotations(),
301
                    'class'  => $registry->forClassName($className)->symbolAnnotations(),
302
                ];
303
            } catch (Exception $methodNotFound) {
304
                // ignored
305
            }
306
        }
307
308
        return [
309
            'method' => null,
310
            'class'  => $registry->forClassName($className)->symbolAnnotations(),
311
        ];
312
    }
313
314
    /**
315
     * @psalm-param class-string $className
316
     */
317
    public static function getInlineAnnotations(string $className, string $methodName): array
318
    {
319
        return Registry::getInstance()->forMethod($className, $methodName)->getInlineAnnotations();
320
    }
321
322
    /** @psalm-param class-string $className */
323
    public static function getBackupSettings(string $className, string $methodName): array
324
    {
325
        return [
326
            'backupGlobals' => self::getBooleanAnnotationSetting(
327
                $className,
328
                $methodName,
329
                'backupGlobals'
330
            ),
331
            'backupStaticAttributes' => self::getBooleanAnnotationSetting(
332
                $className,
333
                $methodName,
334
                'backupStaticAttributes'
335
            ),
336
        ];
337
    }
338
339
    /** @psalm-param class-string $className */
340
    public static function getDependencies(string $className, string $methodName): array
341
    {
342
        $annotations = self::parseTestMethodAnnotations(
343
            $className,
344
            $methodName
345
        );
346
347
        $dependencies = $annotations['class']['depends'] ?? [];
348
349
        if (isset($annotations['method']['depends'])) {
350
            $dependencies = \array_merge(
351
                $dependencies,
352
                $annotations['method']['depends']
353
            );
354
        }
355
356
        return \array_unique($dependencies);
357
    }
358
359
    /** @psalm-param class-string $className */
360
    public static function getGroups(string $className, ?string $methodName = ''): array
361
    {
362
        $annotations = self::parseTestMethodAnnotations(
363
            $className,
364
            $methodName
365
        );
366
367
        $groups = [];
368
369
        if (isset($annotations['method']['author'])) {
370
            $groups[] = $annotations['method']['author'];
371
        } elseif (isset($annotations['class']['author'])) {
372
            $groups[] = $annotations['class']['author'];
373
        }
374
375
        if (isset($annotations['class']['group'])) {
376
            $groups[] = $annotations['class']['group'];
377
        }
378
379
        if (isset($annotations['method']['group'])) {
380
            $groups[] = $annotations['method']['group'];
381
        }
382
383
        if (isset($annotations['class']['ticket'])) {
384
            $groups[] = $annotations['class']['ticket'];
385
        }
386
387
        if (isset($annotations['method']['ticket'])) {
388
            $groups[] = $annotations['method']['ticket'];
389
        }
390
391
        foreach (['method', 'class'] as $element) {
392
            foreach (['small', 'medium', 'large'] as $size) {
393
                if (isset($annotations[$element][$size])) {
394
                    $groups[] = [$size];
395
396
                    break 2;
397
                }
398
            }
399
        }
400
401
        return \array_unique(\array_merge([], ...$groups));
402
    }
403
404
    /** @psalm-param class-string $className */
405
    public static function getSize(string $className, ?string $methodName): int
406
    {
407
        $groups = \array_flip(self::getGroups($className, $methodName));
408
409
        if (isset($groups['large'])) {
410
            return self::LARGE;
411
        }
412
413
        if (isset($groups['medium'])) {
414
            return self::MEDIUM;
415
        }
416
417
        if (isset($groups['small'])) {
418
            return self::SMALL;
419
        }
420
421
        return self::UNKNOWN;
422
    }
423
424
    /** @psalm-param class-string $className */
425
    public static function getProcessIsolationSettings(string $className, string $methodName): bool
426
    {
427
        $annotations = self::parseTestMethodAnnotations(
428
            $className,
429
            $methodName
430
        );
431
432
        return isset($annotations['class']['runTestsInSeparateProcesses']) || isset($annotations['method']['runInSeparateProcess']);
433
    }
434
435
    /** @psalm-param class-string $className */
436
    public static function getClassProcessIsolationSettings(string $className, string $methodName): bool
437
    {
438
        $annotations = self::parseTestMethodAnnotations(
439
            $className,
440
            $methodName
441
        );
442
443
        return isset($annotations['class']['runClassInSeparateProcess']);
444
    }
445
446
    /** @psalm-param class-string $className */
447
    public static function getPreserveGlobalStateSettings(string $className, string $methodName): ?bool
448
    {
449
        return self::getBooleanAnnotationSetting(
450
            $className,
451
            $methodName,
452
            'preserveGlobalState'
453
        );
454
    }
455
456
    /** @psalm-param class-string $className */
457
    public static function getHookMethods(string $className): array
458
    {
459
        if (!\class_exists($className, false)) {
460
            return self::emptyHookMethodsArray();
461
        }
462
463
        if (!isset(self::$hookMethods[$className])) {
464
            self::$hookMethods[$className] = self::emptyHookMethodsArray();
465
466
            try {
467
                foreach ((new \ReflectionClass($className))->getMethods() as $method) {
468
                    if ($method->getDeclaringClass()->getName() === Assert::class) {
469
                        continue;
470
                    }
471
472
                    if ($method->getDeclaringClass()->getName() === TestCase::class) {
473
                        continue;
474
                    }
475
476
                    $docBlock = Registry::getInstance()->forMethod($className, $method->getName());
477
478
                    if ($method->isStatic()) {
479
                        if ($docBlock->isHookToBeExecutedBeforeClass()) {
480
                            \array_unshift(
481
                                self::$hookMethods[$className]['beforeClass'],
482
                                $method->getName()
483
                            );
484
                        }
485
486
                        if ($docBlock->isHookToBeExecutedAfterClass()) {
487
                            self::$hookMethods[$className]['afterClass'][] = $method->getName();
488
                        }
489
                    }
490
491
                    if ($docBlock->isToBeExecutedBeforeTest()) {
492
                        \array_unshift(
493
                            self::$hookMethods[$className]['before'],
494
                            $method->getName()
495
                        );
496
                    }
497
498
                    if ($docBlock->isToBeExecutedAsPreCondition()) {
499
                        \array_unshift(
500
                            self::$hookMethods[$className]['preCondition'],
501
                            $method->getName()
502
                        );
503
                    }
504
505
                    if ($docBlock->isToBeExecutedAsPostCondition()) {
506
                        self::$hookMethods[$className]['postCondition'][] = $method->getName();
507
                    }
508
509
                    if ($docBlock->isToBeExecutedAfterTest()) {
510
                        self::$hookMethods[$className]['after'][] = $method->getName();
511
                    }
512
                }
513
            } catch (\ReflectionException $e) {
514
            }
515
        }
516
517
        return self::$hookMethods[$className];
518
    }
519
520
    public static function isTestMethod(\ReflectionMethod $method): bool
521
    {
522
        if (\strpos($method->getName(), 'test') === 0) {
523
            return true;
524
        }
525
526
        return \array_key_exists(
527
            'test',
528
            Registry::getInstance()->forMethod(
529
                $method->getDeclaringClass()->getName(),
530
                $method->getName()
531
            )
532
            ->symbolAnnotations()
533
        );
534
    }
535
536
    /**
537
     * @throws CodeCoverageException
538
     * @psalm-param class-string $className
539
     */
540
    private static function getLinesToBeCoveredOrUsed(string $className, string $methodName, string $mode): array
541
    {
542
        $annotations = self::parseTestMethodAnnotations(
543
            $className,
544
            $methodName
545
        );
546
547
        $classShortcut = null;
548
549
        if (!empty($annotations['class'][$mode . 'DefaultClass'])) {
550
            if (\count($annotations['class'][$mode . 'DefaultClass']) > 1) {
551
                throw new CodeCoverageException(
552
                    \sprintf(
553
                        'More than one @%sClass annotation in class or interface "%s".',
554
                        $mode,
555
                        $className
556
                    )
557
                );
558
            }
559
560
            $classShortcut = $annotations['class'][$mode . 'DefaultClass'][0];
561
        }
562
563
        $list = $annotations['class'][$mode] ?? [];
564
565
        if (isset($annotations['method'][$mode])) {
566
            $list = \array_merge($list, $annotations['method'][$mode]);
567
        }
568
569
        $codeUnits = CodeUnitCollection::fromArray([]);
570
        $mapper    = new Mapper;
571
572
        foreach (\array_unique($list) as $element) {
573
            if ($classShortcut && \strncmp($element, '::', 2) === 0) {
574
                $element = $classShortcut . $element;
575
            }
576
577
            $element = \preg_replace('/[\s()]+$/', '', $element);
578
            $element = \explode(' ', $element);
579
            $element = $element[0];
580
581
            if ($mode === 'covers' && \interface_exists($element)) {
582
                throw new InvalidCoversTargetException(
583
                    \sprintf(
584
                        'Trying to @cover interface "%s".',
585
                        $element
586
                    )
587
                );
588
            }
589
590
            try {
591
                $codeUnits = $codeUnits->mergeWith($mapper->stringToCodeUnits($element));
592
            } catch (InvalidCodeUnitException $e) {
593
                throw new InvalidCoversTargetException(
594
                    \sprintf(
595
                        '"@%s %s" is invalid',
596
                        $mode,
597
                        $element
598
                    ),
599
                    (int) $e->getCode(),
600
                    $e
601
                );
602
            }
603
        }
604
605
        return $mapper->codeUnitsToSourceLines($codeUnits);
606
    }
607
608
    private static function emptyHookMethodsArray(): array
609
    {
610
        return [
611
            'beforeClass'   => ['setUpBeforeClass'],
612
            'before'        => ['setUp'],
613
            'preCondition'  => ['assertPreConditions'],
614
            'postCondition' => ['assertPostConditions'],
615
            'after'         => ['tearDown'],
616
            'afterClass'    => ['tearDownAfterClass'],
617
        ];
618
    }
619
620
    /** @psalm-param class-string $className */
621
    private static function getBooleanAnnotationSetting(string $className, ?string $methodName, string $settingName): ?bool
622
    {
623
        $annotations = self::parseTestMethodAnnotations(
624
            $className,
625
            $methodName
626
        );
627
628
        if (isset($annotations['method'][$settingName])) {
629
            if ($annotations['method'][$settingName][0] === 'enabled') {
630
                return true;
631
            }
632
633
            if ($annotations['method'][$settingName][0] === 'disabled') {
634
                return false;
635
            }
636
        }
637
638
        if (isset($annotations['class'][$settingName])) {
639
            if ($annotations['class'][$settingName][0] === 'enabled') {
640
                return true;
641
            }
642
643
            if ($annotations['class'][$settingName][0] === 'disabled') {
644
                return false;
645
            }
646
        }
647
648
        return null;
649
    }
650
651
    /**
652
     * Trims any extensions from version string that follows after
653
     * the <major>.<minor>[.<patch>] format
654
     */
655
    private static function sanitizeVersionNumber(string $version)
656
    {
657
        return \preg_replace(
658
            '/^(\d+\.\d+(?:.\d+)?).*$/',
659
            '$1',
660
            $version
661
        );
662
    }
663
664
    private static function shouldCoversAnnotationBeUsed(array $annotations): bool
665
    {
666
        if (isset($annotations['method']['coversNothing'])) {
667
            return false;
668
        }
669
670
        if (isset($annotations['method']['covers'])) {
671
            return true;
672
        }
673
674
        if (isset($annotations['class']['coversNothing'])) {
675
            return false;
676
        }
677
678
        return true;
679
    }
680
681
    /**
682
     * Merge two arrays together.
683
     *
684
     * If an integer key exists in both arrays and preserveNumericKeys is false, the value
685
     * from the second array will be appended to the first array. If both values are arrays, they
686
     * are merged together, else the value of the second array overwrites the one of the first array.
687
     *
688
     * This implementation is copied from https://github.com/zendframework/zend-stdlib/blob/76b653c5e99b40eccf5966e3122c90615134ae46/src/ArrayUtils.php
689
     *
690
     * Zend Framework (http://framework.zend.com/)
691
     *
692
     * @link      http://github.com/zendframework/zf2 for the canonical source repository
693
     *
694
     * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
695
     * @license   http://framework.zend.com/license/new-bsd New BSD License
696
     */
697
    private static function mergeArraysRecursively(array $a, array $b): array
698
    {
699
        foreach ($b as $key => $value) {
700
            if (\array_key_exists($key, $a)) {
701
                if (\is_int($key)) {
702
                    $a[] = $value;
703
                } elseif (\is_array($value) && \is_array($a[$key])) {
704
                    $a[$key] = self::mergeArraysRecursively($a[$key], $value);
705
                } else {
706
                    $a[$key] = $value;
707
                }
708
            } else {
709
                $a[$key] = $value;
710
            }
711
        }
712
713
        return $a;
714
    }
715
}
716