Completed
Pull Request — master (#290)
by
unknown
13:33 queued 05:47
created

ExecutableTest::getTestCount()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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