*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\TextUI;
use const PHP_EOL;
use function is_file;
use function is_readable;
use function printf;
use function realpath;
use function sprintf;
use function trim;
use function unlink;
use PHPUnit\Event\Facade as EventFacade;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Logging\EventLogger;
use PHPUnit\Logging\JUnit\JunitXmlLogger;
use PHPUnit\Logging\TeamCity\TeamCityLogger;
use PHPUnit\Logging\TestDox\HtmlRenderer as TestDoxHtmlRenderer;
use PHPUnit\Logging\TestDox\PlainTextRenderer as TestDoxTextRenderer;
use PHPUnit\Logging\TestDox\TestResultCollector as TestDoxResultCollector;
use PHPUnit\Runner\CodeCoverage;
use PHPUnit\Runner\Extension\ExtensionBootstrapper;
use PHPUnit\Runner\Extension\Facade as ExtensionFacade;
use PHPUnit\Runner\Extension\PharLoader;
use PHPUnit\Runner\ResultCache\DefaultResultCache;
use PHPUnit\Runner\ResultCache\NullResultCache;
use PHPUnit\Runner\ResultCache\ResultCache;
use PHPUnit\Runner\ResultCache\ResultCacheHandler;
use PHPUnit\Runner\TestSuiteSorter;
use PHPUnit\Runner\Version;
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
use PHPUnit\TextUI\CliArguments\Builder;
use PHPUnit\TextUI\CliArguments\Configuration as CliConfiguration;
use PHPUnit\TextUI\CliArguments\Exception as ArgumentsException;
use PHPUnit\TextUI\CliArguments\XmlConfigurationFileFinder;
use PHPUnit\TextUI\Command\AtLeastVersionCommand;
use PHPUnit\TextUI\Command\GenerateConfigurationCommand;
use PHPUnit\TextUI\Command\ListGroupsCommand;
use PHPUnit\TextUI\Command\ListTestsAsTextCommand;
use PHPUnit\TextUI\Command\ListTestsAsXmlCommand;
use PHPUnit\TextUI\Command\ListTestSuitesCommand;
use PHPUnit\TextUI\Command\MigrateConfigurationCommand;
use PHPUnit\TextUI\Command\Result;
use PHPUnit\TextUI\Command\ShowHelpCommand;
use PHPUnit\TextUI\Command\ShowVersionCommand;
use PHPUnit\TextUI\Command\VersionCheckCommand;
use PHPUnit\TextUI\Command\WarmCodeCoverageCacheCommand;
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
use PHPUnit\TextUI\Configuration\Configuration;
use PHPUnit\TextUI\Configuration\PhpHandler;
use PHPUnit\TextUI\Configuration\Registry;
use PHPUnit\TextUI\Configuration\TestSuiteBuilder;
use PHPUnit\TextUI\Output\DefaultPrinter;
use PHPUnit\TextUI\Output\Facade as OutputFacade;
use PHPUnit\TextUI\Output\Printer;
use PHPUnit\TextUI\XmlConfiguration\Configuration as XmlConfiguration;
use PHPUnit\TextUI\XmlConfiguration\DefaultConfiguration;
use PHPUnit\TextUI\XmlConfiguration\Loader;
use Throwable;
/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class Application
{
public function run(array $argv): int
{
try {
EventFacade::emitter()->applicationStarted();
$cliConfiguration = $this->buildCliConfiguration($argv);
$pathToXmlConfigurationFile = (new XmlConfigurationFileFinder)->find($cliConfiguration);
$this->executeCommandsThatOnlyRequireCliConfiguration($cliConfiguration, $pathToXmlConfigurationFile);
$xmlConfiguration = $this->loadXmlConfiguration($pathToXmlConfigurationFile);
$configuration = Registry::init(
$cliConfiguration,
$xmlConfiguration
);
(new PhpHandler)->handle($configuration->php());
if ($configuration->hasBootstrap()) {
$this->loadBootstrapScript($configuration->bootstrap());
}
$this->executeCommandsThatRequireCompleteConfiguration($configuration, $cliConfiguration);
$testSuite = $this->buildTestSuite($configuration);
$this->executeCommandsThatRequireCliConfigurationAndTestSuite($cliConfiguration, $testSuite);
$this->executeHelpCommandWhenThereIsNothingElseToDo($configuration, $testSuite);
$pharExtensions = null;
if (!$configuration->noExtensions()) {
if ($configuration->hasPharExtensionDirectory()) {
$pharExtensions = (new PharLoader)->loadPharExtensionsInDirectory(
$configuration->pharExtensionDirectory()
);
}
$this->bootstrapExtensions($configuration);
}
CodeCoverage::instance()->init($configuration, CodeCoverageFilterRegistry::instance());
$printer = OutputFacade::init($configuration);
$this->writeRuntimeInformation($printer, $configuration);
$this->writePharExtensionInformation($printer, $pharExtensions);
$this->writeRandomSeedInformation($printer, $configuration);
$printer->print(PHP_EOL);
$this->registerLogfileWriters($configuration);
$testDoxResultCollector = $this->testDoxResultCollector($configuration);
TestResultFacade::init();
$resultCache = $this->initializeTestResultCache($configuration);
EventFacade::seal();
$runner = new TestRunner;
$runner->run(
$configuration,
$resultCache,
$testSuite
);
$testDoxResult = null;
if (isset($testDoxResultCollector)) {
$testDoxResult = $testDoxResultCollector->testMethodsGroupedByClass();
}
if ($testDoxResult !== null &&
$configuration->hasLogfileTestdoxHtml()) {
OutputFacade::printerFor($configuration->logfileTestdoxHtml())->print(
(new TestDoxHtmlRenderer)->render($testDoxResult)
);
}
if ($testDoxResult !== null &&
$configuration->hasLogfileTestdoxText()) {
OutputFacade::printerFor($configuration->logfileTestdoxText())->print(
(new TestDoxTextRenderer)->render($testDoxResult)
);
}
$result = TestResultFacade::result();
OutputFacade::printResult($result, $testDoxResult);
CodeCoverage::instance()->generateReports($printer, $configuration);
$shellExitCode = (new ShellExitCodeCalculator)->calculate(
$configuration->failOnEmptyTestSuite(),
$configuration->failOnRisky(),
$configuration->failOnWarning(),
$configuration->failOnIncomplete(),
$configuration->failOnSkipped(),
$result
);
EventFacade::emitter()->applicationFinished($shellExitCode);
return $shellExitCode;
} catch (Throwable $t) {
$this->exitWithCrashMessage($t);
}
}
private function exitWithCrashMessage(Throwable $t): never
{
$message = $t->getMessage();
if (empty(trim($message))) {
$message = '(no message)';
}
printf(
'%s%sAn error occurred inside PHPUnit.%s%sMessage: %s%sLocation: %s:%d%s%s%s%s',
PHP_EOL,
PHP_EOL,
PHP_EOL,
PHP_EOL,
$message,
PHP_EOL,
$t->getFile(),
$t->getLine(),
PHP_EOL,
PHP_EOL,
$t->getTraceAsString(),
PHP_EOL
);
exit(Result::CRASH);
}
private function exitWithErrorMessage(string $message): never
{
print Version::getVersionString() . PHP_EOL . PHP_EOL . $message . PHP_EOL;
exit(Result::EXCEPTION);
}
private function execute(Command\Command $command): never
{
print Version::getVersionString() . PHP_EOL . PHP_EOL;
$result = $command->execute();
print $result->output();
exit($result->shellExitCode());
}
private function loadBootstrapScript(string $filename): void
{
if (!is_readable($filename)) {
$this->exitWithErrorMessage(
sprintf(
'Cannot open bootstrap script "%s"',
$filename
)
);
}
try {
include_once $filename;
} catch (Throwable $t) {
$this->exitWithErrorMessage(
sprintf(
'Error in bootstrap script: %s:%s%s%s%s',
$t::class,
PHP_EOL,
$t->getMessage(),
PHP_EOL,
$t->getTraceAsString()
)
);
}
EventFacade::emitter()->testRunnerBootstrapFinished($filename);
}
private function buildCliConfiguration(array $argv): CliConfiguration
{
try {
$cliConfiguration = (new Builder)->fromParameters($argv);
} catch (ArgumentsException $e) {
$this->exitWithErrorMessage($e->getMessage());
}
return $cliConfiguration;
}
private function loadXmlConfiguration(string|false $configurationFile): XmlConfiguration
{
if (!$configurationFile) {
return DefaultConfiguration::create();
}
try {
return (new Loader)->load($configurationFile);
} catch (Throwable $e) {
$this->exitWithErrorMessage($e->getMessage());
}
}
private function buildTestSuite(Configuration $configuration): TestSuite
{
try {
return (new TestSuiteBuilder)->build($configuration);
} catch (Exception $e) {
$this->exitWithErrorMessage($e->getMessage());
}
}
private function bootstrapExtensions(Configuration $configuration): void
{
$extensionBootstrapper = new ExtensionBootstrapper(
$configuration,
new ExtensionFacade
);
foreach ($configuration->extensionBootstrappers() as $bootstrapper) {
try {
$extensionBootstrapper->bootstrap(
$bootstrapper['className'],
$bootstrapper['parameters']
);
} catch (\PHPUnit\Runner\Exception $e) {
$this->exitWithErrorMessage(
sprintf(
'Error while bootstrapping extension: %s',
$e->getMessage()
)
);
}
}
}
private function executeCommandsThatOnlyRequireCliConfiguration(CliConfiguration $cliConfiguration, string|false $configurationFile): void
{
if ($cliConfiguration->generateConfiguration()) {
$this->execute(new GenerateConfigurationCommand);
}
if ($cliConfiguration->migrateConfiguration()) {
if (!$configurationFile) {
$this->exitWithErrorMessage('No configuration file found to migrate');
}
$this->execute(new MigrateConfigurationCommand(realpath($configurationFile)));
}
if ($cliConfiguration->hasAtLeastVersion()) {
$this->execute(new AtLeastVersionCommand($cliConfiguration->atLeastVersion()));
}
if ($cliConfiguration->version()) {
$this->execute(new ShowVersionCommand);
}
if ($cliConfiguration->checkVersion()) {
$this->execute(new VersionCheckCommand);
}
if ($cliConfiguration->help()) {
$this->execute(new ShowHelpCommand(Result::SUCCESS));
}
}
private function executeCommandsThatRequireCliConfigurationAndTestSuite(CliConfiguration $cliConfiguration, TestSuite $testSuite): void
{
if ($cliConfiguration->listGroups()) {
$this->execute(new ListGroupsCommand($testSuite));
}
if ($cliConfiguration->listTests()) {
$this->execute(new ListTestsAsTextCommand($testSuite));
}
if ($cliConfiguration->hasListTestsXml()) {
$this->execute(
new ListTestsAsXmlCommand(
$cliConfiguration->listTestsXml(),
$testSuite
)
);
}
}
private function executeCommandsThatRequireCompleteConfiguration(Configuration $configuration, CliConfiguration $cliConfiguration): void
{
if ($cliConfiguration->listSuites()) {
$this->execute(new ListTestSuitesCommand($configuration->testSuite()));
}
if ($cliConfiguration->warmCoverageCache()) {
$this->execute(new WarmCodeCoverageCacheCommand($configuration, CodeCoverageFilterRegistry::instance()));
}
}
private function executeHelpCommandWhenThereIsNothingElseToDo(Configuration $configuration, TestSuite $testSuite): void
{
if ($testSuite->isEmpty() && !$configuration->hasCliArgument() && !$configuration->hasDefaultTestSuite()) {
$this->execute(new ShowHelpCommand(Result::FAILURE));
}
}
private function writeRuntimeInformation(Printer $printer, Configuration $configuration): void
{
$printer->print(Version::getVersionString() . PHP_EOL . PHP_EOL);
$runtime = 'PHP ' . PHP_VERSION;
if (CodeCoverage::instance()->isActive()) {
$runtime .= ' with ' . CodeCoverage::instance()->driver()->nameAndVersion();
}
$this->writeMessage($printer, 'Runtime', $runtime);
if ($configuration->hasConfigurationFile()) {
$this->writeMessage(
$printer,
'Configuration',
$configuration->configurationFile()
);
}
}
/**
* @psalm-param ?array{loadedExtensions: list