Add support for External Store for unit tests
authorMatthew Flaschen <mflaschen@wikimedia.org>
Thu, 3 Mar 2016 07:30:24 +0000 (02:30 -0500)
committerMatthew Flaschen <mflaschen@wikimedia.org>
Sat, 12 Mar 2016 00:58:32 +0000 (19:58 -0500)
This allow running PHP tests with External Store DB configured.
This means it needs to do the unittest_ prefixing similar to the main
tables.

It could do resetting (truncating the tables) similar to the tablesUsed
functionality.  However, if External Store is truncated unconditionally,
revision, etc. also needs to be.  This can cause performance issues.
I was thinking of making resetting ES optional, but it wasn't needed
to pass any of the core tests, so I just left it out entirely for now.

Bug: T95870
Change-Id: If27435210d5a2165795dd759b17a7e51fe9bba10

tests/phpunit/MediaWikiTestCase.php

index b9cf51d..f3306be 100644 (file)
@@ -29,6 +29,8 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        public static $users;
 
        /**
+        * Primary database
+        *
         * @var DatabaseBase
         * @since 1.18
         */
@@ -132,17 +134,16 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                        $this->checkDbIsSupported();
 
                        if ( !self::$dbSetup ) {
-                               // switch to a temporary clone of the database
-                               self::setupTestDB( $this->db, $this->dbPrefix() );
+                               $this->setupAllTestDBs();
                                $this->addCoreDBData();
 
                                if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
-                                       $this->resetDB();
+                                       $this->resetDB( $this->db, $this->tablesUsed );
                                }
                        }
 
                        // TODO: the DB setup should be done in setUpBeforeClass(), so the test DB
-                       // is available in subclasse's setUpBeforeClass() and setUp() methods.
+                       // is available in subclass's setUpBeforeClass() and setUp() methods.
                        // This would also remove the need for the HACK that is oncePerClass().
                        if ( $this->oncePerClass() ) {
                                $this->addDBDataOnce();
@@ -155,7 +156,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                parent::run( $result );
 
                if ( $needsResetDB ) {
-                       $this->resetDB();
+                       $this->resetDB( $this->db, $this->tablesUsed );
                }
        }
 
@@ -564,10 +565,6 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                $user = User::newFromName( 'UTSysop' );
                $comment = __METHOD__ . ': Sample page for unit test.';
 
-               // Avoid memory leak...?
-               // LinkCache::singleton()->clear();
-               // Maybe.  But doing this absolutely breaks $title->isRedirect() when called during unit tests....
-
                $page = WikiPage::factory( $title );
                $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
 
@@ -690,6 +687,52 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                self::$dbSetup = false;
        }
 
+       /**
+        * Setups a database with 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 DatabaseBase $db Database to use
+        * @param string $prefix Prefix to use for test tables
+        * @return bool True if tables were cloned, false if only the prefix was changed
+        */
+       protected static function setupDatabaseWithTestPrefix( DatabaseBase $db, $prefix ) {
+               $tablesCloned = self::listTables( $db );
+               $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
+               $dbClone->useTemporaryTables( self::$useTemporaryTables );
+
+               if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
+                       CloneDatabase::changePrefix( $prefix );
+
+                       return false;
+               } else {
+                       $dbClone->cloneTableStructure();
+                       return true;
+               }
+       }
+
+       /**
+        * Set up all test DBs
+        */
+       public function setupAllTestDBs() {
+               global $wgDBprefix;
+
+               self::$oldTablePrefix = $wgDBprefix;
+
+               $testPrefix = $this->dbPrefix();
+
+               // switch to a temporary clone of the database
+               self::setupTestDB( $this->db, $testPrefix );
+
+               if ( self::isUsingExternalStoreDB() ) {
+                       self::setupExternalStoreTestDBs( $testPrefix );
+               }
+       }
+
        /**
         * Creates an empty skeleton of the wiki database by cloning its structure
         * to equivalent tables using the given $prefix. Then sets MediaWiki to
@@ -712,8 +755,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         * @throws MWException If the database table prefix is already $prefix
         */
        public static function setupTestDB( DatabaseBase $db, $prefix ) {
-               global $wgDBprefix;
-               if ( $wgDBprefix === $prefix ) {
+               if ( $db->tablePrefix() === $prefix ) {
                        throw new MWException(
                                'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
                }
@@ -722,42 +764,110 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                        return;
                }
 
-               $tablesCloned = self::listTables( $db );
-               $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
-               $dbClone->useTemporaryTables( self::$useTemporaryTables );
+               if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
+                       return;
+               }
+
+               // 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;' );
+               }
 
                self::$dbSetup = true;
-               self::$oldTablePrefix = $wgDBprefix;
+       }
 
