Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | 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.
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 PHPUnit\TextUI;
11
 
12
use const PATH_SEPARATOR;
13
use const PHP_EOL;
14
use const STDIN;
15
use function array_keys;
16
use function assert;
17
use function class_exists;
18
use function copy;
19
use function extension_loaded;
20
use function fgets;
21
use function file_get_contents;
22
use function file_put_contents;
23
use function get_class;
24
use function getcwd;
25
use function ini_get;
26
use function ini_set;
27
use function is_array;
28
use function is_callable;
29
use function is_dir;
30
use function is_file;
31
use function is_string;
32
use function printf;
33
use function realpath;
34
use function sort;
35
use function sprintf;
36
use function stream_resolve_include_path;
37
use function strpos;
38
use function trim;
39
use function version_compare;
40
use PHPUnit\Framework\TestSuite;
41
use PHPUnit\Runner\Extension\PharLoader;
42
use PHPUnit\Runner\StandardTestSuiteLoader;
43
use PHPUnit\Runner\TestSuiteLoader;
44
use PHPUnit\Runner\Version;
45
use PHPUnit\TextUI\CliArguments\Builder;
46
use PHPUnit\TextUI\CliArguments\Configuration;
47
use PHPUnit\TextUI\CliArguments\Exception as ArgumentsException;
48
use PHPUnit\TextUI\CliArguments\Mapper;
49
use PHPUnit\TextUI\XmlConfiguration\CodeCoverage\FilterMapper;
50
use PHPUnit\TextUI\XmlConfiguration\Generator;
51
use PHPUnit\TextUI\XmlConfiguration\Loader;
52
use PHPUnit\TextUI\XmlConfiguration\Migrator;
53
use PHPUnit\TextUI\XmlConfiguration\PhpHandler;
54
use PHPUnit\Util\FileLoader;
55
use PHPUnit\Util\Filesystem;
56
use PHPUnit\Util\Printer;
57
use PHPUnit\Util\TextTestListRenderer;
58
use PHPUnit\Util\Xml\SchemaDetector;
59
use PHPUnit\Util\XmlTestListRenderer;
60
use ReflectionClass;
61
use SebastianBergmann\CodeCoverage\Filter;
62
use SebastianBergmann\CodeCoverage\StaticAnalysis\CacheWarmer;
63
use SebastianBergmann\Timer\Timer;
64
use Throwable;
65
 
66
/**
67
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
68
 */
