Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen 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.
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
                'testsuite',
725
            ]
726
        );
727
 
728
        $renderer = new TextTestListRenderer;
729
 
730
        print $renderer->render($suite);
731
 
732
        if ($exit) {
733
            exit(TestRunner::SUCCESS_EXIT);
734
        }
735
 
736
        return TestRunner::SUCCESS_EXIT;
737
    }
738
 
739
    /**
740
     * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
741
     */
742
    private function handleListTestsXml(TestSuite $suite, string $target, bool $exit): int
743
    {
744
        $this->printVersionString();
745
 
746
        $this->warnAboutConflictingOptions(
747
            'listTestsXml',
748
            [
749
                'filter',
750
                'groups',
751
                'excludeGroups',
752
                'testsuite',
753
            ]
754
        );
755
 
756
        $renderer = new XmlTestListRenderer;
757
 
758
        file_put_contents($target, $renderer->render($suite));
759
 
760
        printf(
761
            'Wrote list of tests that would have been run to %s' . PHP_EOL,
762
            $target
763
        );
764
 
765
        if ($exit) {
766
            exit(TestRunner::SUCCESS_EXIT);
767
        }
768
 
769
        return TestRunner::SUCCESS_EXIT;
770
    }
771
 
772
    private function generateConfiguration(): void
773
    {
774
        $this->printVersionString();
775
 
776
        print 'Generating phpunit.xml in ' . getcwd() . PHP_EOL . PHP_EOL;
777
        print 'Bootstrap script (relative to path shown above; default: vendor/autoload.php): ';
778
 
779
        $bootstrapScript = trim(fgets(STDIN));
780
 
781
        print 'Tests directory (relative to path shown above; default: tests): ';
782
 
783
        $testsDirectory = trim(fgets(STDIN));
784
 
785
        print 'Source directory (relative to path shown above; default: src): ';
786
 
787
        $src = trim(fgets(STDIN));
788
 
789
        print 'Cache directory (relative to path shown above; default: .phpunit.cache): ';
790
 
791
        $cacheDirectory = trim(fgets(STDIN));
792
 
793
        if ($bootstrapScript === '') {
794
            $bootstrapScript = 'vendor/autoload.php';
795
        }
796
 
797
        if ($testsDirectory === '') {
798
            $testsDirectory = 'tests';
799
        }
800
 
801
        if ($src === '') {
802
            $src = 'src';
803
        }
804
 
805
        if ($cacheDirectory === '') {
806
            $cacheDirectory = '.phpunit.cache';
807
        }
808
 
809
        $generator = new Generator;
810
 
811
        file_put_contents(
812
            'phpunit.xml',
813
            $generator->generateDefaultConfiguration(
814
                Version::series(),
815
                $bootstrapScript,
816
                $testsDirectory,
817
                $src,
818
                $cacheDirectory
819
            )
820
        );
821
 
822
        print PHP_EOL . 'Generated phpunit.xml in ' . getcwd() . '.' . PHP_EOL;
823
        print 'Make sure to exclude the ' . $cacheDirectory . ' directory from version control.' . PHP_EOL;
824
 
825
        exit(TestRunner::SUCCESS_EXIT);
826
    }
827
 
828
    private function migrateConfiguration(string $filename): void
829
    {
830
        $this->printVersionString();
831
 
832
        if (!(new SchemaDetector)->detect($filename)->detected()) {
833
            print $filename . ' does not need to be migrated.' . PHP_EOL;
834
 
835
            exit(TestRunner::EXCEPTION_EXIT);
836
        }
837
 
838
        copy($filename, $filename . '.bak');
839
 
840
        print 'Created backup:         ' . $filename . '.bak' . PHP_EOL;
841
 
842
        try {
843
            file_put_contents(
844
                $filename,
845
                (new Migrator)->migrate($filename)
846
            );
847
 
848
            print 'Migrated configuration: ' . $filename . PHP_EOL;
849
        } catch (Throwable $t) {
850
            print 'Migration failed: ' . $t->getMessage() . PHP_EOL;
851
 
852
            exit(TestRunner::EXCEPTION_EXIT);
853
        }
854
 
855
        exit(TestRunner::SUCCESS_EXIT);
856
    }
857
 
858
    private function handleCustomOptions(array $unrecognizedOptions): void
859
    {
860
        foreach ($unrecognizedOptions as $name => $value) {
861
            if (isset($this->longOptions[$name])) {
862
                $handler = $this->longOptions[$name];
863
            }
864
 
865
            $name .= '=';
866
 
867
            if (isset($this->longOptions[$name])) {
868
                $handler = $this->longOptions[$name];
869
            }
870
 
871
            if (isset($handler) && is_callable([$this, $handler])) {
872
                $this->{$handler}($value);
873
 
874
                unset($handler);
875
            }
876
        }
877
    }
878
 
879
    private function handleWarmCoverageCache(XmlConfiguration\Configuration $configuration): void
880
    {
881
        $this->printVersionString();
882
 
883
        if (isset($this->arguments['coverageCacheDirectory'])) {
884
            $cacheDirectory = $this->arguments['coverageCacheDirectory'];
885
        } elseif ($configuration->codeCoverage()->hasCacheDirectory()) {
886
            $cacheDirectory = $configuration->codeCoverage()->cacheDirectory()->path();
887
        } else {
888
            print 'Cache for static analysis has not been configured' . PHP_EOL;
889
 
890
            exit(TestRunner::EXCEPTION_EXIT);
891
        }
892
 
893
        $filter = new Filter;
894
 
895
        if ($configuration->codeCoverage()->hasNonEmptyListOfFilesToBeIncludedInCodeCoverageReport()) {
896
            (new FilterMapper)->map(
897
                $filter,
898
                $configuration->codeCoverage()
899
            );
900
        } elseif (isset($this->arguments['coverageFilter'])) {
901
            if (!is_array($this->arguments['coverageFilter'])) {
902
                $coverageFilterDirectories = [$this->arguments['coverageFilter']];
903
            } else {
904
                $coverageFilterDirectories = $this->arguments['coverageFilter'];
905
            }
906
 
907
            foreach ($coverageFilterDirectories as $coverageFilterDirectory) {
908
                $filter->includeDirectory($coverageFilterDirectory);
909
            }
910
        } else {
911
            print 'Filter for code coverage has not been configured' . PHP_EOL;
912
 
913
            exit(TestRunner::EXCEPTION_EXIT);
914
        }
915
 
916
        $timer = new Timer;
917
        $timer->start();
918
 
919
        print 'Warming cache for static analysis ... ';
920
 
921
        (new CacheWarmer)->warmCache(
922
            $cacheDirectory,
923
            !$configuration->codeCoverage()->disableCodeCoverageIgnore(),
924
            $configuration->codeCoverage()->ignoreDeprecatedCodeUnits(),
925
            $filter
926
        );
927
 
928
        print 'done [' . $timer->stop()->asString() . ']' . PHP_EOL;
929
 
930
        exit(TestRunner::SUCCESS_EXIT);
931
    }
932
 
933
    private function configurationFileInDirectory(string $directory): ?string
934
    {
935
        $candidates = [
936
            $directory . '/phpunit.xml',
937
            $directory . '/phpunit.xml.dist',
938
        ];
939
 
940
        foreach ($candidates as $candidate) {
941
            if (is_file($candidate)) {
942
                return realpath($candidate);
943
            }
944
        }
945
 
946
        return null;
947
    }
948
 
949
    /**
950
     * @psalm-param "listGroups"|"listSuites"|"listTests"|"listTestsXml"|"filter"|"groups"|"excludeGroups"|"testsuite" $key
951
     * @psalm-param list<"listGroups"|"listSuites"|"listTests"|"listTestsXml"|"filter"|"groups"|"excludeGroups"|"testsuite"> $keys
952
     */
953
    private function warnAboutConflictingOptions(string $key, array $keys): void
954
    {
955
        $warningPrinted = false;
956
 
957
        foreach ($keys as $_key) {
958
            if (!empty($this->arguments[$_key])) {
959
                printf(
960
                    'The %s and %s options cannot be combined, %s is ignored' . PHP_EOL,
961
                    $this->mapKeyToOptionForWarning($_key),
962
                    $this->mapKeyToOptionForWarning($key),
963
                    $this->mapKeyToOptionForWarning($_key)
964
                );
965
 
966
                $warningPrinted = true;
967
            }
968
        }
969
 
970
        if ($warningPrinted) {
971
            print PHP_EOL;
972
        }
973
    }
974
 
975
    /**
976
     * @psalm-param "listGroups"|"listSuites"|"listTests"|"listTestsXml"|"filter"|"groups"|"excludeGroups"|"testsuite" $key
977
     */
978
    private function mapKeyToOptionForWarning(string $key): string
979
    {
980
        switch ($key) {
981
            case 'listGroups':
982
                return '--list-groups';
983
 
984
            case 'listSuites':
985
                return '--list-suites';
986
 
987
            case 'listTests':
988
                return '--list-tests';
989
 
990
            case 'listTestsXml':
991
                return '--list-tests-xml';
992
 
993
            case 'filter':
994
                return '--filter';
995
 
996
            case 'groups':
997
                return '--group';
998
 
999
            case 'excludeGroups':
1000
                return '--exclude-group';
1001
 
1002
            case 'testsuite':
1003
                return '--testsuite';
1004
        }
1005
    }
1006
}