-               if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
-                       CloneDatabase::changePrefix( $prefix );
+       /**
+        * Clones the External Store database(s) for testing
+        *
+        * @param string $testPrefix Prefix for test tables
+        */
+       protected static function setupExternalStoreTestDBs( $testPrefix ) {
+               $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 );
+               }
+       }
 
-                       return;
-               } else {
-                       $dbClone->cloneTableStructure();
+       /**
+        * Gets master database connections for all of the ExternalStoreDB
+        * stores configured in $wgDefaultExternalStore.
+        *
+        * @return array Array of DatabaseBase master connections
+        */
+
+       protected static function getExternalStoreDatabaseConnections() {
+               global $wgDefaultExternalStore;
+
+               $externalStoreDB = ExternalStore::getStoreObject( 'DB' );
+               $defaultArray = (array) $wgDefaultExternalStore;
+               $dbws = [];
+               foreach ( $defaultArray as $url ) {
+                       if ( strpos( $url, 'DB://' ) === 0 ) {
+                               list( $proto, $cluster ) = explode( '://', $url, 2 );
+                               $dbw = $externalStoreDB->getMaster( $cluster );
+                               $dbws[] = $dbw;
+                       }
                }
 
-               if ( $db->getType() == 'oracle' ) {
-                       $db->query( 'BEGIN FILL_WIKI_INFO; END;' );
+               return $dbws;
+       }
+
+       /**
+        * Check whether ExternalStoreDB is being used
+        *
+        * @return bool True if it's being used
+        */
+       protected static function isUsingExternalStoreDB() {
+               global $wgDefaultExternalStore;
+               if ( !$wgDefaultExternalStore ) {
+                       return false;
                }
+
+               $defaultArray = (array) $wgDefaultExternalStore;
+               foreach ( $defaultArray as $url ) {
+                       if ( strpos( $url, 'DB://' ) === 0 ) {
+                               return true;
+                       }
+               }
+
+               return false;
        }
 
        /**
         * Empty all tables so they can be repopulated for tests
+        *
+        * @param DatabaseBase $db|null Database to reset
+        * @param array $tablesUsed Tables to reset
         */
-       private function resetDB() {
-               if ( $this->db ) {
-                       $truncate = in_array( $this->db->getType(), [ 'oracle', 'mysql' ] );
-                       foreach ( $this->tablesUsed as $tbl ) {
+       private function resetDB( $db, $tablesUsed ) {
+               if ( $db ) {
+                       $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
+                       foreach ( $tablesUsed as $tbl ) {
                                // TODO: reset interwiki and user tables to their original content.
                                if ( $tbl == 'interwiki' || $tbl == 'user' ) {
                                        continue;
                                }
 
                                if ( $truncate ) {
-                                       $this->db->query( 'TRUNCATE TABLE ' . $this->db->tableName( $tbl ), __METHOD__ );
+                                       $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tbl ), __METHOD__ );
                                } else {
-                                       $this->db->delete( $tbl, '*', __METHOD__ );
+
+                                       $db->delete( $tbl, '*', __METHOD__ );
+                               }
+
+                               if ( $tbl === 'page' ) {
+                                       // Forget about the pages since they don't
+                                       // exist in the DB.
+                                       LinkCache::singleton()->clear();
                                }
                        }
                }
@@ -794,10 +904,8 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                $this->assertTrue( $value == '', $msg );
        }
 
-       private static function unprefixTable( $tableName ) {
-               global $wgDBprefix;
-
-               return substr( $tableName, strlen( $wgDBprefix ) );
+       private static function unprefixTable( &$tableName, $ind, $prefix ) {
+               $tableName = substr( $tableName, strlen( $prefix ) );
        }
 
        private static function isNotUnittest( $table ) {
@@ -812,16 +920,15 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         * @return array
         */
        public static function listTables( $db ) {
-               global $wgDBprefix;
-
-               $tables = $db->listTables( $wgDBprefix, __METHOD__ );
+               $prefix = $db->tablePrefix();
+               $tables = $db->listTables( $prefix, __METHOD__ );
 
                if ( $db->getType() === 'mysql' ) {
                        # bug 43571: cannot clone VIEWs under MySQL
-                       $views = $db->listViews( $wgDBprefix, __METHOD__ );
+                       $views = $db->listViews( $prefix, __METHOD__ );
                        $tables = array_diff( $tables, $views );
                }
-               $tables = array_map( [ __CLASS__, 'unprefixTable' ], $tables );
+               array_walk( $tables, [ __CLASS__, 'unprefixTable' ], $prefix );
 
                // Don't duplicate test tables from the previous fataled run
                $tables = array_filter( $tables, [ __CLASS__, 'isNotUnittest' ] );