From: daniel Date: Thu, 16 Nov 2017 16:48:25 +0000 (+0100) Subject: Introduce DB schema overrides for unit tests. X-Git-Tag: 1.31.0-rc.0~985^2 X-Git-Url: http://git.cyclocoop.org/?a=commitdiff_plain;h=047151c6924de280b736de64e76faacd6bb7d539;p=lhc%2Fweb%2Fwiklou.git Introduce DB schema overrides for unit tests. This introduces MediaWikiTestCase::getSchemaOverrides, which can be overwritten to return information about which tables are going to be altered, and which SQL files should be used to set up the target schema. This allows tests for a class that interacts with the database can have a subclass for each supported database schema. NOTE: this has only been tested with MySQL. Bug: T180705 Change-Id: I7a4071072d802a82ecf7d16fbf8882ff8c79287f --- diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index 16d10d1d54..edb54aee89 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -50,12 +50,12 @@ class CloneDatabase { * @param bool $dropCurrentTables */ public function __construct( IMaintainableDatabase $db, array $tablesToClone, - $newTablePrefix, $oldTablePrefix = '', $dropCurrentTables = true + $newTablePrefix, $oldTablePrefix = null, $dropCurrentTables = true ) { $this->db = $db; $this->tablesToClone = $tablesToClone; $this->newTablePrefix = $newTablePrefix; - $this->oldTablePrefix = $oldTablePrefix ? $oldTablePrefix : $this->db->tablePrefix(); + $this->oldTablePrefix = $oldTablePrefix !== null ? $oldTablePrefix : $this->db->tablePrefix(); $this->dropCurrentTables = $dropCurrentTables; } diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index 30c9cdd744..0ee10edcbb 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -3424,7 +3424,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware if ( $done || feof( $fp ) ) { $cmd = $this->replaceVars( $cmd ); - if ( !$inputCallback || call_user_func( $inputCallback, $cmd ) ) { + if ( $inputCallback ) { + $callbackResult = call_user_func( $inputCallback, $cmd ); + + if ( is_string( $callbackResult ) || !$callbackResult ) { + $cmd = $callbackResult; + } + } + + if ( $cmd ) { $res = $this->query( $cmd, $fname ); if ( $resultCallback ) { diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index d5428268b5..10f5d41fb3 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -5,8 +5,10 @@ use MediaWiki\Logger\LoggerFactory; use MediaWiki\Logger\MonologSpi; use MediaWiki\MediaWikiServices; use Psr\Log\LoggerInterface; +use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\IMaintainableDatabase; use Wikimedia\Rdbms\Database; +use Wikimedia\Rdbms\LBFactory; use Wikimedia\TestingAccessWrapper; /** @@ -408,6 +410,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { // 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->setUpSchema( $this->db ); $this->addDBDataOnce(); } @@ -1152,6 +1155,8 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix ); $dbClone->useTemporaryTables( self::$useTemporaryTables ); + $db->_originalTablePrefix = $db->tablePrefix(); + if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) { CloneDatabase::changePrefix( $prefix ); @@ -1295,6 +1300,133 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { return false; } + /** + * @throws LogicException if the given database connection is not a set up to use + * mock tables. + */ + private 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.' + ); + } + } + + /** + * Stub. If a test suite needs to test against a specific database schema, it should + * override this method and return the appropriate information from it. + * + * @return [ $tables, $scripts ] A tuple of two lists, with $tables being a list of tables + * that will be re-created by the scripts, and $scripts being a list of SQL script + * files for creating the tables listed. + */ + protected function getSchemaOverrides() { + return [ [], [] ]; + } + + /** + * Applies any schema changes requested by calling setDbSchema(). + * Called once per test class, just before addDataOnce(). + */ + private function setUpSchema( IMaintainableDatabase $db ) { + list( $tablesToAlter, $scriptsToRun ) = $this->getSchemaOverrides(); + + if ( $tablesToAlter && !$scriptsToRun ) { + throw new InvalidArgumentException( + 'No scripts supplied for applying the database schema.' + ); + } + + if ( !$tablesToAlter && $scriptsToRun ) { + throw new InvalidArgumentException( + 'No tables declared to be altered by schema scripts.' + ); + } + + $this->ensureMockDatabaseConnection( $db ); + + $previouslyAlteredTables = isset( $db->_alteredMockTables ) ? $db->_alteredMockTables : []; + + if ( !$tablesToAlter && !$previouslyAlteredTables ) { + return; // nothing to do + } + + $tablesToDrop = array_merge( $previouslyAlteredTables, $tablesToAlter ); + $tablesToRestore = array_diff( $previouslyAlteredTables, $tablesToAlter ); + + if ( $tablesToDrop ) { + $this->dropMockTables( $db, $tablesToDrop ); + } + + if ( $tablesToRestore ) { + $this->recloneMockTables( $db, $tablesToRestore ); + } + + foreach ( $scriptsToRun as $script ) { + $db->sourceFile( + $script, + null, + null, + __METHOD__, + function ( $cmd ) { + return $this->mungeSchemaUpdateQuery( $cmd ); + } + ); + } + + $db->_alteredMockTables = $tablesToAlter; + } + + private function mungeSchemaUpdateQuery( $cmd ) { + return self::$useTemporaryTables + ? preg_replace( '/\bCREATE\s+TABLE\b/i', 'CREATE TEMPORARY TABLE', $cmd ) + : $cmd; + } + + /** + * Drops the given mock tables. + * + * @param IMaintainableDatabase $db + * @param array $tables + */ + private function dropMockTables( IMaintainableDatabase $db, array $tables ) { + $this->ensureMockDatabaseConnection( $db ); + + foreach ( $tables as $tbl ) { + $tmp = self::$useTemporaryTables ? ' TEMPORARY ' : ''; + $tbl = $db->tableName( $tbl ); + $db->query( "DROP $tmp TABLE IF EXISTS $tbl", __METHOD__ ); + + if ( $tbl === 'page' ) { + // Forget about the pages since they don't + // exist in the DB. + LinkCache::singleton()->clear(); + } + } + } + + /** + * Re-clones the given mock tables to restore them based on the live database schema. + * + * @param IMaintainableDatabase $db + * @param array $tables + */ + private function recloneMockTables( IMaintainableDatabase $db, array $tables ) { + $this->ensureMockDatabaseConnection( $db ); + + if ( !isset( $db->_originalTablePrefix ) ) { + throw new LogicException( 'No original table prefix know, cannot restore tables!' ); + } + + $originalTables = $db->listTables( $db->_originalTablePrefix, __METHOD__ ); + $tables = array_intersect( $tables, $originalTables ); + + $dbClone = new CloneDatabase( $db, $tables, $db->tablePrefix(), $db->_originalTablePrefix ); + $dbClone->useTemporaryTables( self::$useTemporaryTables ); + + $dbClone->cloneTableStructure(); + } + /** * Empty all tables so they can be repopulated for tests * @@ -1386,7 +1518,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { } private static function isNotUnittest( $table ) { - return strpos( $table, 'unittest_' ) !== 0; + return strpos( $table, self::DB_PREFIX ) !== 0; } /** diff --git a/tests/phpunit/tests/MediaWikiTestCaseSchema1Test.php b/tests/phpunit/tests/MediaWikiTestCaseSchema1Test.php new file mode 100644 index 0000000000..4b0e0bf6d1 --- /dev/null +++ b/tests/phpunit/tests/MediaWikiTestCaseSchema1Test.php @@ -0,0 +1,51 @@ + '5', 'name' => 'Test' ]; + + $this->db->insert( + 'MediaWikiTestCaseTestTable', + $input + ); + + $output = $this->db->selectRow( 'MediaWikiTestCaseTestTable', array_keys( $input ), [] ); + $this->assertEquals( (object)$input, $output ); + } + + public function testSchemaOverride() { + // make sure we can use the il_frobniz field + + $input = [ + 'il_from' => '7', + 'il_from_namespace' => '0', + 'il_to' => 'Foo.jpg', + 'il_frobniz' => 'Xyzzy', + ]; + + $this->db->insert( + 'imagelinks', + $input + ); + + $output = $this->db->selectRow( 'imagelinks', array_keys( $input ), [] ); + $this->assertEquals( (object)$input, $output ); + } + +} diff --git a/tests/phpunit/tests/MediaWikiTestCaseSchema2Test.php b/tests/phpunit/tests/MediaWikiTestCaseSchema2Test.php new file mode 100644 index 0000000000..b1c65ee778 --- /dev/null +++ b/tests/phpunit/tests/MediaWikiTestCaseSchema2Test.php @@ -0,0 +1,29 @@ +assertFalse( $this->db->tableExists( 'MediaWikiTestCaseTestTable' ) ); + } + + public function testSchemaOverride() { + // Make sure imagelinks modified by MediaWikiTestCaseSchema1Test + // was restored to the original schema before executing MediaWikiTestCaseSchema2Test. + $this->assertTrue( $this->db->tableExists( 'imagelinks' ) ); + $this->assertFalse( $this->db->fieldExists( 'imagelinks', 'il_frobniz' ) ); + } + +} diff --git a/tests/phpunit/tests/MediaWikiTestCaseSchemaTest.sql b/tests/phpunit/tests/MediaWikiTestCaseSchemaTest.sql new file mode 100644 index 0000000000..e5ef5c62cc --- /dev/null +++ b/tests/phpunit/tests/MediaWikiTestCaseSchemaTest.sql @@ -0,0 +1,13 @@ +CREATE TABLE /*_*/MediaWikiTestCaseTestTable ( + id INT NOT NULL, + name VARCHAR(20) NOT NULL, + PRIMARY KEY (id) +) /*$wgDBTableOptions*/; + +CREATE TABLE /*_*/imagelinks ( + il_from int(10) unsigned NOT NULL DEFAULT 0, + il_from_namespace int(11) NOT NULL DEFAULT 0, + il_to varbinary(255) NOT NULL DEFAULT '', + il_frobniz varchar(255) NOT NULL DEFAULT 'FROB', + PRIMARY KEY (il_from,il_to) +) /*$wgDBTableOptions*/; diff --git a/tests/phpunit/tests/MediaWikiTestCaseTest.php b/tests/phpunit/tests/MediaWikiTestCaseTest.php index 7d75ffe648..fb2957be83 100644 --- a/tests/phpunit/tests/MediaWikiTestCaseTest.php +++ b/tests/phpunit/tests/MediaWikiTestCaseTest.php @@ -2,9 +2,12 @@ use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; use Psr\Log\LoggerInterface; +use Wikimedia\Rdbms\LoadBalancer; /** * @covers MediaWikiTestCase + * @group MediaWikiTestCaseTest + * * @author Addshore */ class MediaWikiTestCaseTest extends MediaWikiTestCase {