*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Runner;
use const DEBUG_BACKTRACE_IGNORE_ARGS;
use const DIRECTORY_SEPARATOR;
use function array_merge;
use function basename;
use function debug_backtrace;
use function defined;
use function dirname;
use function explode;
use function extension_loaded;
use function file;
use function file_get_contents;
use function file_put_contents;
use function is_array;
use function is_file;
use function is_readable;
use function is_string;
use function ltrim;
use function preg_match;
use function preg_replace;
use function preg_split;
use function realpath;
use function rtrim;
use function str_contains;
use function str_replace;
use function str_starts_with;
use function strncasecmp;
use function substr;
use function trim;
use function unlink;
use function unserialize;
use function var_export;
use PHPUnit\Event\Code\Phpt;
use PHPUnit\Event\Code\ThrowableBuilder;
use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Event\NoPreviousThrowableException;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExecutionOrderDependency;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\IncompleteTestError;
use PHPUnit\Framework\PhptAssertionFailedError;
use PHPUnit\Framework\Reorderable;
use PHPUnit\Framework\SelfDescribing;
use PHPUnit\Framework\Test;
use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry;
use PHPUnit\Util\PHP\AbstractPhpProcess;
use SebastianBergmann\CodeCoverage\Data\RawCodeCoverageData;
use SebastianBergmann\CodeCoverage\InvalidArgumentException;
use SebastianBergmann\CodeCoverage\ReflectionException;
use SebastianBergmann\CodeCoverage\Test\TestSize\TestSize;
use SebastianBergmann\CodeCoverage\Test\TestStatus\TestStatus;
use SebastianBergmann\CodeCoverage\TestIdMissingException;
use SebastianBergmann\CodeCoverage\UnintentionallyCoveredCodeException;
use SebastianBergmann\Template\Template;
use Throwable;
/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class PhptTestCase implements Reorderable, SelfDescribing, Test
{
/**
* @psalm-var non-empty-string
*/
private readonly string $filename;
private readonly AbstractPhpProcess $phpUtil;
private string $output = '';
/**
* Constructs a test case with the given filename.
*
* @psalm-param non-empty-string $filename
*
* @throws Exception
*/
public function __construct(string $filename, ?AbstractPhpProcess $phpUtil = null)
{
$this->filename = $filename;
$this->phpUtil = $phpUtil ?: AbstractPhpProcess::factory();
}
/**
* Counts the number of test cases executed by run(TestResult result).
*/
public function count(): int
{
return 1;
}
/**
* Runs a test and collects its result in a TestResult instance.
*
* @throws \PHPUnit\Framework\Exception
* @throws \SebastianBergmann\Template\InvalidArgumentException
* @throws Exception
* @throws InvalidArgumentException
* @throws NoPreviousThrowableException
* @throws ReflectionException
* @throws TestIdMissingException
* @throws UnintentionallyCoveredCodeException
*
* @noinspection RepetitiveMethodCallsInspection
*/
public function run(): void
{
$emitter = EventFacade::emitter();
$emitter->testPreparationStarted(
$this->valueObjectForEvents(),
);
try {
$sections = $this->parse();
} catch (Exception $e) {
$emitter->testPrepared($this->valueObjectForEvents());
$emitter->testErrored($this->valueObjectForEvents(), ThrowableBuilder::from($e));
$emitter->testFinished($this->valueObjectForEvents(), 0);
return;
}
$code = $this->render($sections['FILE']);
$xfail = false;
$settings = $this->parseIniSection($this->settings(CodeCoverage::instance()->isActive()));
$emitter->testPrepared($this->valueObjectForEvents());
if (isset($sections['INI'])) {
$settings = $this->parseIniSection($sections['INI'], $settings);
}
if (isset($sections['ENV'])) {
$env = $this->parseEnvSection($sections['ENV']);
$this->phpUtil->setEnv($env);
}
$this->phpUtil->setUseStderrRedirection(true);
if ($this->shouldTestBeSkipped($sections, $settings)) {
return;
}
if (isset($sections['XFAIL'])) {
$xfail = trim($sections['XFAIL']);
}
if (isset($sections['STDIN'])) {
$this->phpUtil->setStdin($sections['STDIN']);
}
if (isset($sections['ARGS'])) {
$this->phpUtil->setArgs($sections['ARGS']);
}
if (CodeCoverage::instance()->isActive()) {
$codeCoverageCacheDirectory = null;
if (CodeCoverage::instance()->codeCoverage()->cachesStaticAnalysis()) {
/** @psalm-suppress MissingThrowsDocblock */
$codeCoverageCacheDirectory = CodeCoverage::instance()->codeCoverage()->cacheDirectory();
}
$this->renderForCoverage(
$code,
CodeCoverage::instance()->codeCoverage()->collectsBranchAndPathCoverage(),
$codeCoverageCacheDirectory,
);
}
$jobResult = $this->phpUtil->runJob($code, $this->stringifyIni($settings));
$this->output = $jobResult['stdout'] ?? '';
if (CodeCoverage::instance()->isActive()) {
$coverage = $this->cleanupForCoverage();
CodeCoverage::instance()->codeCoverage()->start($this->filename, TestSize::large());
CodeCoverage::instance()->codeCoverage()->append(
$coverage,
$this->filename,
true,
TestStatus::unknown(),
);
}
$passed = true;
try {
$this->assertPhptExpectation($sections, $this->output);
} catch (AssertionFailedError $e) {
$failure = $e;
if ($xfail !== false) {
$failure = new IncompleteTestError($xfail, 0, $e);
} elseif ($e instanceof ExpectationFailedException) {
$comparisonFailure = $e->getComparisonFailure();
if ($comparisonFailure) {
$diff = $comparisonFailure->getDiff();
} else {
$diff = $e->getMessage();
}
$hint = $this->getLocationHintFromDiff($diff, $sections);
$trace = array_merge($hint, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
$failure = new PhptAssertionFailedError(
$e->getMessage(),
0,
(string) $trace[0]['file'],
(int) $trace[0]['line'],
$trace,
$comparisonFailure ? $diff : '',
);
}
if ($failure instanceof IncompleteTestError) {
$emitter->testMarkedAsIncomplete($this->valueObjectForEvents(), ThrowableBuilder::from($failure));
} else {
$emitter->testFailed($this->valueObjectForEvents(), ThrowableBuilder::from($failure), null);
}
$passed = false;
} catch (Throwable $t) {
$emitter->testErrored($this->valueObjectForEvents(), ThrowableBuilder::from($t));
$passed = false;
}
if ($passed) {
$emitter->testPassed($this->valueObjectForEvents());
}
$this->runClean($sections, CodeCoverage::instance()->isActive());
$emitter->testFinished($this->valueObjectForEvents(), 1);
}
/**
* Returns the name of the test case.
*/
public function getName(): string
{
return $this->toString();
}
/**
* Returns a string representation of the test case.
*/
public function toString(): string
{
return $this->filename;
}
public function usesDataProvider(): bool
{
return false;
}
public function numberOfAssertionsPerformed(): int
{
return 1;
}
public function output(): string
{
return $this->output;
}
public function hasOutput(): bool
{
return !empty($this->output);
}
public function sortId(): string
{
return $this->filename;
}
/**
* @psalm-return list