Completed
Push — master ( e546fc...9f6d8c )
by Julian
11s
created

ExecutableTest   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 348
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 84.34%

Importance

Changes 0
Metric Value
wmc 30
lcom 1
cbo 2
dl 0
loc 348
ccs 70
cts 83
cp 0.8434
rs 10
c 0
b 0
f 0

22 Methods

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