Daniel Kinzler has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/391866 )
Change subject: Introduce DB schema overrides for unit tests. ...................................................................... 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. Bug: T180705 Change-Id: I7a4071072d802a82ecf7d16fbf8882ff8c79287f --- M includes/db/CloneDatabase.php M includes/libs/rdbms/database/Database.php M tests/phpunit/MediaWikiTestCase.php A tests/phpunit/tests/MediaWikiTestCaseSchema1Test.php A tests/phpunit/tests/MediaWikiTestCaseSchema2Test.php A tests/phpunit/tests/MediaWikiTestCaseSchemaTest.sql M tests/phpunit/tests/MediaWikiTestCaseTest.php 7 files changed, 238 insertions(+), 4 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/66/391866/1 diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index 3d22c03..98275db 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -53,12 +53,12 @@ * @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 e04566e..6ad5959 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -3390,7 +3390,15 @@ 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 === false ) { + $cmd = $callbackResult; + } + } + + if ( $cmd ) { $res = $this->query( $cmd, $fname ); if ( $resultCallback ) { diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index f04eec7..10b2a38 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -5,8 +5,10 @@ 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; /** @@ -406,6 +408,7 @@ // 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 @@ $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 ); @@ -1296,6 +1301,134 @@ } /** + * Fail with a LogicException if the given database connection is not a set up to use + * mock tables. + */ + private function ensureMockDatabaseConnection( IDatabase $db ) { + if ( $db->tablePrefix() !== self::DB_PREFIX ) { + 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 inormation 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 origina 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 * * @param Database $db|null Database to reset @@ -1386,7 +1519,7 @@ } 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 0000000..4b0e0bf --- /dev/null +++ b/tests/phpunit/tests/MediaWikiTestCaseSchema1Test.php @@ -0,0 +1,51 @@ +<?php + +/** + * @covers MediaWikiTestCase + * + * @group Database + * @group MediaWikiTestCaseTest + */ +class MediaWikiTestCaseSchema1Test extends MediaWikiTestCase { + + public function getSchemaOverrides() { + return [ + [ 'imagelinks', 'MediaWikiTestCaseTestTable' ], + [ __DIR__ . '/MediaWikiTestCaseSchemaTest.sql' ] + ]; + } + + public function testSchemaExtension() { + // make sure we can use the MediaWikiTestCaseTestTable table + + $input = [ 'id' => '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 0000000..bd740bf --- /dev/null +++ b/tests/phpunit/tests/MediaWikiTestCaseSchema2Test.php @@ -0,0 +1,26 @@ +<?php + +/** + * @covers MediaWikiTestCase + * + * @group Database + * @group MediaWikiTestCaseTest + * + * This test is intended to be executed AFTER MediaWikiTestCaseSchema1Test to ensure + * that any schema modifications have been cleaned up between test cases. + */ +class MediaWikiTestCaseSchema2Test extends MediaWikiTestCase { + + public function testSchemaExtension() { + // Make sure MediaWikiTestCaseTestTable created by MediaWikiTestCaseSchema1Test + // was dropped before executing MediaWikiTestCaseSchema2Test. + $this->assertFalse( $this->db->tableExists( 'MediaWikiTestCaseTestTable' ) ); + } + + public function testSchemaOverride() { + // Make sure imagelinks modified by MediaWikiTestCaseSchema1Test + // was restored to the original schema before executing MediaWikiTestCaseSchema2Test. + $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 0000000..bd8a3bd --- /dev/null +++ b/tests/phpunit/tests/MediaWikiTestCaseSchemaTest.sql @@ -0,0 +1,13 @@ +CREATE TABLE /*_*/MediaWikiTestCaseTestTable ( + id INT AUTO_INCREMENT 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 7d75ffe..fb2957b 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 { -- To view, visit https://gerrit.wikimedia.org/r/391866 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I7a4071072d802a82ecf7d16fbf8882ff8c79287f Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: Daniel Kinzler <daniel.kinz...@wikimedia.de> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits