Passed
Pull Request — master (#1)
by Guillaume
04:03
created

TestSuite   F

Complexity

Total Complexity 120

Size/Duplication

Total Lines 806
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 300
dl 0
loc 806
rs 2
c 0
b 0
f 0
wmc 120

How to fix   Complexity   

Complex Class

Complex classes like TestSuite often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TestSuite, and based on these observations, apply Extract Interface, too.

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\Framework;
11
12
use PHPUnit\Runner\BaseTestRunner;
13
use PHPUnit\Runner\Filter\Factory;
14
use PHPUnit\Runner\PhptTestCase;
15
use PHPUnit\Util\FileLoader;
16
use PHPUnit\Util\Test as TestUtil;
17
18
/**
19
 * @internal This class is not covered by the backward compatibility promise for PHPUnit
20
 */
21
class TestSuite implements \IteratorAggregate, SelfDescribing, Test
22
{
23
    /**
24
     * Enable or disable the backup and restoration of the $GLOBALS array.
25
     *
26
     * @var bool
27
     */
28
    protected $backupGlobals;
29
30
    /**
31
     * Enable or disable the backup and restoration of static attributes.
32
     *
33
     * @var bool
34
     */
35
    protected $backupStaticAttributes;
36
37
    /**
38
     * @var bool
39
     */
40
    protected $runTestInSeparateProcess = false;
41
42
    /**
43
     * The name of the test suite.
44
     *
45
     * @var string
46
     */
47
    protected $name = '';
48
49
    /**
50
     * The test groups of the test suite.
51
     *
52
     * @var array
53
     */
54
    protected $groups = [];
55
56
    /**
57
     * The tests in the test suite.
58
     *
59
     * @var Test[]
60
     */
61
    protected $tests = [];
62
63
    /**
64
     * The number of tests in the test suite.
65
     *
66
     * @var int
67
     */
68
    protected $numTests = -1;
69
70
    /**
71
     * @var bool
72
     */
73
    protected $testCase = false;
74
75
    /**
76
     * @var string[]
77
     */
78
    protected $foundClasses = [];
79
80
    /**
81
     * Last count of tests in this suite.
82
     *
83
     * @var null|int
84
     */
85
    private $cachedNumTests;
86
87
    /**
88
     * @var bool
89
     */
90
    private $beStrictAboutChangesToGlobalState;
91
92
    /**
93
     * @var Factory
94
     */
95
    private $iteratorFilter;
96
97
    /**
98
     * @var string[]
99
     */
100
    private $declaredClasses;
101
102
    /**
103
     * @psalm-var array<int,string>
104
     */
105
    private $warnings = [];
106
107
    /**
108
     * Constructs a new TestSuite:
109
     *
110
     *   - PHPUnit\Framework\TestSuite() constructs an empty TestSuite.
111
     *
112
     *   - PHPUnit\Framework\TestSuite(ReflectionClass) constructs a
113
     *     TestSuite from the given class.
114
     *
115
     *   - PHPUnit\Framework\TestSuite(ReflectionClass, String)
116
     *     constructs a TestSuite from the given class with the given
117
     *     name.
118
     *
119
     *   - PHPUnit\Framework\TestSuite(String) either constructs a
120
     *     TestSuite from the given class (if the passed string is the
121
     *     name of an existing class) or constructs an empty TestSuite
122
     *     with the given name.
123
     *
124
     * @param \ReflectionClass|string $theClass
125
     *
126
     * @throws Exception
127
     */
128
    public function __construct($theClass = '', string $name = '')
129
    {
130
        if (!\is_string($theClass) && !$theClass instanceof \ReflectionClass) {
131
            throw InvalidArgumentException::create(
132
                1,
133
                'ReflectionClass object or string'
134
            );
135
        }
136
137
        $this->declaredClasses = \get_declared_classes();
138
139
        if (!$theClass instanceof \ReflectionClass) {
140
            if (\class_exists($theClass, true)) {
141
                if ($name === '') {
142
                    $name = $theClass;
143
                }
144
145
                try {
146
                    $theClass = new \ReflectionClass($theClass);
147
                    // @codeCoverageIgnoreStart
148
                } catch (\ReflectionException $e) {
149
                    throw new Exception(
150
                        $e->getMessage(),
151
                        (int) $e->getCode(),
152
                        $e
153
                    );
154
                }
155
                // @codeCoverageIgnoreEnd
156
            } else {
157
                $this->setName($theClass);
158
159
                return;
160
            }
161
        }
162
163
        if (!$theClass->isSubclassOf(TestCase::class)) {
164
            $this->setName((string) $theClass);
165
166
            return;
167
        }
168
169
        if ($name !== '') {
170
            $this->setName($name);
171
        } else {
172
            $this->setName($theClass->getName());
173
        }
174
175
        $constructor = $theClass->getConstructor();
176
177
        if ($constructor !== null &&
178
            !$constructor->isPublic()) {
179
            $this->addTest(
180
                new WarningTestCase(
181
                    \sprintf(
182
                        'Class "%s" has no public constructor.',
183
                        $theClass->getName()
184
                    )
185
                )
186
            );
187
188
            return;
189
        }
190
191
        foreach ($theClass->getMethods() as $method) {
192
            if ($method->getDeclaringClass()->getName() === Assert::class) {
193
                continue;
194
            }
195
196
            if ($method->getDeclaringClass()->getName() === TestCase::class) {
197
                continue;
198
            }
199
200
            $this->addTestMethod($theClass, $method);
201
        }
202
203
        if (empty($this->tests)) {
204
            $this->addTest(
205
                new WarningTestCase(
206
                    \sprintf(
207
                        'No tests found in class "%s".',
208
                        $theClass->getName()
209
                    )
210
                )
211
            );
212
        }
213
214
        $this->testCase = true;
215
    }
216
217
    /**
218
     * Returns a string representation of the test suite.
219
     */
220
    public function toString(): string
221
    {
222
        return $this->getName();
223
    }
224
225
    /**
226
     * Adds a test to the suite.
227
     *
228
     * @param array $groups
229
     */
230
    public function addTest(Test $test, $groups = []): void
231
    {
232
        try {
233
            $class = new \ReflectionClass($test);
234
            // @codeCoverageIgnoreStart
235
        } catch (\ReflectionException $e) {
236
            throw new Exception(
237
                $e->getMessage(),
238
                (int) $e->getCode(),
239
                $e
240
            );
241
        }
242
        // @codeCoverageIgnoreEnd
243
244
        if (!$class->isAbstract()) {
245
            $this->tests[]  = $test;
246
            $this->numTests = -1;
247
248
            if ($test instanceof self && empty($groups)) {
249
                $groups = $test->getGroups();
250
            }
251
252
            if (empty($groups)) {
253
                $groups = ['default'];
254
            }
255
256
            foreach ($groups as $group) {
257
                if (!isset($this->groups[$group])) {
258
                    $this->groups[$group] = [$test];
259
                } else {
260
                    $this->groups[$group][] = $test;
261
                }
262
            }
263
264
            if ($test instanceof TestCase) {
265
                $test->setGroups($groups);
266
            }
267
        }
268
    }
269
270
    /**
271
     * Adds the tests from the given class to the suite.
272
     *
273
     * @param object|string $testClass
274
     *
275
     * @throws Exception
276
     */
277
    public function addTestSuite($testClass): void
278
    {
279
        if (!(\is_object($testClass) || (\is_string($testClass) && \class_exists($testClass)))) {
280
            throw InvalidArgumentException::create(
281
                1,
282
                'class name or object'
283
            );
284
        }
285
286
        if (!\is_object($testClass)) {
287
            try {
288
                $testClass = new \ReflectionClass($testClass);
289
                // @codeCoverageIgnoreStart
290
            } catch (\ReflectionException $e) {
291
                throw new Exception(
292
                    $e->getMessage(),
293
                    (int) $e->getCode(),
294
                    $e
295
                );
296
            }
297
            // @codeCoverageIgnoreEnd
298
        }
299
300
        if ($testClass instanceof self) {
301
            $this->addTest($testClass);
302
        } elseif ($testClass instanceof \ReflectionClass) {
303
            $suiteMethod = false;
304
305
            if (!$testClass->isAbstract() && $testClass->hasMethod(BaseTestRunner::SUITE_METHODNAME)) {
306
                try {
307
                    $method = $testClass->getMethod(
308
                        BaseTestRunner::SUITE_METHODNAME
309
                    );
310
                    // @codeCoverageIgnoreStart
311
                } catch (\ReflectionException $e) {
312
                    throw new Exception(
313
                        $e->getMessage(),
314
                        (int) $e->getCode(),
315
                        $e
316
                    );
317
                }
318
                // @codeCoverageIgnoreEnd
319
320
                if ($method->isStatic()) {
321
                    $this->addTest(
322
                        $method->invoke(null, $testClass->getName())
323
                    );
324
325
                    $suiteMethod = true;
326
                }
327
            }
328
329
            if (!$suiteMethod && !$testClass->isAbstract() && $testClass->isSubclassOf(TestCase::class)) {
330
                $this->addTest(new self($testClass));
331
            }
332
        } else {
333
            throw new Exception;
334
        }
335
    }
336
337
    public function addWarning(string $warning): void
338
    {
339
        $this->warnings[] = $warning;
340
    }
341
342
    /**
343
     * Wraps both <code>addTest()</code> and <code>addTestSuite</code>
344
     * as well as the separate import statements for the user's convenience.
345
     *
346
     * If the named file cannot be read or there are no new tests that can be
347
     * added, a <code>PHPUnit\Framework\WarningTestCase</code> will be created instead,
348
     * leaving the current test run untouched.
349
     *
350
     * @throws Exception
351
     */
352
    public function addTestFile(string $filename): void
353
    {
354
        if (\file_exists($filename) && \substr($filename, -5) === '.phpt') {
355
            $this->addTest(new PhptTestCase($filename));
356
357
            $this->declaredClasses = \get_declared_classes();
358
359
            return;
360
        }
361
362
        $numTests = \count($this->tests);
363
364
        // The given file may contain further stub classes in addition to the
365
        // test class itself. Figure out the actual test class.
366
        $filename   = FileLoader::checkAndLoad($filename);
367
        $newClasses = \array_diff(\get_declared_classes(), $this->declaredClasses);
368
369
        // The diff is empty in case a parent class (with test methods) is added
370
        // AFTER a child class that inherited from it. To account for that case,
371
        // accumulate all discovered classes, so the parent class may be found in
372
        // a later invocation.
373
        if (!empty($newClasses)) {
374
            // On the assumption that test classes are defined first in files,
375
            // process discovered classes in approximate LIFO order, so as to
376
            // avoid unnecessary reflection.
377
            $this->foundClasses    = \array_merge($newClasses, $this->foundClasses);
378
            $this->declaredClasses = \get_declared_classes();
379
        }
380
381
        // The test class's name must match the filename, either in full, or as
382
        // a PEAR/PSR-0 prefixed short name ('NameSpace_ShortName'), or as a
383
        // PSR-1 local short name ('NameSpace\ShortName'). The comparison must be
384
        // anchored to prevent false-positive matches (e.g., 'OtherShortName').
385
        $shortName      = \basename($filename, '.php');
386
        $shortNameRegEx = '/(?:^|_|\\\\)' . \preg_quote($shortName, '/') . '$/';
387
388
        foreach ($this->foundClasses as $i => $className) {
389
            if (\preg_match($shortNameRegEx, $className)) {
390
                try {
391
                    $class = new \ReflectionClass($className);
392
                    // @codeCoverageIgnoreStart
393
                } catch (\ReflectionException $e) {
394
                    throw new Exception(
395
                        $e->getMessage(),
396
                        (int) $e->getCode(),
397
                        $e
398
                    );
399
                }
400
                // @codeCoverageIgnoreEnd
401
402
                if ($class->getFileName() == $filename) {
403
                    $newClasses = [$className];
404
                    unset($this->foundClasses[$i]);
405
406
                    break;
407
                }
408
            }
409
        }
410
411
        foreach ($newClasses as $className) {
412
            try {
413
                $class = new \ReflectionClass($className);
414
                // @codeCoverageIgnoreStart
415
            } catch (\ReflectionException $e) {
416
                throw new Exception(
417
                    $e->getMessage(),
418
                    (int) $e->getCode(),
419
                    $e
420
                );
421
            }
422
            // @codeCoverageIgnoreEnd
423
424
            if (\dirname($class->getFileName()) === __DIR__) {
425
                continue;
426
            }
427
428
            if (!$class->isAbstract()) {
429
                if ($class->hasMethod(BaseTestRunner::SUITE_METHODNAME)) {
430
                    try {
431
                        $method = $class->getMethod(
432
                            BaseTestRunner::SUITE_METHODNAME
433
                        );
434
                        // @codeCoverageIgnoreStart
435
                    } catch (\ReflectionException $e) {
436
                        throw new Exception(
437
                            $e->getMessage(),
438
                            (int) $e->getCode(),
439
                            $e
440
                        );
441
                    }
442
                    // @codeCoverageIgnoreEnd
443
444
                    if ($method->isStatic()) {
445
                        $this->addTest($method->invoke(null, $className));
446
                    }
447
                } elseif ($class->implementsInterface(Test::class)) {
448
                    $expectedClassName = $shortName;
449
450
                    if (($pos = \strpos($expectedClassName, '.')) !== false) {
451
                        $expectedClassName = \substr(
452
                            $expectedClassName,
453
                            0,
454
                            $pos
455
                        );
456
                    }
457
458
                    if ($class->getShortName() !== $expectedClassName) {
459
                        $this->addWarning(
460
                            \sprintf(
461
                                "Test case class not matching filename is deprecated\n               in %s\n               Class name was '%s', expected '%s'",
462
                                $filename,
463
                                $class->getShortName(),
464
                                $expectedClassName
465
                            )
466
                        );
467
                    }
468
469
                    $this->addTestSuite($class);
470
                }
471
            }
472
        }
473
474
        if (\count($this->tests) > ++$numTests) {
475
            $this->addWarning(
476
                \sprintf(
477
                    "Multiple test case classes per file is deprecated\n               in %s",
478
                    $filename
479
                )
480
            );
481
        }
482
483
        $this->numTests = -1;
484
    }
485
486
    /**
487
     * Wrapper for addTestFile() that adds multiple test files.
488
     *
489
     * @throws Exception
490
     */
491
    public function addTestFiles(iterable $fileNames): void
492
    {
493
        foreach ($fileNames as $filename) {
494
            $this->addTestFile((string) $filename);
495
        }
496
    }
497
498
    /**
499
     * Counts the number of test cases that will be run by this test.
500
     */
501
    public function count(bool $preferCache = false): int
502
    {
503
        if ($preferCache && $this->cachedNumTests !== null) {
504
            return $this->cachedNumTests;
505
        }
506
507
        $numTests = 0;
508
509
        foreach ($this as $test) {
510
            $numTests += \count($test);
511
        }
512
513
        $this->cachedNumTests = $numTests;
514
515
        return $numTests;
516
    }
517
518
    /**
519
     * Returns the name of the suite.
520
     */
521
    public function getName(): string
522
    {
523
        return $this->name;
524
    }
525
526
    /**
527
     * Returns the test groups of the suite.
528
     */
529
    public function getGroups(): array
530
    {
531
        return \array_keys($this->groups);
532
    }
533
534
    public function getGroupDetails(): array
535
    {
536
        return $this->groups;
537
    }
538
539
    /**
540
     * Set tests groups of the test case
541
     */
542
    public function setGroupDetails(array $groups): void
543
    {
544
        $this->groups = $groups;
545
    }
546
547
    /**
548
     * Runs the tests and collects their result in a TestResult.
549
     *
550
     * @throws \PHPUnit\Framework\CodeCoverageException
551
     * @throws \SebastianBergmann\CodeCoverage\CoveredCodeNotExecutedException
552
     * @throws \SebastianBergmann\CodeCoverage\InvalidArgumentException
553
     * @throws \SebastianBergmann\CodeCoverage\RuntimeException
554
     * @throws \SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException
555
     * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
556
     * @throws Warning
557
     */
558
    public function run(TestResult $result = null): TestResult
559
    {
560
        if ($result === null) {
561
            $result = $this->createResult();
562
        }
563
564
        if (\count($this) === 0) {
565
            return $result;
566
        }
567
568
        /** @psalm-var class-string $className */
569
        $className   = $this->name;
570
        $hookMethods = TestUtil::getHookMethods($className);
571
572
        $result->startTestSuite($this);
573
574
        try {
575
            foreach ($hookMethods['beforeClass'] as $beforeClassMethod) {
576
                if ($this->testCase &&
577
                    \class_exists($this->name, false) &&
578
                    \method_exists($this->name, $beforeClassMethod)) {
579
                    if ($missingRequirements = TestUtil::getMissingRequirements($this->name, $beforeClassMethod)) {
580
                        $this->markTestSuiteSkipped(\implode(\PHP_EOL, $missingRequirements));
581
                    }
582
583
                    \call_user_func([$this->name, $beforeClassMethod]);
584
                }
585
            }
586
        } catch (SkippedTestSuiteError $error) {
587
            foreach ($this->tests() as $test) {
588
                $result->startTest($test);
589
                $result->addFailure($test, $error, 0);
590
                $result->endTest($test, 0);
591
            }
592
593
            $result->endTestSuite($this);
594
595
            return $result;
596
        } catch (\Throwable $t) {
597
            $errorAdded = false;
598
599
            foreach ($this->tests() as $test) {
600
                if ($result->shouldStop()) {
601
                    break;
602
                }
603
604
                $result->startTest($test);
605
606
                if (!$errorAdded) {
607
                    $result->addError($test, $t, 0);
608
609
                    $errorAdded = true;
610
                } else {
611
                    $result->addFailure(
612
                        $test,
613
                        new SkippedTestError('Test skipped because of an error in hook method'),
614
                        0
615
                    );
616
                }
617
618
                $result->endTest($test, 0);
619
            }
620
621
            $result->endTestSuite($this);
622
623
            return $result;
624
        }
625
626
        foreach ($this as $test) {
627
            if ($result->shouldStop()) {
628
                break;
629
            }
630
631
            if ($test instanceof TestCase || $test instanceof self) {
632
                $test->setBeStrictAboutChangesToGlobalState($this->beStrictAboutChangesToGlobalState);
633
                $test->setBackupGlobals($this->backupGlobals);
634
                $test->setBackupStaticAttributes($this->backupStaticAttributes);
635
                $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);
636
            }
637
638
            $test->run($result);
639
        }
640
641
        try {
642
            foreach ($hookMethods['afterClass'] as $afterClassMethod) {
643
                if ($this->testCase &&
644
                    \class_exists($this->name, false) &&
645
                    \method_exists($this->name, $afterClassMethod)) {
646
                    \call_user_func([$this->name, $afterClassMethod]);
647
                }
648
            }
649
        } catch (\Throwable $t) {
650
            $message = "Exception in {$this->name}::{$afterClassMethod}" . \PHP_EOL . $t->getMessage();
651
            $error   = new SyntheticError($message, 0, $t->getFile(), $t->getLine(), $t->getTrace());
652
653
            $placeholderTest = clone $test;
654
            $placeholderTest->setName($afterClassMethod);
655
656
            $result->startTest($placeholderTest);
657
            $result->addFailure($placeholderTest, $error, 0);
658
            $result->endTest($placeholderTest, 0);
659
        }
660
661
        $result->endTestSuite($this);
662
663
        return $result;
664
    }
665
666
    public function setRunTestInSeparateProcess(bool $runTestInSeparateProcess): void
667
    {
668
        $this->runTestInSeparateProcess = $runTestInSeparateProcess;
669
    }
670
671
    public function setName(string $name): void
672
    {
673
        $this->name = $name;
674
    }
675
676
    /**
677
     * Returns the test at the given index.
678
     *
679
     * @return false|Test
680
     */
681
    public function testAt(int $index)
682
    {
683
        return $this->tests[$index] ?? false;
684
    }
685
686
    /**
687
     * Returns the tests as an enumeration.
688
     *
689
     * @return Test[]
690
     */
691
    public function tests(): array
692
    {
693
        return $this->tests;
694
    }
695
696
    /**
697
     * Set tests of the test suite
698
     *
699
     * @param Test[] $tests
700
     */
701
    public function setTests(array $tests): void
702
    {
703
        $this->tests = $tests;
704
    }
705
706
    /**
707
     * Mark the test suite as skipped.
708
     *
709
     * @param string $message
710
     *
711
     * @throws SkippedTestSuiteError
712
     *
713
     * @psalm-return never-return
714
     */
715
    public function markTestSuiteSkipped($message = ''): void
716
    {
717
        throw new SkippedTestSuiteError($message);
718
    }
719
720
    /**
721
     * @param bool $beStrictAboutChangesToGlobalState
722
     */
723
    public function setBeStrictAboutChangesToGlobalState($beStrictAboutChangesToGlobalState): void
724
    {
725
        if (null === $this->beStrictAboutChangesToGlobalState && \is_bool($beStrictAboutChangesToGlobalState)) {
726
            $this->beStrictAboutChangesToGlobalState = $beStrictAboutChangesToGlobalState;
727
        }
728
    }
729
730
    /**
731
     * @param bool $backupGlobals
732
     */
733
    public function setBackupGlobals($backupGlobals): void
734
    {
735
        if (null === $this->backupGlobals && \is_bool($backupGlobals)) {
736
            $this->backupGlobals = $backupGlobals;
737
        }
738
    }
739
740
    /**
741
     * @param bool $backupStaticAttributes
742
     */
743
    public function setBackupStaticAttributes($backupStaticAttributes): void
744
    {
745
        if (null === $this->backupStaticAttributes && \is_bool($backupStaticAttributes)) {
746
            $this->backupStaticAttributes = $backupStaticAttributes;
747
        }
748
    }
749
750
    /**
751
     * Returns an iterator for this test suite.
752
     */
753
    public function getIterator(): \Iterator
754
    {
755
        $iterator = new TestSuiteIterator($this);
756
757
        if ($this->iteratorFilter !== null) {
758
            $iterator = $this->iteratorFilter->factory($iterator, $this);
759
        }
760
761
        return $iterator;
762
    }
763
764
    public function injectFilter(Factory $filter): void
765
    {
766
        $this->iteratorFilter = $filter;
767
768
        foreach ($this as $test) {
769
            if ($test instanceof self) {
770
                $test->injectFilter($filter);
771
            }
772
        }
773
    }
774
775
    /**
776
     * @psalm-return array<int,string>
777
     */
778
    public function warnings(): array
779
    {
780
        return \array_unique($this->warnings);
781
    }
782
783
    /**
784
     * Creates a default TestResult object.
785
     */
786
    protected function createResult(): TestResult
787
    {
788
        return new TestResult;
789
    }
790
791
    /**
792
     * @throws Exception
793
     */
794
    protected function addTestMethod(\ReflectionClass $class, \ReflectionMethod $method): void
795
    {
796
        if (!TestUtil::isTestMethod($method)) {
797
            return;
798
        }
799
800
        $methodName = $method->getName();
801
802
        if (!$method->isPublic()) {
803
            $this->addTest(
804
                new WarningTestCase(
805
                    \sprintf(
806
                        'Test method "%s" in test class "%s" is not public.',
807
                        $methodName,
808
                        $class->getName()
809
                    )
810
                )
811
            );
812
813
            return;
814
        }
815
816
        $test = (new TestBuilder)->build($class, $methodName);
817
818
        if ($test instanceof TestCase || $test instanceof DataProviderTestSuite) {
819
            $test->setDependencies(
820
                TestUtil::getDependencies($class->getName(), $methodName)
821
            );
822
        }
823
824
        $this->addTest(
825
            $test,
826
            TestUtil::getGroups($class->getName(), $methodName)
827
        );
828
    }
829
}
830