Completed
Pull Request — master (#38)
by Jonathan
04:55 queued 02:08
created

Run::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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