Completed
Push — master ( 80d94b...95c442 )
by Jonathan
31s
created

Run::execute()   F

Complexity

Conditions 25
Paths > 20000

Size

Total Lines 172
Code Lines 104

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 105.9233

Importance

Changes 0
Metric Value
dl 0
loc 172
ccs 42
cts 85
cp 0.4941
rs 2
c 0
b 0
f 0
cc 25
eloc 104
nc 59266
nop 2
crap 105.9233

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types = 1);
4
5
namespace PHPChunkit\Command;
6
7
use PHPChunkit\ChunkedTests;
8
use PHPChunkit\TestChunker;
9
use PHPChunkit\TestFinder;
10
use PHPChunkit\TestRunner;
11
use PHPChunkit\Configuration;
12
use Symfony\Component\Console\Command\Command;
13
use Symfony\Component\Console\Helper\ProgressBar;
14
use Symfony\Component\Console\Input\InputInterface;
15
use Symfony\Component\Console\Input\InputOption;
16
use Symfony\Component\Console\Output\OutputInterface;
17
use Symfony\Component\Process\Process;
18
use Symfony\Component\Stopwatch\Stopwatch;
19
20
/**
21
 * @testClass PHPChunkit\Test\Command\RunTest
22
 */
23
class Run implements CommandInterface
24
{
25
    const NAME = 'run';
26
27
    /**
28
     * @var TestRunner
29
     */
30
    private $testRunner;
31
32
    /**
33
     * @var Configuration
34
     */
35
    private $configuration;
36
37
    /**
38
     * @var TestChunker
39
     */
40
    private $testChunker;
41
42
    /**
43
     * @var TestFinder
44
     */
45
    private $testFinder;
46
47 1
    public function __construct(
48
        TestRunner $testRunner,
49
        Configuration $configuration,
50
        TestChunker $testChunker,
51
        TestFinder $testFinder)
52
    {
53 1
        $this->testRunner = $testRunner;
54 1
        $this->configuration = $configuration;
55 1
        $this->testChunker = $testChunker;
56 1
        $this->testFinder = $testFinder;
57 1
    }
58
59
    public function getName() : string
60
    {
61
        return self::NAME;
62
    }
63
64
    public function configure(Command $command)
65
    {
66
        $command
67
            ->setDescription('Run tests.')
68
            ->addOption('debug', null, InputOption::VALUE_NONE, 'Run tests in debug mode.')
69
            ->addOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for PHP.')
70
            ->addOption('stop', null, InputOption::VALUE_NONE, 'Stop on failure or error.')
71
            ->addOption('failed', null, InputOption::VALUE_NONE, 'Track tests that have failed.')
72
            ->addOption('create-dbs', null, InputOption::VALUE_NONE, 'Create the test databases before running tests.')
73
            ->addOption('sandbox', null, InputOption::VALUE_NONE, 'Configure unique names.')
74
            ->addOption('chunk', null, InputOption::VALUE_REQUIRED, 'Run a specific chunk of tests.')
75
            ->addOption('num-chunks', null, InputOption::VALUE_REQUIRED, 'The number of chunks to run tests in.')
76
            ->addOption('group', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run all tests in these groups.')
77
            ->addOption('exclude-group', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run all tests excluding these groups.')
78
            ->addOption('changed', null, InputOption::VALUE_NONE, 'Run changed tests.')
79
            ->addOption('parallel', null, InputOption::VALUE_REQUIRED, 'Run test chunks in parallel.')
80
            ->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Filter tests by path/file name and run them.')
81
            ->addOption('contains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run tests that match the given content.')
82
            ->addOption('not-contains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run tests that do not match the given content.')
83
            ->addOption('file', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run test file.')
84
            ->addOption('phpunit-opt', null, InputOption::VALUE_REQUIRED, 'Pass through phpunit options.')
85
        ;
86
    }
87
88 1
    public function execute(InputInterface $input, OutputInterface $output)
89
    {
90 1
        $stopwatch = new Stopwatch();
91 1
        $stopwatch->start('Tests');
92
93 1
        $verbose = $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE;
94 1
        $parallel = $input->getOption('parallel');
95 1
        $showProgressBar = !$verbose && !$parallel;
96
97 1
        $chunkedTests = $this->chunkTestFiles($input);
98
99 1
        $chunks = $chunkedTests->getChunks();
100 1
        $testsPerChunk = $chunkedTests->getTestsPerChunk();
101 1
        $totalTests = $chunkedTests->getTotalTests();
102 1
        $numChunks = $chunkedTests->getNumChunks();
103
104 1
        if (!$totalTests) {
105
            $output->writeln('<error>No tests found to run.</error>');
106
107
            return;
108
        }
109
110 1
        $output->writeln(sprintf('Total Tests: <info>%s</info>', $totalTests));
111 1
        $output->writeln(sprintf('Number of Chunks Configured: <info>%s</info>', $numChunks));
112 1
        $output->writeln(sprintf('Number of Chunks Produced: <info>%s</info>', count($chunks)));
113 1
        $output->writeln(sprintf('Tests Per Chunk: <info>~%s</info>', $testsPerChunk));
114
115 1
        if ($chunk = $chunkedTests->getChunk()) {
116
            $output->writeln(sprintf('Chunk: <info>%s</info>', $chunk));
117
        }
118
119 1
        $output->writeln('-----------');
120 1
        $output->writeln('');
121
122
        // sandbox this run
123 1
        if ($input->getOption('sandbox')) {
124 1
            $this->testRunner->runTestCommand('sandbox');
125
        }
126
127
        // environment vars
128 1
        $env = [];
129
130 1
        if (empty($chunks)) {
131 1
            $output->writeln('<error>No tests to run.</error>');
132
        }
133
134 1
        $codes = [];
135 1
        $processes = [];
136 1
        $numChunkFailures = 0;
137 1
        $totalTestsRan = 0;
138 1
        $numProcesses = $parallel;
139
140 1
        foreach ($chunks as $i => $chunk) {
141
            $chunkNum = $i + 1;
142
143
            // drop and recreate dbs before running this chunk of tests
144
            if ($input->getOption('create-dbs')) {
145
                $this->testRunner->runTestCommand('create-dbs', [
146
                    '--quiet' => true,
147
                ]);
148
            }
149
150
            $numTests = $this->countNumTestsInChunk($chunk);
151
152
            $totalTestsRan += $numTests;
153
154
            $progressBar = $showProgressBar
155
                ? $this->createChunkProgressBar($output, $numTests)
156
                : null
157
            ;
158
159
            if ($showProgressBar) {
160
                $callback = $this->createProgressCallback($progressBar);
161
            } else {
162
                if ($verbose) {
163
                    $callback = function($type, $out) use ($output) {
164
                        $output->write($out);
165
                    };
166
                } else {
167
                    $callback = null;
168
                }
169
            }
170
171
            $processes[$chunkNum] = $process = $this->getChunkProcess(
172
                $chunk, $env
173
            );
174
175
            if ($parallel) {
176
                $output->writeln(sprintf('Starting chunk <info>#%s</info>', $chunkNum));
177
178
                $process->start($callback);
179
180
                if (count($processes) >= $numProcesses) {
181
                    $this->waitForProcesses(
182
                        $processes,
183
                        $input,
184
                        $output,
185
                        $verbose,
186
                        $codes,
187
                        $numChunkFailures
188
                    );
189
                }
190
191
            } else {
192
                if ($verbose) {
193
                    $output->writeln('');
194
                    $output->writeln(sprintf('Running chunk <info>#%s</info>', $chunkNum));
195
                }
196
197
                $codes[] = $code = $process->run($callback);
198
199
                if ($code > 0) {
200
                    $numChunkFailures++;
201
202
                    if ($verbose) {
203
                        $output->writeln(sprintf('Chunk #%s <error>FAILED</error>', $chunkNum));
204
                    }
205
206
                    if ($input->getOption('stop')) {
207
                        $output->writeln('');
208
                        $output->writeln($process->getOutput());
209
210
                        return $code;
211
                    }
212
                }
213
214
                if (!$verbose) {
215
                    $progressBar->finish();
216
                    $output->writeln('');
217
                }
218
219
                if ($code > 0) {
220
                    $output->writeln('');
221
222
                    if (!$verbose) {
223
                        $output->writeln($process->getOutput());
224
                    }
225
                }
226
            }
227
        }
228
229 1
        if ($parallel) {
230
            $this->waitForProcesses(
231
                $processes,
232
                $input,
233
                $output,
234
                $verbose,
235
                $codes,
236
                $numChunkFailures
237
            );
238
        }
239
240 1
        $failed = array_sum($codes) ? true : false;
241
242 1
        $event = $stopwatch->stop('Tests');
243
244 1
        $output->writeln('');
245 1
        $output->writeln(sprintf('Time: %s seconds, Memory: %s',
246 1
            round($event->getDuration() / 1000, 2),
247 1
            $this->formatBytes($event->getMemory())
248
        ));
249
250 1
        $output->writeln('');
251 1
        $output->writeln(sprintf('%s (%s chunks, %s tests%s)',
252 1
            $failed ? '<error>FAILED</error>' : '<info>PASSED</info>',
253
            count($chunks),
254
            $totalTestsRan,
255 1
            $failed ? sprintf(', Failed chunks: %s', $numChunkFailures) : ''
256
        ));
257
258 1
        return $failed ? 1 : 0;
259
    }
260
261
    private function waitForProcesses(
262
        array &$processes,
263
        InputInterface $input,
264
        OutputInterface $output,
265
        bool $verbose,
266
        &$codes,
267
        &$numChunkFailures)
268
    {
269
        while (count($processes)) {
270
            foreach ($processes as $chunkNum => $process) {
271
                if ($process->isRunning()) {
272
                    continue;
273
                }
274
275
                unset($processes[$chunkNum]);
276
277
                $codes[] = $code = $process->getExitCode();
278
279
                if ($code > 0) {
280
                    $numChunkFailures++;
281
282
                    $output->writeln(sprintf('Chunk #%s <error>FAILED</error>', $chunkNum));
283
284
                    $output->writeln('');
285
                    $output->write($process->getOutput());
286
287
                    if ($input->getOption('stop')) {
288
                        return $code;
289
                    }
290
                } else {
291
                    $output->writeln(sprintf('Chunk #%s <info>PASSED</info>', $chunkNum));
292
293
                    if ($verbose) {
294
                        $output->writeln('');
295
                        $output->write($process->getOutput());
296
                    }
297
                }
298
            }
299
        }
300
    }
301
302 1
    private function formatBytes(int $size, int $precision = 2) : string
303
    {
304 1
        if (!$size) {
305
            return 0;
306
        }
307
308 1
        $base = log($size, 1024);
309 1
        $suffixes = ['', 'KB', 'MB', 'GB', 'TB'];
310
311 1
        return round(pow(1024, $base - floor($base)), $precision).$suffixes[floor($base)];
312
    }
313
314
    private function getChunkProcess(array $chunk, array $env) : Process
315
    {
316
        $command = $this->createChunkCommand($chunk);
317
318
        return $this->testRunner->getPhpunitProcess($command, $env);
319
    }
320
321 1
    private function chunkTestFiles(InputInterface $input) : ChunkedTests
322
    {
323 1
        $testFiles = $this->findTestFiles($input);
324
325 1
        $numChunks = (int) $input->getOption('num-chunks')
326 1
            ?: $this->configuration->getNumChunks() ?: 1;
327 1
        $chunk = (int) $input->getOption('chunk');
328
329 1
        $chunkedTests = (new ChunkedTests())
330 1
            ->setNumChunks($numChunks)
331 1
            ->setChunk($chunk)
332
        ;
333
334 1
        if (!$testFiles) {
335
            return $chunkedTests;
336
        }
337
338 1
        $this->testChunker->chunkTestFiles($chunkedTests, $testFiles);
339
340 1
        return $chunkedTests;
341
    }
342
343 1
    private function findTestFiles(InputInterface $input)
344
    {
345 1
        $files = $input->getOption('file');
346
347 1
        if (!empty($files)) {
348
            return $files;
349
        }
350
351 1
        $groups = $input->getOption('group');
352 1
        $excludeGroups = $input->getOption('exclude-group');
353 1
        $changed = $input->getOption('changed');
354 1
        $filters = $input->getOption('filter');
355 1
        $contains = $input->getOption('contains');
356 1
        $notContains = $input->getOption('not-contains');
357
358 1
        $this->testFinder
359 1
            ->inGroups($groups)
360 1
            ->notInGroups($excludeGroups)
361 1
            ->changed($changed)
362
        ;
363
364 1
        foreach ($filters as $filter) {
365
            $this->testFinder->filter($filter);
366
        }
367
368 1
        foreach ($contains as $contain) {
369
            $this->testFinder->contains($contain);
370
        }
371
372 1
        foreach ($notContains as $notContain) {
373
            $this->testFinder->notContains($notContain);
374
        }
375
376 1
        return $this->testFinder->getFiles();
377
    }
378
379
    private function createChunkCommand(array $chunk) : string
380
    {
381
        $files = $this->buildFilesFromChunk($chunk);
382
383
        $config = $this->testRunner->generatePhpunitXml($files);
384
385
        return sprintf('-c %s', $config);
386
    }
387
388
    private function countNumTestsInChunk(array $chunk) : int
389
    {
390
        return array_sum(array_map(function(array $chunkFile) {
391
            return $chunkFile['numTests'];
392
        }, $chunk));
393
    }
394
395
    private function buildFilesFromChunk(array $chunk) : array
396
    {
397
        return array_map(function(array $chunkFile) {
398
            return $chunkFile['file'];
399
        }, $chunk);
400
    }
401
402
    private function createChunkProgressBar(
403
        OutputInterface $output,
404
        int $numTests) : ProgressBar
405
    {
406
        $progressBar = new ProgressBar($output, $numTests);
407
        $progressBar->setBarCharacter('<fg=green>=</>');
408
        $progressBar->setProgressCharacter("\xF0\x9F\x8C\xAD");
409
410
        return $progressBar;
411
    }
412
413
    private function createProgressCallback(ProgressBar $progressBar = null)
414
    {
415
        return function($type, $buffer) use ($progressBar) {
416
            if ($progressBar) {
417
                if (in_array($buffer, ['F', 'E'])) {
418
                    $progressBar->setBarCharacter('<fg=red>=</>');
419
                }
420
421
                if (in_array($buffer, ['F', 'E', 'S', '.'])) {
422
                    $progressBar->advance();
423
                }
424
            }
425
        };
426
    }
427
}
428