*/
private $mwGlobalsToUnset = [];
+ /**
+ * Holds original contents of interwiki table
+ * @var IResultWrapper
+ */
+ private $interwikiTable = null;
+
/**
* Holds original loggers which have been replaced by setLogger()
* @var LoggerInterface[]
return self::getTestUser( [ 'sysop', 'bureaucrat' ] );
}
+ /**
+ * Returns a WikiPage representing an existing page.
+ *
+ * @since 1.32
+ *
+ * @param Title|string|null $title
+ * @return WikiPage
+ * @throws MWException
+ */
+ protected function getExistingTestPage( $title = null ) {
+ $title = ( $title === null ) ? 'UTPage' : $title;
+ $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
+ $page = WikiPage::factory( $title );
+
+ if ( !$page->exists() ) {
+ $user = self::getTestSysop()->getUser();
+ $page->doEditContent(
+ new WikitextContent( 'UTContent' ),
+ 'UTPageSummary',
+ EDIT_NEW | EDIT_SUPPRESS_RC,
+ false,
+ $user
+ );
+ }
+
+ return $page;
+ }
+
+ /**
+ * Returns a WikiPage representing a non-existing page.
+ *
+ * @since 1.32
+ *
+ * @param Title|string|null $title
+ * @return WikiPage
+ * @throws MWException
+ */
+ protected function getNonexistingTestPage( $title = null ) {
+ $title = ( $title === null ) ? 'UTPage-' . rand( 0, 100000 ) : $title;
+ $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
+ $page = WikiPage::factory( $title );
+
+ if ( $page->exists() ) {
+ $page->doDeleteArticle( 'Testing' );
+ }
+
+ return $page;
+ }
+
/**
* Prepare service configuration for unit testing.
*
}
}
+ // 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();
foreach ( $this->mwGlobalsToUnset as $value ) {
unset( $GLOBALS[$value] );
}
+ if (
+ array_key_exists( 'wgExtraNamespaces', $this->mwGlobals ) ||
+ in_array( 'wgExtraNamespaces', $this->mwGlobalsToUnset )
+ ) {
+ $this->resetNamespaces();
+ }
$this->mwGlobals = [];
$this->mwGlobalsToUnset = [];
$this->restoreLoggers();
*
* @param array|string $pairs Key to the global variable, or an array
* of key/value pairs.
- * @param mixed $value Value to set the global to (ignored
+ * @param mixed|null $value Value to set the global to (ignored
* if an array is given as first argument).
*
* @note To allow changes to global variables to take effect on global service instances,
foreach ( $pairs as $key => $value ) {
$GLOBALS[$key] = $value;
}
+
+ if ( array_key_exists( 'wgExtraNamespaces', $pairs ) ) {
+ $this->resetNamespaces();
+ }
+ }
+
+ /**
+ * Must be called whenever namespaces are changed, e.g., $wgExtraNamespaces is altered.
+ * Otherwise old namespace data will lurk and cause bugs.
+ */
+ private function resetNamespaces() {
+ 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' );
}
/**
*
* @since 1.27
*
- * @param Config $configOverrides Configuration overrides for the new MediaWikiServices instance.
+ * @param Config|null $configOverrides Configuration overrides for the new MediaWikiServices
+ * instance.
* @param callable[] $services An associative array of services to re-define. Keys are service
* names, values are callables.
*
* in which case the next two parameters are ignored; or a single string
* identifying a group, to use with the next two parameters.
* @param string|null $newKey
- * @param mixed $newValue
+ * @param mixed|null $newValue
*/
public function setGroupPermissions( $newPerms, $newKey = null, $newValue = null ) {
global $wgGroupPermissions;
* @since 1.18
*/
public function dbPrefix() {
- return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
+ return self::getTestPrefixFor( $this->db );
+ }
+
+ /**
+ * @param IDatabase $db
+ * @return string
+ * @since 1.32
+ */
+ public static function getTestPrefixFor( IDatabase $db ) {
+ return $db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
}
/**
* @since 1.25 ($namespace in 1.28)
* @param string|Title $pageName Page name or title
* @param string $text Page's content
- * @param int $namespace Namespace id (name cannot already contain namespace)
+ * @param int|null $namespace Namespace id (name cannot already contain namespace)
+ * @param User|null $user If null, static::getTestSysop()->getUser() is used.
* @return array Title object and page id
*/
protected function insertPage(
$pageName,
$text = 'Sample page for unit test.',
- $namespace = null
+ $namespace = null,
+ User $user = null
) {
if ( is_string( $pageName ) ) {
$title = Title::newFromText( $pageName, $namespace );
$title = $pageName;
}
- $user = static::getTestSysop()->getUser();
+ if ( !$user ) {
+ $user = static::getTestSysop()->getUser();
+ }
$comment = __METHOD__ . ': Sample page for unit test.';
$page = WikiPage::factory( $title );
public function addDBData() {
}
- private function addCoreDBData() {
+ /**
+ * @since 1.32
+ */
+ protected function addCoreDBData() {
if ( $this->db->getType() == 'oracle' ) {
# Insert 0 user to prevent FK violations
# Anonymous user
}
/**
- * Setups a database with the given prefix.
+ * 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.
*
* If reuseDB is true and certain conditions apply, it will just change the prefix.
* Otherwise, it will clone the tables and change the prefix.
*
- * Clones all tables in the given database (whatever database that connection has
- * open), to versions with the test prefix.
- *
* @param IMaintainableDatabase $db Database to use
- * @param string $prefix Prefix to use for test tables
+ * @param string|null $prefix Prefix to use for test tables. If not given, the prefix is determined
+ * automatically for $db.
* @return bool True if tables were cloned, false if only the prefix was changed
*/
- protected static function setupDatabaseWithTestPrefix( IMaintainableDatabase $db, $prefix ) {
- $tablesCloned = self::listTables( $db );
- $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
- $dbClone->useTemporaryTables( self::$useTemporaryTables );
-
- $db->_originalTablePrefix = $db->tablePrefix();
+ protected static function setupDatabaseWithTestPrefix(
+ IMaintainableDatabase $db,
+ $prefix = null
+ ) {
+ if ( $prefix === null ) {
+ $prefix = self::getTestPrefixFor( $db );
+ }
if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
- CloneDatabase::changePrefix( $prefix );
-
+ $db->tablePrefix( $prefix );
return false;
- } else {
+ }
+
+ if ( !isset( $db->_originalTablePrefix ) ) {
+ $oldPrefix = $db->tablePrefix();
+
+ if ( $oldPrefix === $prefix ) {
+ // table already has the correct prefix, but presumably no cloned tables
+ $oldPrefix = self::$oldTablePrefix;
+ }
+
+ $db->tablePrefix( $oldPrefix );
+ $tablesCloned = self::listTables( $db );
+ $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix, $oldPrefix );
+ $dbClone->useTemporaryTables( self::$useTemporaryTables );
+
$dbClone->cloneTableStructure();
- return true;
+
+ $db->tablePrefix( $prefix );
+ $db->_originalTablePrefix = $oldPrefix;
}
+
+ return true;
}
/**
if ( self::isUsingExternalStoreDB() ) {
self::setupExternalStoreTestDBs( $testPrefix );
}
+
+ // NOTE: Change the prefix in the LBFactory and $wgDBprefix, to prevent
+ // *any* database connections to operate on live data.
+ CloneDatabase::changePrefix( $testPrefix );
}
/**
/**
* Clones the External Store database(s) for testing
*
- * @param string $testPrefix Prefix for test tables
+ * @param string|null $testPrefix Prefix for test tables. Will be determined automatically
+ * if not given.
*/
- protected static function setupExternalStoreTestDBs( $testPrefix ) {
+ protected static function setupExternalStoreTestDBs( $testPrefix = null ) {
$connections = self::getExternalStoreDatabaseConnections();
foreach ( $connections as $dbw ) {
- // Hack: cloneTableStructure sets $wgDBprefix to the unit test
- // prefix,. Even though listTables now uses tablePrefix, that
- // itself is populated from $wgDBprefix by default.
-
- // We have to set it back, or we won't find the original 'blobs'
- // table to copy.
-
- $dbw->tablePrefix( self::$oldTablePrefix );
self::setupDatabaseWithTestPrefix( $dbw, $testPrefix );
}
}
/**
* @throws LogicException if the given database connection is not a set up to use
* mock tables.
+ *
+ * @since 1.31 this is no longer private.
*/
- private function ensureMockDatabaseConnection( IDatabase $db ) {
+ protected function ensureMockDatabaseConnection( IDatabase $db ) {
if ( $db->tablePrefix() !== $this->dbPrefix() ) {
throw new LogicException(
'Trying to delete mock tables, but table prefix does not indicate a mock database.'
if ( $tbl === 'page' ) {
// Forget about the pages since they don't
// exist in the DB.
- LinkCache::singleton()->clear();
+ MediaWikiServices::getInstance()->getLinkCache()->clear();
}
}
}
private function resetDB( $db, $tablesUsed ) {
if ( $db ) {
$userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ];
- $pageTables = [ 'page', 'revision', 'ip_changes', 'revision_comment_temp',
- 'revision_actor_temp', 'comment', 'archive' ];
+ $pageTables = [
+ 'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment', 'archive',
+ 'revision_actor_temp', 'slots', 'content', 'content_models', 'slot_roles',
+ ];
$coreDBDataTables = array_merge( $userTables, $pageTables );
// If any of the user or page tables were marked as used, we should clear all of them.
$truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
foreach ( $tablesUsed as $tbl ) {
- // TODO: reset interwiki table to its original content.
- if ( $tbl == 'interwiki' ) {
- continue;
- }
-
if ( !$db->tableExists( $tbl ) ) {
continue;
}
$db->resetSequenceForTable( $tbl, __METHOD__ );
}
+ if ( $tbl === '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.
+ continue;
+ }
+ $db->insert(
+ 'interwiki',
+ array_map( 'get_object_vars', iterator_to_array( $this->interwikiTable ) ),
+ __METHOD__
+ );
+ }
+
if ( $tbl === 'page' ) {
// Forget about the pages since they don't
// exist in the DB.
- LinkCache::singleton()->clear();
+ MediaWikiServices::getInstance()->getLinkCache()->clear();
}
}
return $tables;
}
+ /**
+ * Copy test data from one database connection to another.
+ *
+ * This should only be used for small data sets.
+ *
+ * @param IDatabase $source
+ * @param IDatabase $target
+ */
+ public function copyTestData( IDatabase $source, IDatabase $target ) {
+ $tables = self::listOriginalTables( $source, 'unprefixed' );
+
+ foreach ( $tables as $table ) {
+ $res = $source->select( $table, '*', [], __METHOD__ );
+ $allRows = [];
+
+ foreach ( $res as $row ) {
+ $allRows[] = (array)$row;
+ }
+
+ $target->insert( $table, $allRows, __METHOD__, [ 'IGNORE' ] );
+ }
+ }
+
/**
* @throws MWException
* @since 1.18
}
self::assertEquals( file_get_contents( $fileName ), $actualData, $msg );
}
+
+ /**
+ * Edits or creates a page/revision
+ * @param string $pageName Page title
+ * @param string $text Content of the page
+ * @param string $summary Optional summary string for the revision
+ * @param int $defaultNs Optional namespace id
+ * @return array Array as returned by WikiPage::doEditContent()
+ */
+ protected function editPage( $pageName, $text, $summary = '', $defaultNs = NS_MAIN ) {
+ $title = Title::newFromText( $pageName, $defaultNs );
+ $page = WikiPage::factory( $title );
+
+ return $page->doEditContent( ContentHandler::makeContent( $text, $title ), $summary );
+ }
+
+ /**
+ * Revision-deletes a revision.
+ *
+ * @param Revision|int $rev Revision to delete
+ * @param array $value Keys are Revision::DELETED_* flags. Values are 1 to set the bit, 0 to
+ * clear, -1 to leave alone. (All other values also clear the bit.)
+ * @param string $comment Deletion comment
+ */
+ protected function revisionDelete(
+ $rev, array $value = [ Revision::DELETED_TEXT => 1 ], $comment = ''
+ ) {
+ if ( is_int( $rev ) ) {
+ $rev = Revision::newFromId( $rev );
+ }
+ RevisionDeleter::createList(
+ 'revision', RequestContext::getMain(), $rev->getTitle(), [ $rev->getId() ]
+ )->setVisibility( [
+ 'value' => $value,
+ 'comment' => $comment,
+ ] );
+ }
}