use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\IMaintainableDatabase;
use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\LBFactory;
use Wikimedia\TestingAccessWrapper;
/**
/**
* The local service locator, created during setUp().
+ * @var MediaWikiServices
*/
private $localServices;
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
* Resets some non-service singleton instances and other static caches. It's not necessary to
* reset services here.
*/
- private function resetNonServiceCaches() {
+ public static function resetNonServiceCaches() {
global $wgRequest, $wgJobClasses;
foreach ( $wgJobClasses as $type => $class ) {
}
$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
$this->resetDB( $this->db, $this->tablesUsed );
}
- $this->localServices->destroy();
+ self::restoreMwServices();
$this->localServices = null;
- MediaWikiServices::forceGlobalInstance( self::$originalServices );
}
/**
}
// Reset all caches between tests.
- $this->resetNonServiceCaches();
+ self::resetNonServiceCaches();
// XXX: reset maintenance triggers
// Hook into period lag checks which often happen in long-running scripts
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.' );
+ }
+
$this->localServices->disableService( $name );
$this->localServices->redefineService(
$name,
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();
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();
}
$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
- );
+ $currentServices = MediaWikiServices::getInstance();
- if ( $this->localServices ) {
- $this->localServices->destroy();
+ if ( self::$originalServices === $currentServices ) {
+ return false;
}
- MediaWikiServices::forceGlobalInstance( $newInstance );
- $this->localServices = $newInstance;
+ MediaWikiServices::forceGlobalInstance( self::$originalServices );
+ $currentServices->destroy();
- return $newInstance;
+ return true;
}
/**
*/
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() );
}
/**
self::$dbSetup = false;
}
- /**
- * Prepares the given database connection for usage in the context of usage tests.
- * This sets up clones database tables and changes the table prefix as appropriate.
- * If the database connection already has cloned tables, calling this method has no
- * effect. The tables are not re-cloned or reset in that case.
- *
- * @param IMaintainableDatabase $db
- */
- protected function prepareConnectionForTesting( IMaintainableDatabase $db ) {
- if ( !self::$dbSetup ) {
- throw new LogicException(
- 'Cannot use prepareConnectionForTesting()'
- . ' if the test case is not defined to use the database!'
- );
- }
-
- if ( isset( $db->_originalTablePrefix ) ) {
- // The DB connection was already prepared for testing.
- return;
- }
-
- $testPrefix = self::getTestPrefixFor( $db );
- $oldPrefix = $db->tablePrefix();
-
- $tablesCloned = self::listTables( $db );
-
- if ( $oldPrefix === $testPrefix ) {
- // The database connection already has the test prefix, but presumably not
- // the cloned tables. This is the typical case, since the LBFactory will
- // have the prefix set during testing, but LoadBalancers will still return
- // connections that don't have the cloned table structure.
- $oldPrefix = self::$oldTablePrefix;
- }
-
- $dbClone = new CloneDatabase( $db, $tablesCloned, $testPrefix, $oldPrefix );
- $dbClone->useTemporaryTables( self::$useTemporaryTables );
-
- $db->_originalTablePrefix = $oldPrefix;
-
- if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
- throw new LogicException( 'Cannot clone database tables' );
- } else {
- $dbClone->cloneTableStructure();
- }
- }
-
/**
* Setups a database with cloned tables using the given prefix.
*
$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 );
*/
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 );
}
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();
}
$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__ );
}
+
+ // re-initialize site_stats table
+ if ( $tableName === 'site_stats' ) {
+ SiteStatsInit::doPlaceholderInit();
+ }
}
private static function unprefixTable( &$tableName, $ind, $prefix ) {
protected function assertFileContains(
$fileName,
$actualData,
- $createIfMissing = true,
+ $createIfMissing = false,
$msg = ''
) {
if ( $createIfMissing ) {