Completed
Pull Request — master (#287)
by Matthias
03:43
created

ExecutableTest::getExitCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
315 6
        $builder->setPrefix($binary);
316 6
        foreach ($options as $key => $value) {
317 5
            $builder->add("--$key");
318 5
            if ($value !== null) {
319 5
                $builder->add($value);
320
            }
321
        }
322
323 6
        $builder->add($this->fullyQualifiedClassName);
324 6
        $builder->add($this->getPath());
325
326 6
        $process = $builder->getProcess();
327
328 6
        return $process->getCommandLine();
329
    }
330
331
    /**
332
     * Checks environment variables for the presence of a TEST_TOKEN
333
     * variable and sets $this->token based on its value.
334
     *
335
     * @param $environmentVariables
336
     */
337 3
    protected function handleEnvironmentVariables(array $environmentVariables)
338
    {
339 3
        if (isset($environmentVariables['TEST_TOKEN'])) {
340 3
            $this->token = $environmentVariables['TEST_TOKEN'];
341
        }
342 3
    }
343
344
    /**
345
     * Checks if the coverage-php option is set and redirects it to a unique temp file.
346
     * This will ensure, that multiple tests write to separate coverage-files.
347
     *
348
     * @param array $options
349
     *
350
     * @return array $options
351
     */
352 3
    protected function redirectCoverageOption(array $options): array
353
    {
354 3
        if (isset($options['coverage-php'])) {
355 3
            $options['coverage-php'] = $this->getCoverageFileName();
356
        }
357
358 3
        unset($options['coverage-html'], $options['coverage-clover']);
359
360 3
        return $options;
361
    }
362
}
363