Completed
Pull Request — master (#38)
by Jonathan
02:53
created

Run::createProgressCallback()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 0
cts 2
cp 0
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 1
nop 1
crap 20
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);
0 ignored issues
show
Bug introduced by
The method notContains() does not exist on PHPChunkit\TestFinder. Did you maybe mean contains()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
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