69
class Command
70
{
71
    /**
72
     * @var array<string,mixed>
73
     */
74
    protected $arguments = [];
75
 
76
    /**
77
     * @var array<string,mixed>
78
     */
79
    protected $longOptions = [];
80
 
81
    /**
82
     * @var bool
83
     */
84
    private $versionStringPrinted = false;
85
 
86
    /**
87
     * @psalm-var list<string>
88
     */
89
    private $warnings = [];
90
 
91
    /**
92
     * @throws Exception
93
     */
94
    public static function main(bool $exit = true): int
95
    {
96
        try {
97
            return (new static)->run($_SERVER['argv'], $exit);
98
        } catch (Throwable $t) {
99
            throw new RuntimeException(
100
                $t->getMessage(),
101
                (int) $t->getCode(),
102
                $t
103
            );
104
        }
105
    }
106
 
107
    /**
108
     * @throws Exception
109
     */
110
    public function run(array $argv, bool $exit = true): int
111
    {
112
        $this->handleArguments($argv);
113
 
114
        $runner = $this->createRunner();
115
 
116
        if ($this->arguments['test'] instanceof TestSuite) {
117
            $suite = $this->arguments['test'];
118
        } else {
119
            $suite = $runner->getTest(
120
                $this->arguments['test'],
121
                $this->arguments['testSuffixes']
122
            );
123
        }
124
 
125
        if ($this->arguments['listGroups']) {
126
            return $this->handleListGroups($suite, $exit);
127
        }
128
 
129
        if ($this->arguments['listSuites']) {
130
            return $this->handleListSuites($exit);
131
        }
132
 
133
        if ($this->arguments['listTests']) {
134
            return $this->handleListTests($suite, $exit);
135
        }
136
 
137
        if ($this->arguments['listTestsXml']) {
138
            return $this->handleListTestsXml($suite, $this->arguments['listTestsXml'], $exit);
139
        }
140
 
141
        unset($this->arguments['test'], $this->arguments['testFile']);
142
 
143
        try {
144
            $result = $runner->run($suite, $this->arguments, $this->warnings, $exit);
145
        } catch (Throwable $t) {
146
            print $t->getMessage() . PHP_EOL;
147
        }
148
 
149
        $return = TestRunner::FAILURE_EXIT;
150
 
151
        if (isset($result) && $result->wasSuccessful()) {
152
            $return = TestRunner::SUCCESS_EXIT;
153
        } elseif (!isset($result) || $result->errorCount() > 0) {
154
            $return = TestRunner::EXCEPTION_EXIT;
155
        }
156
 
157
        if ($exit) {
158
            exit($return);
159
        }
160
 
161
        return $return;
162
    }
163
 
164
    /**
165
     * Create a TestRunner, override in subclasses.
166
     */
167
    protected function createRunner(): TestRunner
168
    {
169
        return new TestRunner($this->arguments['loader']);
170
    }
171
 
172
    /**
173
     * Handles the command-line arguments.
174
     *
175
     * A child class of PHPUnit\TextUI\Command can hook into the argument
176
     * parsing by adding the switch(es) to the $longOptions array and point to a
177
     * callback method that handles the switch(es) in the child class like this
178
     *
179
     * <code>
180
     * <?php
181
     * class MyCommand extends PHPUnit\TextUI\Command
182
     * {
183
     *     public function __construct()
184
     *     {
185
     *         // my-switch won't accept a value, it's an on/off
186
     *         $this->longOptions['my-switch'] = 'myHandler';
187
     *         // my-secondswitch will accept a value - note the equals sign
188
     *         $this->longOptions['my-secondswitch='] = 'myOtherHandler';
189
     *     }
190
     *
191
     *     // --my-switch  -> myHandler()
192
     *     protected function myHandler()
193
     *     {
194
     *     }
195
     *
196
     *     // --my-secondswitch foo -> myOtherHandler('foo')
197
     *     protected function myOtherHandler ($value)
198
     *     {
199
     *     }
200
     *
201
     *     // You will also need this - the static keyword in the
202
     *     // PHPUnit\TextUI\Command will mean that it'll be
203
     *     // PHPUnit\TextUI\Command that gets instantiated,
204
     *     // not MyCommand
205
     *     public static function main($exit = true)
206
     *     {
207
     *         $command = new static;
208
     *
209
     *         return $command->run($_SERVER['argv'], $exit);
210
     *     }
211
     *
212
     * }
213
     * </code>
214
     *
215
     * @throws Exception
216
     */
217
    protected function handleArguments(array $argv): void
218
    {
219
        try {
220
            $arguments = (new Builder)->fromParameters($argv, array_keys($this->longOptions));
221
        } catch (ArgumentsException $e) {
222
            $this->exitWithErrorMessage($e->getMessage());
223
        }
224
 
225
        assert(isset($arguments) && $arguments instanceof Configuration);
226
 
227
        if ($arguments->hasGenerateConfiguration() && $arguments->generateConfiguration()) {
228
            $this->generateConfiguration();
229
        }
230
 
231
        if ($arguments->hasAtLeastVersion()) {
232
            if (version_compare(Version::id(), $arguments->atLeastVersion(), '>=')) {
233
                exit(TestRunner::SUCCESS_EXIT);
234
            }
235
 
236
            exit(TestRunner::FAILURE_EXIT);
237
        }
238
 
239
        if ($arguments->hasVersion() && $arguments->version()) {
240
            $this->printVersionString();
241
 
242
            exit(TestRunner::SUCCESS_EXIT);
243
        }
244
 
245
        if ($arguments->hasCheckVersion() && $arguments->checkVersion()) {
246
            $this->handleVersionCheck();
247
        }
248
 
249
        if ($arguments->hasHelp()) {
250
            $this->showHelp();
251
 
252
            exit(TestRunner::SUCCESS_EXIT);
253
        }
254
 
255
        if ($arguments->hasUnrecognizedOrderBy()) {
256
            $this->exitWithErrorMessage(
257
                sprintf(
258
                    'unrecognized --order-by option: %s',
259
                    $arguments->unrecognizedOrderBy()
260
                )
261
            );
262
        }
263
 
264
        if ($arguments->hasIniSettings()) {
265
            foreach ($arguments->iniSettings() as $name => $value) {
266
                ini_set($name, $value);
267
            }
268
        }
269
 
270
        if ($arguments->hasIncludePath()) {
271
            ini_set(
272
                'include_path',
273
                $arguments->includePath() . PATH_SEPARATOR . ini_get('include_path')
274
            );
275
        }
276
 
277
        $this->arguments = (new Mapper)->mapToLegacyArray($arguments);
278
 
279
        $this->handleCustomOptions($arguments->unrecognizedOptions());
280
        $this->handleCustomTestSuite();
281
 
282
        if (!isset($this->arguments['testSuffixes'])) {
283
            $this->arguments['testSuffixes'] = ['Test.php', '.phpt'];
284
        }
285
 
286
        if (!isset($this->arguments['test']) && $arguments->hasArgument()) {
287
            $this->arguments['test'] = realpath($arguments->argument());
288
 
289
            if ($this->arguments['test'] === false) {
290
                $this->exitWithErrorMessage(
291
                    sprintf(
292
                        'Cannot open file "%s".',
293
                        $arguments->argument()
294
                    )
295
                );
296
            }
297
        }
298
 
299
        if ($this->arguments['loader'] !== null) {
300
            $this->arguments['loader'] = $this->handleLoader($this->arguments['loader']);
301
        }
302
 
303
        if (isset($this->arguments['configuration'])) {
304
            if (is_dir($this->arguments['configuration'])) {
305
                $candidate = $this->configurationFileInDirectory($this->arguments['configuration']);
306
 
307
                if ($candidate !== null) {
308
                    $this->arguments['configuration'] = $candidate;
309
                }
310
            }
311
        } elseif ($this->arguments['useDefaultConfiguration']) {
312
            $candidate = $this->configurationFileInDirectory(getcwd());
313
 
314
            if ($candidate !== null) {
315
                $this->arguments['configuration'] = $candidate;
316
            }
317
        }
318
 
319
        if ($arguments->hasMigrateConfiguration() && $arguments->migrateConfiguration()) {
320
            if (!isset($this->arguments['configuration'])) {
321
                print 'No configuration file found to migrate.' . PHP_EOL;
322
 
323
                exit(TestRunner::EXCEPTION_EXIT);
324
            }
325
 
326
            $this->migrateConfiguration(realpath($this->arguments['configuration']));
327
        }
328
 
329
        if (isset($this->arguments['configuration'])) {
330
            try {
331
                $this->arguments['configurationObject'] = (new Loader)->load($this->arguments['configuration']);
332
            } catch (Throwable $e) {
333
                print $e->getMessage() . PHP_EOL;
334
 
335
                exit(TestRunner::FAILURE_EXIT);
336
            }
337
 
338
            $phpunitConfiguration = $this->arguments['configurationObject']->phpunit();
339
 
340
            (new PhpHandler)->handle($this->arguments['configurationObject']->php());
341
 
342
            if (isset($this->arguments['bootstrap'])) {
343
                $this->handleBootstrap($this->arguments['bootstrap']);
344
            } elseif ($phpunitConfiguration->hasBootstrap()) {
345
                $this->handleBootstrap($phpunitConfiguration->bootstrap());
346
            }
347
 
348
            if (!isset($this->arguments['stderr'])) {
349
                $this->arguments['stderr'] = $phpunitConfiguration->stderr();
350
            }
351
 
352
            if (!isset($this->arguments['noExtensions']) && $phpunitConfiguration->hasExtensionsDirectory() && extension_loaded('phar')) {
353
                $result = (new PharLoader)->loadPharExtensionsInDirectory($phpunitConfiguration->extensionsDirectory());
354
 
355
                $this->arguments['loadedExtensions']    = $result['loadedExtensions'];
356
                $this->arguments['notLoadedExtensions'] = $result['notLoadedExtensions'];
357
 
358
                unset($result);
359
            }
360
 
361
            if (!isset($this->arguments['columns'])) {
362
                $this->arguments['columns'] = $phpunitConfiguration->columns();
363
            }
364
 
365
            if (!isset($this->arguments['printer']) && $phpunitConfiguration->hasPrinterClass()) {
366
                $file = $phpunitConfiguration->hasPrinterFile() ? $phpunitConfiguration->printerFile() : '';
367
 
368
                $this->arguments['printer'] = $this->handlePrinter(
369
                    $phpunitConfiguration->printerClass(),
370
                    $file
371
                );
372
            }
373
 
374
            if ($phpunitConfiguration->hasTestSuiteLoaderClass()) {
375
                $file = $phpunitConfiguration->hasTestSuiteLoaderFile() ? $phpunitConfiguration->testSuiteLoaderFile() : '';
376
 
377
                $this->arguments['loader'] = $this->handleLoader(
378
                    $phpunitConfiguration->testSuiteLoaderClass(),
379
                    $file
380
                );
381
            }
382
 
383
            if (!isset($this->arguments['testsuite']) && $phpunitConfiguration->hasDefaultTestSuite()) {
384
                $this->arguments['testsuite'] = $phpunitConfiguration->defaultTestSuite();
385
            }
386
 
387
            if (!isset($this->arguments['test'])) {
388
                try {
389
                    $this->arguments['test'] = (new TestSuiteMapper)->map(
390
                        $this->arguments['configurationObject']->testSuite(),
391
                        $this->arguments['testsuite'] ?? ''
392
                    );
393
                } catch (Exception $e) {
394
                    $this->printVersionString();
395
 
396
                    print $e->getMessage() . PHP_EOL;
397
 
398
                    exit(TestRunner::EXCEPTION_EXIT);
399
                }
400
            }
401
        } elseif (isset($this->arguments['bootstrap'])) {
402
            $this->handleBootstrap($this->arguments['bootstrap']);
403
        }
404
 
405
        if (isset($this->arguments['printer']) && is_string($this->arguments['printer'])) {
406
            $this->arguments['printer'] = $this->handlePrinter($this->arguments['printer']);
407
        }
408
 
409
        if (isset($this->arguments['configurationObject'], $this->arguments['warmCoverageCache'])) {
410
            $this->handleWarmCoverageCache($this->arguments['configurationObject']);
411
        }
412
 
413
        if (!isset($this->arguments['test'])) {
414
            $this->showHelp();
415
 
416
            exit(TestRunner::EXCEPTION_EXIT);
417
        }
418
    }
419
 
420
    /**
421
     * Handles the loading of the PHPUnit\Runner\TestSuiteLoader implementation.
422
     *
423
     * @deprecated see https://github.com/sebastianbergmann/phpunit/issues/4039
424
     */
425
    protected function handleLoader(string $loaderClass, string $loaderFile = ''): ?TestSuiteLoader
426
    {
427
        $this->warnings[] = 'Using a custom test suite loader is deprecated';
428
 
429
        if (!class_exists($loaderClass, false)) {
430
            if ($loaderFile == '') {
431
                $loaderFile = Filesystem::classNameToFilename(
432
                    $loaderClass
433
                );
434
            }
435
 
436
            $loaderFile = stream_resolve_include_path($loaderFile);
437
 
438
            if ($loaderFile) {
439
                /**
440
                 * @noinspection PhpIncludeInspection
441
                 *
442
                 * @psalm-suppress UnresolvableInclude
443
                 */
444
                require $loaderFile;
445
            }
446
        }
447
 
448
        if (class_exists($loaderClass, false)) {
449
            try {
450
                $class = new ReflectionClass($loaderClass);
451
                // @codeCoverageIgnoreStart
452
            } catch (\ReflectionException $e) {
453
                throw new ReflectionException(
454
                    $e->getMessage(),
455
                    $e->getCode(),
456
                    $e
457
                );
458
            }
459
            // @codeCoverageIgnoreEnd
460
 
461
            if ($class->implementsInterface(TestSuiteLoader::class) && $class->isInstantiable()) {
462
                $object = $class->newInstance();
463
 
464
                assert($object instanceof TestSuiteLoader);
465
 
466
                return $object;
467
            }
468
        }
469
 
470
        if ($loaderClass == StandardTestSuiteLoader::class) {
471
            return null;
472
        }
473
 
474
        $this->exitWithErrorMessage(
475
            sprintf(
476
                'Could not use "%s" as loader.',
477
                $loaderClass
478
            )
479
        );
480
 
481
        return null;
482
    }
483
 
484
    /**
485
     * Handles the loading of the PHPUnit\Util\Printer implementation.
486
     *
487
     * @return null|Printer|string
488
     */
489
    protected function handlePrinter(string $printerClass, string $printerFile = '')
490
    {
491
        if (!class_exists($printerClass, false)) {
492
            if ($printerFile === '') {
493
                $printerFile = Filesystem::classNameToFilename(
494
                    $printerClass
495
                );
496
            }
497
 
498
            $printerFile = stream_resolve_include_path($printerFile);
499
 
500
            if ($printerFile) {
501
                /**
502
                 * @noinspection PhpIncludeInspection
503
                 *
504
                 * @psalm-suppress UnresolvableInclude
505
                 */
506
                require $printerFile;
507
            }
508
        }
509
 
510
        if (!class_exists($printerClass)) {
511
            $this->exitWithErrorMessage(
512
                sprintf(
513
                    'Could not use "%s" as printer: class does not exist',
514
                    $printerClass
515
                )
516
            );
517
        }
518
 
519
        try {
520
            $class = new ReflectionClass($printerClass);
521
            // @codeCoverageIgnoreStart
522
        } catch (\ReflectionException $e) {
523
            throw new ReflectionException(
524
                $e->getMessage(),
525
                $e->getCode(),
526
                $e
527
            );
528
            // @codeCoverageIgnoreEnd
529
        }
530
 
531
        if (!$class->implementsInterface(ResultPrinter::class)) {
532
            $this->exitWithErrorMessage(
533
                sprintf(
534
                    'Could not use "%s" as printer: class does not implement %s',
535
                    $printerClass,
536
                    ResultPrinter::class
537
                )
538
            );
539
        }
540
 
541
        if (!$class->isInstantiable()) {
542
            $this->exitWithErrorMessage(
543
                sprintf(
544
                    'Could not use "%s" as printer: class cannot be instantiated',
545
                    $printerClass
546
                )
547
            );
548
        }
549
 
550
        if ($class->isSubclassOf(ResultPrinter::class)) {
551
            return $printerClass;
552
        }
553
 
554
        $outputStream = isset($this->arguments['stderr']) ? 'php://stderr' : null;
555
 
556
        return $class->newInstance($outputStream);
557
    }
558
 
559
    /**
560
     * Loads a bootstrap file.
561
     */
562
    protected function handleBootstrap(string $filename): void
563
    {
564
        try {
565
            FileLoader::checkAndLoad($filename);
566
        } catch (Throwable $t) {
567
            if ($t instanceof \PHPUnit\Exception) {
568
                $this->exitWithErrorMessage($t->getMessage());
569
            }
570
 
571
            $this->exitWithErrorMessage(
572
                sprintf(
573
                    'Error in bootstrap script: %s:%s%s%s%s',
574
                    get_class($t),
575
                    PHP_EOL,
576
                    $t->getMessage(),
577
                    PHP_EOL,
578
                    $t->getTraceAsString()
579
                )
580
            );
581
        }
582
    }
583
 
584
    protected function handleVersionCheck(): void
585
    {
586
        $this->printVersionString();
587
 
588
        $latestVersion = file_get_contents('https://phar.phpunit.de/latest-version-of/phpunit');
589
        $isOutdated    = version_compare($latestVersion, Version::id(), '>');
590
 
591
        if ($isOutdated) {
592
            printf(
593
                'You are not using the latest version of PHPUnit.' . PHP_EOL .
594
                'The latest version is PHPUnit %s.' . PHP_EOL,
595
                $latestVersion
596
            );
597
        } else {
598
            print 'You are using the latest version of PHPUnit.' . PHP_EOL;
599
        }
600
 
601
        exit(TestRunner::SUCCESS_EXIT);
602
    }
603
 
604
    /**
605
     * Show the help message.
606
     */
607
    protected function showHelp(): void
608
    {
609
        $this->printVersionString();
610
        (new Help)->writeToConsole();
611
    }
612
 
613
    /**
614
     * Custom callback for test suite discovery.
615
     */
616
    protected function handleCustomTestSuite(): void
617
    {
618
    }
619
 
620
    private function printVersionString(): void
621
    {
622
        if ($this->versionStringPrinted) {
623
            return;
624
        }
625
 
626
        print Version::getVersionString() . PHP_EOL . PHP_EOL;
627
 
628
        $this->versionStringPrinted = true;
629
    }
630
 
631
    private function exitWithErrorMessage(string $message): void
632
    {
633
        $this->printVersionString();
634
 
635
        print $message . PHP_EOL;
636
 
637
        exit(TestRunner::FAILURE_EXIT);
638
    }
639
 
640
    private function handleListGroups(TestSuite $suite, bool $exit): int
641
    {
642
        $this->printVersionString();
643
 
644
        $this->warnAboutConflictingOptions(
645
            'listGroups',
646
            [
647
                'filter',
648
                'groups',
649
                'excludeGroups',
650
                'testsuite',
651
            ]
652
        );
653
 
654
        print 'Available test group(s):' . PHP_EOL;
655
 
656
        $groups = $suite->getGroups();
657
        sort($groups);
658
 
659
        foreach ($groups as $group) {
660
            if (strpos($group, '__phpunit_') === 0) {
661
                continue;
662
            }
663
 
664
            printf(
665
                ' - %s' . PHP_EOL,
666
                $group
667
            );
668
        }
669
 
670
        if ($exit) {
671
            exit(TestRunner::SUCCESS_EXIT);
672
        }
673
 
674
        return TestRunner::SUCCESS_EXIT;
675
    }
676
 
677
    /**
678
     * @throws \PHPUnit\Framework\Exception
679
     * @throws \PHPUnit\TextUI\XmlConfiguration\Exception
680
     */
681
    private function handleListSuites(bool $exit): int
682
    {
683
        $this->printVersionString();
684
 
685
        $this->warnAboutConflictingOptions(
686
            'listSuites',
687
            [
688
                'filter',
689
                'groups',
690
                'excludeGroups',
691
                'testsuite',
692
            ]
693
        );
694
 
695
        print 'Available test suite(s):' . PHP_EOL;
696
 
697
        foreach ($this->arguments['configurationObject']->testSuite() as $testSuite) {
698
            printf(
699
                ' - %s' . PHP_EOL,
700
                $testSuite->name()
701
            );
702
        }
703
 
704
        if ($exit) {
705
            exit(TestRunner::SUCCESS_EXIT);
706
        }
707
 
708
        return TestRunner::SUCCESS_EXIT;
709
    }
710
 
711
    /**
712
     * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
713
     */
714
    private function handleListTests(TestSuite $suite, bool $exit): int
715
    {
716
        $this->printVersionString();
717
 
718
        $this->warnAboutConflictingOptions(
719
            'listTests',
720
            [
721
                'filter',
722
                'groups',
723
                'excludeGroups',
724
            ]
725
        );
726
 
727
        $renderer = new TextTestListRenderer;
728
 
729
        print $renderer->render($suite);
730
 
731
        if ($exit) {
732
            exit(TestRunner::SUCCESS_EXIT);
733
        }
734
 
735
        return TestRunner::SUCCESS_EXIT;
736
    }
737
 
738
    /**
739
     * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
740
     */
741
    private function handleListTestsXml(TestSuite $suite, string $target, bool $exit): int
742
    {
743
        $this->printVersionString();
744
 
745
        $this->warnAboutConflictingOptions(
746
            'listTestsXml',
747
            [
748
                'filter',
749
                'groups',
750
                'excludeGroups',
751
            ]
752
        );
753
 
754
        $renderer = new XmlTestListRenderer;
755
 
756
        file_put_contents($target, $renderer->render($suite));
757
 
758
        printf(
759
            'Wrote list of tests that would have been run to %s' . PHP_EOL,
760
            $target
761
        );
762
 
763
        if ($exit) {
764
            exit(TestRunner::SUCCESS_EXIT);
765
        }
766
 
767
        return TestRunner::SUCCESS_EXIT;
768
    }
769
 
770
    private function generateConfiguration(): void
771
    {
772
        $this->printVersionString();
773
 
774
        print 'Generating phpunit.xml in ' . getcwd() . PHP_EOL . PHP_EOL;
775
        print 'Bootstrap script (relative to path shown above; default: vendor/autoload.php): ';
776
 
777
        $bootstrapScript = trim(fgets(STDIN));
778
 
779
        print 'Tests directory (relative to path shown above; default: tests): ';
780
 
781
        $testsDirectory = trim(fgets(STDIN));
782
 
783
        print 'Source directory (relative to path shown above; default: src): ';
784
 
785
        $src = trim(fgets(STDIN));
786
 
787
        print 'Cache directory (relative to path shown above; default: .phpunit.cache): ';
788
 
789
        $cacheDirectory = trim(fgets(STDIN));
790
 
791
        if ($bootstrapScript === '') {
792
            $bootstrapScript = 'vendor/autoload.php';
793
        }
794
 
795
        if ($testsDirectory === '') {
796
            $testsDirectory = 'tests';
797
        }
798
 
799
        if ($src === '') {
800
            $src = 'src';
801
        }
802
 
803
        if ($cacheDirectory === '') {
804
            $cacheDirectory = '.phpunit.cache';
805
        }
806
 
807
        $generator = new Generator;
808
 
809
        file_put_contents(
810
            'phpunit.xml',
811
            $generator->generateDefaultConfiguration(
812
                Version::series(),
813
                $bootstrapScript,
814
                $testsDirectory,
815
                $src,
816
                $cacheDirectory
817
            )
818
        );
819
 
820
        print PHP_EOL . 'Generated phpunit.xml in ' . getcwd() . '.' . PHP_EOL;
821
        print 'Make sure to exclude the ' . $cacheDirectory . ' directory from version control.' . PHP_EOL;
822
 
823
        exit(TestRunner::SUCCESS_EXIT);
824
    }
825
 
826
    private function migrateConfiguration(string $filename): void
827
    {
828
        $this->printVersionString();
829
 
830
        if (!(new SchemaDetector)->detect($filename)->detected()) {
831
            print $filename . ' does not need to be migrated.' . PHP_EOL;
832
 
833
            exit(TestRunner::EXCEPTION_EXIT);
834
        }
835
 
836
        copy($filename, $filename . '.bak');
837
 
838
        print 'Created backup:         ' . $filename . '.bak' . PHP_EOL;
839
 
840
        try {
841
            file_put_contents(
842
                $filename,
843
                (new Migrator)->migrate($filename)
844
            );
845
 
846
            print 'Migrated configuration: ' . $filename . PHP_EOL;
847
        } catch (Throwable $t) {
848
            print 'Migration failed: ' . $t->getMessage() . PHP_EOL;
849
 
850
            exit(TestRunner::EXCEPTION_EXIT);
851
        }
852
 
853
        exit(TestRunner::SUCCESS_EXIT);
854
    }
855
 
856
    private function handleCustomOptions(array $unrecognizedOptions): void
857
    {
858
        foreach ($unrecognizedOptions as $name => $value) {
859
            if (isset($this->longOptions[$name])) {
860
                $handler = $this->longOptions[$name];
861
            }
862
 
863
            $name .= '=';
864
 
865
            if (isset($this->longOptions[$name])) {
866
                $handler = $this->longOptions[$name];
867
            }
868
 
869
            if (isset($handler) && is_callable([$this, $handler])) {
870
                $this->{$handler}($value);
871
 
872
                unset($handler);
873
            }
874
        }
875
    }
876
 
877
    private function handleWarmCoverageCache(XmlConfiguration\Configuration $configuration): void
878
    {
879
        $this->printVersionString();
880
 
881
        if (isset($this->arguments['coverageCacheDirectory'])) {
882
            $cacheDirectory = $this->arguments['coverageCacheDirectory'];
883
        } elseif ($configuration->codeCoverage()->hasCacheDirectory()) {
884
            $cacheDirectory = $configuration->codeCoverage()->cacheDirectory()->path();
885
        } else {
886
            print 'Cache for static analysis has not been configured' . PHP_EOL;
887
 
888
            exit(TestRunner::EXCEPTION_EXIT);
889
        }
890
 
891
        $filter = new Filter;
892
 
893
        if ($configuration->codeCoverage()->hasNonEmptyListOfFilesToBeIncludedInCodeCoverageReport()) {
894
            (new FilterMapper)->map(
895
                $filter,
896
                $configuration->codeCoverage()
897
            );
898
        } elseif (isset($this->arguments['coverageFilter'])) {
899
            if (!is_array($this->arguments['coverageFilter'])) {
900
                $coverageFilterDirectories = [$this->arguments['coverageFilter']];
901
            } else {
902
                $coverageFilterDirectories = $this->arguments['coverageFilter'];
903
            }
904
 
905
            foreach ($coverageFilterDirectories as $coverageFilterDirectory) {
906
                $filter->includeDirectory($coverageFilterDirectory);
907
            }
908
        } else {
909
            print 'Filter for code coverage has not been configured' . PHP_EOL;
910
 
911
            exit(TestRunner::EXCEPTION_EXIT);
912
        }
913
 
914
        $timer = new Timer;
915
        $timer->start();
916
 
917
        print 'Warming cache for static analysis ... ';
918
 
919
        (new CacheWarmer)->warmCache(
920
            $cacheDirectory,
921
            !$configuration->codeCoverage()->disableCodeCoverageIgnore(),
922
            $configuration->codeCoverage()->ignoreDeprecatedCodeUnits(),
923
            $filter
924
        );
925
 
926
        print 'done [' . $timer->stop()->asString() . ']' . PHP_EOL;
927
 
928
        exit(TestRunner::SUCCESS_EXIT);
929
    }
930
 
931
    private function configurationFileInDirectory(string $directory): ?string
932
    {
933
        $candidates = [
934
            $directory . '/phpunit.xml',
935
            $directory . '/phpunit.xml.dist',
936
        ];
937
 
938
        foreach ($candidates as $candidate) {
939
            if (is_file($candidate)) {
940
                return realpath($candidate);
941
            }
942
        }
943
 
944
        return null;
945
    }
946
 
947
    /**
948
     * @psalm-param "listGroups"|"listSuites"|"listTests"|"listTestsXml"|"filter"|"groups"|"excludeGroups"|"testsuite" $key
949
     * @psalm-param list<"listGroups"|"listSuites"|"listTests"|"listTestsXml"|"filter"|"groups"|"excludeGroups"|"testsuite"> $keys
950
     */
951
    private function warnAboutConflictingOptions(string $key, array $keys): void
952
    {
953
        $warningPrinted = false;
954
 
955
        foreach ($keys as $_key) {
956
            if (!empty($this->arguments[$_key])) {
957
                printf(
958
                    'The %s and %s options cannot be combined, %s is ignored' . PHP_EOL,
959
                    $this->mapKeyToOptionForWarning($_key),
960
                    $this->mapKeyToOptionForWarning($key),
961
                    $this->mapKeyToOptionForWarning($_key)
962
                );
963
 
964
                $warningPrinted = true;
965
            }
966
        }
967
 
968
        if ($warningPrinted) {
969
            print PHP_EOL;
970
        }
971
    }
972
 
973
    /**
974
     * @psalm-param "listGroups"|"listSuites"|"listTests"|"listTestsXml"|"filter"|"groups"|"excludeGroups"|"testsuite" $key
975
     */
976
    private function mapKeyToOptionForWarning(string $key): string
977
    {
978
        switch ($key) {
979
            case 'listGroups':
980
                return '--list-groups';
981
 
982
            case 'listSuites':
983
                return '--list-suites';
984
 
985
            case 'listTests':
986
                return '--list-tests';
987
 
988
            case 'listTestsXml':
989
                return '--list-tests-xml';
990
 
991
            case 'filter':
992
                return '--filter';
993
 
994
            case 'groups':
995
                return '--group';
996
 
997
            case 'excludeGroups':
998
                return '--exclude-group';
999
 
1000
            case 'testsuite':
1001
                return '--testsuite';
1002
        }
1003
    }
1004
}