Completed
Push — master ( a2f0f3...c16100 )
by Julian
02:29
created

ExecutableTest::command()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ParaTest\Runners\PHPUnit;
6
7
use Symfony\Component\Process\Process;
8
use Symfony\Component\Process\ProcessBuilder;
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
        $command = PHP_BINARY . ' ' . $this->command($binary, $options);
192 2
        $this->assertValidCommandLineLength($command);
193 2
        $this->lastCommand = $command;
194 2
        $this->process = new Process($command, null, $environmentVariables);
195 2
        if (method_exists($this->process, 'inheritEnvironmentVariables')) {
196 2
            $this->process->inheritEnvironmentVariables();  // no such method in 3.0, but emits warning if this isn't done in 3.3
197
        }
198 2
        $this->process->start();
199
200 2
        return $this;
201
    }
202
203
    /**
204
     * Returns the unique token for this test process.
205
     *
206
     * @return int
207
     */
208 1
    public function getToken(): int
209
    {
210 1
        return $this->token;
211
    }
212
213
    /**
214
     * Generate command line with passed options suitable to handle through paratest.
215
     *
216
     * @param string $binary  executable binary name
217
     * @param array  $options command line options
218
     *
219
     * @return string command line
220
     */
221 3
    public function command(string $binary, array $options = []): string
222
    {
223 3
        $options = array_merge($this->prepareOptions($options), ['log-junit' => $this->getTempFile()]);
224 3
        $options = $this->redirectCoverageOption($options);
225
226 3
        return $this->getCommandString($binary, $options);
227
    }
228
229
    /**
230
     * Get covertage filename.
231
     *
232
     * @return string
233
     */
234 3
    public function getCoverageFileName(): string
235
    {
236 3
        if ($this->coverageFileName === null) {
237 3
            $this->coverageFileName = tempnam(sys_get_temp_dir(), 'CV_');
238
        }
239
240 3
        return $this->coverageFileName;
241
    }
242
243
    /**
244
     * Get process stdout content.
245
     *
246
     * @return string
247
     */
248
    public function getStdout(): string
249
    {
250
        return $this->process->getOutput();
251
    }
252
253
    /**
254
     * Set process termporary filename.
255
     *
256
     * @param string $temp
257
     */
258 37
    public function setTempFile(string $temp)
259
    {
260 37
        $this->temp = $temp;
261 37
    }
262
263
    /**
264
     * Assert that command line lenght is valid.
265
     *
266
     * In some situations process command line can became too long when combining different test
267
     * cases in single --filter arguments so it's better to show error regarding that to user
268
     * and propose him to decrease max batch size.
269
     *
270
     * @param string $cmd Command line
271
     *
272
     * @throws \RuntimeException on too long command line
273
     */
274 2
    protected function assertValidCommandLineLength(string $cmd)
275
    {
276 2
        if (DIRECTORY_SEPARATOR === '\\') { // windows
277
            // symfony's process wrapper
278
            $cmd = 'cmd /V:ON /E:ON /C "(' . $cmd . ')';
279
            if (strlen($cmd) > 32767) {
280
                throw new \RuntimeException('Command line is too long, try to decrease max batch size');
281
            }
282
        }
283
            // TODO: Implement command line length validation for linux/osx/freebsd
284
            //       Please note that on unix environment variables also became part of command line
285
            // linux: echo | xargs --show-limits
286
            // osx/linux: getconf ARG_MAX
287 2
    }
288
289
    /**
290
     * A template method that can be overridden to add necessary options for a test.
291
     *
292
     * @param array $options the options that are passed to the run method
293
     *
294
     * @return array $options the prepared options
295
     */
296 3
    protected function prepareOptions(array $options): array
297
    {
298 3
        return $options;
299
    }
300
301
    /**
302
     * Returns the command string that will be executed
303
     * by proc_open.
304
     *
305
     * @param $binary
306
     * @param array $options
307
     *
308
     * @return mixed
309
     */
310 6
    protected function getCommandString(string $binary, array $options = [])
311
    {
312 6
        $builder = new ProcessBuilder();
313 6
        $builder->setPrefix($binary);
314 6
        foreach ($options as $key => $value) {
315 5
            $builder->add("--$key");
316 5
            if ($value !== null) {
317 5
                $builder->add($value);
318
            }
319
        }
320
321 6
        $builder->add($this->fullyQualifiedClassName);
322 6
        $builder->add($this->getPath());
323
324 6
        $process = $builder->getProcess();
325
326 6
        return $process->getCommandLine();
327
    }
328
329
    /**
330
     * Checks environment variables for the presence of a TEST_TOKEN
331
     * variable and sets $this->token based on its value.
332
     *
333
     * @param $environmentVariables
334
     */
335 3
    protected function handleEnvironmentVariables(array $environmentVariables)
336
    {
337 3
        if (isset($environmentVariables['TEST_TOKEN'])) {
338 3
            $this->token = $environmentVariables['TEST_TOKEN'];
339
        }
340 3
    }
341
342
    /**
343
     * Checks if the coverage-php option is set and redirects it to a unique temp file.
344
     * This will ensure, that multiple tests write to separate coverage-files.
345
     *
346
     * @param array $options
347
     *
348
     * @return array $options
349
     */
350 3
    protected function redirectCoverageOption(array $options): array
351
    {
352 3
        if (isset($options['coverage-php'])) {
353 3
            $options['coverage-php'] = $this->getCoverageFileName();
354
        }
355
356 3
        unset($options['coverage-html'], $options['coverage-clover']);
357
358 3
        return $options;
359
    }
360
}
361