phpunit: Add setNullLogger() and make tests default to LegacySpi

== Motivation

Mute a log channel, for which the Logger object is injected by
service wiring, for a service that is overridden by default,
such as 'DBLoadBalancerFactory'. For that, calling setLogger()
mid-test would be too late.

== Changes

* Add a test-only method to LegacyLogger that makes it possible
  to change its `minimumLevel` attribute, thus making it turn
  itself into a NullLogger if raised to infinity. This is the
  same principle we use already for disabled log channels when
  using MediaWiki normally (see LegacyLogger::__construct).

* Previously, the developer's LocalSettings.php was loaded
  which includes the Spi configuration. This meant other Spi's
  could be configured which means we might not be dealing with
  a LegacyLogger object.

  Similar to what we do with ObjectCache and JobQueue already,
  make the default Spi in tests the same as the normal MW default.

* Add setNullLogger() which makes use of these two.

Bug: T248195
Change-Id: Ieade3585812de47342259afa765e230fff06f526
This commit is contained in:
Timo Tijhof 2020-03-29 22:40:16 +01:00 committed by Krinkle
parent ccddf29d97
commit e0ed6df864
6 changed files with 162 additions and 32 deletions

View file

@ -26,6 +26,7 @@ use MWDebug;
use MWExceptionHandler;
use Psr\Log\AbstractLogger;
use Psr\Log\LogLevel;
use RuntimeException;
use Throwable;
use UDPTransport;
use WikiMap;
@ -135,6 +136,23 @@ class LegacyLogger extends AbstractLogger {
}
}
/**
* Change an existing Logger singleton to act like NullLogger.
*
* @internal For use by MediaWikiIntegrationTestCase::setNullLogger
* @param null|int $level
* @return int
*/
public function setMinimumForTest( ?int $level ) {
if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
throw new RuntimeException( 'Not allowed outside tests' );
}
// Set LEVEL_INFINITY if given null, or restore the original level.
$original = $this->minimumLevel;
$this->minimumLevel = $level ?? self::LEVEL_INFINITY;
return $original;
}
/**
* Logs with an arbitrary level.
*

View file

@ -20,6 +20,8 @@
namespace MediaWiki\Logger;
use Psr\Log\LoggerInterface;
/**
* LoggerFactory service provider that creates LegacyLogger instances.
*
@ -54,4 +56,16 @@ class LegacySpi implements Spi {
return $this->singletons[$channel];
}
/**
* @internal For use by MediaWikiIntegrationTestCase
* @param string $channel
* @param LoggerInterface|null $logger
* @return LoggerInterface|null
*/
public function setLoggerForTest( $channel, LoggerInterface $logger = null ) {
$ret = $this->singletons[$channel] ?? null;
$this->singletons[$channel] = $logger;
return $ret;
}
}

View file

@ -80,4 +80,24 @@ class LogCapturingSpi implements Spi {
}
};
}
/**
* @internal For use by MediaWikiIntegrationTestCase
* @return Spi
*/
public function getInnerSpi() : Spi {
return $this->inner;
}
/**
* @internal For use by MediaWikiIntegrationTestCase
* @param string $channel
* @param LoggerInterface|null $logger
* @return LoggerInterface|null
*/
public function setLoggerForTest( $channel, LoggerInterface $logger = null ) {
$ret = $this->singletons[$channel] ?? null;
$this->singletons[$channel] = $logger;
return $ret;
}
}

View file

@ -35,6 +35,7 @@ class TestSetup {
global $wgDevelopmentWarnings;
global $wgSessionProviders, $wgSessionPbkdf2Iterations;
global $wgJobTypeConf;
global $wgMWLoggerDefaultSpi;
global $wgAuthManagerConfig;
global $wgShowExceptionDetails;
@ -64,6 +65,13 @@ class TestSetup {
$wgJobTypeConf = [
'default' => [ 'class' => JobQueueMemory::class, 'order' => 'fifo' ],
];
// Always default to LegacySpi and LegacyLogger during test
// See also MediaWikiIntegrationTestCase::setNullLogger().
// Note that MediaWikiLoggerPHPUnitTestListener may wrap this in
// a MediaWiki\Logger\LogCapturingSpi at run-time.
$wgMWLoggerDefaultSpi = [
'class' => \MediaWiki\Logger\LegacySpi::class,
];
$wgUseDatabaseMessages = false; # Set for future resets

View file

@ -1,19 +1,19 @@
<?php
// phpcs:disable MediaWiki.Commenting.FunctionAnnotations.UnrecognizedAnnotation
use MediaWiki\Logger\LegacyLogger;
use MediaWiki\Logger\LegacySpi;
use MediaWiki\Logger\LogCapturingSpi;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Logger\MonologSpi;
use MediaWiki\MediaWikiServices;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestResult;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use SebastianBergmann\Comparator\ComparisonFailure;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\IMaintainableDatabase;
use Wikimedia\TestingAccessWrapper;
use Wikimedia\Timestamp\ConvertibleTimestamp;
/**
@ -112,6 +112,12 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
*/
private $loggers = [];
/**
* Holds original loggers which have been ignored by setNullLogger()
* @var array<array<LegacyLogger|int>>
*/
private $ignoredLoggers = [];
/**
* Holds a list of services that were overridden with setService(). Used for printing an error
* if overrideMwServices() overrides a service that was previously set.
@ -1119,7 +1125,7 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
}
/**
* Sets the logger for a specified channel, for the duration of the test.
* Set the logger for a specified channel, for the duration of the test.
* @since 1.27
* @param string $channel
* @param LoggerInterface $logger
@ -1129,50 +1135,67 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
// resetServiceForTesting() to set loggers.
$provider = LoggerFactory::getProvider();
$wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
$singletons = $wrappedProvider->singletons;
if ( $provider instanceof MonologSpi ) {
if ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
$prev = $provider->setLoggerForTest( $channel, $logger );
if ( !isset( $this->loggers[$channel] ) ) {
$this->loggers[$channel] = $singletons['loggers'][$channel] ?? null;
// Remember for restoreLoggers()
$this->loggers[$channel] = $prev;
}
$singletons['loggers'][$channel] = $logger;
} elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
if ( !isset( $this->loggers[$channel] ) ) {
$this->loggers[$channel] = $singletons[$channel] ?? null;
}
$singletons[$channel] = $logger;
} else {
throw new LogicException( __METHOD__ . ': setting a logger for ' . get_class( $provider )
. ' is not implemented' );
throw new LogicException( __METHOD__ . ': cannot set logger for ' . get_class( $provider ) );
}
$wrappedProvider->singletons = $singletons;
}
/**
* Restores loggers replaced by setLogger().
* Restore loggers replaced by setLogger() or setNullLogger().
* @since 1.27
*/
private function restoreLoggers() {
$provider = LoggerFactory::getProvider();
$wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
$singletons = $wrappedProvider->singletons;
foreach ( $this->loggers as $channel => $logger ) {
if ( $provider instanceof MonologSpi ) {
if ( $logger === null ) {
unset( $singletons['loggers'][$channel] );
} else {
$singletons['loggers'][$channel] = $logger;
}
} elseif ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
if ( $logger === null ) {
unset( $singletons[$channel] );
} else {
$singletons[$channel] = $logger;
}
if ( $provider instanceof LegacySpi || $provider instanceof LogCapturingSpi ) {
// Replace override with original object or null
$provider->setLoggerForTest( $channel, $logger );
}
}
$wrappedProvider->singletons = $singletons;
$this->loggers = [];
foreach (
array_splice( $this->ignoredLoggers, 0 )
as list( $logger, $level )
) {
$logger->setMinimumForTest( $level );
}
}
/**
* Ignore all messages for the specified log channel.
*
* This is an alternative to setLogger() for when an existing logger
* must be changed as well (T248195).
*
* @since 1.35
* @param string $channel
*/
protected function setNullLogger( $channel ) {
$spi = LoggerFactory::getProvider();
$spiCapture = null;
if ( $spi instanceof LogCapturingSpi ) {
$spiCapture = $spi;
$spi = $spiCapture->getInnerSpi();
}
if ( !$spi instanceof LegacySpi ) {
throw new LogicException( __METHOD__ . ': cannot set logger for ' . get_class( $spi ) );
}
$existing = $spi->getLogger( $channel );
$level = $existing->setMinimumForTest( null );
$this->ignoredLoggers[] = [ $existing, $level ];
if ( $spiCapture ) {
$spiCapture->setLoggerForTest( $channel, new NullLogger() );
// Remember to unset in restoreLoggers()
$this->loggers[$channel] = null;
}
}
/**

View file

@ -4,6 +4,7 @@ use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use Psr\Log\LoggerInterface;
use Wikimedia\Rdbms\LoadBalancer;
use Wikimedia\TestingAccessWrapper;
/**
* @covers MediaWikiTestCase
@ -151,6 +152,52 @@ class MediaWikiTestCaseTest extends MediaWikiTestCase {
$this->assertSame( $logger1, $logger2 );
}
/**
* @covers MediaWikiTestCase::setNullLogger
* @covers MediaWikiTestCase::restoreLoggers
*/
public function testNullLogger_createAndRemove() {
$this->setNullLogger( 'tocreate' );
$logger = LoggerFactory::getInstance( 'tocreate' );
$this->assertInstanceOf( \Psr\Log\NullLogger::class, $logger );
$this->mediaWikiTearDown();
$logger = LoggerFactory::getInstance( 'tocreate' );
// Unwrap from LogCapturingSpi
$inner = TestingAccessWrapper::newFromObject( $logger )->logger;
$this->assertInstanceOf( \MediaWiki\Logger\LegacyLogger::class, $inner );
}
/**
* @covers MediaWikiTestCase::setNullLogger
* @covers MediaWikiTestCase::restoreLoggers
*/
public function testNullLogger_mutateAndRestore() {
$logger = LoggerFactory::getInstance( 'tomutate' );
// Unwrap from LogCapturingSpi
$inner = TestingAccessWrapper::newFromObject( $logger )->logger;
$this->assertInstanceOf( \MediaWiki\Logger\LegacyLogger::class, $inner );
$this->assertSame(
100,
TestingAccessWrapper::newFromObject( $inner )->minimumLevel,
'original minimumLevel'
);
$this->setNullLogger( 'tomutate' );
$this->assertSame(
999,
TestingAccessWrapper::newFromObject( $inner )->minimumLevel,
'changed minimumLevel'
);
$this->mediaWikiTearDown();
$this->assertSame(
100,
TestingAccessWrapper::newFromObject( $inner )->minimumLevel,
'restored minimumLevel'
);
}
/**
* @covers MediaWikiTestCase::setupDatabaseWithTestPrefix
* @covers MediaWikiTestCase::copyTestData