ExecutableTest::getCommandString()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 9
cts 9
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 9
nc 3
nop 2
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ParaTest\Runners\PHPUnit;
6
7
use Symfony\Component\Process\PhpExecutableFinder;
8
use Symfony\Component\Process\Process;
9
10
abstract class ExecutableTest
11
{
12
    /**
13
     * The path to the test to run.
14
     *
15
     * @var string
16
     */
17
    protected $path;
18
19
    /**
20
     * A path to the temp file created
21
     * for this test.
22
     *
23
     * @var string
24
     */
25
    protected $temp;
26
    protected $fullyQualifiedClassName;
27
    protected $pipes = [];
28
29
    /**
30
     * Path where the coveragereport is stored.
31
     *
32
     * @var string
33
     */
34
    protected $coverageFileName;
35
36
    /**
37
     * @var Process
38
     */
39
    protected $process;
40
41
    /**
42
     * A unique token value for a given
43
     * process.
44
     *
45
     * @var int
46
     */
47
    protected $token;
48
49
    /**
50
     * Last executed process command.
51
     *
52
     * @var string
53
     */
54
    protected $lastCommand;
55
56 66
    public function __construct(string $path, string $fullyQualifiedClassName = null)
57
    {
58 66
        $this->path = $path;
59 66
        $this->fullyQualifiedClassName = $fullyQualifiedClassName;
60 66
    }
61
62
    /**
63
     * Get the expected count of tests to be executed.
64
     *
65
     * @return int
66
     */
67
    abstract public function getTestCount(): int;
68
69
    /**
70
     * Get the path to the test being executed.
71
     *
72
     * @return string
73
     */
74 6
    public function getPath(): string
75
    {
76 6
        return $this->path;
77
    }
78
79
    /**
80
     * Returns the path to this test's temp file.
81
     * If the temp file does not exist, it will be
82
     * created.
83
     *
84
     * @return string
85
     */
86 32
    public function getTempFile(): string
87
    {
88 32
        if (null === $this->temp) {
89 5
            $this->temp = tempnam(sys_get_temp_dir(), 'PT_');
90
        }
91
92 32
        return $this->temp;
93
    }
94
95
    /**
96
     * Return the test process' stderr contents.
97
     *
98
     * @return string
99
     */
100
    public function getStderr(): string
101
    {
102
        return $this->process->getErrorOutput();
103
    }
104
105
    /**
106
     * Return any warnings that are in the test output, or false if there are none.
107
     *
108
     * @return mixed
109
     */
110 11
    public function getWarnings()
111
    {
112 11
        if (!$this->process) {
113 9
            return false;
114
        }
115
116
        // PHPUnit has a bug where by it doesn't include warnings in the junit
117
        // output, but still fails. This is a hacky, imperfect method for extracting them
118
        // see https://github.com/sebastianbergmann/phpunit/issues/1317
119 2
        preg_match_all(
120 2
            '/^\d+\) Warning\n(.+?)$/ms',
121 2
            $this->process->getOutput(),
122 2
            $matches
123
        );
124
125 2
        return $matches[1] ?? false;
126
    }
127
128
    /**
129
     * Stop the process and return it's
130
     * exit code.
131
     *
132
     * @return int
133
     */
134 2
    public function stop(): int
135
    {
136 2
        return $this->process->stop();
137
    }
138
139
    /**
140
     * Removes the test file.
141
     */
142
    public function deleteFile()
143
    {
144
        $outputFile = $this->getTempFile();
145
        unlink($outputFile);
146
    }
147
148
    /**
149
     * Check if the process has terminated.
150
     *
151
     * @return bool
152
     */
153 2
    public function isDoneRunning(): bool
154
    {
155 2
        return $this->process->isTerminated();
156
    }
157
158
    /**
159
     * Return the exit code of the process.
160
     *
161
     * @return int
162
     */
163 2
    public function getExitCode(): int
164
    {
165 2
        return $this->process->getExitCode();
166
    }
167
168
    /**
169
     * Return the last process command.
170
     *
171
     * @return string
172
     */
173
    public function getLastCommand(): string
174
    {
175
        return $this->lastCommand;
176
    }
177
178
    /**
179
     * Executes the test by creating a separate process.
180
     *
181
     * @param $binary
182
     * @param array $options
183
     * @param array $environmentVariables
184
     *
185
     * @return $this
186
     */
187 2
    public function run(string $binary, array $options = [], array $environmentVariables = [])
188
    {
189 2
        $environmentVariables['PARATEST'] = 1;
190 2
        $this->handleEnvironmentVariables($environmentVariables);
191 2
        $finder = new PhpExecutableFinder();
192 2
        $command = $finder->find() . ' ' . $this->command($binary, $options);
193 2
        $this->assertValidCommandLineLength($command);
194 2
        $this->lastCommand = $command;
195 2
        $this->process = new Process($command, null, $environmentVariables);
196 2
        if (method_exists($this->process, 'inheritEnvironmentVariables')) {
197 2
            $this->process->inheritEnvironmentVariables();  // no such method in 3.0, but emits warning if this isn't done in 3.3
198
        }
199 2
        $this->process->start();
200
201 2
        return $this;
202
    }
203
204
    /**
205
     * Returns the unique token for this test process.
206
     *
207
     * @return int
208
     */
209 1
    public function getToken(): int
210
    {
211 1
        return $this->token;
212
    }
213
214
    /**
215
     * Generate command line with passed options suitable to handle through paratest.
216
     *
217
     * @param string $binary  executable binary name
218
     * @param array  $options command line options
219
     *
220
     * @return string command line
221
     */
222 3
    public function command(string $binary, array $options = []): string
223
    {
224 3
        $options = array_merge($this->prepareOptions($options), ['log-junit' => $this->getTempFile()]);
225 3
        $options = $this->redirectCoverageOption($options);
226
227 3
        return $this->getCommandString($binary, $options);
228
    }
229
230
    /**
231
     * Get covertage filename.
232
     *
233
     * @return string
234
     */
235 3
    public function getCoverageFileName(): string
236
    {
237 3
        if ($this->coverageFileName === null) {
238 3
            $this->coverageFileName = tempnam(sys_get_temp_dir(), 'CV_');
239
        }
240
241 3
        return $this->coverageFileName;
242
    }
243
244
    /**
245
     * Get process stdout content.
246
     *
247
     * @return string
248
     */
249
    public function getStdout(): string
250
    {
251
        return $this->process->getOutput();
252
    }
253
254
    /**
255
     * Set process termporary filename.
256
     *
257
     * @param string $temp
258
     */
259 37
    public function setTempFile(string $temp)
260
    {
261 37
        $this->temp = $temp;
262 37
    }
263
264
    /**
265
     * Assert that command line lenght is valid.
266
     *
267
     * In some situations process command line can became too long when combining different test
268
     * cases in single --filter arguments so it's better to show error regarding that to user
269
     * and propose him to decrease max batch size.
270
     *
271
     * @param string $cmd Command line
272
     *
273
     * @throws \RuntimeException on too long command line
274
     */
275 2
    protected function assertValidCommandLineLength(string $cmd)
276
    {
277 2
        if (DIRECTORY_SEPARATOR === '\\') { // windows
278
            // symfony's process wrapper
279
            $cmd = 'cmd /V:ON /E:ON /C "(' . $cmd . ')';
280
            if (strlen($cmd) > 32767) {
281
                throw new \RuntimeException('Command line is too long, try to decrease max batch size');
282
            }
283
        }
284
        /*
285
         * @todo Implement command line length validation for linux/osx/freebsd.
286
         *       Please note that on unix environment variables also became part of command line:
287
         *         - linux: echo | xargs --show-limits
288
         *         - osx/linux: getconf ARG_MAX
289
         */
290 2
    }
291
292
    /**
293
     * A template method that can be overridden to add necessary options for a test.
294
     *
295
     * @param array $options the options that are passed to the run method
296
     *
297
     * @return array $options the prepared options
298
     */
299 3
    protected function prepareOptions(array $options): array
300
    {
301 3
        return $options;
302
    }
303
304
    /**
305
     * Returns the command string that will be executed
306
     * by proc_open.
307
     *
308
     * @param string $binary
309
     * @param array  $options
310
     *
311
     * @return mixed
312
     */
313 6
    protected function getCommandString(string $binary, array $options = [])
314
    {
315
        // The order we add stuff into $arguments is important
316 6
        $arguments = [$binary];
317 6
        foreach ($options as $key => $value) {
318 5
            $arguments[] = "--$key";
319 5
            if ($value !== null) {
320 5
                $arguments[] = $value;
321
            }
322
        }
323
324 6
        $arguments[] = $this->fullyQualifiedClassName ?? '';
325 6
        $arguments[] = $this->getPath();
326
327 6
        return (new Process($arguments))->getCommandLine();
328
    }
329
330
    /**
331
     * Checks environment variables for the presence of a TEST_TOKEN
332
     * variable and sets $this->token based on its value.
333
     *
334
     * @param $environmentVariables
335
     */
336 3
    protected function handleEnvironmentVariables(array $environmentVariables)
337
    {
338 3
        if (isset($environmentVariables['TEST_TOKEN'])) {
339 3
            $this->token = $environmentVariables['TEST_TOKEN'];
340
        }
341 3
    }
342
343
    /**
344
     * Checks if the coverage-php option is set and redirects it to a unique temp file.
345
     * This will ensure, that multiple tests write to separate coverage-files.
346
     *
347
     * @param array $options
348
     *
349
     * @return array $options
350
     */
351 3
    protected function redirectCoverageOption(array $options): array
352
    {
353 3
        if (isset($options['coverage-php'])) {
354 3
            $options['coverage-php'] = $this->getCoverageFileName();
355
        }
356
357 3
        unset($options['coverage-html'], $options['coverage-clover']);
358
359 3
        return $options;
360
    }
361
}
362