1<?php
2/*
3 * This file is part of PHPUnit.
4 *
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11use SebastianBergmann\Environment\Runtime;
12
13/**
14 * Utility methods for PHP sub-processes.
15 */
16abstract class PHPUnit_Util_PHP
17{
18    /**
19     * @var Runtime
20     */
21    protected $runtime;
22
23    /**
24     * @var bool
25     */
26    protected $stderrRedirection = false;
27
28    /**
29     * @var string
30     */
31    protected $stdin = '';
32
33    /**
34     * @var string
35     */
36    protected $args = '';
37
38    /**
39     * @var array
40     */
41    protected $env = [];
42
43    /**
44     * @var int
45     */
46    protected $timeout = 0;
47
48    /**
49     * Creates internal Runtime instance.
50     */
51    public function __construct()
52    {
53        $this->runtime = new Runtime();
54    }
55
56    /**
57     * Defines if should use STDERR redirection or not.
58     *
59     * Then $stderrRedirection is TRUE, STDERR is redirected to STDOUT.
60     *
61     * @throws PHPUnit_Framework_Exception
62     *
63     * @param bool $stderrRedirection
64     */
65    public function setUseStderrRedirection($stderrRedirection)
66    {
67        if (!is_bool($stderrRedirection)) {
68            throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean');
69        }
70
71        $this->stderrRedirection = $stderrRedirection;
72    }
73
74    /**
75     * Returns TRUE if uses STDERR redirection or FALSE if not.
76     *
77     * @return bool
78     */
79    public function useStderrRedirection()
80    {
81        return $this->stderrRedirection;
82    }
83
84    /**
85     * Sets the input string to be sent via STDIN
86     *
87     * @param string $stdin
88     */
89    public function setStdin($stdin)
90    {
91        $this->stdin = (string) $stdin;
92    }
93
94    /**
95     * Returns the input string to be sent via STDIN
96     *
97     * @return string
98     */
99    public function getStdin()
100    {
101        return $this->stdin;
102    }
103
104    /**
105     * Sets the string of arguments to pass to the php job
106     *
107     * @param string $args
108     */
109    public function setArgs($args)
110    {
111        $this->args = (string) $args;
112    }
113
114    /**
115     * Returns the string of arguments to pass to the php job
116     *
117     * @retrun string
118     */
119    public function getArgs()
120    {
121        return $this->args;
122    }
123
124    /**
125     * Sets the array of environment variables to start the child process with
126     *
127     * @param array $env
128     */
129    public function setEnv(array $env)
130    {
131        $this->env = $env;
132    }
133
134    /**
135     * Returns the array of environment variables to start the child process with
136     *
137     * @return array
138     */
139    public function getEnv()
140    {
141        return $this->env;
142    }
143
144    /**
145     * Sets the amount of seconds to wait before timing out
146     *
147     * @param int $timeout
148     */
149    public function setTimeout($timeout)
150    {
151        $this->timeout = (int) $timeout;
152    }
153
154    /**
155     * Returns the amount of seconds to wait before timing out
156     *
157     * @return int
158     */
159    public function getTimeout()
160    {
161        return $this->timeout;
162    }
163
164    /**
165     * @return PHPUnit_Util_PHP
166     */
167    public static function factory()
168    {
169        if (DIRECTORY_SEPARATOR == '\\') {
170            return new PHPUnit_Util_PHP_Windows;
171        }
172
173        return new PHPUnit_Util_PHP_Default;
174    }
175
176    /**
177     * Runs a single test in a separate PHP process.
178     *
179     * @param string                       $job
180     * @param PHPUnit_Framework_Test       $test
181     * @param PHPUnit_Framework_TestResult $result
182     *
183     * @throws PHPUnit_Framework_Exception
184     */
185    public function runTestJob($job, PHPUnit_Framework_Test $test, PHPUnit_Framework_TestResult $result)
186    {
187        $result->startTest($test);
188
189        $_result = $this->runJob($job);
190
191        $this->processChildResult(
192            $test,
193            $result,
194            $_result['stdout'],
195            $_result['stderr']
196        );
197    }
198
199    /**
200     * Returns the command based into the configurations.
201     *
202     * @param array       $settings
203     * @param string|null $file
204     *
205     * @return string
206     */
207    public function getCommand(array $settings, $file = null)
208    {
209        $command = $this->runtime->getBinary();
210        $command .= $this->settingsToParameters($settings);
211
212        if ('phpdbg' === PHP_SAPI) {
213            $command .= ' -qrr ';
214
215            if ($file) {
216                $command .= '-e ' . escapeshellarg($file);
217            } else {
218                $command .= escapeshellarg(__DIR__ . '/PHP/eval-stdin.php');
219            }
220        } elseif ($file) {
221            $command .= ' -f ' . escapeshellarg($file);
222        }
223
224        if ($this->args) {
225            $command .= ' -- ' . $this->args;
226        }
227
228        if (true === $this->stderrRedirection) {
229            $command .= ' 2>&1';
230        }
231
232        return $command;
233    }
234
235    /**
236     * Runs a single job (PHP code) using a separate PHP process.
237     *
238     * @param string $job
239     * @param array  $settings
240     *
241     * @return array
242     *
243     * @throws PHPUnit_Framework_Exception
244     */
245    abstract public function runJob($job, array $settings = []);
246
247    /**
248     * @param array $settings
249     *
250     * @return string
251     */
252    protected function settingsToParameters(array $settings)
253    {
254        $buffer = '';
255
256        foreach ($settings as $setting) {
257            $buffer .= ' -d ' . $setting;
258        }
259
260        return $buffer;
261    }
262
263    /**
264     * Processes the TestResult object from an isolated process.
265     *
266     * @param PHPUnit_Framework_Test       $test
267     * @param PHPUnit_Framework_TestResult $result
268     * @param string                       $stdout
269     * @param string                       $stderr
270     */
271    private function processChildResult(PHPUnit_Framework_Test $test, PHPUnit_Framework_TestResult $result, $stdout, $stderr)
272    {
273        $time = 0;
274
275        if (!empty($stderr)) {
276            $result->addError(
277                $test,
278                new PHPUnit_Framework_Exception(trim($stderr)),
279                $time
280            );
281        } else {
282            set_error_handler(function ($errno, $errstr, $errfile, $errline) {
283                throw new ErrorException($errstr, $errno, $errno, $errfile, $errline);
284            });
285            try {
286                if (strpos($stdout, "#!/usr/bin/env php\n") === 0) {
287                    $stdout = substr($stdout, 19);
288                }
289
290                $childResult = unserialize(str_replace("#!/usr/bin/env php\n", '', $stdout));
291                restore_error_handler();
292            } catch (ErrorException $e) {
293                restore_error_handler();
294                $childResult = false;
295
296                $result->addError(
297                    $test,
298                    new PHPUnit_Framework_Exception(trim($stdout), 0, $e),
299                    $time
300                );
301            }
302
303            if ($childResult !== false) {
304                if (!empty($childResult['output'])) {
305                    $output = $childResult['output'];
306                }
307
308                $test->setResult($childResult['testResult']);
309                $test->addToAssertionCount($childResult['numAssertions']);
310
311                $childResult = $childResult['result'];
312                /* @var $childResult PHPUnit_Framework_TestResult */
313
314                if ($result->getCollectCodeCoverageInformation()) {
315                    $result->getCodeCoverage()->merge(
316                        $childResult->getCodeCoverage()
317                    );
318                }
319
320                $time           = $childResult->time();
321                $notImplemented = $childResult->notImplemented();
322                $risky          = $childResult->risky();
323                $skipped        = $childResult->skipped();
324                $errors         = $childResult->errors();
325                $warnings       = $childResult->warnings();
326                $failures       = $childResult->failures();
327
328                if (!empty($notImplemented)) {
329                    $result->addError(
330                        $test,
331                        $this->getException($notImplemented[0]),
332                        $time
333                    );
334                } elseif (!empty($risky)) {
335                    $result->addError(
336                        $test,
337                        $this->getException($risky[0]),
338                        $time
339                    );
340                } elseif (!empty($skipped)) {
341                    $result->addError(
342                        $test,
343                        $this->getException($skipped[0]),
344                        $time
345                    );
346                } elseif (!empty($errors)) {
347                    $result->addError(
348                        $test,
349                        $this->getException($errors[0]),
350                        $time
351                    );
352                } elseif (!empty($warnings)) {
353                    $result->addWarning(
354                        $test,
355                        $this->getException($warnings[0]),
356                        $time
357                    );
358                } elseif (!empty($failures)) {
359                    $result->addFailure(
360                        $test,
361                        $this->getException($failures[0]),
362                        $time
363                    );
364                }
365            }
366        }
367
368        $result->endTest($test, $time);
369
370        if (!empty($output)) {
371            print $output;
372        }
373    }
374
375    /**
376     * Gets the thrown exception from a PHPUnit_Framework_TestFailure.
377     *
378     * @param PHPUnit_Framework_TestFailure $error
379     *
380     * @return Exception
381     *
382     * @see    https://github.com/sebastianbergmann/phpunit/issues/74
383     */
384    private function getException(PHPUnit_Framework_TestFailure $error)
385    {
386        $exception = $error->thrownException();
387
388        if ($exception instanceof __PHP_Incomplete_Class) {
389            $exceptionArray = [];
390            foreach ((array) $exception as $key => $value) {
391                $key                  = substr($key, strrpos($key, "\0") + 1);
392                $exceptionArray[$key] = $value;
393            }
394
395            $exception = new PHPUnit_Framework_SyntheticError(
396                sprintf(
397                    '%s: %s',
398                    $exceptionArray['_PHP_Incomplete_Class_Name'],
399                    $exceptionArray['message']
400                ),
401                $exceptionArray['code'],
402                $exceptionArray['file'],
403                $exceptionArray['line'],
404                $exceptionArray['trace']
405            );
406        }
407
408        return $exception;
409    }
410}
411