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

Run::findTestFiles()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2.0014

Importance

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