Completed
Pull Request — master (#223)
by Michael
03:44 queued 01:35
created

ExecutableTest   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 86.96%

Importance

Changes 0
Metric Value
wmc 31
lcom 1
cbo 2
dl 0
loc 340
ccs 80
cts 92
cp 0.8696
rs 9.8
c 0
b 0
f 0

22 Methods

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

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
270
            // TODO: Implement command line length validation for linux/osx/freebsd
271
            //       Please note that on unix environment variables also became part of command line
272
            // linux: echo | xargs --show-limits
273
            // osx/linux: getconf ARG_MAX
274
        }
275 2
    }
276
277
    /**
278
     * A template method that can be overridden to add necessary options for a test
279
     *
280
     * @param  array $options the options that are passed to the run method
281
     * @return array $options the prepared options
282
     */
283 3
    protected function prepareOptions($options)
284
    {
285 3
        return $options;
286
    }
287
288
    /**
289
     * Returns the command string that will be executed
290
     * by proc_open
291
     *
292
     * @param $binary
293
     * @param array $options
294
     * @return mixed
295
     */
296 6
    protected function getCommandString($binary, $options = array())
297
    {
298 6
        $builder = new ProcessBuilder();
299 6
        $builder->setPrefix($binary);
300 6
        foreach ($options as $key => $value) {
301 5
            $builder->add("--$key");
302 5
            if ($value !== null) {
303 5
                $builder->add($value);
304 5
            }
305 6
        }
306
307 6
        $builder->add($this->fullyQualifiedClassName);
308 6
        $builder->add($this->getPath());
309
310 6
        $process = $builder->getProcess();
311
312 6
        return $process->getCommandLine();
313
    }
314
315
    /**
316
     * Checks environment variables for the presence of a TEST_TOKEN
317
     * variable and sets $this->token based on its value
318
     *
319
     * @param $environmentVariables
320
     */
321 3
    protected function handleEnvironmentVariables($environmentVariables)
322
    {
323 3
        if (isset($environmentVariables['TEST_TOKEN'])) {
324 3
            $this->token = $environmentVariables['TEST_TOKEN'];
325 3
        }
326 3
    }
327
328
    /**
329
     * Checks if the coverage-php option is set and redirects it to a unique temp file.
330
     * This will ensure, that multiple tests write to separate coverage-files.
331
     *
332
     * @param array $options
333
     * @return array $options
334
     */
335 3
    protected function redirectCoverageOption($options)
336
    {
337 3
        if (isset($options['coverage-php'])) {
338 3
            $options['coverage-php'] = $this->getCoverageFileName();
339 3
        }
340
341 3
        unset($options['coverage-html']);
342 3
        unset($options['coverage-clover']);
343
344 3
        return $options;
345
    }
346
}
347