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;
/**
*/
abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
+ use MediaWikiCoversValidator;
+
/**
* The service locator created by prepareServices(). This service locator will
* be restored after each test. Tests that pollute the global service locator
// 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();
}
// XXX: reset maintenance triggers
// Hook into period lag checks which often happen in long-running scripts
- $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
- Maintenance::setLBFactoryTriggers( $lbFactory );
+ $services = MediaWikiServices::getInstance();
+ $lbFactory = $services->getDBLoadBalancerFactory();
+ Maintenance::setLBFactoryTriggers( $lbFactory, $services->getMainConfig() );
ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' );
}
* @since 1.18
*/
public function needsDB() {
- # if the test says it uses database tables, it needs the database
+ // If the test says it uses database tables, it needs the database
if ( $this->tablesUsed ) {
return true;
}
- # if the test says it belongs to the Database group, it needs the database
+ // If the test class says it belongs to the Database group, it needs the database.
+ // NOTE: This ONLY checks for the group in the class level doc comment.
$rc = new ReflectionClass( $this );
if ( preg_match( '/@group +Database/im', $rc->getDocComment() ) ) {
return true;
$user = static::getTestSysop()->getUser();
$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 );
$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 );
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 ) {
+ $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.
+ 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
*
}
private static function isNotUnittest( $table ) {
- return strpos( $table, 'unittest_' ) !== 0;
+ return strpos( $table, self::DB_PREFIX ) !== 0;
}
/**
* @param string|array $fields The columns to include in the result (and to sort by)
* @param string|array $condition "where" condition(s)
* @param array $expectedRows An array of arrays giving the expected rows.
+ * @param array $options Options for the query
+ * @param array $join_conds Join conditions for the query
*
* @throws MWException If this test cases's needsDB() method doesn't return true.
* Test cases can use "@group Database" to enable database test support,
* or list the tables under testing in $this->tablesUsed, or override the
* needsDB() method.
*/
- protected function assertSelect( $table, $fields, $condition, array $expectedRows ) {
+ protected function assertSelect(
+ $table, $fields, $condition, array $expectedRows, array $options = [], array $join_conds = []
+ ) {
if ( !$this->needsDB() ) {
throw new MWException( 'When testing database state, the test cases\'s needDB()' .
' method should return true. Use @group Database or $this->tablesUsed.' );
$db = wfGetDB( DB_REPLICA );
- $res = $db->select( $table, $fields, $condition, wfGetCaller(), [ 'ORDER BY' => $fields ] );
+ $res = $db->select(
+ $table,
+ $fields,
+ $condition,
+ wfGetCaller(),
+ $options + [ 'ORDER BY' => $fields ],
+ $join_conds
+ );
$this->assertNotEmpty( $res, "query failed: " . $db->lastError() );
$i = 0;