Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
declare(strict_types=1);
4
 
5
namespace NunoMaduro\Collision\Adapters\Laravel\Commands;
6
 
7
use Dotenv\Exception\InvalidPathException;
8
use Dotenv\Parser\Parser;
9
use Dotenv\Store\StoreBuilder;
10
use Illuminate\Console\Command;
11
use Illuminate\Support\Env;
12
use Illuminate\Support\Str;
13
use NunoMaduro\Collision\Adapters\Laravel\Exceptions\RequirementsException;
14
use NunoMaduro\Collision\Coverage;
15
use RuntimeException;
16
use Symfony\Component\Process\Exception\ProcessSignaledException;
17
use Symfony\Component\Process\Process;
18
 
19
/**
20
 * @internal
21
 *
22
 * @final
23
 */
24
class TestCommand extends Command
25
{
26
    /**
27
     * The name and signature of the console command.
28
     *
29
     * @var string
30
     */
31
    protected $signature = 'test
32
        {--without-tty : Disable output to TTY}
33
        {--coverage : Indicates whether code coverage information should be collected}
34
        {--min= : Indicates the minimum threshold enforcement for code coverage}
35
        {--p|parallel : Indicates if the tests should run in parallel}
36
        {--recreate-databases : Indicates if the test databases should be re-created}
37
        {--drop-databases : Indicates if the test databases should be dropped}
38
    ';
39
 
40
    /**
41
     * The console command description.
42
     *
43
     * @var string
44
     */
45
    protected $description = 'Run the application tests';
46
 
47
    /**
48
     * Create a new command instance.
49
     *
50
     * @return void
51
     */
52
    public function __construct()
53
    {
54
        parent::__construct();
55
 
56
        $this->ignoreValidationErrors();
57
    }
58
 
59
    /**
60
     * Execute the console command.
61
     *
62
     * @return mixed
63
     */
64
    public function handle()
65
    {
66
        $phpunitVersion = \PHPUnit\Runner\Version::id();
67
 
68
        if ((int) $phpunitVersion[0] === 1) {
69
            throw new RequirementsException('Running PHPUnit 10.x or Pest 2.x requires Collision 7.x.');
70
        }
71
 
72
        if ((int) $phpunitVersion[0] < 9) {
73
            throw new RequirementsException('Running Collision 6.x artisan test command requires at least PHPUnit 9.x.');
74
        }
75
 
76
        $laravelVersion = (int) \Illuminate\Foundation\Application::VERSION;
77
 
78
        // @phpstan-ignore-next-line
79
        if ($laravelVersion < 9) {
80
            throw new RequirementsException('Running Collision 6.x artisan test command requires at least Laravel 9.x.');
81
        }
82
 
83
        if ($this->option('coverage') && ! Coverage::isAvailable()) {
84
            $this->output->writeln(sprintf(
85
                "\n  <fg=white;bg=red;options=bold> ERROR </> Code coverage driver not available.%s</>",
86
                Coverage::usingXdebug()
87
                    ? " Did you set <href=https://xdebug.org/docs/code_coverage#mode>Xdebug's coverage mode</>?"
88
                    : ''
89
            ));
90
 
91
            $this->newLine();
92
 
93
            return 1;
94
        }
95
 
96
        if ($this->option('parallel') && ! $this->isParallelDependenciesInstalled()) {
97
            if (! $this->confirm('Running tests in parallel requires "brianium/paratest". Do you wish to install it as a dev dependency?')) {
98
                return 1;
99
            }
100
 
101
            $this->installParallelDependencies();
102
        }
103
 
104
        $options = array_slice($_SERVER['argv'], $this->option('without-tty') ? 3 : 2);
105
 
106
        $this->clearEnv();
107
 
108
        $parallel = $this->option('parallel');
109
 
110
        $process = (new Process(array_merge(
111
            // Binary ...
112
            $this->binary(),
113
            // Arguments ...
114
            $parallel ? $this->paratestArguments($options) : $this->phpunitArguments($options)
115
        ),
116
            null,
117
            // Envs ...
118
            $parallel ? $this->paratestEnvironmentVariables() : $this->phpunitEnvironmentVariables(),
119
        ))->setTimeout(null);
120
 
121
        try {
122
            $process->setTty(! $this->option('without-tty'));
123
        } catch (RuntimeException $e) {
124
            $this->output->writeln('Warning: '.$e->getMessage());
125
        }
126
 
127
        $exitCode = 1;
128
 
129
        try {
130
            $exitCode = $process->run(function ($type, $line) {
131
                $this->output->write($line);
132
            });
133
        } catch (ProcessSignaledException $e) {
134
            if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
135
                throw $e;
136
            }
137
        }
138
 
139
        if ($exitCode === 0 && $this->option('coverage')) {
140
            if (! $this->usingPest() && $this->option('parallel')) {
141
                $this->newLine();
142
            }
143
 
144
            $coverage = Coverage::report($this->output);
145
 
146
            $exitCode = (int) ($coverage < $this->option('min'));
147
 
148
            if ($exitCode === 1) {
149
                $this->output->writeln(sprintf(
150
                    "\n  <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected:<fg=red;options=bold> %s %%</>. Minimum:<fg=white;options=bold> %s %%</>.",
151
                    number_format($coverage, 1),
152
                    number_format((float) $this->option('min'), 1)
153
                ));
154
            }
155
        }
156
 
157
        $this->newLine();
158
 
159
        return $exitCode;
160
    }
161
 
162
    /**
163
     * Get the PHP binary to execute.
164
     *
165
     * @return array
166
     */
167
    protected function binary()
168
    {
169
        if ($this->usingPest()) {
170
            $command = $this->option('parallel') ? ['vendor/pestphp/pest/bin/pest', '--parallel'] : ['vendor/pestphp/pest/bin/pest'];
171
        } else {
172
            $command = $this->option('parallel') ? ['vendor/brianium/paratest/bin/paratest'] : ['vendor/phpunit/phpunit/phpunit'];
173
        }
174
 
175
        if ('phpdbg' === PHP_SAPI) {
176
            return array_merge([PHP_BINARY, '-qrr'], $command);
177
        }
178
 
179
        return array_merge([PHP_BINARY], $command);
180
    }
181
 
182
    /**
183
     * Gets the common arguments of PHPUnit and Pest.
184
     *
185
     * @return array
186
     */
187
    protected function commonArguments()
188
    {
189
        $arguments = [];
190
 
191
        if ($this->option('coverage')) {
192
            $arguments[] = '--coverage-php';
193
            $arguments[] = Coverage::getPath();
194
        }
195
 
196
        return $arguments;
197
    }
198
 
199
    /**
200
     * Determines if Pest is being used.
201
     *
202
     * @return bool
203
     */
204
    protected function usingPest()
205
    {
206
        return class_exists(\Pest\Laravel\PestServiceProvider::class);
207
    }
208
 
209
    /**
210
     * Get the array of arguments for running PHPUnit.
211
     *
212
     * @param  array  $options
213
     * @return array
214
     */
215
    protected function phpunitArguments($options)
216
    {
217
        $options = array_merge(['--printer=NunoMaduro\\Collision\\Adapters\\Phpunit\\Printer'], $options);
218
 
219
        $options = array_values(array_filter($options, function ($option) {
220
            return ! Str::startsWith($option, '--env=')
221
                && $option != '-q'
222
                && $option != '--quiet'
223
                && $option != '--coverage'
224
                && ! Str::startsWith($option, '--min');
225
        }));
226
 
227
        if (! file_exists($file = base_path('phpunit.xml'))) {
228
            $file = base_path('phpunit.xml.dist');
229
        }
230
 
231
        return array_merge($this->commonArguments(), ["--configuration=$file"], $options);
232
    }
233
 
234
    /**
235
     * Get the array of arguments for running Paratest.
236
     *
237
     * @param  array  $options
238
     * @return array
239
     */
240
    protected function paratestArguments($options)
241
    {
242
        $options = array_values(array_filter($options, function ($option) {
243
            return ! Str::startsWith($option, '--env=')
244
                && $option != '--coverage'
245
                && $option != '-q'
246
                && $option != '--quiet'
247
                && ! Str::startsWith($option, '--min')
248
                && ! Str::startsWith($option, '-p')
249
                && ! Str::startsWith($option, '--parallel')
250
                && ! Str::startsWith($option, '--recreate-databases')
251
                && ! Str::startsWith($option, '--drop-databases');
252
        }));
253
 
254
        if (! file_exists($file = base_path('phpunit.xml'))) {
255
            $file = base_path('phpunit.xml.dist');
256
        }
257
 
258
        return array_merge($this->commonArguments(), [
259
            "--configuration=$file",
260
            "--runner=\Illuminate\Testing\ParallelRunner",
261
        ], $options);
262
    }
263
 
264
    /**
265
     * Get the array of environment variables for running PHPUnit.
266
     *
267
     * @return array
268
     */
269
    protected function phpunitEnvironmentVariables()
270
    {
271
        return [];
272
    }
273
 
274
    /**
275
     * Get the array of environment variables for running Paratest.
276
     *
277
     * @return array
278
     */
279
    protected function paratestEnvironmentVariables()
280
    {
281
        return [
282
            'LARAVEL_PARALLEL_TESTING' => 1,
283
            'LARAVEL_PARALLEL_TESTING_RECREATE_DATABASES' => $this->option('recreate-databases'),
284
            'LARAVEL_PARALLEL_TESTING_DROP_DATABASES' => $this->option('drop-databases'),
285
        ];
286
    }
287
 
288
    /**
289
     * Clears any set Environment variables set by Laravel if the --env option is empty.
290
     *
291
     * @return void
292
     */
293
    protected function clearEnv()
294
    {
295
        if (! $this->option('env')) {
296
            $vars = self::getEnvironmentVariables(
297
                // @phpstan-ignore-next-line
298
                $this->laravel->environmentPath(),
299
                // @phpstan-ignore-next-line
300
                $this->laravel->environmentFile()
301
            );
302
 
303
            $repository = Env::getRepository();
304
 
305
            foreach ($vars as $name) {
306
                $repository->clear($name);
307
            }
308
        }
309
    }
310
 
311
    /**
312
     * @param  string  $path
313
     * @param  string  $file
314
     * @return array
315
     */
316
    protected static function getEnvironmentVariables($path, $file)
317
    {
318
        try {
319
            $content = StoreBuilder::createWithNoNames()
320
                ->addPath($path)
321
                ->addName($file)
322
                ->make()
323
                ->read();
324
        } catch (InvalidPathException $e) {
325
            return [];
326
        }
327
 
328
        $vars = [];
329
 
330
        foreach ((new Parser())->parse($content) as $entry) {
331
            $vars[] = $entry->getName();
332
        }
333
 
334
        return $vars;
335
    }
336
 
337
    /**
338
     * Check if the parallel dependencies are installed.
339
     *
340
     * @return bool
341
     */
342
    protected function isParallelDependenciesInstalled()
343
    {
344
        return class_exists(\ParaTest\Console\Commands\ParaTestCommand::class);
345
    }
346
 
347
    /**
348
     * Install parallel testing needed dependencies.
349
     *
350
     * @return void
351
     */
352
    protected function installParallelDependencies()
353
    {
354
        $command = $this->findComposer().' require brianium/paratest --dev';
355
 
356
        $process = Process::fromShellCommandline($command, null, null, null, null);
357
 
358
        if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
359
            try {
360
                $process->setTty(true);
361
            } catch (RuntimeException $e) {
362
                $this->output->writeln('Warning: '.$e->getMessage());
363
            }
364
        }
365
 
366
        try {
367
            $process->run(function ($type, $line) {
368
                $this->output->write($line);
369
            });
370
        } catch (ProcessSignaledException $e) {
371
            if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
372
                throw $e;
373
            }
374
        }
375
    }
376
 
377
    /**
378
     * Get the composer command for the environment.
379
     *
380
     * @return string
381
     */
382
    protected function findComposer()
383
    {
384
        $composerPath = getcwd().'/composer.phar';
385
 
386
        if (file_exists($composerPath)) {
387
            return '"'.PHP_BINARY.'" '.$composerPath;
388
        }
389
 
390
        return 'composer';
391
    }
392
}