{
"extends": "wikimedia",
"env": {
- "browser": true,
- "jquery": true
+ "browser": true
},
"globals": {
"require": false,
"module": false,
+ "mw": false,
+ "$": false,
"mediaWiki": false,
+ "jQuery": false,
"OO": false
},
"rules": {
use MediaWiki\MediaWikiServices;
use MediaWiki\Shell\Shell;
use Wikimedia\ScopedCallback;
-use Wikimedia\Rdbms\DBReplicationWaitError;
use Wikimedia\WrappedString;
/**
* @param float|null $ifWritesSince Only wait if writes were done since this UNIX timestamp
* @param string|bool $wiki Wiki identifier accepted by wfGetLB
* @param string|bool $cluster Cluster name accepted by LBFactory. Default: false.
- * @param int|null $timeout Max wait time. Default: 1 day (cli), ~10 seconds (web)
+ * @param int|null $timeout Max wait time. Default: 60 seconds (cli), 1 second (web)
* @return bool Success (able to connect and no timeouts reached)
* @deprecated since 1.27 Use LBFactory::waitForReplication
*/
function wfWaitForSlaves(
$ifWritesSince = null, $wiki = false, $cluster = false, $timeout = null
) {
- if ( $timeout === null ) {
- $timeout = wfIsCLI() ? 60 : 10;
- }
-
if ( $cluster === '*' ) {
$cluster = false;
$wiki = false;
$wiki = wfWikiID();
}
- try {
- $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
- $lbFactory->waitForReplication( [
- 'wiki' => $wiki,
- 'cluster' => $cluster,
- 'timeout' => $timeout,
- // B/C: first argument used to be "max seconds of lag"; ignore such values
- 'ifWritesSince' => ( $ifWritesSince > 1e9 ) ? $ifWritesSince : null
- ] );
- } catch ( DBReplicationWaitError $e ) {
- return false;
+ $opts = [
+ 'wiki' => $wiki,
+ 'cluster' => $cluster,
+ // B/C: first argument used to be "max seconds of lag"; ignore such values
+ 'ifWritesSince' => ( $ifWritesSince > 1e9 ) ? $ifWritesSince : null
+ ];
+ if ( $timeout !== null ) {
+ $opts['timeout'] = $timeout;
}
- return true;
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ return $lbFactory->waitForReplication( $opts );
}
/**
use MediaWiki\Storage\BlobStore;
use MediaWiki\Storage\BlobStoreFactory;
use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\NameTableStoreFactory;
use MediaWiki\Storage\RevisionFactory;
use MediaWiki\Storage\RevisionLookup;
use MediaWiki\Storage\RevisionStore;
* @return NameTableStore
*/
public function getChangeTagDefStore() {
- return $this->getService( 'ChangeTagDefStore' );
+ return $this->getService( 'NameTableStoreFactory' )->getChangeTagDef();
}
/**
* @return NameTableStore
*/
public function getContentModelStore() {
- return $this->getService( 'ContentModelStore' );
+ return $this->getService( 'NameTableStoreFactory' )->getContentModels();
}
/**
/**
* @since 1.32
+ * @return NameTableStoreFactory
+ */
+ public function getNameTableStoreFactory() {
+ return $this->getService( 'NameTableStoreFactory' );
+ }
+
+ /**
* @return OldRevisionImporter
*/
public function getOldRevisionImporter() {
* @return NameTableStore
*/
public function getSlotRoleStore() {
- return $this->getService( 'SlotRoleStore' );
+ return $this->getService( 'NameTableStoreFactory' )->getSlotRoles();
}
/**
use MediaWiki\Storage\BlobStore;
use MediaWiki\Revision\RevisionRenderer;
use MediaWiki\Storage\BlobStoreFactory;
-use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\NameTableStoreFactory;
use MediaWiki\Storage\RevisionFactory;
use MediaWiki\Storage\RevisionLookup;
use MediaWiki\Storage\RevisionStore;
);
},
- 'ChangeTagDefStore' => function ( MediaWikiServices $services ) : NameTableStore {
- return new NameTableStore(
- $services->getDBLoadBalancer(),
- $services->getMainWANObjectCache(),
- LoggerFactory::getInstance( 'NameTableSqlStore' ),
- 'change_tag_def',
- 'ctd_id',
- 'ctd_name',
- null,
- false,
- function ( $insertFields ) {
- $insertFields['ctd_user_defined'] = 0;
- $insertFields['ctd_count'] = 0;
- return $insertFields;
- }
- );
- },
-
'CommentStore' => function ( MediaWikiServices $services ) : CommentStore {
return new CommentStore(
$services->getContentLanguage(),
return Language::factory( $services->getMainConfig()->get( 'LanguageCode' ) );
},
- 'ContentModelStore' => function ( MediaWikiServices $services ) : NameTableStore {
- return new NameTableStore(
- $services->getDBLoadBalancer(),
- $services->getMainWANObjectCache(),
- LoggerFactory::getInstance( 'NameTableSqlStore' ),
- 'content_models',
- 'model_id',
- 'model_name'
- /**
- * No strtolower normalization is added to the service as there are examples of
- * extensions that do not stick to this assumption.
- * - extensions/examples/DataPages define( 'CONTENT_MODEL_XML_DATA','XML_DATA' );
- * - extensions/Scribunto define( 'CONTENT_MODEL_SCRIBUNTO', 'Scribunto' );
- */
- );
- },
-
'CryptHKDF' => function ( MediaWikiServices $services ) : CryptHKDF {
$config = $services->getMainConfig();
return new MimeMagic( $params );
},
+ 'NameTableStoreFactory' => function ( MediaWikiServices $services ) : NameTableStoreFactory {
+ return new NameTableStoreFactory(
+ $services->getDBLoadBalancerFactory(),
+ $services->getMainWANObjectCache(),
+ LoggerFactory::getInstance( 'NameTableSqlStore' )
+ );
+ },
+
'OldRevisionImporter' => function ( MediaWikiServices $services ) : OldRevisionImporter {
return new ImportableOldRevisionImporter(
true,
$store = new RevisionStoreFactory(
$services->getDBLoadBalancerFactory(),
$services->getBlobStoreFactory(),
+ $services->getNameTableStoreFactory(),
$services->getMainWANObjectCache(),
$services->getCommentStore(),
$services->getActorMigration(),
return $factory;
},
- 'SlotRoleStore' => function ( MediaWikiServices $services ) : NameTableStore {
- return new NameTableStore(
- $services->getDBLoadBalancer(),
- $services->getMainWANObjectCache(),
- LoggerFactory::getInstance( 'NameTableSqlStore' ),
- 'slot_roles',
- 'role_id',
- 'role_name',
- 'strtolower'
- );
- },
-
'SpecialPageFactory' => function ( MediaWikiServices $services ) : SpecialPageFactory {
return new SpecialPageFactory(
$services->getMainConfig(),
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use Wikimedia\Rdbms\ILBFactory;
+use WANObjectCache;
+use Psr\Log\LoggerInterface;
+
+class NameTableStoreFactory {
+ private static $info;
+ private $stores = [];
+
+ /** @var ILBFactory */
+ private $lbFactory;
+
+ /** @var WANObjectCache */
+ private $cache;
+
+ /** @var LoggerInterface */
+ private $logger;
+
+ private static function getTableInfo() {
+ if ( self::$info ) {
+ return self::$info;
+ }
+ self::$info = [
+ 'change_tag_def' => [
+ 'idField' => 'ctd_id',
+ 'nameField' => 'ctd_name',
+ 'normalizationCallback' => null,
+ 'insertCallback' => function ( $insertFields ) {
+ $insertFields['ctd_user_defined'] = 0;
+ $insertFields['ctd_count'] = 0;
+ return $insertFields;
+ }
+ ],
+
+ 'content_models' => [
+ 'idField' => 'model_id',
+ 'nameField' => 'model_name',
+ /**
+ * No strtolower normalization is added to the service as there are examples of
+ * extensions that do not stick to this assumption.
+ * - extensions/examples/DataPages define( 'CONTENT_MODEL_XML_DATA','XML_DATA' );
+ * - extensions/Scribunto define( 'CONTENT_MODEL_SCRIBUNTO', 'Scribunto' );
+ */
+ ],
+
+ 'slot_roles' => [
+ 'idField' => 'role_id',
+ 'nameField' => 'role_name',
+ 'normalizationCallback' => 'strtolower',
+ ],
+ ];
+ return self::$info;
+ }
+
+ public function __construct(
+ ILBFactory $lbFactory,
+ WANObjectCache $cache,
+ LoggerInterface $logger
+ ) {
+ $this->lbFactory = $lbFactory;
+ $this->cache = $cache;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Get a NameTableStore for a specific table
+ *
+ * @param string $tableName The table name
+ * @param string|false $wiki The target wiki ID, or false for the current wiki
+ * @return NameTableStore
+ */
+ public function get( $tableName, $wiki = false ) : NameTableStore {
+ $infos = self::getTableInfo();
+ if ( !isset( $infos[$tableName] ) ) {
+ throw new \InvalidArgumentException( "Invalid table name \$tableName" );
+ }
+ if ( $wiki === wfWikiID() ) {
+ $wiki = false;
+ }
+ if ( isset( $this->stores[$tableName][$wiki] ) ) {
+ return $this->stores[$tableName][$wiki];
+ }
+
+ $info = $infos[$tableName];
+ $store = new NameTableStore(
+ $this->lbFactory->getMainLB( $wiki ),
+ $this->cache,
+ $this->logger,
+ $tableName,
+ $info['idField'],
+ $info['nameField'],
+ $info['normalizationCallback'] ?? null,
+ $wiki,
+ $info['insertCallback'] ?? null
+ );
+ $this->stores[$tableName][$wiki] = $store;
+ return $store;
+ }
+
+ /**
+ * Get a NameTableStore for the change_tag_def table
+ *
+ * @param string|bool $wiki
+ * @return NameTableStore
+ */
+ public function getChangeTagDef( $wiki = false ) : NameTableStore {
+ return $this->get( 'change_tag_def', $wiki );
+ }
+
+ /**
+ * Get a NameTableStore for the content_models table
+ *
+ * @param string|bool $wiki
+ * @return NameTableStore
+ */
+ public function getContentModels( $wiki = false ) : NameTableStore {
+ return $this->get( 'content_models', $wiki );
+ }
+
+ /**
+ * Get a NameTableStore for the slot_roles table
+ *
+ * @param string|bool $wiki
+ * @return NameTableStore
+ */
+ public function getSlotRoles( $wiki = false ) : NameTableStore {
+ return $this->get( 'slot_roles', $wiki );
+ }
+}
*/
private $contentHandlerUseDB;
+ /** @var NameTableStoreFactory */
+ private $nameTables;
+
/**
* @param ILBFactory $dbLoadBalancerFactory
* @param BlobStoreFactory $blobStoreFactory
+ * @param NameTableStoreFactory $nameTables
* @param WANObjectCache $cache
* @param CommentStore $commentStore
* @param ActorMigration $actorMigration
public function __construct(
ILBFactory $dbLoadBalancerFactory,
BlobStoreFactory $blobStoreFactory,
+ NameTableStoreFactory $nameTables,
WANObjectCache $cache,
CommentStore $commentStore,
ActorMigration $actorMigration,
Assert::parameterType( 'integer', $migrationStage, '$migrationStage' );
$this->dbLoadBalancerFactory = $dbLoadBalancerFactory;
$this->blobStoreFactory = $blobStoreFactory;
+ $this->nameTables = $nameTables;
$this->cache = $cache;
$this->commentStore = $commentStore;
$this->actorMigration = $actorMigration;
$this->loggerProvider = $loggerProvider;
$this->contentHandlerUseDB = $contentHandlerUseDB;
}
- /**
/**
* @since 1.32
$this->blobStoreFactory->newSqlBlobStore( $wikiId ),
$this->cache, // Pass local cache instance; Leave cache sharing to RevisionStore.
$this->commentStore,
- $this->getContentModelStore( $wikiId ),
- $this->getSlotRoleStore( $wikiId ),
+ $this->nameTables->getContentModels( $wikiId ),
+ $this->nameTables->getSlotRoles( $wikiId ),
$this->mcrMigrationStage,
$this->actorMigration,
$wikiId
return $store;
}
-
- /**
- * @param string $wikiId
- * @return NameTableStore
- */
- private function getContentModelStore( $wikiId ) {
- // XXX: a dedicated ContentModelStore subclass would avoid hard-coding
- // knowledge about the schema here.
- return new NameTableStore(
- $this->dbLoadBalancerFactory->getMainLB( $wikiId ),
- $this->cache, // Pass local cache instance; Leave cache sharing to NameTableStore.
- $this->loggerProvider->getLogger( 'NameTableSqlStore' ),
- 'content_models',
- 'model_id',
- 'model_name',
- null,
- $wikiId
- );
- }
-
- /**
- * @param string $wikiId
- * @return NameTableStore
- */
- private function getSlotRoleStore( $wikiId ) {
- // XXX: a dedicated ContentModelStore subclass would avoid hard-coding
- // knowledge about the schema here.
- return new NameTableStore(
- $this->dbLoadBalancerFactory->getMainLB( $wikiId ),
- $this->cache, // Pass local cache instance; Leave cache sharing to NameTableStore.
- $this->loggerProvider->getLogger( 'NameTableSqlStore' ),
- 'slot_roles',
- 'role_id',
- 'role_name',
- 'strtolower',
- $wikiId
- );
- }
-
}
* @since 1.17
*/
abstract class DatabaseUpdater {
+ const REPLICATION_WAIT_TIMEOUT = 300;
+
/**
* Array of updates to perform on the database
*
flush();
if ( $ret !== false ) {
$updatesDone[] = $origParams;
- $lbFactory->waitForReplication();
+ $lbFactory->waitForReplication( [ 'timeout' => self::REPLICATION_WAIT_TIMEOUT ] );
} else {
$updatesSkipped[] = [ $func, $params, $origParams ];
}
$count = ( $count + 1 ) % 100;
if ( $count == 0 ) {
$lbFactory = $services->getDBLoadBalancerFactory();
- $lbFactory->waitForReplication( [ 'wiki' => wfWikiID() ] );
+ $lbFactory->waitForReplication( [
+ 'wiki' => wfWikiID(), 'timeout' => self::REPLICATION_WAIT_TIMEOUT ] );
}
$this->db->insert( 'templatelinks',
[
use Wikimedia\ScopedCallback;
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\Rdbms\DBError;
-use Wikimedia\Rdbms\DBReplicationWaitError;
/**
* Job queue runner utility methods
// other wikis in the farm (on different masters) get a chance.
$timePassed = microtime( true ) - $lastCheckTime;
if ( $timePassed >= self::LAG_CHECK_PERIOD || $timePassed < 0 ) {
- try {
- $lbFactory->waitForReplication( [
- 'ifWritesSince' => $lastCheckTime,
- 'timeout' => self::MAX_ALLOWED_LAG
- ] );
- } catch ( DBReplicationWaitError $e ) {
+ $success = $lbFactory->waitForReplication( [
+ 'ifWritesSince' => $lastCheckTime,
+ 'timeout' => self::MAX_ALLOWED_LAG,
+ ] );
+ if ( !$success ) {
$response['reached'] = 'replica-lag-limit';
break;
}
$lastCheckTime = microtime( true );
}
- // Don't let any queue replica DBs/backups fall behind
- if ( $jobsPopped > 0 && ( $jobsPopped % 100 ) == 0 ) {
- $group->waitForBackups();
- }
// Bail if near-OOM instead of in a job
if ( !$this->checkMemoryOK() ) {
* @ingroup JobQueue
*/
use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\DBReplicationWaitError;
/**
* Job for pruning recent changes
$dbw->delete( 'recentchanges', [ 'rc_id' => $rcIds ], __METHOD__ );
Hooks::run( 'RecentChangesPurgeRows', [ $rows ] );
// There might be more, so try waiting for replica DBs
- try {
- $factory->commitAndWaitForReplication(
- __METHOD__, $ticket, [ 'timeout' => 3 ]
- );
- } catch ( DBReplicationWaitError $e ) {
+ if ( !$factory->commitAndWaitForReplication(
+ __METHOD__, $ticket, [ 'timeout' => 3 ]
+ ) ) {
// Another job will continue anyway
break;
}
* @ingroup JobQueue
*/
use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\DBReplicationWaitError;
/**
* Job to update link tables for pages
// From then on, we know that any template changes at the time the base job was
// enqueued will be reflected in backlink page parses when the leaf jobs run.
if ( !isset( $this->params['range'] ) ) {
- try {
- $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
- $lbFactory->waitForReplication( [
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ if ( !$lbFactory->waitForReplication( [
'wiki' => wfWikiID(),
'timeout' => self::LAG_WAIT_TIMEOUT
- ] );
- } catch ( DBReplicationWaitError $e ) { // only try so hard
+ ] ) ) { // only try so hard
$stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
$stats->increment( 'refreshlinks.lag_wait_failed' );
}
/**
* Exception class for replica DB wait timeouts
+ * @deprecated since 1.32
* @ingroup Database
*/
class DBReplicationWaitError extends DBExpectedError {
* @param array $opts Optional fields that include:
* - domain : wait on the load balancer DBs that handles the given domain ID
* - cluster : wait on the given external load balancer DBs
- * - timeout : Max wait time. Default: ~60 seconds
+ * - timeout : Max wait time. Default: 60 seconds for CLI, 1 second for web.
* - ifWritesSince: Only wait if writes were done since this UNIX timestamp
- * @throws DBReplicationWaitError If a timeout or error occurred waiting on a DB cluster
+ * @return bool True on success, false if a timeout or error occurred while waiting
*/
public function waitForReplication( array $opts = [] );
* @param string $fname Caller name (e.g. __METHOD__)
* @param mixed $ticket Result of getEmptyTransactionTicket()
* @param array $opts Options to waitForReplication()
- * @throws DBReplicationWaitError
+ * @return bool True if the wait was successful, false on timeout
*/
public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] );
$opts += [
'domain' => false,
'cluster' => false,
- 'timeout' => $this->cliMode ? 60 : 10,
+ 'timeout' => $this->cliMode ? 60 : 1,
'ifWritesSince' => null
];
}
}
- if ( $failed ) {
- throw new DBReplicationWaitError(
- null,
- "Could not wait for replica DBs to catch up to " .
- implode( ', ', $failed )
- );
- }
+ return !$failed;
}
public function setWaitForReplicationListener( $name, callable $callback = null ) {
}
$this->commitMasterChanges( $fnameEffective );
- $this->waitForReplication( $opts );
+ $waitSucceeded = $this->waitForReplication( $opts );
// If a nested caller committed on behalf of $fname, start another empty $fname
// transaction, leaving the caller with the same empty transaction state as before.
if ( $fnameEffective !== $fname ) {
$this->beginMasterChanges( $fnameEffective );
}
+ return $waitSucceeded;
}
public function getChronologyProtectorTouched( $dbName ) {
* @return array
*/
public function getAttribute( $name ) {
- if ( isset( $this->attributes[$name] ) ) {
- return $this->attributes[$name];
- } else {
- return [];
- }
+ return $this->attributes[$name] ?? [];
}
/**
/**
* Add an error to the 'errors' array and log it.
*
- * Should only be called from within respond().
- *
+ * @private For internal use by ResourceLoader and ResourceLoaderStartUpModule.
* @since 1.29
* @param Exception $e
* @param string $msg
* @param array $context
*/
- protected function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
+ public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
MWExceptionHandler::logException( $e );
$this->logger->warning(
$msg,
try {
return $this->getModule( $module )->getVersionHash( $context );
} catch ( Exception $e ) {
- // If modules fail to compute a version, do still consider the versions
- // of other modules - don't set an empty string E-Tag for the whole request.
- // See also T152266 and StartupModule::getModuleRegistrations().
+ // If modules fail to compute a version, don't fail the request (T152266)
+ // and still compute versions of other modules.
$this->outputErrorAndLog( $e,
'Calculating version for "{module}" failed: {exception}',
[
* See also: OutputPage::disallowUserJs()
*/
class ResourceLoaderStartUpModule extends ResourceLoaderModule {
-
- // Cache for getConfigSettings() as it's called by multiple methods
- protected $configVars = [];
protected $targets = [ 'desktop', 'mobile' ];
/**
* @param ResourceLoaderContext $context
* @return array
*/
- protected function getConfigSettings( $context ) {
- $hash = $context->getHash();
- if ( isset( $this->configVars[$hash] ) ) {
- return $this->configVars[$hash];
- }
-
+ private function getConfigSettings( $context ) {
$conf = $this->getConfig();
// We can't use Title::newMainPage() if 'mainpage' is in
Hooks::run( 'ResourceLoaderGetConfigVars', [ &$vars ] );
- $this->configVars[$hash] = $vars;
- return $this->configVars[$hash];
+ return $vars;
}
/**
$out = '';
$states = [];
$registryData = [];
+ $moduleNames = $resourceLoader->getModuleNames();
+
+ // Preload with a batch so that the below calls to getVersionHash() for each module
+ // don't require on-demand loading of more information.
+ try {
+ $resourceLoader->preloadModuleInfo( $moduleNames, $context );
+ } catch ( Exception $e ) {
+ // Don't fail the request (T152266)
+ // Also print the error in the main output
+ $resourceLoader->outputErrorAndLog( $e,
+ 'Preloading module info from startup failed: {exception}',
+ [ 'exception' => $e ]
+ );
+ }
// Get registry data
- foreach ( $resourceLoader->getModuleNames() as $name ) {
+ foreach ( $moduleNames as $name ) {
$module = $resourceLoader->getModule( $name );
$moduleTargets = $module->getTargets();
if (
}
if ( $module->isRaw() ) {
- // Don't register "raw" modules (like 'jquery' and 'mediawiki') client-side because
- // depending on them is illegal anyway and would only lead to them being reloaded
- // causing any state to be lost (like jQuery plugins, mw.config etc.)
+ // Don't register "raw" modules (like 'startup') client-side because depending on them
+ // is illegal anyway and would only lead to them being loaded a second time,
+ // causing any state to be lost.
+
+ // ATTENTION: Because of the line below, this is not going to cause infinite recursion.
+ // Think carefully before making changes to this code!
+ // The below code is going to call ResourceLoaderModule::getVersionHash() for every module.
+ // For StartUpModule (this module) the hash is computed based on the manifest content,
+ // which is the very thing we are computing right here. As such, this must skip iterating
+ // over 'startup' itself.
continue;
}
try {
$versionHash = $module->getVersionHash( $context );
} catch ( Exception $e ) {
- // See also T152266 and ResourceLoader::getCombinedVersion()
- MWExceptionHandler::logException( $e );
- $context->getLogger()->warning(
+ // Don't fail the request (T152266)
+ // Also print the error in the main output
+ $resourceLoader->outputErrorAndLog( $e,
'Calculating version for "{module}" failed: {exception}',
[
'module' => $name,
}
/**
- * Get the definition summary for this module.
- *
- * @param ResourceLoaderContext $context
- * @return array
- */
- public function getDefinitionSummary( ResourceLoaderContext $context ) {
- global $IP;
- $summary = parent::getDefinitionSummary( $context );
- $startup = [
- // getScript() exposes these variables to mw.config (T30899).
- 'vars' => $this->getConfigSettings( $context ),
- // getScript() uses this to decide how configure mw.Map for mw.config.
- 'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
- // Detect changes to the module registrations output by getScript().
- 'moduleHashes' => $this->getAllModuleHashes( $context ),
- // Detect changes to base modules listed by getScript().
- 'baseModules' => $this->getBaseModules(),
-
- 'fileHashes' => [
- $this->safeFileHash( "$IP/resources/src/startup/startup.js" ),
- $this->safeFileHash( "$IP/resources/src/startup/mediawiki.js" ),
- $this->safeFileHash( "$IP/resources/src/startup/mediawiki.requestIdleCallback.js" ),
- ],
- ];
- if ( $context->getDebug() ) {
- $startup['fileHashes'][] = $this->safeFileHash( "$IP/resources/src/startup/mediawiki.log.js" );
- }
- if ( $this->getConfig()->get( 'ResourceLoaderEnableJSProfiler' ) ) {
- $startup['fileHashes'][] = $this->safeFileHash( "$IP/resources/src/startup/profiling.js" );
- }
- $summary[] = $startup;
- return $summary;
- }
-
- /**
- * Helper method for getDefinitionSummary().
- *
- * @param ResourceLoaderContext $context
- * @return string SHA-1
+ * @return bool
*/
- protected function getAllModuleHashes( ResourceLoaderContext $context ) {
- $rl = $context->getResourceLoader();
- // Preload for getCombinedVersion()
- $rl->preloadModuleInfo( $rl->getModuleNames(), $context );
-
- // ATTENTION: Because of the line below, this is not going to cause infinite recursion.
- // Think carefully before making changes to this code!
- // Pre-populate versionHash with something because the loop over all modules below includes
- // the startup module (this module).
- // See ResourceLoaderModule::getVersionHash() for usage of this cache.
- $this->versionHash[$context->getHash()] = null;
-
- return $rl->getCombinedVersion( $context, $rl->getModuleNames() );
+ public function enableModuleContentVersion() {
+ // Enabling this means that ResourceLoader::getVersionHash will simply call getScript()
+ // and hash it to determine the version (as used by E-Tag HTTP response header).
+ return true;
}
/**
}
}
+ // Break circular references due to the $this reference in closures, by
+ // erasing the instantiator array. This allows the ServiceContainer to
+ // be deleted when it goes out of scope.
+ $this->serviceInstantiators = [];
+ // Also remove the services themselves, to avoid confusion.
+ $this->services = [];
$this->destroyed = true;
}
wfEntryPointCheck( 'cli' );
use MediaWiki\Shell\Shell;
-use Wikimedia\Rdbms\DBReplicationWaitError;
/**
* @defgroup MaintenanceArchive Maintenance archives
*/
protected function commitTransaction( IDatabase $dbw, $fname ) {
$dbw->commit( $fname );
- try {
- $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
- $lbFactory->waitForReplication(
- [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ]
- );
- $this->lastReplicationWait = microtime( true );
-
- return true;
- } catch ( DBReplicationWaitError $e ) {
- return false;
- }
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $waitSucceeded = $lbFactory->waitForReplication(
+ [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ]
+ );
+ $this->lastReplicationWait = microtime( true );
+ return $waitSucceeded;
}
/**
require_once __DIR__ . '/Maintenance.php';
use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\DBReplicationWaitError;
/**
* Maintenance script to update cached special pages.
$this->output( "Reconnected\n\n" );
}
// Wait for the replica DB to catch up
- try {
- $lbFactory->waitForReplication();
- } catch ( DBReplicationWaitError $e ) {
- // ignore
- }
+ $lbFactory->waitForReplication();
}
public function doSpecialPageCacheUpdates( $dbw ) {
* "mediawiki" module, and will remain a default/implicit dependency for all
* regular modules, just like jquery and wikibits already are.
*/
-/* globals mw */
( function () {
'use strict';
* @class mw.errorLogger
* @singleton
*/
-( function ( mw ) {
+( function () {
'use strict';
mw.errorLogger = {
};
mw.errorLogger.installGlobalHandler( window );
-}( mediaWiki ) );
+}() );
/**
* Base library for MediaWiki.
*
- * Exposed globally as `mediaWiki` with `mw` as shortcut.
+ * Exposed globally as `mw`, with `mediaWiki` as alias.
*
* @class mw
* @alternateClassName mediaWiki
mw.loader.register( name );
}
// Check for duplicate implementation
- if ( hasOwn.call( registry, name ) && registry[ name ].script !== undefined ) {
+ if ( registry[ name ].script !== undefined ) {
throw new Error( 'module already implemented: ' + name );
}
if ( version ) {
* @author Michael Dale <mdale@wikimedia.org>
* @author Trevor Parscal <tparscal@wikimedia.org>
*/
-( function ( mw ) {
+( function () {
/* global console */
/* eslint-disable no-console */
var original = mw.log;
mw.log.error = original.error;
mw.log.deprecate = original.deprecate;
}
-}( mediaWiki ) );
+}() );
-( function ( mw ) {
+( function () {
var maxBusy = 50;
mw.requestIdleCallbackInternal = function ( callback ) {
// Note: Polyfill was previously disabled due to
// https://bugs.chromium.org/p/chromium/issues/detail?id=647870
// See also <http://codepen.io/Krinkle/full/XNGEvv>
-}( mediaWiki ) );
+}() );
* @author Timo Tijhof
* @since 1.32
*/
-/* global mw */
( function () {
'use strict';
* - Beware: This file MUST parse without errors on even the most ancient of browsers!
*/
/* eslint-disable vars-on-top, no-unmodified-loop-condition */
-/* global mw, isCompatible, $VARS, $CODE */
+/* global isCompatible, $VARS, $CODE */
/**
* See <https://www.mediawiki.org/wiki/Compatibility#Browsers>
// may break testing against floating point values
// treated with PHP's serialize()
ini_set( 'serialize_precision', 17 );
-
- // TODO: we should call MediaWikiTestCase::prepareServices( new GlobalVarConfig() ) here.
- // But PHPUnit may not be loaded yet, so we have to wait until just
- // before PHPUnit_TextUI_Command::main() is executed.
}
}
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\IMaintainableDatabase;
use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\LBFactory;
use Wikimedia\TestingAccessWrapper;
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().
+ */
+ private $localServices;
/**
* $called tracks whether the setUp and tearDown method has been called.
*/
private $mwGlobalsToUnset = [];
- /**
- * Holds original contents of interwiki table
- * @var IResultWrapper
- */
- private $interwikiTable = null;
-
/**
* Holds original loggers which have been replaced by setLogger()
* @var LoggerInterface[]
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();
+ }
}
/**
}
/**
- * 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;
}
/**
$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,
}
/**
- * 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() {
+ private function resetNonServiceCaches() {
global $wgRequest, $wgJobClasses;
foreach ( $wgJobClasses as $type => $class ) {
JobQueueGroup::destroySingletons();
ObjectCache::clear();
- $services = MediaWikiServices::getInstance();
- $services->resetServiceForTesting( 'MainObjectStash' );
- $services->resetServiceForTesting( 'LocalServerObjectCache' );
- $services->getMainWANObjectCache()->clearProcessCache();
FileBackendGroup::destroySingleton();
DeferredUpdates::clearPendingUpdates();
}
public function run( PHPUnit_Framework_TestResult $result = null ) {
+ $this->overrideMwServices();
+
$needsResetDB = false;
if ( !self::$dbSetup || $this->needsDB() ) {
$this->testLogger->info( "Resetting DB" );
$this->resetDB( $this->db, $this->tablesUsed );
}
+
+ $this->localServices->destroy();
+ $this->localServices = null;
+ MediaWikiServices::forceGlobalInstance( self::$originalServices );
}
/**
}
}
- // 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();
+ $this->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' );
}
$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() !== '' ) {
* @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()' );
}
- MediaWikiServices::getInstance()->disableService( $name );
- MediaWikiServices::getInstance()->redefineService(
+ $this->localServices->disableService( $name );
+ $this->localServices->redefineService(
$name,
function () use ( $object ) {
return $object;
* 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()' );
+ }
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' );
}
/**
* @return MediaWikiServices
* @throws MWException
*/
- protected static function overrideMwServices(
+ protected function overrideMwServices(
Config $configOverrides = null, array $services = []
) {
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 );
$oldLoadBalancerFactory,
$newInstance
);
+
+ if ( $this->localServices ) {
+ $this->localServices->destroy();
+ }
+
MediaWikiServices::forceGlobalInstance( $newInstance );
+ $this->localServices = $newInstance;
return $newInstance;
}
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();
- }
}
}
*/
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__ );
}
- 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();
}
}
parent::tearDown();
TestingAccessWrapper::newFromClass( Hooks::class )->handlers = $this->originalHandlers;
-
- $this->overrideMwServices();
}
protected function searchProvision( array $results = null ) {
Revision::DELETED_TEXT,
Revision::DELETED_TEXT,
[ 'sysop' ],
- Title::newFromText( __METHOD__ ),
+ __METHOD__,
true,
];
yield [
Revision::DELETED_TEXT,
Revision::DELETED_TEXT,
[],
- Title::newFromText( __METHOD__ ),
+ __METHOD__,
false,
];
}
* @covers Revision::userCanBitfield
*/
public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
+ $title = Title::newFromText( $title );
+
$this->setMwGlobals(
'wgGroupPermissions',
[
return $rev;
}
+ /**
+ * @param int $id
+ * @return Title
+ */
+ private function getMockTitle( $id = 23 ) {
+ $mock = $this->getMockBuilder( Title::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'getDBkey' )
+ ->will( $this->returnValue( __CLASS__ ) );
+ $mock->expects( $this->any() )
+ ->method( 'getArticleID' )
+ ->will( $this->returnValue( $id ) );
+
+ return $mock;
+ }
+
public function provideIsReusableFor() {
- $title = Title::makeTitleSafe( NS_MAIN, __METHOD__ );
+ $title = $this->getMockTitle();
$user1 = User::newFromName( 'Alice' );
$user2 = User::newFromName( 'Bob' );
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Storage;
+
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\NameTableStoreFactory;
+use MediaWikiTestCase;
+use Wikimedia\Rdbms\ILBFactory;
+use Wikimedia\Rdbms\ILoadBalancer;
+
+/**
+ * @covers MediaWiki\Storage\NameTableStoreFactory
+ * @group Database
+ */
+class NameTableStoreFactoryTest extends MediaWikiTestCase {
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|ILoadBalancer
+ */
+ private function getMockLoadBalancer() {
+ return $this->getMockBuilder( ILoadBalancer::class )
+ ->disableOriginalConstructor()->getMock();
+ }
+
+ /**
+ * @return \PHPUnit_Framework_MockObject_MockObject|ILBFactory
+ */
+ private function getMockLoadBalancerFactory( $expectedWiki ) {
+ $mock = $this->getMockBuilder( ILBFactory::class )
+ ->disableOriginalConstructor()->getMock();
+
+ $mock->expects( $this->once() )
+ ->method( 'getMainLB' )
+ ->with( $this->equalTo( $expectedWiki ) )
+ ->willReturnCallback( function ( $domain ) use ( $expectedWiki ) {
+ return $this->getMockLoadBalancer();
+ } );
+
+ return $mock;
+ }
+
+ public static function provideTestGet() {
+ return [
+ [
+ 'change_tag_def',
+ false,
+ false,
+ ],
+ [
+ 'content_models',
+ false,
+ false,
+ ],
+ [
+ 'slot_roles',
+ false,
+ false,
+ ],
+ [
+ 'change_tag_def',
+ 'test7245',
+ 'test7245',
+ ],
+ ];
+ }
+
+ /** @dataProvider provideTestGet */
+ public function testGet( $tableName, $wiki, $expectedWiki ) {
+ $services = MediaWikiServices::getInstance();
+ $db = wfGetDB( DB_MASTER );
+ if ( $wiki === false ) {
+ $wiki2 = $db->getWikiID();
+ } else {
+ $wiki2 = $wiki;
+ }
+ $names = new NameTableStoreFactory(
+ $this->getMockLoadBalancerFactory( $expectedWiki ),
+ $services->getMainWANObjectCache(),
+ LoggerFactory::getInstance( 'NameTableStoreFactory' )
+ );
+
+ $table = $names->get( $tableName, $wiki );
+ $table2 = $names->get( $tableName, $wiki2 );
+ $this->assertSame( $table, $table2 );
+ $this->assertInstanceOf( NameTableStore::class, $table );
+ }
+
+ /*
+ * The next three integration tests verify that the schema information is correct by loading
+ * the relevant information from the database.
+ */
+
+ public function testIntegratedGetChangeTagDef() {
+ $services = MediaWikiServices::getInstance();
+ $factory = $services->getNameTableStoreFactory();
+ $store = $factory->getChangeTagDef();
+ $this->assertType( 'array', $store->getMap() );
+ }
+
+ public function testIntegratedGetContentModels() {
+ $services = MediaWikiServices::getInstance();
+ $factory = $services->getNameTableStoreFactory();
+ $store = $factory->getContentModels();
+ $this->assertType( 'array', $store->getMap() );
+ }
+
+ public function testIntegratedGetSlotRoles() {
+ $services = MediaWikiServices::getInstance();
+ $factory = $services->getNameTableStoreFactory();
+ $store = $factory->getSlotRoles();
+ $this->assertType( 'array', $store->getMap() );
+ }
+}
* @group Database
*/
class PageUpdaterTest extends MediaWikiTestCase {
-
- public static function setUpBeforeClass() {
- parent::setUpBeforeClass();
-
- // force service reset!
- MediaWikiServices::getInstance()->resetServiceForTesting( 'RevisionStore' );
- }
-
private function getDummyTitle( $method ) {
return Title::newFromText( $method, $this->getDefaultWikitextNS() );
}
RevisionRecord::DELETED_TEXT,
RevisionRecord::DELETED_TEXT,
[ 'sysop' ],
- Title::newFromText( __METHOD__ ),
+ __METHOD__,
true,
];
yield [
RevisionRecord::DELETED_TEXT,
RevisionRecord::DELETED_TEXT,
[],
- Title::newFromText( __METHOD__ ),
+ __METHOD__,
false,
];
}
* @covers \MediaWiki\Storage\RevisionRecord::userCanBitfield
*/
public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
+ if ( is_string( $title ) ) {
+ // NOTE: Data providers cannot instantiate Title objects! See T202641.
+ $title = Title::newFromText( $title );
+ }
+
$this->forceStandardPermissions();
$user = $this->getTestUser( $userGroups )->getUser();
}
public function provideHasSameContent() {
- /**
- * @param SlotRecord[] $slots
- * @param int $revId
- * @return RevisionStoreRecord
- */
- $recordCreator = function ( array $slots, $revId ) {
- $title = Title::newFromText( 'provideHasSameContent' );
- $title->resetArticleID( 19 );
- $slots = new RevisionSlots( $slots );
-
- return new RevisionStoreRecord(
- $title,
- new UserIdentityValue( 11, __METHOD__, 0 ),
- CommentStoreComment::newUnsavedComment( __METHOD__ ),
- (object)[
- 'rev_id' => strval( $revId ),
- 'rev_page' => strval( $title->getArticleID() ),
- 'rev_timestamp' => '20200101000000',
- 'rev_deleted' => 0,
- 'rev_minor_edit' => 0,
- 'rev_parent_id' => '5',
- 'rev_len' => $slots->computeSize(),
- 'rev_sha1' => $slots->computeSha1(),
- 'page_latest' => '18',
- ],
- $slots
- );
- };
-
// Create some slots with content
$mainA = SlotRecord::newUnsaved( 'main', new TextContent( 'A' ) );
$mainB = SlotRecord::newUnsaved( 'main', new TextContent( 'B' ) );
$auxA = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
$auxB = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
- $initialRecord = $recordCreator( [ $mainA ], 12 );
+ $initialRecordSpec = [ [ $mainA ], 12 ];
return [
'same record object' => [
true,
- $initialRecord,
- $initialRecord,
+ $initialRecordSpec,
+ $initialRecordSpec,
],
'same record content, different object' => [
true,
- $recordCreator( [ $mainA ], 12 ),
- $recordCreator( [ $mainA ], 13 ),
+ [ [ $mainA ], 12 ],
+ [ [ $mainA ], 13 ],
],
'same record content, aux slot, different object' => [
true,
- $recordCreator( [ $auxA ], 12 ),
- $recordCreator( [ $auxB ], 13 ),
+ [ [ $auxA ], 12 ],
+ [ [ $auxB ], 13 ],
],
'different content' => [
false,
- $recordCreator( [ $mainA ], 12 ),
- $recordCreator( [ $mainB ], 13 ),
+ [ [ $mainA ], 12 ],
+ [ [ $mainB ], 13 ],
],
'different content and number of slots' => [
false,
- $recordCreator( [ $mainA ], 12 ),
- $recordCreator( [ $mainA, $mainB ], 13 ),
+ [ [ $mainA ], 12 ],
+ [ [ $mainA, $mainB ], 13 ],
],
];
}
+ /**
+ * @note Do not call directly from a data provider! Data providers cannot instantiate
+ * Title objects! See T202641.
+ *
+ * @param SlotRecord[] $slots
+ * @param int $revId
+ * @return RevisionStoreRecord
+ */
+ private function makeHasSameContentTestRecord( array $slots, $revId ) {
+ $title = Title::newFromText( 'provideHasSameContent' );
+ $title->resetArticleID( 19 );
+ $slots = new RevisionSlots( $slots );
+
+ return new RevisionStoreRecord(
+ $title,
+ new UserIdentityValue( 11, __METHOD__, 0 ),
+ CommentStoreComment::newUnsavedComment( __METHOD__ ),
+ (object)[
+ 'rev_id' => strval( $revId ),
+ 'rev_page' => strval( $title->getArticleID() ),
+ 'rev_timestamp' => '20200101000000',
+ 'rev_deleted' => 0,
+ 'rev_minor_edit' => 0,
+ 'rev_parent_id' => '5',
+ 'rev_len' => $slots->computeSize(),
+ 'rev_sha1' => $slots->computeSha1(),
+ 'page_latest' => '18',
+ ],
+ $slots
+ );
+ }
+
/**
* @dataProvider provideHasSameContent
* @covers \MediaWiki\Storage\RevisionRecord::hasSameContent
*/
public function testHasSameContent(
$expected,
- RevisionRecord $record1,
- RevisionRecord $record2
+ $recordSpec1,
+ $recordSpec2
) {
+ $record1 = $this->makeHasSameContentTestRecord( ...$recordSpec1 );
+ $record2 = $this->makeHasSameContentTestRecord( ...$recordSpec2 );
+
$this->assertSame(
$expected,
$record1->hasSameContent( $record2 )
use MediaWiki\Storage\BlobStore;
use MediaWiki\Storage\BlobStoreFactory;
use MediaWiki\Storage\NameTableStore;
+use MediaWiki\Storage\NameTableStoreFactory;
use MediaWiki\Storage\RevisionStore;
use MediaWiki\Storage\RevisionStoreFactory;
use MediaWiki\Storage\SqlBlobStore;
new RevisionStoreFactory(
$this->getMockLoadBalancerFactory(),
$this->getMockBlobStoreFactory(),
+ $this->getNameTableStoreFactory(),
$this->getHashWANObjectCache(),
$this->getMockCommentStore(),
ActorMigration::newMigration(),
) {
$lbFactory = $this->getMockLoadBalancerFactory();
$blobStoreFactory = $this->getMockBlobStoreFactory();
+ $nameTableStoreFactory = $this->getNameTableStoreFactory();
$cache = $this->getHashWANObjectCache();
$commentStore = $this->getMockCommentStore();
$actorMigration = ActorMigration::newMigration();
$factory = new RevisionStoreFactory(
$lbFactory,
$blobStoreFactory,
+ $nameTableStoreFactory,
$cache,
$commentStore,
$actorMigration,
return $mock;
}
+ /**
+ * @return NameTableStoreFactory
+ */
+ private function getNameTableStoreFactory() {
+ return new NameTableStoreFactory(
+ $this->getMockLoadBalancerFactory(),
+ $this->getHashWANObjectCache(),
+ new NullLogger() );
+ }
+
/**
* @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
*/
$this->setExpectedException( MWException::class );
}
+ $nameTables = MediaWikiServices::getInstance()->getNameTableStoreFactory();
+
$store = new RevisionStore(
$this->getMockLoadBalancer(),
$this->getMockSqlBlobStore(),
$this->getHashWANObjectCache(),
$this->getMockCommentStore(),
- MediaWikiServices::getInstance()->getContentModelStore(),
- MediaWikiServices::getInstance()->getSlotRoleStore(),
+ $nameTables->getContentModels(),
+ $nameTables->getSlotRoles(),
$migrationMode,
MediaWikiServices::getInstance()->getActorMigration()
);
$blobStore = $this->getMockSqlBlobStore();
$cache = $this->getHashWANObjectCache();
$commentStore = $this->getMockCommentStore();
- $contentModelStore = MediaWikiServices::getInstance()->getContentModelStore();
- $slotRoleStore = MediaWikiServices::getInstance()->getSlotRoleStore();
+ $services = MediaWikiServices::getInstance();
+ $nameTables = $services->getNameTableStoreFactory();
+ $contentModelStore = $nameTables->getContentModels();
+ $slotRoleStore = $nameTables->getSlotRoles();
$store = new RevisionStore(
$loadBalancer,
$blobStore,
$cache,
$commentStore,
- MediaWikiServices::getInstance()->getContentModelStore(),
- MediaWikiServices::getInstance()->getSlotRoleStore(),
+ $nameTables->getContentModels(),
+ $nameTables->getSlotRoles(),
$migration,
- MediaWikiServices::getInstance()->getActorMigration()
+ $services->getActorMigration()
);
if ( !$expectException ) {
$store = TestingAccessWrapper::newFromObject( $store );
'empty search result' => [ [], [] ],
'has search results' => [
[ 'Zomg' ],
- [ $this->mockResult( 'Zomg' ) ],
+ [ $this->mockResultClosure( 'Zomg' ) ],
],
'filters broken search results' => [
[ 'A', 'B' ],
[
- $this->mockResult( 'a' ),
- $this->mockResult( 'Zomg' )->setBrokenTitle( true ),
- $this->mockResult( 'b' ),
+ $this->mockResultClosure( 'a' ),
+ $this->mockResultClosure( 'Zomg', [ 'setBrokenTitle' => true ] ),
+ $this->mockResultClosure( 'b' ),
],
],
'filters results with missing revision' => [
[ 'B', 'A' ],
[
- $this->mockResult( 'Zomg' )->setMissingRevision( true ),
- $this->mockResult( 'b' ),
- $this->mockResult( 'a' ),
+ $this->mockResultClosure( 'Zomg', [ 'setMissingRevision' => true ] ),
+ $this->mockResultClosure( 'b' ),
+ $this->mockResultClosure( 'a' ),
],
],
];
[
SearchResultSet::SECONDARY_RESULTS => [
'utwiki' => new MockSearchResultSet( [
- $this->mockResult( 'Qwerty' )->setInterwikiPrefix( 'utwiki' ),
+ $this->mockResultClosure(
+ 'Qwerty',
+ [ 'setInterwikiPrefix' => 'utwiki' ]
+ ),
] ),
],
]
] );
}
- private function mockResult( $title ) {
- return MockSearchResult::newFromtitle( Title::newFromText( $title ) );
+ /**
+ * Returns a closure that evaluates to a MockSearchResult, to be resolved by
+ * MockSearchEngine::addMockResults() or MockresultSet::extractResults().
+ *
+ * This is needed because MockSearchResults cannot be instantiated in a data provider,
+ * since they load revisions. This would hit the "real" database instead of the mock
+ * database, which in turn may cause cache pollution and other inconsistencies, see T202641.
+ *
+ * @param string $title
+ * @param array $setters
+ * @return callable function(): MockSearchResult
+ */
+ private function mockResultClosure( $title, $setters = [] ) {
+ return function () use ( $title, $setters ){
+ $result = MockSearchResult::newFromTitle( Title::newFromText( $title ) );
+
+ foreach ( $setters as $method => $param ) {
+ $result->$method( $param );
+ }
+
+ return $result;
+ };
}
}
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'change_tag', '*' );
$dbw->delete( 'change_tag_def', '*' );
- MediaWikiServices::getInstance()->resetServiceForTesting( 'ChangeTagDefStore' );
+ MediaWikiServices::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
$rcId = 123;
ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'change_tag', '*' );
$dbw->delete( 'change_tag_def', '*' );
- MediaWikiServices::getInstance()->resetServiceForTesting( 'ChangeTagDefStore' );
+ MediaWikiServices::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
$rcId = 123;
ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'change_tag', '*' );
$dbw->delete( 'change_tag_def', '*' );
- MediaWikiServices::getInstance()->resetServiceForTesting( 'ChangeTagDefStore' );
+ MediaWikiServices::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
$rcId = 123;
ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
trim( strip_tags( $diff ), "\n" ) );
}
+ /**
+ * @param int $id
+ * @return Title
+ */
+ private function getMockTitle( $id = 23 ) {
+ $mock = $this->getMockBuilder( Title::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $mock->expects( $this->any() )
+ ->method( 'getDBkey' )
+ ->will( $this->returnValue( __CLASS__ ) );
+ $mock->expects( $this->any() )
+ ->method( 'getArticleID' )
+ ->will( $this->returnValue( $id ) );
+
+ return $mock;
+ }
+
/**
* @param SlotRecord[] $slots
* @return MutableRevisionRecord
*/
private function getRevisionRecord( ...$slots ) {
- $title = Title::newFromText( 'Foo' );
+ $title = $this->getMockTitle();
$revision = new MutableRevisionRecord( $title );
foreach ( $slots as $slot ) {
$revision->setSlot( $slot );
$version1 = $module->getVersionHash( $context );
$module = new ResourceLoaderStartupModule();
$version2 = $module->getVersionHash( $context );
+
$this->setMwGlobals( 'wgArticlePath', '/w3' );
$module = new ResourceLoaderStartupModule();
$version3 = $module->getVersionHash( $context );
}
/**
- * @covers ResourceLoaderStartupModule::getAllModuleHashes
- * @covers ResourceLoaderStartupModule::getDefinitionSummary
+ * @covers ResourceLoaderStartupModule
*/
public function testGetVersionHash_varyModule() {
$context1 = $this->getResourceLoaderContext();
$module = new ResourceLoaderStartupModule();
$version3 = $module->getVersionHash( $context3 );
- $this->assertEquals(
+ // Module name *is* significant (T201686)
+ $this->assertNotEquals(
$version1,
$version2,
- 'Module name is insignificant'
+ 'Module name is significant'
);
$this->assertNotEquals(
);
}
+ /**
+ * @covers ResourceLoaderStartupModule
+ */
+ public function testGetVersionHash_varyDeps() {
+ $context = $this->getResourceLoaderContext();
+ $rl = $context->getResourceLoader();
+ $rl->register( [
+ 'test.a' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'x', 'y' ] ] ),
+ ] );
+ $module = new ResourceLoaderStartupModule();
+ $version1 = $module->getVersionHash( $context );
+
+ $context = $this->getResourceLoaderContext();
+ $rl = $context->getResourceLoader();
+ $rl->register( [
+ 'test.a' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'x', 'z' ] ] ),
+ ] );
+ $module = new ResourceLoaderStartupModule();
+ $version2 = $module->getVersionHash( $context );
+
+ // Dependencies *are* significant (T201686)
+ $this->assertNotEquals(
+ $version1,
+ $version2,
+ 'Dependencies are significant'
+ );
+ }
+
}
parent::tearDown();
TestingAccessWrapper::newFromClass( Hooks::class )->handlers = $this->originalHandlers;
-
- $this->overrideMwServices();
}
protected function searchProvision( array $results = null ) {
* @param SearchResult[] $results The results to return for $query
*/
public static function addMockResults( $query, array $results ) {
- self::$results[$query] = $results;
$lc = MediaWikiServices::getInstance()->getLinkCache();
- foreach ( $results as $result ) {
+ foreach ( $results as &$result ) {
+ // Resolve deferred results; needed to work around T203279
+ if ( is_callable( $result ) ) {
+ $result = $result();
+ }
+
// TODO: better page ids? Does it matter?
$lc->addGoodLinkObj( mt_rand(), $result->getTitle() );
}
+ self::$results[$query] = $results;
}
/**
private $interwikiResults;
/**
- * @param SearchResult[] $results
- * @param SearchResultSet[][] $interwikiResults Map from result type
+ * @param SearchResult[]|callable[] $results
+ * @param SearchResultSet[][]|callable[][] $interwikiResults Map from result type
* to list of results for that type.
*/
public function __construct( array $results, array $interwikiResults = [] ) {
count( $this->interwikiResults[$type] ) > 0;
}
+ public function extractResults() {
+ $results = parent::extractResults();
+
+ foreach ( $results as &$result ) {
+ // Resolve deferred results; needed to work around T203279
+ if ( is_callable( $result ) ) {
+ $result = $result();
+ }
+ }
+
+ return $results;
+ }
+
public function getInterwikiResults( $type = self::SECONDARY_RESULTS ) {
if ( $this->hasInterwikiResults( $type ) ) {
return $this->interwikiResults[$type];
'Using HHVM ' . HHVM_VERSION . ' (' . PHP_VERSION . ")\n" :
'Using PHP ' . PHP_VERSION . "\n";
- // Prepare global services for unit tests.
- MediaWikiTestCase::prepareServices( new GlobalVarConfig() );
-
$phpUnitClass::main();
}
* @author Addshore
*/
class SpecialPageFatalTest extends MediaWikiTestCase {
-
- public static function setUpBeforeClass() {
- parent::setUpBeforeClass();
- self::overrideMwServices();
- }
-
- public static function tearDownAfterClass() {
- self::overrideMwServices();
- parent::tearDownAfterClass();
- }
-
public function provideSpecialPages() {
$specialPages = [];
$spf = MediaWikiServices::getInstance()->getSpecialPageFactory();
$this->overrideMwServices();
$this->assertNotSame( $initialServices, MediaWikiServices::getInstance() );
-
- $this->tearDown();
- $this->assertSame( $initialServices, MediaWikiServices::getInstance() );
}
public function testSetService() {
->disableOriginalConstructor()->getMock();
$this->setService( 'DBLoadBalancer', $mockService );
- $this->assertNotSame( $initialServices, MediaWikiServices::getInstance() );
$this->assertNotSame(
$initialService,
MediaWikiServices::getInstance()->getDBLoadBalancer()
);
$this->assertSame( $mockService, MediaWikiServices::getInstance()->getDBLoadBalancer() );
-
- $this->tearDown();
- $this->assertSame( $initialServices, MediaWikiServices::getInstance() );
- $this->assertNotSame( $mockService, MediaWikiServices::getInstance()->getDBLoadBalancer() );
- $this->assertSame( $initialService, MediaWikiServices::getInstance()->getDBLoadBalancer() );
}
/**