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 
11 use SebastianBergmann\Environment\Runtime;
12 
13 /**
14  * Utility methods for PHP sub-processes.
15  */
16 abstract 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