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