Skip to content

Commit

Permalink
resolve sebastianbergmann#5903 Add support for external logger
Browse files Browse the repository at this point in the history
Added support for both XML and CLI definition of an external logger.
This --log-external option adds simialar functionality that existed with
the --printer option in 9.x and earlier.

Implementations of an external logger do need to implement the
\PHPUnit\Logger\ExternalLogger interface and must use the PHPUnit 10.x
and later event mechanism for capturing logging events.
  • Loading branch information
BlairCooper committed Jul 30, 2024
1 parent bdded73 commit de04dd2
Show file tree
Hide file tree
Showing 27 changed files with 724 additions and 10 deletions.
4 changes: 4 additions & 0 deletions phpunit.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@
<xs:element name="teamcity" type="logToFileType" minOccurs="0" />
<xs:element name="testdoxHtml" type="logToFileType" minOccurs="0" />
<xs:element name="testdoxText" type="logToFileType" minOccurs="0" />
<xs:element name="external" type="logToExternalClassType" minOccurs="0" />
</xs:all>
</xs:group>
<xs:complexType name="logToFileType">
Expand All @@ -302,6 +303,9 @@
<xs:complexType name="logToDirectoryType">
<xs:attribute name="outputDirectory" type="xs:anyURI" use="required"/>
</xs:complexType>
<xs:complexType name="logToExternalClassType">
<xs:attribute name="class" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="coverageReportCrap4JType">
<xs:attribute name="outputFile" type="xs:anyURI" use="required"/>
<xs:attribute name="threshold" type="xs:integer"/>
Expand Down
337 changes: 337 additions & 0 deletions schema/11.3.xsd

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions src/Logging/ExternalLogger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?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\Logging;

use PHPUnit\Event\EventFacadeIsSealedException;
use PHPUnit\Event\Facade;
use PHPUnit\Event\UnknownSubscriberTypeException;
use PHPUnit\TextUI\Output\Printer;

interface ExternalLogger
{
/**
* @throws EventFacadeIsSealedException
* @throws UnknownSubscriberTypeException
*/
public static function createInstance(Facade $facade): object;
}
28 changes: 28 additions & 0 deletions src/TextUI/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
use PHPUnit\TextUI\XmlConfiguration\Loader;
use SebastianBergmann\Timer\Timer;
use Throwable;
use PHPUnit\Runner\ClassCannotBeFoundException;

/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
Expand Down Expand Up @@ -629,6 +630,33 @@ private function registerLogfileWriters(Configuration $configuration): void
);
}
}

