'MediaWiki\\Widget\\TitlesMultiselectWidget' => __DIR__ . '/includes/widget/TitlesMultiselectWidget.php',
'MediaWiki\\Widget\\UserInputWidget' => __DIR__ . '/includes/widget/UserInputWidget.php',
'MediaWiki\\Widget\\UsersMultiselectWidget' => __DIR__ . '/includes/widget/UsersMultiselectWidget.php',
+ 'Mediawiki\\Logger\\LogCapturingSpi' => __DIR__ . '/includes/debug/logger/LogCapturingSpi.php',
'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php',
'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php',
'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php',
--- /dev/null
+<?php
+
+namespace Mediawiki\Logger;
+
+use Psr\Log\AbstractLogger;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Wraps another spi to capture all logs generated. This can be
+ * used, for example, to collect all logs generated during a
+ * unit test and report them when the test fails.
+ */
+class LogCapturingSpi implements Spi {
+ /** @var LoggerInterface[] */
+ private $singletons;
+ /** @var Spi */
+ private $inner;
+ /** @var array */
+ private $logs = [];
+
+ public function __construct( Spi $inner ) {
+ $this->inner = $inner;
+ }
+
+ /**
+ * @return array
+ */
+ public function getLogs() {
+ return $this->logs;
+ }
+
+ /**
+ * @param string $channel
+ * @return LoggerInterface
+ */
+ public function getLogger( $channel ) {
+ if ( !isset( $this->singletons[$channel] ) ) {
+ $this->singletons[$channel] = $this->createLogger( $channel );
+ }
+ return $this->singletons[$channel];
+ }
+
+ /**
+ * @param array $log
+ */
+ public function capture( $log ) {
+ $this->logs[] = $log;
+ }
+
+ /**
+ * @param string $channel
+ * @return LoggerInterface
+ */
+ private function createLogger( $channel ) {
+ $inner = $this->inner->getLogger( $channel );
+ return new class( $channel, $inner, $this ) extends AbstractLogger {
+ /** @var string */
+ private $channel;
+ /** @var LoggerInterface */
+ private $logger;
+ /** @var LogCapturingSpi */
+ private $parent;
+
+ // phpcs:ignore MediaWiki.Usage.NestedFunctions.NestedFunction
+ public function __construct( $channel, LoggerInterface $logger, LogCapturingSpi $parent ) {
+ $this->channel = $channel;
+ $this->logger = $logger;
+ $this->parent = $parent;
+ }
+
+ // phpcs:ignore MediaWiki.Usage.NestedFunctions.NestedFunction
+ public function log( $level, $message, array $context = [] ) {
+ $this->parent->capture( [
+ 'channel' => $this->channel,
+ 'level' => $level,
+ 'message' => $message,
+ 'context' => $context
+ ] );
+ $this->logger->log( $level, $message, $context );
+ }
+ };
+ }
+}
'LessFileCompilationTest' => "$testDir/phpunit/LessFileCompilationTest.php",
'MediaWikiCoversValidator' => "$testDir/phpunit/MediaWikiCoversValidator.php",
'MediaWikiLangTestCase' => "$testDir/phpunit/MediaWikiLangTestCase.php",
+ 'MediaWikiLoggerPHPUnitTestListener' => "$testDir/phpunit/MediaWikiLoggerPHPUnitTestListener.php",
'MediaWikiPHPUnitCommand' => "$testDir/phpunit/MediaWikiPHPUnitCommand.php",
+ 'MediaWikiPHPUnitResultPrinter' => "$testDir/phpunit/MediaWikiPHPUnitResultPrinter.php",
'MediaWikiPHPUnitTestListener' => "$testDir/phpunit/MediaWikiPHPUnitTestListener.php",
'MediaWikiTestCase' => "$testDir/phpunit/MediaWikiTestCase.php",
'MediaWikiTestResult' => "$testDir/phpunit/MediaWikiTestResult.php",
--- /dev/null
+<?php
+
+use Mediawiki\Logger\LoggerFactory;
+use Mediawiki\Logger\Spi;
+use Mediawiki\Logger\LogCapturingSpi;
+
+/**
+ * Replaces the logging SPI on each test run. This allows
+ * another component (the printer) to fetch the logs when
+ * reporting why a test failed.
+ */
+class MediaWikiLoggerPHPUnitTestListener extends PHPUnit_Framework_BaseTestListener {
+ /** @var Spi|null */
+ private $originalSpi;
+ /** @var Spi|null */
+ private $spi;
+ /** @var array|null */
+ private $lastTestLogs;
+
+ /**
+ * A test started.
+ *
+ * @param PHPUnit_Framework_Test $test
+ */
+ public function startTest( PHPUnit_Framework_Test $test ) {
+ $this->lastTestLogs = null;
+ $this->originalSpi = LoggerFactory::getProvider();
+ $this->spi = new LogCapturingSpi( $this->originalSpi );
+ LoggerFactory::registerProvider( $this->spi );
+ }
+
+ /**
+ * A test ended.
+ *
+ * @param PHPUnit_Framework_Test $test
+ * @param float $time
+ */
+ public function endTest( PHPUnit_Framework_Test $test, $time ) {
+ $this->lastTestLogs = $this->spi->getLogs();
+ LoggerFactory::registerProvider( $this->originalSpi );
+ $this->originalSpi = null;
+ $this->spi = null;
+ }
+
+ /**
+ * Get string formatted logs generated during the last
+ * test to execute.
+ *
+ * @return string
+ */
+ public function getLog() {
+ $logs = $this->lastTestLogs;
+ if ( !$logs ) {
+ return '';
+ }
+ $message = [];
+ foreach ( $logs as $log ) {
+ $message[] = sprintf(
+ '[%s] [%s] %s %s',
+ $log['channel'],
+ $log['level'],
+ $log['message'],
+ json_encode( $log['context'] )
+ );
+ }
+ return implode( "\n", $message );
+ }
+}
class MediaWikiPHPUnitCommand extends PHPUnit_TextUI_Command {
private $cliArgs;
+ private $logListener;
public function __construct( $ignorableOptions, $cliArgs ) {
$ignore = function ( $arg ) {
$this->arguments['configuration'] = __DIR__ . '/suite.xml';
}
- // Add our own listener
+ // Add our own listeners
$this->arguments['listeners'][] = new MediaWikiPHPUnitTestListener;
+ $this->logListener = new MediaWikiLoggerPHPUnitTestListener;
+ $this->arguments['listeners'][] = $this->logListener;
// Output only to stderr to avoid "Headers already sent" problems
$this->arguments['stderr'] = true;
+
+ // We could create a printer instance and avoid passing the
+ // listener statically, but then we have to recreate the
+ // appropriate arguments handling + defaults.
+ if ( !isset( $this->arguments['printer'] ) ) {
+ $this->arguments['printer'] = MediaWikiPHPUnitResultPrinter::class;
+ }
}
protected function createRunner() {
+ MediaWikiPHPUnitResultPrinter::setLogListener( $this->logListener );
$runner = new MediaWikiTestRunner;
$runner->setMwCliArgs( $this->cliArgs );
return $runner;
--- /dev/null
+<?php
+
+class MediaWikiPHPUnitResultPrinter extends PHPUnit_TextUI_ResultPrinter {
+ /** @var MediaWikiLoggerPHPUnitTestListener */
+ private static $logListener;
+
+ public static function setLogListener( MediaWikiLoggerPHPUnitTestListener $logListener ) {
+ self::$logListener = $logListener;
+ }
+
+ protected function printDefectTrace( PHPUnit_Framework_TestFailure $defect ) {
+ $log = self::$logListener->getLog();
+ if ( $log ) {
+ $this->write( "=== Logs generated by test case\n{$log}\n===\n" );
+ }
+ parent::printDefectTrace( $defect );
+ }
+}
use MediaWiki\Logger\LegacySpi;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Logger\MonologSpi;
+use MediaWiki\Logger\LogCapturingSpi;
use MediaWiki\MediaWikiServices;
use Psr\Log\LoggerInterface;
use Wikimedia\Rdbms\IDatabase;
$this->loggers[$channel] = $singletons['loggers'][$channel] ?? null;
}
$singletons['loggers'][$channel] = $logger;
- } elseif ( $provider instanceof LegacySpi ) {
+ } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
if ( !isset( $this->loggers[$channel] ) ) {
$this->loggers[$channel] = $singletons[$channel] ?? null;
}
} else {
$singletons['loggers'][$channel] = $logger;
}
- } elseif ( $provider instanceof LegacySpi ) {
+ } elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
if ( $logger === null ) {
unset( $singletons[$channel] );
} else {