<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Util\PHP;
use const DIRECTORY_SEPARATOR;
use const PHP_SAPI;
use function array_keys;
use function array_merge;
use function assert;
use function escapeshellarg;
use function ini_get_all;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use function str_replace;
use function strpos;
use function strrpos;
use function substr;
use function trim;
use function unserialize;
use __PHP_Incomplete_Class;
use ErrorException;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Exception;
use PHPUnit\Framework\SyntheticError;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestFailure;
use PHPUnit\Framework\TestResult;
use SebastianBergmann\Environment\Runtime;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
abstract class AbstractPhpProcess
{
/**
* @var Runtime
*/
protected $runtime;
/**
* @var bool
*/
protected $stderrRedirection = false;
/**
* @var string
*/
protected $stdin = '';
/**
* @var string
*/
protected $args = '';
/**
* @var array<string, string>
*/
protected $env = [];
/**
* @var int
*/
protected $timeout = 0;
public static function factory(): self
{
if (DIRECTORY_SEPARATOR === '\\') {
return new WindowsPhpProcess;
}
return new DefaultPhpProcess;
}
public function __construct()
{
$this->runtime = new Runtime;
}
/**
* Defines if should use STDERR redirection or not.
*
* Then $stderrRedirection is TRUE, STDERR is redirected to STDOUT.
*/
public function setUseStderrRedirection(bool $stderrRedirection): void
{
$this->stderrRedirection = $stderrRedirection;
}
/**
* Returns TRUE if uses STDERR redirection or FALSE if not.
*/
public function useStderrRedirection(): bool
{
return $this->stderrRedirection;
}
/**
* Sets the input string to be sent via STDIN.
*/
public function setStdin(string $stdin): void
{
$this->stdin = $stdin;
}
/**
* Returns the input string to be sent via STDIN.
*/
public function getStdin(): string
{
return $this->stdin;
}
/**
* Sets the string of arguments to pass to the php job.
*/
public function setArgs(string $args): void
{
$this->args = $args;
}
/**
* Returns the string of arguments to pass to the php job.
*/
public function getArgs(): string
{
return $this->args;
}
/**
* Sets the array of environment variables to start the child process with.
*
* @param array<string, string> $env
*/
public function setEnv(array $env): void
{
$this->env = $env;
}
/**
* Returns the array of environment variables to start the child process with.
*/
public function getEnv(): array
{
return $this->env;
}
/**
* Sets the amount of seconds to wait before timing out.
*/
public function setTimeout(int $timeout): void
{
$this->timeout = $timeout;
}
/**
* Returns the amount of seconds to wait before timing out.
*/
public function getTimeout(): int
{
return $this->timeout;
}
/**
* Runs a single test in a separate PHP process.
*
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function runTestJob(string $job, Test $test, TestResult $result): void
{
$result->startTest($test);
$_result = $this->runJob($job);
$this->processChildResult(
$test,
$result,
$_result['stdout'],
$_result['stderr']
);
}
/**
* Returns the command based into the configurations.
*/
public function getCommand(array $settings, string $file = null): string
{
$command = $this->runtime->getBinary();
if ($this->runtime->hasPCOV()) {
$settings = array_merge(
$settings,
$this->runtime->getCurrentSettings(
array_keys(ini_get_all('pcov'))
)
);
} elseif ($this->runtime->hasXdebug()) {
$settings = array_merge(
$settings,
$this->runtime->getCurrentSettings(
array_keys(ini_get_all('xdebug'))
)
);
}
$command .= $this->settingsToParameters($settings);
if (PHP_SAPI === 'phpdbg') {
$command .= ' -qrr';
if (!$file) {
$command .= 's=';
}
}
if ($file) {
$command .= ' ' . escapeshellarg($file);
}
if ($this->args) {
if (!$file) {
$command .= ' --';
}
$command .= ' ' . $this->args;
}
if ($this->stderrRedirection) {
$command .= ' 2>&1';
}
return $command;
}
/**
* Runs a single job (PHP code) using a separate PHP process.
*/
abstract public function runJob(string $job, array $settings = []): array;
protected function settingsToParameters(array $settings): string
{
$buffer = '';
foreach ($settings as $setting) {
$buffer .= ' -d ' . escapeshellarg($setting);
}
return $buffer;
}
/**
* Processes the TestResult object from an isolated process.
*
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
private function processChildResult(Test $test, TestResult $result, string $stdout, string $stderr): void
{
$time = 0;
if (!empty($stderr)) {
$result->addError(
$test,
new Exception(trim($stderr)),
$time
);
} else {
set_error_handler(
/**
* @throws ErrorException
*/
static function ($errno, $errstr, $errfile, $errline): void
{
throw new ErrorException($errstr, $errno, $errno, $errfile, $errline);
}
);
try {
if (strpos($stdout, "#!/usr/bin/env php\n") === 0) {
$stdout = substr($stdout, 19);
}
$childResult = unserialize(str_replace("#!/usr/bin/env php\n", '', $stdout));
restore_error_handler();
if ($childResult === false) {
$result->addFailure(
$test,
new AssertionFailedError('Test was run in child process and ended unexpectedly'),
$time
);
}
} catch (ErrorException $e) {
restore_error_handler();
$childResult = false;
$result->addError(
$test,
new Exception(trim($stdout), 0, $e),
$time
);
}
if ($childResult !== false) {
if (!empty($childResult['output'])) {
$output = $childResult['output'];
}
/* @var TestCase $test */
$test->setResult($childResult['testResult']);
$test->addToAssertionCount($childResult['numAssertions']);
$childResult = $childResult['result'];
assert($childResult instanceof TestResult);
if ($result->getCollectCodeCoverageInformation()) {
$result->getCodeCoverage()->merge(
$childResult->getCodeCoverage()
);
}
$time = $childResult->time();
$notImplemented = $childResult->notImplemented();
$risky = $childResult->risky();
$skipped = $childResult->skipped();
$errors = $childResult->errors();
$warnings = $childResult->warnings();
$failures = $childResult->failures();
if (!empty($notImplemented)) {
$result->addError(
$test,
$this->getException($notImplemented[0]),
$time
);
} elseif (!empty($risky)) {
$result->addError(
$test,
$this->getException($risky[0]),
$time
);
} elseif (!empty($skipped)) {
$result->addError(
$test,
$this->getException($skipped[0]),
$time
);
} elseif (!empty($errors)) {
$result->addError(
$test,
$this->getException($errors[0]),
$time
);
} elseif (!empty($warnings)) {
$result->addWarning(
$test,
$this->getException($warnings[0]),
$time
);
} elseif (!empty($failures)) {
$result->addFailure(
$test,
$this->getException($failures[0]),
$time
);
}
}
}
$result->endTest($test, $time);
if (!empty($output)) {
print $output;
}
}
/**
* Gets the thrown exception from a PHPUnit\Framework\TestFailure.
*
* @see https://github.com/sebastianbergmann/phpunit/issues/74
*/
private function getException(TestFailure $error): Exception
{
$exception = $error->thrownException();
if ($exception instanceof __PHP_Incomplete_Class) {
$exceptionArray = [];
foreach ((array) $exception as $key => $value) {
$key = substr($key, strrpos($key, "\0") + 1);
$exceptionArray[$key] = $value;
}
$exception = new SyntheticError(
sprintf(
'%s: %s',
$exceptionArray['_PHP_Incomplete_Class_Name'],
$exceptionArray['message']
),
$exceptionArray['code'],
$exceptionArray['file'],
$exceptionArray['line'],
$exceptionArray['trace']
);
}
return $exception;
}
}