if ($configuration->hasExternalLogger()) {
try {
$className = $configuration->externalLogger();

if (class_exists($className)) {
$interfaces = class_implements($className);

if (isset($interfaces[\PHPUnit\Logging\ExternalLogger::class])) {
$className::createInstance(
EventFacade::instance(),
);
} else {
throw new InvalidExternalLoggerException($className, false);
}
} else {
throw new InvalidExternalLoggerException($className);
}
} catch (InvalidExternalLoggerException $e) {
EventFacade::emitter()->testRunnerTriggeredWarning(
sprintf(
'Cannot log test results to external logger: %s',
$e->getMessage(),
),
);
}
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/TextUI/Configuration/Cli/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ final class Builder
'list-tests-xml=',
'log-junit=',
'log-teamcity=',
'log-external=',
'migrate-configuration',
'no-configuration',
'no-coverage',
Expand Down Expand Up @@ -250,6 +251,7 @@ public function fromParameters(array $parameters): Configuration
$teamcityLogfile = null;
$testdoxHtmlFile = null;
$testdoxTextFile = null;
$externalLogger = null;
$testSuffixes = null;
$testSuite = null;
$excludeTestSuite = null;
Expand Down Expand Up @@ -568,6 +570,11 @@ public function fromParameters(array $parameters): Configuration

break;

case '--log-external':
$externalLogger = $option[1];

break;

case '--order-by':
foreach (explode(',', $option[1]) as $order) {
switch ($order) {
Expand Down Expand Up @@ -1053,6 +1060,7 @@ public function fromParameters(array $parameters): Configuration
$printerTestDox,
$printerTestDoxSummary,
$debug,
$externalLogger,
);
}

Expand Down
26 changes: 24 additions & 2 deletions src/TextUI/Configuration/Cli/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@
private ?string $testdoxTextFile;
private ?bool $testdoxPrinter;
private ?bool $testdoxPrinterSummary;

private ?string $externalLogger;

/**
* @var ?non-empty-list<non-empty-string>
*/
Expand Down Expand Up @@ -159,7 +160,7 @@
* @param ?non-empty-list<non-empty-string> $testSuffixes
* @param ?non-empty-list<non-empty-string> $coverageFilter
*/
public function __construct(array $arguments, ?string $atLeastVersion, ?bool $backupGlobals, ?bool $backupStaticProperties, ?bool $beStrictAboutChangesToGlobalState, ?string $bootstrap, ?string $cacheDirectory, ?bool $cacheResult, bool $checkVersion, ?string $colors, null|int|string $columns, ?string $configurationFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4J, ?string $coverageHtml, ?string $coveragePhp, ?string $coverageText, ?bool $coverageTextShowUncoveredFiles, ?bool $coverageTextShowOnlySummary, ?string $coverageXml, ?bool $pathCoverage, bool $warmCoverageCache, ?int $defaultTimeLimit, ?bool $disableCodeCoverageIgnore, ?bool $disallowTestOutput, ?bool $enforceTimeLimit, ?array $excludeGroups, ?int $executionOrder, ?int $executionOrderDefects, ?bool $failOnDeprecation, ?bool $failOnEmptyTestSuite, ?bool $failOnIncomplete, ?bool $failOnNotice, ?bool $failOnRisky, ?bool $failOnSkipped, ?bool $failOnWarning, ?bool $stopOnDefect, ?bool $stopOnDeprecation, ?bool $stopOnError, ?bool $stopOnFailure, ?bool $stopOnIncomplete, ?bool $stopOnNotice, ?bool $stopOnRisky, ?bool $stopOnSkipped, ?bool $stopOnWarning, ?string $filter, ?string $excludeFilter, ?string $generateBaseline, ?string $useBaseline, bool $ignoreBaseline, bool $generateConfiguration, bool $migrateConfiguration, ?array $groups, ?array $testsCovering, ?array $testsUsing, bool $help, ?string $includePath, ?array $iniSettings, ?string $junitLogfile, bool $listGroups, bool $listSuites, bool $listTestFiles, bool $listTests, ?string $listTestsXml, ?bool $noCoverage, ?bool $noExtensions, ?bool $noOutput, ?bool $noProgress, ?bool $noResults, ?bool $noLogging, ?bool $processIsolation, ?int $randomOrderSeed, ?bool $reportUselessTests, ?bool $resolveDependencies, ?bool $reverseList, ?bool $stderr, ?bool $strictCoverage, ?string $teamcityLogfile, ?string $testdoxHtmlFile, ?string $testdoxTextFile, ?array $testSuffixes, ?string $testSuite, ?string $excludeTestSuite, bool $useDefaultConfiguration, ?bool $displayDetailsOnIncompleteTests, ?bool $displayDetailsOnSkippedTests, ?bool $displayDetailsOnTestsThatTriggerDeprecations, ?bool $displayDetailsOnTestsThatTriggerErrors, ?bool $displayDetailsOnTestsThatTriggerNotices, ?bool $displayDetailsOnTestsThatTriggerWarnings, bool $version, ?array $coverageFilter, ?string $logEventsText, ?string $logEventsVerboseText, ?bool $printerTeamCity, ?bool $testdoxPrinter, ?bool $testdoxPrinterSummary, bool $debug)
public function __construct(array $arguments, ?string $atLeastVersion, ?bool $backupGlobals, ?bool $backupStaticProperties, ?bool $beStrictAboutChangesToGlobalState, ?string $bootstrap, ?string $cacheDirectory, ?bool $cacheResult, bool $checkVersion, ?string $colors, null|int|string $columns, ?string $configurationFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4J, ?string $coverageHtml, ?string $coveragePhp, ?string $coverageText, ?bool $coverageTextShowUncoveredFiles, ?bool $coverageTextShowOnlySummary, ?string $coverageXml, ?bool $pathCoverage, bool $warmCoverageCache, ?int $defaultTimeLimit, ?bool $disableCodeCoverageIgnore, ?bool $disallowTestOutput, ?bool $enforceTimeLimit, ?array $excludeGroups, ?int $executionOrder, ?int $executionOrderDefects, ?bool $failOnDeprecation, ?bool $failOnEmptyTestSuite, ?bool $failOnIncomplete, ?bool $failOnNotice, ?bool $failOnRisky, ?bool $failOnSkipped, ?bool $failOnWarning, ?bool $stopOnDefect, ?bool $stopOnDeprecation, ?bool $stopOnError, ?bool $stopOnFailure, ?bool $stopOnIncomplete, ?bool $stopOnNotice, ?bool $stopOnRisky, ?bool $stopOnSkipped, ?bool $stopOnWarning, ?string $filter, ?string $excludeFilter, ?string $generateBaseline, ?string $useBaseline, bool $ignoreBaseline, bool $generateConfiguration, bool $migrateConfiguration, ?array $groups, ?array $testsCovering, ?array $testsUsing, bool $help, ?string $includePath, ?array $iniSettings, ?string $junitLogfile, bool $listGroups, bool $listSuites, bool $listTestFiles, bool $listTests, ?string $listTestsXml, ?bool $noCoverage, ?bool $noExtensions, ?bool $noOutput, ?bool $noProgress, ?bool $noResults, ?bool $noLogging, ?bool $processIsolation, ?int $randomOrderSeed, ?bool $reportUselessTests, ?bool $resolveDependencies, ?bool $reverseList, ?bool $stderr, ?bool $strictCoverage, ?string $teamcityLogfile, ?string $testdoxHtmlFile, ?string $testdoxTextFile, ?array $testSuffixes, ?string $testSuite, ?string $excludeTestSuite, bool $useDefaultConfiguration, ?bool $displayDetailsOnIncompleteTests, ?bool $displayDetailsOnSkippedTests, ?bool $displayDetailsOnTestsThatTriggerDeprecations, ?bool $displayDetailsOnTestsThatTriggerErrors, ?bool $displayDetailsOnTestsThatTriggerNotices, ?bool $displayDetailsOnTestsThatTriggerWarnings, bool $version, ?array $coverageFilter, ?string $logEventsText, ?string $logEventsVerboseText, ?bool $printerTeamCity, ?bool $testdoxPrinter, ?bool $testdoxPrinterSummary, bool $debug, ?string $externalLogger)
{
$this->arguments = $arguments;
$this->atLeastVersion = $atLeastVersion;
Expand Down Expand Up @@ -260,6 +261,7 @@ public function __construct(array $arguments, ?string $atLeastVersion, ?bool $ba
$this->testdoxPrinter = $testdoxPrinter;
$this->testdoxPrinterSummary = $testdoxPrinterSummary;
$this->debug = $debug;
$this->externalLogger = $externalLogger;
}

/**
Expand Down Expand Up @@ -2048,4 +2050,24 @@ public function debug(): bool
{
return $this->debug;
}

/**
* @phpstan-assert-if-true !null $this->externalLogger
*/
public function hasExternalLogger(): bool
{
return $this->externalLogger !== null;
}

/**
* @throws Exception
*/
public function externalLogger(): string
{
if (!$this->hasExternalLogger()) {
throw new Exception;
}

return $this->externalLogger;
}
}
26 changes: 24 additions & 2 deletions src/TextUI/Configuration/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@
private ?string $logfileTestdoxText;
private ?string $logEventsText;
private ?string $logEventsVerboseText;

private ?string $externalLogger;

/**
* @var ?non-empty-list<non-empty-string>
*/
Expand Down Expand Up @@ -173,7 +174,7 @@
* @param non-empty-list<non-empty-string> $testSuffixes
* @param non-negative-int $shortenArraysForExportThreshold
*/
public function __construct(array $cliArguments, ?string $configurationFile, ?string $bootstrap, bool $cacheResult, ?string $cacheDirectory, ?string $coverageCacheDirectory, Source $source, string $testResultCacheFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4j, int $coverageCrap4jThreshold, ?string $coverageHtml, int $coverageHtmlLowUpperBound, int $coverageHtmlHighLowerBound, string $coverageHtmlColorSuccessLow, string $coverageHtmlColorSuccessMedium, string $coverageHtmlColorSuccessHigh, string $coverageHtmlColorWarning, string $coverageHtmlColorDanger, ?string $coverageHtmlCustomCssFile, ?string $coveragePhp, ?string $coverageText, bool $coverageTextShowUncoveredFiles, bool $coverageTextShowOnlySummary, ?string $coverageXml, bool $pathCoverage, bool $ignoreDeprecatedCodeUnitsFromCodeCoverage, bool $disableCodeCoverageIgnore, bool $failOnDeprecation, bool $failOnEmptyTestSuite, bool $failOnIncomplete, bool $failOnNotice, bool $failOnRisky, bool $failOnSkipped, bool $failOnWarning, bool $stopOnDefect, bool $stopOnDeprecation, bool $stopOnError, bool $stopOnFailure, bool $stopOnIncomplete, bool $stopOnNotice, bool $stopOnRisky, bool $stopOnSkipped, bool $stopOnWarning, bool $outputToStandardErrorStream, int|string $columns, bool $noExtensions, ?string $pharExtensionDirectory, array $extensionBootstrappers, bool $backupGlobals, bool $backupStaticProperties, bool $beStrictAboutChangesToGlobalState, bool $colors, bool $processIsolation, bool $enforceTimeLimit, int $defaultTimeLimit, int $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, bool $reportUselessTests, bool $strictCoverage, bool $disallowTestOutput, bool $displayDetailsOnIncompleteTests, bool $displayDetailsOnSkippedTests, bool $displayDetailsOnTestsThatTriggerDeprecations, bool $displayDetailsOnTestsThatTriggerErrors, bool $displayDetailsOnTestsThatTriggerNotices, bool $displayDetailsOnTestsThatTriggerWarnings, bool $reverseDefectList, bool $requireCoverageMetadata, bool $noProgress, bool $noResults, bool $noOutput, int $executionOrder, int $executionOrderDefects, bool $resolveDependencies, ?string $logfileTeamcity, ?string $logfileJunit, ?string $logfileTestdoxHtml, ?string $logfileTestdoxText, ?string $logEventsText, ?string $logEventsVerboseText, bool $teamCityOutput, bool $testDoxOutput, bool $testDoxOutputSummary, ?array $testsCovering, ?array $testsUsing, ?string $filter, ?string $excludeFilter, array $groups, array $excludeGroups, int $randomOrderSeed, bool $includeUncoveredFiles, TestSuiteCollection $testSuite, string $includeTestSuite, string $excludeTestSuite, ?string $defaultTestSuite, array $testSuffixes, Php $php, bool $controlGarbageCollector, int $numberOfTestsBeforeGarbageCollection, ?string $generateBaseline, bool $debug, int $shortenArraysForExportThreshold)
public function __construct(array $cliArguments, ?string $configurationFile, ?string $bootstrap, bool $cacheResult, ?string $cacheDirectory, ?string $coverageCacheDirectory, Source $source, string $testResultCacheFile, ?string $coverageClover, ?string $coverageCobertura, ?string $coverageCrap4j, int $coverageCrap4jThreshold, ?string $coverageHtml, int $coverageHtmlLowUpperBound, int $coverageHtmlHighLowerBound, string $coverageHtmlColorSuccessLow, string $coverageHtmlColorSuccessMedium, string $coverageHtmlColorSuccessHigh, string $coverageHtmlColorWarning, string $coverageHtmlColorDanger, ?string $coverageHtmlCustomCssFile, ?string $coveragePhp, ?string $coverageText, bool $coverageTextShowUncoveredFiles, bool $coverageTextShowOnlySummary, ?string $coverageXml, bool $pathCoverage, bool $ignoreDeprecatedCodeUnitsFromCodeCoverage, bool $disableCodeCoverageIgnore, bool $failOnDeprecation, bool $failOnEmptyTestSuite, bool $failOnIncomplete, bool $failOnNotice, bool $failOnRisky, bool $failOnSkipped, bool $failOnWarning, bool $stopOnDefect, bool $stopOnDeprecation, bool $stopOnError, bool $stopOnFailure, bool $stopOnIncomplete, bool $stopOnNotice, bool $stopOnRisky, bool $stopOnSkipped, bool $stopOnWarning, bool $outputToStandardErrorStream, int|string $columns, bool $noExtensions, ?string $pharExtensionDirectory, array $extensionBootstrappers, bool $backupGlobals, bool $backupStaticProperties, bool $beStrictAboutChangesToGlobalState, bool $colors, bool $processIsolation, bool $enforceTimeLimit, int $defaultTimeLimit, int $timeoutForSmallTests, int $timeoutForMediumTests, int $timeoutForLargeTests, bool $reportUselessTests, bool $strictCoverage, bool $disallowTestOutput, bool $displayDetailsOnIncompleteTests, bool $displayDetailsOnSkippedTests, bool $displayDetailsOnTestsThatTriggerDeprecations, bool $displayDetailsOnTestsThatTriggerErrors, bool $displayDetailsOnTestsThatTriggerNotices, bool $displayDetailsOnTestsThatTriggerWarnings, bool $reverseDefectList, bool $requireCoverageMetadata, bool $noProgress, bool $noResults, bool $noOutput, int $executionOrder, int $executionOrderDefects, bool $resolveDependencies, ?string $logfileTeamcity, ?string $logfileJunit, ?string $logfileTestdoxHtml, ?string $logfileTestdoxText, ?string $logEventsText, ?string $logEventsVerboseText, bool $teamCityOutput, bool $testDoxOutput, bool $testDoxOutputSummary, ?array $testsCovering, ?array $testsUsing, ?string $filter, ?string $excludeFilter, array $groups, array $excludeGroups, int $randomOrderSeed, bool $includeUncoveredFiles, TestSuiteCollection $testSuite, string $includeTestSuite, string $excludeTestSuite, ?string $defaultTestSuite, array $testSuffixes, Php $php, bool $controlGarbageCollector, int $numberOfTestsBeforeGarbageCollection, ?string $generateBaseline, bool $debug, int $shortenArraysForExportThreshold, ?string $externalLogger)
{
$this->cliArguments = $cliArguments;
$this->configurationFile = $configurationFile;
Expand Down Expand Up @@ -280,6 +281,7 @@ public function __construct(array $cliArguments, ?string $configurationFile, ?st
$this->generateBaseline = $generateBaseline;
$this->debug = $debug;
$this->shortenArraysForExportThreshold = $shortenArraysForExportThreshold;
$this->externalLogger = $externalLogger;
}

/**
Expand Down Expand Up @@ -1252,4 +1254,24 @@ public function shortenArraysForExportThreshold(): int
{
return $this->shortenArraysForExportThreshold;
}

/**
* @phpstan-assert-if-true !null $this->externalLogger
*/
public function hasExternalLogger(): bool
{
return $this->externalLogger !== null;
}

/**
* @throws LoggingNotConfiguredException
*/
public function externalLogger(): string
{
if (!$this->hasExternalLogger()) {
throw new LoggingNotConfiguredException;
}

return $this->externalLogger;
}
}
10 changes: 9 additions & 1 deletion src/TextUI/Configuration/Merger.php
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,8 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC
$logfileTestdoxHtml = null;
$logfileTestdoxText = null;
$loggingFromXmlConfiguration = true;

$externalLogger = null;

if ($cliConfiguration->hasNoLogging() && $cliConfiguration->noLogging()) {
$loggingFromXmlConfiguration = false;
}
Expand Down Expand Up @@ -533,6 +534,12 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC
$logfileTestdoxText = $xmlConfiguration->logging()->testDoxText()->target()->path();
}

if ($cliConfiguration->hasExternalLogger()) {
$externalLogger = $cliConfiguration->externalLogger();
} elseif ($loggingFromXmlConfiguration && $xmlConfiguration->logging()->hasExternal()) {
$externalLogger = $xmlConfiguration->logging()->external()->target();
}

$logEventsText = null;

if ($cliConfiguration->hasLogEventsText()) {
Expand Down Expand Up @@ -875,6 +882,7 @@ public function merge(CliConfiguration $cliConfiguration, XmlConfiguration $xmlC
$generateBaseline,
$cliConfiguration->debug(),
$xmlConfiguration->phpunit()->shortenArraysForExportThreshold(),
$externalLogger,
);
}
}
3 changes: 2 additions & 1 deletion src/TextUI/Configuration/Xml/DefaultConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ public static function create(): self
null,
null,
null,
),
null,
),
new Php(
DirectoryCollection::fromArray([]),
IniSettingCollection::fromArray([]),
Expand Down
Loading

0 comments on commit de04dd2

Please sign in to comment.