wiki.techinc.nl/tests/phpunit/includes/Storage/RevisionStoreTest.php
Brad Jorsch 27c61fb1e9 Add actor table and code to start using it
Storing the user name or IP in every row in large tables like revision
and logging takes up space and makes operations on these tables slower.
This patch begins the process of moving those into one "actor" table
which other tables can reference with a single integer field.

A subsequent patch will remove the old columns.

Bug: T167246
Depends-On: I9293fd6e0f958d87e52965de925046f1bb8f8a50
Change-Id: I8d825eb02c69cc66d90bd41325133fd3f99f0226
2018-02-23 10:06:20 -08:00

691 lines
18 KiB
PHP

<?php
namespace MediaWiki\Tests\Storage;
use HashBagOStuff;
use Language;
use MediaWiki\MediaWikiServices;
use MediaWiki\Storage\RevisionAccessException;
use MediaWiki\Storage\RevisionStore;
use MediaWiki\Storage\SqlBlobStore;
use MediaWikiTestCase;
use Title;
use WANObjectCache;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\LoadBalancer;
class RevisionStoreTest extends MediaWikiTestCase {
/**
* @param LoadBalancer $loadBalancer
* @param SqlBlobStore $blobStore
* @param WANObjectCache $WANObjectCache
*
* @return RevisionStore
*/
private function getRevisionStore(
$loadBalancer = null,
$blobStore = null,
$WANObjectCache = null
) {
return new RevisionStore(
$loadBalancer ? $loadBalancer : $this->getMockLoadBalancer(),
$blobStore ? $blobStore : $this->getMockSqlBlobStore(),
$WANObjectCache ? $WANObjectCache : $this->getHashWANObjectCache(),
MediaWikiServices::getInstance()->getCommentStore(),
MediaWikiServices::getInstance()->getActorMigration()
);
}
/**
* @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
*/
private function getMockLoadBalancer() {
return $this->getMockBuilder( LoadBalancer::class )
->disableOriginalConstructor()->getMock();
}
/**
* @return \PHPUnit_Framework_MockObject_MockObject|Database
*/
private function getMockDatabase() {
return $this->getMockBuilder( Database::class )
->disableOriginalConstructor()->getMock();
}
/**
* @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
*/
private function getMockSqlBlobStore() {
return $this->getMockBuilder( SqlBlobStore::class )
->disableOriginalConstructor()->getMock();
}
private function getHashWANObjectCache() {
return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
}
/**
* @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
* @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
*/
public function testGetSetContentHandlerDb() {
$store = $this->getRevisionStore();
$this->assertTrue( $store->getContentHandlerUseDB() );
$store->setContentHandlerUseDB( false );
$this->assertFalse( $store->getContentHandlerUseDB() );
$store->setContentHandlerUseDB( true );
$this->assertTrue( $store->getContentHandlerUseDB() );
}
private function getDefaultQueryFields() {
return [
'rev_id',
'rev_page',
'rev_text_id',
'rev_timestamp',
'rev_minor_edit',
'rev_deleted',
'rev_len',
'rev_parent_id',
'rev_sha1',
];
}
private function getCommentQueryFields() {
return [
'rev_comment_text' => 'rev_comment',
'rev_comment_data' => 'NULL',
'rev_comment_cid' => 'NULL',
];
}
private function getActorQueryFields() {
return [
'rev_user' => 'rev_user',
'rev_user_text' => 'rev_user_text',
'rev_actor' => 'NULL',
];
}
private function getContentHandlerQueryFields() {
return [
'rev_content_format',
'rev_content_model',
];
}
public function provideGetQueryInfo() {
yield [
true,
[],
[
'tables' => [ 'revision' ],
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
$this->getActorQueryFields(),
$this->getContentHandlerQueryFields()
),
'joins' => [],
]
];
yield [
false,
[],
[
'tables' => [ 'revision' ],
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
$this->getActorQueryFields()
),
'joins' => [],
]
];
yield [
false,
[ 'page' ],
[
'tables' => [ 'revision', 'page' ],
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
$this->getActorQueryFields(),
[
'page_namespace',
'page_title',
'page_id',
'page_latest',
'page_is_redirect',
'page_len',
]
),
'joins' => [
'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
],
]
];
yield [
false,
[ 'user' ],
[
'tables' => [ 'revision', 'user' ],
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
$this->getActorQueryFields(),
[
'user_name',
]
),
'joins' => [
'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
],
]
];
yield [
false,
[ 'text' ],
[
'tables' => [ 'revision', 'text' ],
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
$this->getActorQueryFields(),
[
'old_text',
'old_flags',
]
),
'joins' => [
'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
],
]
];
yield [
true,
[ 'page', 'user', 'text' ],
[
'tables' => [ 'revision', 'page', 'user', 'text' ],
'fields' => array_merge(
$this->getDefaultQueryFields(),
$this->getCommentQueryFields(),
$this->getActorQueryFields(),
$this->getContentHandlerQueryFields(),
[
'page_namespace',
'page_title',
'page_id',
'page_latest',
'page_is_redirect',
'page_len',
'user_name',
'old_text',
'old_flags',
]
),
'joins' => [
'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
],
]
];
}
/**
* @dataProvider provideGetQueryInfo
* @covers \MediaWiki\Storage\RevisionStore::getQueryInfo
*/
public function testGetQueryInfo( $contentHandlerUseDb, $options, $expected ) {
$this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
$this->overrideMwServices();
$store = $this->getRevisionStore();
$store->setContentHandlerUseDB( $contentHandlerUseDb );
$this->assertEquals( $expected, $store->getQueryInfo( $options ) );
}
private function getDefaultArchiveFields() {
return [
'ar_id',
'ar_page_id',
'ar_namespace',
'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
'ar_timestamp',
'ar_minor_edit',
'ar_deleted',
'ar_len',
'ar_parent_id',
'ar_sha1',
];
}
/**
* @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
*/
public function testGetArchiveQueryInfo_contentHandlerDb() {
$this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
$this->overrideMwServices();
$store = $this->getRevisionStore();
$store->setContentHandlerUseDB( true );
$this->assertEquals(
[
'tables' => [
'archive'
],
'fields' => array_merge(
$this->getDefaultArchiveFields(),
[
'ar_comment_text' => 'ar_comment',
'ar_comment_data' => 'NULL',
'ar_comment_cid' => 'NULL',
'ar_user_text' => 'ar_user_text',
'ar_user' => 'ar_user',
'ar_actor' => 'NULL',
'ar_content_format',
'ar_content_model',
]
),
'joins' => [],
],
$store->getArchiveQueryInfo()
);
}
/**
* @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
*/
public function testGetArchiveQueryInfo_noContentHandlerDb() {
$this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
$this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
$this->overrideMwServices();
$store = $this->getRevisionStore();
$store->setContentHandlerUseDB( false );
$this->assertEquals(
[
'tables' => [
'archive'
],
'fields' => array_merge(
$this->getDefaultArchiveFields(),
[
'ar_comment_text' => 'ar_comment',
'ar_comment_data' => 'NULL',
'ar_comment_cid' => 'NULL',
'ar_user_text' => 'ar_user_text',
'ar_user' => 'ar_user',
'ar_actor' => 'NULL',
]
),
'joins' => [],
],
$store->getArchiveQueryInfo()
);
}
public function testGetTitle_successFromPageId() {
$mockLoadBalancer = $this->getMockLoadBalancer();
// Title calls wfGetDB() so we have to set the main service
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
$db = $this->getMockDatabase();
// Title calls wfGetDB() which uses a regular Connection
$mockLoadBalancer->expects( $this->atLeastOnce() )
->method( 'getConnection' )
->willReturn( $db );
// First call to Title::newFromID, faking no result (db lag?)
$db->expects( $this->at( 0 ) )
->method( 'selectRow' )
->with(
'page',
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( (object)[
'page_namespace' => '1',
'page_title' => 'Food',
] );
$store = $this->getRevisionStore( $mockLoadBalancer );
$title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
$this->assertSame( 1, $title->getNamespace() );
$this->assertSame( 'Food', $title->getDBkey() );
}
public function testGetTitle_successFromPageIdOnFallback() {
$mockLoadBalancer = $this->getMockLoadBalancer();
// Title calls wfGetDB() so we have to set the main service
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
$db = $this->getMockDatabase();
// Title calls wfGetDB() which uses a regular Connection
// Assert that the first call uses a REPLICA and the second falls back to master
$mockLoadBalancer->expects( $this->exactly( 2 ) )
->method( 'getConnection' )
->willReturn( $db );
// RevisionStore getTitle uses a ConnectionRef
$mockLoadBalancer->expects( $this->atLeastOnce() )
->method( 'getConnectionRef' )
->willReturn( $db );
// First call to Title::newFromID, faking no result (db lag?)
$db->expects( $this->at( 0 ) )
->method( 'selectRow' )
->with(
'page',
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( false );
// First select using rev_id, faking no result (db lag?)
$db->expects( $this->at( 1 ) )
->method( 'selectRow' )
->with(
[ 'revision', 'page' ],
$this->anything(),
[ 'rev_id' => 2 ]
)
->willReturn( false );
// Second call to Title::newFromID, no result
$db->expects( $this->at( 2 ) )
->method( 'selectRow' )
->with(
'page',
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( (object)[
'page_namespace' => '2',
'page_title' => 'Foodey',
] );
$store = $this->getRevisionStore( $mockLoadBalancer );
$title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
$this->assertSame( 2, $title->getNamespace() );
$this->assertSame( 'Foodey', $title->getDBkey() );
}
public function testGetTitle_successFromRevId() {
$mockLoadBalancer = $this->getMockLoadBalancer();
// Title calls wfGetDB() so we have to set the main service
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
$db = $this->getMockDatabase();
// Title calls wfGetDB() which uses a regular Connection
$mockLoadBalancer->expects( $this->atLeastOnce() )
->method( 'getConnection' )
->willReturn( $db );
// RevisionStore getTitle uses a ConnectionRef
$mockLoadBalancer->expects( $this->atLeastOnce() )
->method( 'getConnectionRef' )
->willReturn( $db );
// First call to Title::newFromID, faking no result (db lag?)
$db->expects( $this->at( 0 ) )
->method( 'selectRow' )
->with(
'page',
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( false );
// First select using rev_id, faking no result (db lag?)
$db->expects( $this->at( 1 ) )
->method( 'selectRow' )
->with(
[ 'revision', 'page' ],
$this->anything(),
[ 'rev_id' => 2 ]
)
->willReturn( (object)[
'page_namespace' => '1',
'page_title' => 'Food2',
] );
$store = $this->getRevisionStore( $mockLoadBalancer );
$title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
$this->assertSame( 1, $title->getNamespace() );
$this->assertSame( 'Food2', $title->getDBkey() );
}
public function testGetTitle_successFromRevIdOnFallback() {
$mockLoadBalancer = $this->getMockLoadBalancer();
// Title calls wfGetDB() so we have to set the main service
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
$db = $this->getMockDatabase();
// Title calls wfGetDB() which uses a regular Connection
// Assert that the first call uses a REPLICA and the second falls back to master
$mockLoadBalancer->expects( $this->exactly( 2 ) )
->method( 'getConnection' )
->willReturn( $db );
// RevisionStore getTitle uses a ConnectionRef
$mockLoadBalancer->expects( $this->atLeastOnce() )
->method( 'getConnectionRef' )
->willReturn( $db );
// First call to Title::newFromID, faking no result (db lag?)
$db->expects( $this->at( 0 ) )
->method( 'selectRow' )
->with(
'page',
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( false );
// First select using rev_id, faking no result (db lag?)
$db->expects( $this->at( 1 ) )
->method( 'selectRow' )
->with(
[ 'revision', 'page' ],
$this->anything(),
[ 'rev_id' => 2 ]
)
->willReturn( false );
// Second call to Title::newFromID, no result
$db->expects( $this->at( 2 ) )
->method( 'selectRow' )
->with(
'page',
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( false );
// Second select using rev_id, result
$db->expects( $this->at( 3 ) )
->method( 'selectRow' )
->with(
[ 'revision', 'page' ],
$this->anything(),
[ 'rev_id' => 2 ]
)
->willReturn( (object)[
'page_namespace' => '2',
'page_title' => 'Foodey',
] );
$store = $this->getRevisionStore( $mockLoadBalancer );
$title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
$this->assertSame( 2, $title->getNamespace() );
$this->assertSame( 'Foodey', $title->getDBkey() );
}
/**
* @covers \MediaWiki\Storage\RevisionStore::getTitle
*/
public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
$mockLoadBalancer = $this->getMockLoadBalancer();
// Title calls wfGetDB() so we have to set the main service
$this->setService( 'DBLoadBalancer', $mockLoadBalancer );
$db = $this->getMockDatabase();
// Title calls wfGetDB() which uses a regular Connection
// Assert that the first call uses a REPLICA and the second falls back to master
// RevisionStore getTitle uses getConnectionRef
// Title::newFromID uses getConnection
foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
$mockLoadBalancer->expects( $this->exactly( 2 ) )
->method( $method )
->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
static $callCounter = 0;
$callCounter++;
// The first call should be to a REPLICA, and the second a MASTER.
if ( $callCounter === 1 ) {
$this->assertSame( DB_REPLICA, $masterOrReplica );
} elseif ( $callCounter === 2 ) {
$this->assertSame( DB_MASTER, $masterOrReplica );
}
return $db;
} );
}
// First and third call to Title::newFromID, faking no result
foreach ( [ 0, 2 ] as $counter ) {
$db->expects( $this->at( $counter ) )
->method( 'selectRow' )
->with(
'page',
$this->anything(),
[ 'page_id' => 1 ]
)
->willReturn( false );
}
foreach ( [ 1, 3 ] as $counter ) {
$db->expects( $this->at( $counter ) )
->method( 'selectRow' )
->with(
[ 'revision', 'page' ],
$this->anything(),
[ 'rev_id' => 2 ]
)
->willReturn( false );
}
$store = $this->getRevisionStore( $mockLoadBalancer );
$this->setExpectedException( RevisionAccessException::class );
$store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
}
public function provideNewRevisionFromRow_legacyEncoding_applied() {
yield 'windows-1252, old_flags is empty' => [
'windows-1252',
'en',
[
'old_flags' => '',
'old_text' => "S\xF6me Content",
],
'Söme Content'
];
yield 'windows-1252, old_flags is null' => [
'windows-1252',
'en',
[
'old_flags' => null,
'old_text' => "S\xF6me Content",
],
'Söme Content'
];
}
/**
* @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
*
* @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
* @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
*/
public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
$cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
$blobStore = new SqlBlobStore( wfGetLB(), $cache );
$blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
$store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache );
$record = $store->newRevisionFromRow(
$this->makeRow( $row ),
0,
Title::newFromText( __METHOD__ . '-UTPage' )
);
$this->assertSame( $text, $record->getContent( 'main' )->serialize() );
}
/**
* @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
* @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
*/
public function testNewRevisionFromRow_legacyEncoding_ignored() {
$row = [
'old_flags' => 'utf-8',
'old_text' => 'Söme Content',
];
$cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
$blobStore = new SqlBlobStore( wfGetLB(), $cache );
$blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
$store = $this->getRevisionStore( wfGetLB(), $blobStore, $cache );
$record = $store->newRevisionFromRow(
$this->makeRow( $row ),
0,
Title::newFromText( __METHOD__ . '-UTPage' )
);
$this->assertSame( 'Söme Content', $record->getContent( 'main' )->serialize() );
}
private function makeRow( array $array ) {
$row = $array + [
'rev_id' => 7,
'rev_page' => 5,
'rev_text_id' => 11,
'rev_timestamp' => '20110101000000',
'rev_user_text' => 'Tester',
'rev_user' => 17,
'rev_minor_edit' => 0,
'rev_deleted' => 0,
'rev_len' => 100,
'rev_parent_id' => 0,
'rev_sha1' => 'deadbeef',
'rev_comment_text' => 'Testing',
'rev_comment_data' => '{}',
'rev_comment_cid' => 111,
'rev_content_format' => CONTENT_FORMAT_TEXT,
'rev_content_model' => CONTENT_MODEL_TEXT,
'page_namespace' => 0,
'page_title' => 'TEST',
'page_id' => 5,
'page_latest' => 7,
'page_is_redirect' => 0,
'page_len' => 100,
'user_name' => 'Tester',
'old_is' => 13,
'old_text' => 'Hello World',
'old_flags' => 'utf-8',
];
return (object)$row;
}
}