X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;ds=sidebyside;f=tests%2Fphpunit%2FMediaWikiTestCase.php;h=114ad7e2ae2fff2da21831ac572c579085e81980;hb=01cdb1762c7207bd261ad03726a88cb9afc97bfb;hp=34f93ad7a1aef8868c6e0ff5fbe6c6c79e7abb10;hpb=d9fb5f03468ee31fd111236bd48475f8ef5b1918;p=lhc%2Fweb%2Fwiklou.git diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index 34f93ad7a1..114ad7e2ae 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -8,8 +8,6 @@ use Psr\Log\LoggerInterface; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\IMaintainableDatabase; use Wikimedia\Rdbms\Database; -use Wikimedia\Rdbms\IResultWrapper; -use Wikimedia\Rdbms\LBFactory; use Wikimedia\TestingAccessWrapper; /** @@ -21,13 +19,17 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { use PHPUnit4And6Compat; /** - * The service locator created by prepareServices(). This service locator will - * be restored after each test. Tests that pollute the global service locator - * instance should use overrideMwServices() to isolate the test. + * The original service locator. This is overridden during setUp(). * * @var MediaWikiServices|null */ - private static $serviceLocator = null; + private static $originalServices; + + /** + * The local service locator, created during setUp(). + * @var MediaWikiServices + */ + private $localServices; /** * $called tracks whether the setUp and tearDown method has been called. @@ -98,18 +100,18 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { */ private $mwGlobalsToUnset = []; - /** - * Holds original contents of interwiki table - * @var IResultWrapper - */ - private $interwikiTable = null; - /** * Holds original loggers which have been replaced by setLogger() * @var LoggerInterface[] */ private $loggers = []; + /** + * The CLI arguments passed through from phpunit.php + * @var array + */ + private $cliArgs = []; + /** * Table name prefixes. Oracle likes it shorter. */ @@ -145,8 +147,10 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { public static function setUpBeforeClass() { parent::setUpBeforeClass(); - // Get the service locator, and reset services if it's not done already - self::$serviceLocator = self::prepareServices( new GlobalVarConfig() ); + // Get the original service locator + if ( !self::$originalServices ) { + self::$originalServices = MediaWikiServices::getInstance(); + } } /** @@ -235,63 +239,9 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { } /** - * Prepare service configuration for unit testing. - * - * This calls MediaWikiServices::resetGlobalInstance() to allow some critical services - * to be overridden for testing. - * - * prepareServices() only needs to be called once, but should be called as early as possible, - * before any class has a chance to grab a reference to any of the global services - * instances that get discarded by prepareServices(). Only the first call has any effect, - * later calls are ignored. - * - * @note This is called by PHPUnitMaintClass::finalSetup. - * - * @see MediaWikiServices::resetGlobalInstance() - * - * @param Config $bootstrapConfig The bootstrap config to use with the new - * MediaWikiServices. Only used for the first call to this method. - * @return MediaWikiServices + * @deprecated since 1.32 */ public static function prepareServices( Config $bootstrapConfig ) { - static $services = null; - - if ( !$services ) { - $services = self::resetGlobalServices( $bootstrapConfig ); - } - return $services; - } - - /** - * Reset global services, and install testing environment. - * This is the testing equivalent of MediaWikiServices::resetGlobalInstance(). - * This should only be used to set up the testing environment, not when - * running unit tests. Use MediaWikiTestCase::overrideMwServices() for that. - * - * @see MediaWikiServices::resetGlobalInstance() - * @see prepareServices() - * @see MediaWikiTestCase::overrideMwServices() - * - * @param Config|null $bootstrapConfig The bootstrap config to use with the new - * MediaWikiServices. - * @return MediaWikiServices - */ - private static function resetGlobalServices( Config $bootstrapConfig = null ) { - $oldServices = MediaWikiServices::getInstance(); - $oldConfigFactory = $oldServices->getConfigFactory(); - $oldLoadBalancerFactory = $oldServices->getDBLoadBalancerFactory(); - - $testConfig = self::makeTestConfig( $bootstrapConfig ); - - MediaWikiServices::resetGlobalInstance( $testConfig ); - - $serviceLocator = MediaWikiServices::getInstance(); - self::installTestServices( - $oldConfigFactory, - $oldLoadBalancerFactory, - $serviceLocator - ); - return $serviceLocator; } /** @@ -310,7 +260,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { $defaultOverrides = new HashConfig(); if ( !$baseConfig ) { - $baseConfig = MediaWikiServices::getInstance()->getBootstrapConfig(); + $baseConfig = self::$originalServices->getBootstrapConfig(); } /* Some functions require some kind of caching, and will end up using the db, @@ -341,38 +291,6 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { return $testConfig; } - /** - * @param ConfigFactory $oldConfigFactory - * @param LBFactory $oldLoadBalancerFactory - * @param MediaWikiServices $newServices - * - * @throws MWException - */ - private static function installTestServices( - ConfigFactory $oldConfigFactory, - LBFactory $oldLoadBalancerFactory, - MediaWikiServices $newServices - ) { - // Use bootstrap config for all configuration. - // This allows config overrides via global variables to take effect. - $bootstrapConfig = $newServices->getBootstrapConfig(); - $newServices->resetServiceForTesting( 'ConfigFactory' ); - $newServices->redefineService( - 'ConfigFactory', - self::makeTestConfigFactoryInstantiator( - $oldConfigFactory, - [ 'main' => $bootstrapConfig ] - ) - ); - $newServices->resetServiceForTesting( 'DBLoadBalancerFactory' ); - $newServices->redefineService( - 'DBLoadBalancerFactory', - function ( MediaWikiServices $services ) use ( $oldLoadBalancerFactory ) { - return $oldLoadBalancerFactory; - } - ); - } - /** * @param ConfigFactory $oldFactory * @param Config[] $configurations @@ -405,17 +323,10 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { } /** - * Resets some well known services that typically have state that may interfere with unit tests. - * This is a lightweight alternative to resetGlobalServices(). - * - * @note There is no guarantee that no references remain to stale service instances destroyed - * by a call to doLightweightServiceReset(). - * - * @throws MWException if called outside of PHPUnit tests. - * - * @see resetGlobalServices() + * Resets some non-service singleton instances and other static caches. It's not necessary to + * reset services here. */ - private function doLightweightServiceReset() { + public static function resetNonServiceCaches() { global $wgRequest, $wgJobClasses; foreach ( $wgJobClasses as $type => $class ) { @@ -424,10 +335,6 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { JobQueueGroup::destroySingletons(); ObjectCache::clear(); - $services = MediaWikiServices::getInstance(); - $services->resetServiceForTesting( 'MainObjectStash' ); - $services->resetServiceForTesting( 'LocalServerObjectCache' ); - $services->getMainWANObjectCache()->clearProcessCache(); FileBackendGroup::destroySingleton(); DeferredUpdates::clearPendingUpdates(); @@ -443,8 +350,18 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { } public function run( PHPUnit_Framework_TestResult $result = null ) { - $needsResetDB = false; + if ( $result instanceof MediaWikiTestResult ) { + $this->cliArgs = $result->getMediaWikiCliArgs(); + } + $this->overrideMwServices(); + if ( $this->needsDB() && !$this->isTestInDatabaseGroup() ) { + throw new Exception( + get_class( $this ) . ' apparently needsDB but is not in the Database group' + ); + } + + $needsResetDB = false; if ( !self::$dbSetup || $this->needsDB() ) { // set up a DB connection for this test to use @@ -478,6 +395,9 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { if ( $needsResetDB ) { $this->resetDB( $this->db, $this->tablesUsed ); } + + self::restoreMwServices(); + $this->localServices = null; } /** @@ -566,24 +486,17 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { } // Check for unsafe queries if ( $this->db->getType() === 'mysql' ) { - $this->db->query( "SET sql_mode = 'STRICT_ALL_TABLES'" ); + $this->db->query( "SET sql_mode = 'STRICT_ALL_TABLES'", __METHOD__ ); } } - // Store contents of interwiki table in case it changes. Unfortunately, we seem to have no - // way to do this only when needed, because tablesUsed can be changed mid-test. - if ( $this->db ) { - $this->interwikiTable = $this->db->select( 'interwiki', '*', '', __METHOD__ ); - } - // Reset all caches between tests. - $this->doLightweightServiceReset(); + self::resetNonServiceCaches(); // XXX: reset maintenance triggers // Hook into period lag checks which often happen in long-running scripts - $services = MediaWikiServices::getInstance(); - $lbFactory = $services->getDBLoadBalancerFactory(); - Maintenance::setLBFactoryTriggers( $lbFactory, $services->getMainConfig() ); + $lbFactory = $this->localServices->getDBLoadBalancerFactory(); + Maintenance::setLBFactoryTriggers( $lbFactory, $this->localServices->getMainConfig() ); ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' ); } @@ -618,7 +531,8 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { $this->db->rollback( __METHOD__, 'flush' ); } if ( $this->db->getType() === 'mysql' ) { - $this->db->query( "SET sql_mode = " . $this->db->addQuotes( $wgSQLMode ) ); + $this->db->query( "SET sql_mode = " . $this->db->addQuotes( $wgSQLMode ), + __METHOD__ ); } } @@ -639,10 +553,6 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { $this->mwGlobalsToUnset = []; $this->restoreLoggers(); - if ( self::$serviceLocator && MediaWikiServices::getInstance() !== self::$serviceLocator ) { - MediaWikiServices::forceGlobalInstance( self::$serviceLocator ); - } - // TODO: move global state into MediaWikiServices RequestContext::resetMain(); if ( session_id() !== '' ) { @@ -693,13 +603,17 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { * @param object $object */ protected function setService( $name, $object ) { - // If we did not yet override the service locator, so so now. - if ( MediaWikiServices::getInstance() === self::$serviceLocator ) { - $this->overrideMwServices(); + if ( !$this->localServices ) { + throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' ); + } + + if ( $this->localServices !== MediaWikiServices::getInstance() ) { + throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices ' + . 'instance has been replaced by test code.' ); } - MediaWikiServices::getInstance()->disableService( $name ); - MediaWikiServices::getInstance()->redefineService( + $this->localServices->disableService( $name ); + $this->localServices->redefineService( $name, function () use ( $object ) { return $object; @@ -781,15 +695,23 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { * Otherwise old namespace data will lurk and cause bugs. */ private function resetNamespaces() { + if ( !$this->localServices ) { + throw new Exception( __METHOD__ . ' must be called after MediaWikiTestCase::run()' ); + } + + if ( $this->localServices !== MediaWikiServices::getInstance() ) { + throw new Exception( __METHOD__ . ' will not work because the global MediaWikiServices ' + . 'instance has been replaced by test code.' ); + } + MWNamespace::clearCaches(); Language::clearCaches(); // We can't have the TitleFormatter holding on to an old Language object either // @todo We shouldn't need to reset all the aliases here. - $services = MediaWikiServices::getInstance(); - $services->resetServiceForTesting( 'TitleFormatter' ); - $services->resetServiceForTesting( 'TitleParser' ); - $services->resetServiceForTesting( '_MediaWikiTitleCodec' ); + $this->localServices->resetServiceForTesting( 'TitleFormatter' ); + $this->localServices->resetServiceForTesting( 'TitleParser' ); + $this->localServices->resetServiceForTesting( '_MediaWikiTitleCodec' ); } /** @@ -948,40 +870,114 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { * @return MediaWikiServices * @throws MWException */ - protected static function overrideMwServices( + protected function overrideMwServices( Config $configOverrides = null, array $services = [] ) { + $newInstance = self::installMockMwServices( $configOverrides ); + + if ( $this->localServices ) { + $this->localServices->destroy(); + } + + $this->localServices = $newInstance; + + foreach ( $services as $name => $callback ) { + $newInstance->redefineService( $name, $callback ); + } + + return $newInstance; + } + + /** + * Creates a new "mock" MediaWikiServices instance, and installs it. + * This effectively resets all cached states in services, with the exception of + * the ConfigFactory and the DBLoadBalancerFactory service, which are inherited from + * the original MediaWikiServices. + * + * @note The new original MediaWikiServices instance can later be restored by calling + * restoreMwServices(). That original is determined by the first call to this method, or + * by setUpBeforeClass, whichever is called first. The caller is responsible for managing + * and, when appropriate, destroying any other MediaWikiServices instances that may get + * replaced when calling this method. + * + * @param Config|null $configOverrides Configuration overrides for the new MediaWikiServices + * instance. + * + * @return MediaWikiServices the new mock service locator. + */ + public static function installMockMwServices( Config $configOverrides = null ) { + // Make sure we have the original service locator + if ( !self::$originalServices ) { + self::$originalServices = MediaWikiServices::getInstance(); + } + if ( !$configOverrides ) { $configOverrides = new HashConfig(); } - $oldInstance = MediaWikiServices::getInstance(); - $oldConfigFactory = $oldInstance->getConfigFactory(); - $oldLoadBalancerFactory = $oldInstance->getDBLoadBalancerFactory(); + $oldConfigFactory = self::$originalServices->getConfigFactory(); + $oldLoadBalancerFactory = self::$originalServices->getDBLoadBalancerFactory(); $testConfig = self::makeTestConfig( null, $configOverrides ); - $newInstance = new MediaWikiServices( $testConfig ); + $newServices = new MediaWikiServices( $testConfig ); // Load the default wiring from the specified files. // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance. $wiringFiles = $testConfig->get( 'ServiceWiringFiles' ); - $newInstance->loadWiringFiles( $wiringFiles ); + $newServices->loadWiringFiles( $wiringFiles ); // Provide a traditional hook point to allow extensions to configure services. - Hooks::run( 'MediaWikiServices', [ $newInstance ] ); + Hooks::run( 'MediaWikiServices', [ $newServices ] ); - foreach ( $services as $name => $callback ) { - $newInstance->redefineService( $name, $callback ); + // Use bootstrap config for all configuration. + // This allows config overrides via global variables to take effect. + $bootstrapConfig = $newServices->getBootstrapConfig(); + $newServices->resetServiceForTesting( 'ConfigFactory' ); + $newServices->redefineService( + 'ConfigFactory', + self::makeTestConfigFactoryInstantiator( + $oldConfigFactory, + [ 'main' => $bootstrapConfig ] + ) + ); + $newServices->resetServiceForTesting( 'DBLoadBalancerFactory' ); + $newServices->redefineService( + 'DBLoadBalancerFactory', + function ( MediaWikiServices $services ) use ( $oldLoadBalancerFactory ) { + return $oldLoadBalancerFactory; + } + ); + + MediaWikiServices::forceGlobalInstance( $newServices ); + return $newServices; + } + + /** + * Restores the original, non-mock MediaWikiServices instance. + * The previously active MediaWikiServices instance is destroyed, + * if it is different from the original that is to be restored. + * + * @note this if for internal use by test framework code. It should never be + * called from inside a test case, a data provider, or a setUp or tearDown method. + * + * @return bool true if the original service locator was restored, + * false if there was nothing too do. + */ + public static function restoreMwServices() { + if ( !self::$originalServices ) { + return false; } - self::installTestServices( - $oldConfigFactory, - $oldLoadBalancerFactory, - $newInstance - ); - MediaWikiServices::forceGlobalInstance( $newInstance ); + $currentServices = MediaWikiServices::getInstance(); - return $newInstance; + if ( self::$originalServices === $currentServices ) { + return false; + } + + MediaWikiServices::forceGlobalInstance( self::$originalServices ); + $currentServices->destroy(); + + return true; } /** @@ -1119,18 +1115,18 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { */ public function needsDB() { // If the test says it uses database tables, it needs the database - if ( $this->tablesUsed ) { - return true; - } + return $this->tablesUsed || $this->isTestInDatabaseGroup(); + } + /** + * @return bool + * @since 1.32 + */ + protected function isTestInDatabaseGroup() { // If the test class says it belongs to the Database group, it needs the database. // NOTE: This ONLY checks for the group in the class level doc comment. $rc = new ReflectionClass( $this ); - if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) { - return true; - } - - return false; + return (bool)preg_match( '/@group +Database/im', $rc->getDocComment() ); } /** @@ -1449,7 +1445,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { // Assuming this isn't needed for External Store database, and not sure if the procedure // would be available there. if ( $db->getType() == 'oracle' ) { - $db->query( 'BEGIN FILL_WIKI_INFO; END;' ); + $db->query( 'BEGIN FILL_WIKI_INFO; END;', __METHOD__ ); } Hooks::run( 'UnitTestsAfterDatabaseSetup', [ $db, $prefix ] ); @@ -1666,12 +1662,6 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { foreach ( $tables as $tbl ) { $tbl = $db->tableName( $tbl ); $db->query( "DROP TABLE IF EXISTS $tbl", __METHOD__ ); - - if ( $tbl === 'page' ) { - // Forget about the pages since they don't - // exist in the DB. - MediaWikiServices::getInstance()->getLinkCache()->clear(); - } } } @@ -1689,7 +1679,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { $originalTables = $db->listTables( $db->_originalTablePrefix, __METHOD__ ); if ( $prefix === 'unprefixed' ) { - $originalPrefixRegex = '/^' . preg_quote( $db->_originalTablePrefix ) . '/'; + $originalPrefixRegex = '/^' . preg_quote( $db->_originalTablePrefix, '/' ) . '/'; $originalTables = array_map( function ( $pt ) use ( $originalPrefixRegex ) { return preg_replace( $originalPrefixRegex, '', $pt ); @@ -1733,14 +1723,10 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { */ private function resetDB( $db, $tablesUsed ) { if ( $db ) { - // NOTE: Do not reset the slot_roles and content_models tables, but let them - // leak across tests. Resetting them would require to reset all NamedTableStore - // instances for these tables, of which there may be several beyond the ones - // known to MediaWikiServices. See T202641. $userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ]; $pageTables = [ 'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment', 'archive', - 'revision_actor_temp', 'slots', 'content', + 'revision_actor_temp', 'slots', 'content', 'content_models', 'slot_roles', ]; $coreDBDataTables = array_merge( $userTables, $pageTables ); @@ -1771,6 +1757,8 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { } if ( array_intersect( $tablesUsed, $coreDBDataTables ) ) { + // Reset services that may contain information relating to the truncated tables + $this->overrideMwServices(); // Re-add core DB data that was deleted $this->addCoreDBData(); } @@ -1802,28 +1790,14 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { $db->delete( $tableName, '*', __METHOD__ ); } - if ( in_array( $db->getType(), [ 'postgres', 'sqlite' ], true ) ) { + if ( $db instanceof DatabasePostgres || $db instanceof DatabaseSqlite ) { // Reset the table's sequence too. $db->resetSequenceForTable( $tableName, __METHOD__ ); } - if ( $tableName === 'interwiki' ) { - if ( !$this->interwikiTable ) { - // @todo We should probably throw here, but this causes test failures that I - // can't figure out, so for now we silently continue. - return; - } - $db->insert( - 'interwiki', - array_values( array_map( 'get_object_vars', iterator_to_array( $this->interwikiTable ) ) ), - __METHOD__ - ); - } - - if ( $tableName === 'page' ) { - // Forget about the pages since they don't - // exist in the DB. - MediaWikiServices::getInstance()->getLinkCache()->clear(); + // re-initialize site_stats table + if ( $tableName === 'site_stats' ) { + SiteStatsInit::doPlaceholderInit(); } } @@ -1910,11 +1884,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { * @return mixed */ public function getCliArg( $offset ) { - if ( isset( PHPUnitMaintClass::$additionalOptions[$offset] ) ) { - return PHPUnitMaintClass::$additionalOptions[$offset]; - } - - return null; + return $this->cliArgs[$offset] ?? null; } /** @@ -1923,7 +1893,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { * @param mixed $value */ public function setCliArg( $offset, $value ) { - PHPUnitMaintClass::$additionalOptions[$offset] = $value; + $this->cliArgs[$offset] = $value; } /** @@ -2300,7 +2270,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { protected function assertFileContains( $fileName, $actualData, - $createIfMissing = true, + $createIfMissing = false, $msg = '' ) { if ( $createIfMissing ) {