MCR: Deprecate and gut Revision class

This is a re-submission of I4f24e7fbb68.

As a first major step towards Multi-Content-Revisions (MCR),
this patch turns the Revision class into a legacy proxy for
the new RevisionRecord and RevisionStore classes.

Backwards compatibility is maintained for all but some
rare edge cases, like constructing a completely empty
Revision object.

For more information on MCR, see
<https://www.mediawiki.org/wiki/Requests_for_comment/Multi-Content_Revisions>.

NOTE: once this is merged, verify create/delete/restore cycle on beta,
      ideally with emulated replication lag.

Bug: T174025
Change-Id: Ia4c20a91e98df0b9b14b138eb4825c55e5200384
This commit is contained in:
daniel 2017-08-31 20:41:04 +02:00 committed by addshore
parent 13df3d2290
commit 6af796f3e0
13 changed files with 717 additions and 1420 deletions

View file

@ -71,6 +71,10 @@ changes to languages because of Phabricator reports.
* (T180052) Mirandese (mwl) now supports gendered NS_USER/NS_USER_TALK namespaces.
=== Other changes in 1.31 ===
* Introducing multi-content-revision capability into the storage layer. For details,
see <https://www.mediawiki.org/wiki/Requests_for_comment/Multi-Content_Revisions>.
* The Revision class was deprecated in favor of RevisionStore, BlobStore, and
RevisionRecord and its subclasses.
* MessageBlobStore::insertMessageBlob() (deprecated in 1.27) was removed.
* The global function wfBCP47 was renamed to LanguageCode::bcp47.
* The global function wfBCP47 is now deprecated.
@ -123,6 +127,9 @@ changes to languages because of Phabricator reports.
* The Block class will no longer accept usable-but-missing usernames for
'byText' or ->setBlocker(). Callers should either ensure the blocker exists
locally or use a new interwiki-format username like "iw>Example".
* The RevisionInsertComplete hook is now deprecated, use RevisionRecordInserted instead.
RevisionInsertComplete is still called, but the second and third parameter will always be null.
Hard deprecation is scheduled for 1.32.
* The following methods that get and set ParserOutput state are deprecated.
Callers should use the new stateless $options parameter to
ParserOutput::getText() instead.

View file

@ -2810,14 +2810,14 @@ called after the addition of 'qunit' and MediaWiki testing resources.
added to any module.
&$ResourceLoader: object
'RevisionInsertComplete': Called after a revision is inserted into the database.
&$revision: the Revision
$data: the data stored in old_text. The meaning depends on $flags: if external
is set, it's the URL of the revision text in external storage; otherwise,
it's the revision text itself. In either case, if gzip is set, the revision
text is gzipped.
$flags: a comma-delimited list of strings representing the options used. May
include: utf8 (this will always be set for new revisions); gzip; external.
'RevisionRecordInserted': Called after a revision is inserted into the database.
$revisionRecord: the RevisionRecord that has just been inserted.
'RevisionInsertComplete': DEPRECATED! Use RevisionRecordInserted hook instead.
Called after a revision is inserted into the database.
$revision: the Revision
$data: DEPRECATED! Always null!
$flags: DEPRECATED! Always null!
'SearchableNamespaces': An option to modify which namespaces are searchable.
&$arr: Array of namespaces ($nsId => $name) which will be used.

File diff suppressed because it is too large Load diff

View file

@ -450,6 +450,46 @@ return [
return $factory;
},
'RevisionStore' => function ( MediaWikiServices $services ) {
/** @var SqlBlobStore $blobStore */
$blobStore = $services->getService( '_SqlBlobStore' );
$store = new RevisionStore(
$services->getDBLoadBalancer(),
$blobStore,
$services->getMainWANObjectCache()
);
$config = $services->getMainConfig();
$store->setContentHandlerUseDB( $config->get( 'ContentHandlerUseDB' ) );
return $store;
},
'BlobStore' => function ( MediaWikiServices $services ) {
return $services->getService( '_SqlBlobStore' );
},
'_SqlBlobStore' => function ( MediaWikiServices $services ) {
global $wgContLang; // TODO: manage $wgContLang as a service
$store = new SqlBlobStore(
$services->getDBLoadBalancer(),
$services->getMainWANObjectCache()
);
$config = $services->getMainConfig();
$store->setCompressRevisions( $config->get( 'CompressRevisions' ) );
$store->setCacheExpiry( $config->get( 'RevisionCacheExpiry' ) );
$store->setUseExternalStore( $config->get( 'DefaultExternalStore' ) !== false );
if ( $config->get( 'LegacyEncoding' ) ) {
$store->setLegacyEncoding( $config->get( 'LegacyEncoding' ), $wgContLang );
}
return $store;
},
'ExternalStoreFactory' => function ( MediaWikiServices $services ) {
$config = $services->getMainConfig();

View file

@ -335,8 +335,8 @@ class HistoryAction extends FormlessAction {
* @return FeedItem
*/
function feedItem( $row ) {
$rev = new Revision( $row );
$rev->setTitle( $this->getTitle() );
$rev = new Revision( $row, 0, $this->getTitle() );
$text = FeedUtils::formatDiffRow(
$this->getTitle(),
$this->getTitle()->getPreviousRevisionID( $rev->getId() ),
@ -639,12 +639,10 @@ class HistoryPager extends ReverseChronologicalPager {
*/
function historyLine( $row, $next, $notificationtimestamp = false,
$latest = false, $firstInList = false ) {
$rev = new Revision( $row );
$rev->setTitle( $this->getTitle() );
$rev = new Revision( $row, 0, $this->getTitle() );
if ( is_object( $next ) ) {
$prevRev = new Revision( $next );
$prevRev->setTitle( $this->getTitle() );
$prevRev = new Revision( $next, 0, $this->getTitle() );
} else {
$prevRev = null;
}

View file

@ -1048,8 +1048,7 @@ class MessageCache {
if ( $titleObj->getLatestRevID() ) {
$revision = Revision::newKnownCurrent(
$dbr,
$titleObj->getArticleID(),
$titleObj->getLatestRevID()
$titleObj
);
} else {
$revision = false;

View file

@ -23,6 +23,7 @@
use MediaWiki\Edit\PreparedEdit;
use \MediaWiki\Logger\LoggerFactory;
use \MediaWiki\MediaWikiServices;
use Wikimedia\Assert\Assert;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\DBError;
@ -671,7 +672,7 @@ class WikiPage implements Page, IDBAccessObject {
$revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
} else {
$dbr = wfGetDB( DB_REPLICA );
$revision = Revision::newKnownCurrent( $dbr, $this->getId(), $latest );
$revision = Revision::newKnownCurrent( $dbr, $this->getTitle(), $latest );
}
if ( $revision ) { // sanity
@ -1264,8 +1265,11 @@ class WikiPage implements Page, IDBAccessObject {
$conditions['page_latest'] = $lastRevision;
}
$revId = $revision->getId();
Assert::parameter( $revId > 0, '$revision->getId()', 'must be > 0' );
$row = [ /* SET */
'page_latest' => $revision->getId(),
'page_latest' => $revId,
'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
'page_is_redirect' => $rt !== null ? 1 : 0,

View file

@ -3498,13 +3498,7 @@ class Parser {
* @return Revision|bool False if missing
*/
public static function statelessFetchRevision( Title $title, $parser = false ) {
$pageId = $title->getArticleID();
$revId = $title->getLatestRevID();
$rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $pageId, $revId );
if ( $rev ) {
$rev->setTitle( $title );
}
$rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
return $rev;
}

View file

@ -183,12 +183,10 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
* @return Content|null
*/
protected function getContentObj( Title $title ) {
$revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title->getArticleID(),
$title->getLatestRevID() );
$revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
if ( !$revision ) {
return null;
}
$revision->setTitle( $title );
$content = $revision->getContent( Revision::RAW );
if ( !$content ) {
wfDebugLog( 'resourceloader', __METHOD__ . ': failed to load content of JS/CSS page!' );

View file

@ -290,15 +290,16 @@ class SpecialNewpages extends IncludableSpecialPage {
/**
* @param stdClass $result Result row from recent changes
* @return Revision|bool
* @param Title $title
* @return bool|Revision
*/
protected function revisionFromRcResult( stdClass $result ) {
protected function revisionFromRcResult( stdClass $result, Title $title ) {
return new Revision( [
'comment' => CommentStore::newKey( 'rc_comment' )->getComment( $result )->text,
'deleted' => $result->rc_deleted,
'user_text' => $result->rc_user_text,
'user' => $result->rc_user,
] );
], 0, $title );
}
/**
@ -313,8 +314,7 @@ class SpecialNewpages extends IncludableSpecialPage {
// Revision deletion works on revisions,
// so cast our recent change row to a revision row.
$rev = $this->revisionFromRcResult( $result );
$rev->setTitle( $title );
$rev = $this->revisionFromRcResult( $result, $title );
$classes = [];
$attribs = [ 'data-mw-revid' => $result->rev_id ];

View file

@ -7,6 +7,9 @@ use MediaWiki\Services\DestructibleService;
use MediaWiki\Services\SalvageableService;
use MediaWiki\Services\ServiceDisabledException;
use MediaWiki\Shell\CommandFactory;
use MediaWiki\Storage\BlobStore;
use MediaWiki\Storage\RevisionStore;
use MediaWiki\Storage\SqlBlobStore;
/**
* @covers MediaWiki\MediaWikiServices
@ -331,6 +334,9 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
'LocalServerObjectCache' => [ 'LocalServerObjectCache', BagOStuff::class ],
'VirtualRESTServiceClient' => [ 'VirtualRESTServiceClient', VirtualRESTServiceClient::class ],
'ShellCommandFactory' => [ 'ShellCommandFactory', CommandFactory::class ],
'BlobStore' => [ 'BlobStore', BlobStore::class ],
'_SqlBlobStore' => [ '_SqlBlobStore', SqlBlobStore::class ],
'RevisionStore' => [ 'RevisionStore', RevisionStore::class ],
];
}

View file

@ -1,4 +1,8 @@
<?php
use MediaWiki\MediaWikiServices;
use MediaWiki\Storage\RevisionStore;
use MediaWiki\Storage\IncompleteRevisionException;
use MediaWiki\Storage\RevisionRecord;
/**
* RevisionDbTestBase contains test cases for the Revision class that have Database interactions.
@ -72,6 +76,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
MWNamespace::clearCaches();
// Reset namespace cache
$wgContLang->resetNamespaces();
if ( !$this->testPage ) {
/**
* We have to create a new page for each subclass as the page creation may result
@ -102,6 +107,14 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
$props['text'] = 'Lorem Ipsum';
}
if ( !isset( $props['user_text'] ) ) {
$props['user_text'] = 'Tester';
}
if ( !isset( $props['user'] ) ) {
$props['user'] = 0;
}
if ( !isset( $props['comment'] ) ) {
$props['comment'] = 'just a test';
}
@ -110,6 +123,10 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
$props['page'] = $this->testPage->getId();
}
if ( !isset( $props['content_model'] ) ) {
$props['content_model'] = CONTENT_MODEL_WIKITEXT;
}
$rev = new Revision( $props );
$dbw = wfGetDB( DB_MASTER );
@ -202,14 +219,23 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
$revId = $rev->insertOn( wfGetDB( DB_MASTER ) );
$this->assertInternalType( 'integer', $revId );
$this->assertInternalType( 'integer', $rev->getTextId() );
$this->assertSame( $revId, $rev->getId() );
// getTextId() must be an int!
$this->assertInternalType( 'integer', $rev->getTextId() );
$mainSlot = $rev->getRevisionRecord()->getSlot( 'main', RevisionRecord::RAW );
// we currently only support storage in the text table
$textId = MediaWikiServices::getInstance()
->getBlobStore()
->getTextIdFromAddress( $mainSlot->getAddress() );
$this->assertSelect(
'text',
[ 'old_id', 'old_text' ],
"old_id = {$rev->getTextId()}",
[ [ strval( $rev->getTextId() ), 'Revision Text' ] ]
"old_id = $textId",
[ [ strval( $textId ), 'Revision Text' ] ]
);
$this->assertSelect(
'revision',
@ -228,7 +254,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
[ [
strval( $rev->getId() ),
strval( $this->testPage->getId() ),
strval( $rev->getTextId() ),
strval( $textId ),
'0',
'0',
'0',
@ -246,11 +272,12 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
// If an ExternalStore is set don't use it.
$this->setMwGlobals( 'wgDefaultExternalStore', false );
$this->setExpectedException(
MWException::class,
"Cannot insert revision: page ID must be nonzero"
IncompleteRevisionException::class,
"rev_page field must not be 0!"
);
$rev = new Revision( [] );
$title = Title::newFromText( 'Nonexistant-' . __METHOD__ );
$rev = new Revision( [], 0, $title );
$rev->insertOn( wfGetDB( DB_MASTER ) );
}
@ -321,12 +348,42 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
return $f + [ 'ar_namespace', 'ar_title' ];
},
];
yield [
function ( $f ) {
unset( $f['ar_text'] );
return $f;
},
];
yield [
function ( $f ) {
unset( $f['ar_text_id'] );
return $f;
},
];
yield [
function ( $f ) {
unset( $f['ar_page_id'] );
return $f;
},
];
yield [
function ( $f ) {
unset( $f['ar_parent_id'] );
return $f;
},
];
yield [
function ( $f ) {
unset( $f['ar_rev_id'] );
return $f;
},
];
yield [
function ( $f ) {
unset( $f['ar_sha1'] );
return $f;
},
];
}
/**
@ -334,6 +391,17 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
* @covers Revision::newFromArchiveRow
*/
public function testNewFromArchiveRow( $selectModifier ) {
$services = MediaWikiServices::getInstance();
$store = new RevisionStore(
$services->getDBLoadBalancer(),
$services->getService( '_SqlBlobStore' ),
$services->getMainWANObjectCache()
);
$store->setContentHandlerUseDB( $this->getContentHandlerUseDB() );
$this->setService( 'RevisionStore', $store );
$page = $this->createPage(
'RevisionStorageTest_testNewFromArchiveRow',
'Lorem Ipsum',
@ -354,6 +422,8 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
$row = $res->fetchObject();
$res->free();
// MCR migration note: $row is now required to contain ar_title and ar_namespace.
// Alternatively, a Title object can be passed to RevisionStore::newRevisionFromArchiveRow
$rev = Revision::newFromArchiveRow( $row );
$this->assertRevEquals( $orig, $rev );
@ -382,7 +452,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
$row = $res->fetchObject();
$res->free();
$rev = Revision::newFromArchiveRow( $row, [ 'comment' => 'SOMEOVERRIDE' ] );
$rev = Revision::newFromArchiveRow( $row, [ 'comment_text' => 'SOMEOVERRIDE' ] );
$this->assertNotEquals( $orig->getComment(), $rev->getComment() );
$this->assertEquals( 'SOMEOVERRIDE', $rev->getComment() );
@ -426,7 +496,8 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
* @covers Revision::newFromPageId
*/
public function testNewFromPageIdWithNotLatestId() {
$this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
$content = new WikitextContent( __METHOD__ );
$this->testPage->doEditContent( $content, __METHOD__ );
$rev = Revision::newFromPageId(
$this->testPage->getId(),
$this->testPage->getRevision()->getPrevious()->getId()
@ -447,6 +518,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
$this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
$id = $this->testPage->getRevision()->getId();
$this->hideDeprecated( 'Revision::fetchRevision' );
$res = Revision::fetchRevision( $this->testPage->getTitle() );
# note: order is unspecified
@ -455,8 +527,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
$rows[$row->rev_id] = $row;
}
$this->assertEquals( 1, count( $rows ), 'expected exactly one revision' );
$this->assertArrayHasKey( $id, $rows, 'missing revision with id ' . $id );
$this->assertEmpty( $rows, 'expected empty set' );
}
/**
@ -541,6 +612,10 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
'new null revision should have a different id from the original revision' );
$this->assertEquals( $orig->getTextId(), $rev->getTextId(),
'new null revision should have the same text id as the original revision' );
$this->assertEquals( $orig->getSha1(), $rev->getSha1(),
'new null revision should have the same SHA1 as the original revision' );
$this->assertTrue( $orig->getRevisionRecord()->hasSameContent( $rev->getRevisionRecord() ),
'new null revision should have the same content as the original revision' );
$this->assertEquals( __METHOD__, $rev->getContent()->getNativeData() );
}
@ -606,7 +681,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
'user' => $userA->getId(),
'text' => 'zero',
'content_model' => CONTENT_MODEL_WIKITEXT,
'summary' => 'edit zero'
'comment' => 'edit zero'
] );
$revisions[0]->insertOn( $dbw );
@ -618,7 +693,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
'user' => $userA->getId(),
'text' => 'one',
'content_model' => CONTENT_MODEL_WIKITEXT,
'summary' => 'edit one'
'comment' => 'edit one'
] );
$revisions[1]->insertOn( $dbw );
@ -629,7 +704,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
'user' => $userB->getId(),
'text' => 'two',
'content_model' => CONTENT_MODEL_WIKITEXT,
'summary' => 'edit two'
'comment' => 'edit two'
] );
$revisions[2]->insertOn( $dbw );
@ -640,7 +715,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
'user' => $userA->getId(),
'text' => 'three',
'content_model' => CONTENT_MODEL_WIKITEXT,
'summary' => 'edit three'
'comment' => 'edit three'
] );
$revisions[3]->insertOn( $dbw );
@ -651,13 +726,24 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
'user' => $userA->getId(),
'text' => 'zero',
'content_model' => CONTENT_MODEL_WIKITEXT,
'summary' => 'edit four'
'comment' => 'edit four'
] );
$revisions[4]->insertOn( $dbw );
// test it ---------------------------------
$since = $revisions[$sinceIdx]->getTimestamp();
$allRows = iterator_to_array( $dbw->select(
'revision',
[ 'rev_id', 'rev_timestamp', 'rev_user' ],
[
'rev_page' => $page->getId(),
//'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $since ) )
],
__METHOD__,
[ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ]
) );
$wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
$this->assertEquals( $expectedLast, $wasLast );
@ -805,12 +891,16 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
'text_id' => 123456789, // not in the test DB
] );
MediaWiki\suppressWarnings(); // bad text_id will trigger a warning.
$this->assertNull( $rev->getContent(),
"getContent() should return null if the revision's text blob could not be loaded." );
// NOTE: check this twice, once for lazy initialization, and once with the cached value.
$this->assertNull( $rev->getContent(),
"getContent() should return null if the revision's text blob could not be loaded." );
MediaWiki\suppressWarnings( 'end' );
}
public function provideGetSize() {
@ -904,6 +994,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
*/
public function testLoadFromId() {
$rev = $this->testPage->getRevision();
$this->hideDeprecated( 'Revision::loadFromId' );
$this->assertRevEquals(
$rev,
Revision::loadFromId( wfGetDB( DB_MASTER ), $rev->getId() )
@ -1026,7 +1117,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
$rev[1] = $this->testPage->getLatest();
$this->assertSame(
[ $rev[1] => strval( $textLength ) ],
[ $rev[1] => $textLength ],
Revision::getParentLengths(
wfGetDB( DB_MASTER ),
[ $rev[1] ]
@ -1049,7 +1140,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
$rev[2] = $this->testPage->getLatest();
$this->assertSame(
[ $rev[1] => strval( $textOneLength ), $rev[2] => strval( $textTwoLength ) ],
[ $rev[1] => $textOneLength, $rev[2] => $textTwoLength ],
Revision::getParentLengths(
wfGetDB( DB_MASTER ),
[ $rev[1], $rev[2] ]
@ -1080,14 +1171,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
);
}
/**
* @covers Revision::getTitle
*/
public function testGetTitle_forBadRevision() {
$rev = new Revision( [] );
$this->assertNull( $rev->getTitle() );
}
/**
* @covers Revision::isMinor
*/
@ -1263,14 +1346,21 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
$rev = $this->testPage->getRevision();
// Clear any previous cache for the revision during creation
$key = $cache->makeGlobalKey( 'revision', $db->getDomainID(), $rev->getPage(), $rev->getId() );
$key = $cache->makeGlobalKey( 'revision-row-1.29',
$db->getDomainID(),
$rev->getPage(),
$rev->getId()
);
$cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
$this->assertFalse( $cache->get( $key ) );
// Get the new revision and make sure it is in the cache and correct
$newRev = Revision::newKnownCurrent( $db, $rev->getPage(), $rev->getId() );
$this->assertRevEquals( $rev, $newRev );
$this->assertRevEquals( $rev, $cache->get( $key ) );
$cachedRow = $cache->get( $key );
$this->assertNotFalse( $cachedRow );
$this->assertEquals( $rev->getId(), $cachedRow->rev_id );
}
public function provideUserCanBitfield() {
@ -1377,7 +1467,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
]
);
$user = $this->getTestUser( $userGroups )->getUser();
$revision = new Revision( [ 'deleted' => $bitField ] );
$revision = new Revision( [ 'deleted' => $bitField ], 0, $this->testPage->getTitle() );
$this->assertSame(
$expected,

View file

@ -1,6 +1,9 @@
<?php
use Wikimedia\TestingAccessWrapper;
use MediaWiki\Storage\RevisionStore;
use MediaWiki\Storage\SqlBlobStore;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\LoadBalancer;
/**
* Test cases in RevisionTest should not interact with the Database.
@ -54,10 +57,10 @@ class RevisionTest extends MediaWikiTestCase {
/**
* @dataProvider provideConstructFromArray
* @covers Revision::__construct
* @covers Revision::constructFromRowArray
* @covers RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromArray( array $rowArray ) {
$rev = new Revision( $rowArray );
public function testConstructFromArray( $rowArray ) {
$rev = new Revision( $rowArray, 0, $this->getMockTitle() );
$this->assertNotNull( $rev->getContent(), 'no content object available' );
$this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
$this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
@ -65,7 +68,7 @@ class RevisionTest extends MediaWikiTestCase {
/**
* @covers Revision::__construct
* @covers Revision::constructFromRowArray
* @covers RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromEmptyArray() {
$rev = new Revision( [], 0, $this->getMockTitle() );
@ -90,30 +93,20 @@ class RevisionTest extends MediaWikiTestCase {
99,
'SomeTextUserName',
];
// Note: the below XXX test cases are odd and probably result in unexpected behaviour if used
// in production code.
yield 'XXX: user text only' => [
yield 'user text only' => [
[
'content' => new JavaScriptContent( 'hello world.' ),
'user_text' => '111.111.111.111',
],
null,
0,
'111.111.111.111',
];
yield 'XXX: user id only' => [
[
'content' => new JavaScriptContent( 'hello world.' ),
'user' => 9989,
],
9989,
null,
];
}
/**
* @dataProvider provideConstructFromArray_userSetAsExpected
* @covers Revision::__construct
* @covers Revision::constructFromRowArray
* @covers RevisionStore::newMutableRevisionFromArray
*
* @param array $rowArray
* @param mixed $expectedUserId null to expect the current wgUser ID
@ -133,7 +126,7 @@ class RevisionTest extends MediaWikiTestCase {
$expectedUserName = $testUser->getName();
}
$rev = new Revision( $rowArray );
$rev = new Revision( $rowArray, 0, $this->getMockTitle() );
$this->assertEquals( $expectedUserId, $rev->getUser() );
$this->assertEquals( $expectedUserName, $rev->getUserText() );
}
@ -143,28 +136,37 @@ class RevisionTest extends MediaWikiTestCase {
[
'content' => new WikitextContent( 'GOAT' ),
'text_id' => 'someid',
],
],
new MWException( "Text already stored in external store (id someid), " .
"can't serialize content object" )
];
yield 'unknown user id and no user name' => [
[
'content' => new JavaScriptContent( 'hello world.' ),
'user' => 9989,
],
new MWException( 'user_text not given, and unknown user ID 9989' )
];
yield 'with bad content object (class)' => [
[ 'content' => new stdClass() ],
new MWException( '`content` field must contain a Content object.' )
new MWException( 'content field must contain a Content object.' )
];
yield 'with bad content object (string)' => [
[ 'content' => 'ImAGoat' ],
new MWException( '`content` field must contain a Content object.' )
new MWException( 'content field must contain a Content object.' )
];
yield 'bad row format' => [
'imastring, not a row',
new MWException( 'Revision constructor passed invalid row format.' )
new InvalidArgumentException(
'$row must be a row object, an associative array, or a RevisionRecord'
)
];
}
/**
* @dataProvider provideConstructFromArrayThrowsExceptions
* @covers Revision::__construct
* @covers Revision::constructFromRowArray
* @covers RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromArrayThrowsExceptions( $rowArray, Exception $expectedException ) {
$this->setExpectedException(
@ -172,23 +174,25 @@ class RevisionTest extends MediaWikiTestCase {
$expectedException->getMessage(),
$expectedException->getCode()
);
new Revision( $rowArray );
new Revision( $rowArray, 0, $this->getMockTitle() );
}
/**
* @covers Revision::__construct
* @covers Revision::constructFromRowArray
* @covers RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromNothing() {
$rev = new Revision( [] );
$this->assertNull( $rev->getId(), 'getId()' );
$this->setExpectedException(
InvalidArgumentException::class
);
new Revision( [] );
}
public function provideConstructFromRow() {
yield 'Full construction' => [
[
'rev_id' => '2',
'rev_page' => '1',
'rev_id' => '42',
'rev_page' => '23',
'rev_text_id' => '2',
'rev_timestamp' => '20171017114835',
'rev_user_text' => '127.0.0.1',
@ -205,8 +209,8 @@ class RevisionTest extends MediaWikiTestCase {
'rev_content_model' => 'GOATMODEL',
],
function ( RevisionTest $testCase, Revision $rev ) {
$testCase->assertSame( 2, $rev->getId() );
$testCase->assertSame( 1, $rev->getPage() );
$testCase->assertSame( 42, $rev->getId() );
$testCase->assertSame( 23, $rev->getPage() );
$testCase->assertSame( 2, $rev->getTextId() );
$testCase->assertSame( '20171017114835', $rev->getTimestamp() );
$testCase->assertSame( '127.0.0.1', $rev->getUserText() );
@ -221,10 +225,10 @@ class RevisionTest extends MediaWikiTestCase {
$testCase->assertSame( 'GOATMODEL', $rev->getContentModel() );
}
];
yield 'null fields' => [
yield 'default field values' => [
[
'rev_id' => '2',
'rev_page' => '1',
'rev_id' => '42',
'rev_page' => '23',
'rev_text_id' => '2',
'rev_timestamp' => '20171017114835',
'rev_user_text' => '127.0.0.1',
@ -236,11 +240,24 @@ class RevisionTest extends MediaWikiTestCase {
'rev_comment_cid' => null,
],
function ( RevisionTest $testCase, Revision $rev ) {
$testCase->assertNull( $rev->getSize() );
$testCase->assertNull( $rev->getParentId() );
$testCase->assertNull( $rev->getSha1() );
$testCase->assertSame( 'text/x-wiki', $rev->getContentFormat() );
$testCase->assertSame( 'wikitext', $rev->getContentModel() );
// parent ID may be null
$testCase->assertSame( null, $rev->getParentId(), 'revision id' );
// given fields
$testCase->assertSame( $rev->getTimestamp(), '20171017114835', 'timestamp' );
$testCase->assertSame( $rev->getUserText(), '127.0.0.1', 'user name' );
$testCase->assertSame( $rev->getUser(), 0, 'user id' );
$testCase->assertSame( $rev->getComment(), 'Goat Comment!' );
$testCase->assertSame( false, $rev->isMinor(), 'minor edit' );
$testCase->assertSame( 0, $rev->getVisibility(), 'visibility flags' );
// computed fields
$testCase->assertNotNull( $rev->getSize(), 'size' );
$testCase->assertNotNull( $rev->getSha1(), 'hash' );
// NOTE: model and format will be detected based on the namespace of the (mock) title
$testCase->assertSame( 'text/x-wiki', $rev->getContentFormat(), 'format' );
$testCase->assertSame( 'wikitext', $rev->getContentModel(), 'model' );
}
];
}
@ -248,11 +265,34 @@ class RevisionTest extends MediaWikiTestCase {
/**
* @dataProvider provideConstructFromRow
* @covers Revision::__construct
* @covers Revision::constructFromRowArray
* @covers RevisionStore::newMutableRevisionFromArray
*/
public function testConstructFromRow( array $arrayData, $assertions ) {
$data = 'Hello goat.'; // needs to match model and format
$blobStore = $this->getMockBuilder( SqlBlobStore::class )
->disableOriginalConstructor()
->getMock();
$blobStore->method( 'getBlob' )
->will( $this->returnValue( $data ) );
$blobStore->method( 'getTextIdFromAddress' )
->will( $this->returnCallback(
function ( $address ) {
// Turn "tt:1234" into 12345.
// Note that this must be functional so we can test getTextId().
// Ideally, we'd un-mock getTextIdFromAddress and use its actual implementation.
$parts = explode( ':', $address );
return (int)array_pop( $parts );
}
) );
// Note override internal service, so RevisionStore uses it as well.
$this->setService( '_SqlBlobStore', $blobStore );
$row = (object)$arrayData;
$rev = new Revision( $row );
$rev = new Revision( $row, 0, $this->getMockTitle() );
$assertions( $this, $rev );
}
@ -282,7 +322,7 @@ class RevisionTest extends MediaWikiTestCase {
* @covers Revision::getId
*/
public function testGetId( $rowArray, $expectedId ) {
$rev = new Revision( $rowArray );
$rev = new Revision( $rowArray, 0, $this->getMockTitle() );
$this->assertEquals( $expectedId, $rev->getId() );
}
@ -296,7 +336,7 @@ class RevisionTest extends MediaWikiTestCase {
* @covers Revision::setId
*/
public function testSetId( $input, $expected ) {
$rev = new Revision( [] );
$rev = new Revision( [], 0, $this->getMockTitle() );
$rev->setId( $input );
$this->assertSame( $expected, $rev->getId() );
}
@ -311,7 +351,7 @@ class RevisionTest extends MediaWikiTestCase {
* @covers Revision::setUserIdAndName
*/
public function testSetUserIdAndName( $inputId, $expectedId, $name ) {
$rev = new Revision( [] );
$rev = new Revision( [], 0, $this->getMockTitle() );
$rev->setUserIdAndName( $inputId, $name );
$this->assertSame( $expectedId, $rev->getUser( Revision::RAW ) );
$this->assertEquals( $name, $rev->getUserText( Revision::RAW ) );
@ -328,7 +368,7 @@ class RevisionTest extends MediaWikiTestCase {
* @covers Revision::getTextId()
*/
public function testGetTextId( $rowArray, $expected ) {
$rev = new Revision( $rowArray );
$rev = new Revision( $rowArray, 0, $this->getMockTitle() );
$this->assertSame( $expected, $rev->getTextId() );
}
@ -343,7 +383,7 @@ class RevisionTest extends MediaWikiTestCase {
* @covers Revision::getParentId()
*/
public function testGetParentId( $rowArray, $expected ) {
$rev = new Revision( $rowArray );
$rev = new Revision( $rowArray, 0, $this->getMockTitle() );
$this->assertSame( $expected, $rev->getParentId() );
}
@ -376,9 +416,44 @@ class RevisionTest extends MediaWikiTestCase {
$this->testGetRevisionText( $expected, $rowData );
}
private function getWANObjectCache() {
return new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
}
/**
* @return SqlBlobStore
*/
private function getBlobStore() {
/** @var LoadBalancer $lb */
$lb = $this->getMockBuilder( LoadBalancer::class )
->disableOriginalConstructor()
->getMock();
$cache = $this->getWANObjectCache();
$blobStore = new SqlBlobStore( $lb, $cache );
return $blobStore;
}
/**
* @return RevisionStore
*/
private function getRevisionStore() {
/** @var LoadBalancer $lb */
$lb = $this->getMockBuilder( LoadBalancer::class )
->disableOriginalConstructor()
->getMock();
$cache = $this->getWANObjectCache();
$blobStore = new RevisionStore( $lb, $this->getBlobStore(), $cache );
return $blobStore;
}
public function provideGetRevisionTextWithLegacyEncoding() {
yield 'Utf8Native' => [
"Wiki est l'\xc3\xa9cole superieur !",
'fr',
'iso-8859-1',
[
'old_flags' => 'utf-8',
@ -387,6 +462,7 @@ class RevisionTest extends MediaWikiTestCase {
];
yield 'Utf8Legacy' => [
"Wiki est l'\xc3\xa9cole superieur !",
'fr',
'iso-8859-1',
[
'old_flags' => '',
@ -399,8 +475,11 @@ class RevisionTest extends MediaWikiTestCase {
* @covers Revision::getRevisionText
* @dataProvider provideGetRevisionTextWithLegacyEncoding
*/
public function testGetRevisionWithLegacyEncoding( $expected, $encoding, $rowData ) {
$this->setMwGlobals( 'wgLegacyEncoding', $encoding );
public function testGetRevisionWithLegacyEncoding( $expected, $lang, $encoding, $rowData ) {
$blobStore = $this->getBlobStore();
$blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) );
$this->setService( 'BlobStore', $blobStore );
$this->testGetRevisionText( $expected, $rowData );
}
@ -412,6 +491,7 @@ class RevisionTest extends MediaWikiTestCase {
*/
yield 'Utf8NativeGzip' => [
"Wiki est l'\xc3\xa9cole superieur !",
'fr',
'iso-8859-1',
[
'old_flags' => 'gzip,utf-8',
@ -420,6 +500,7 @@ class RevisionTest extends MediaWikiTestCase {
];
yield 'Utf8LegacyGzip' => [
"Wiki est l'\xc3\xa9cole superieur !",
'fr',
'iso-8859-1',
[
'old_flags' => 'gzip',
@ -432,9 +513,13 @@ class RevisionTest extends MediaWikiTestCase {
* @covers Revision::getRevisionText
* @dataProvider provideGetRevisionTextWithGzipAndLegacyEncoding
*/
public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $encoding, $rowData ) {
public function testGetRevisionWithGzipAndLegacyEncoding( $expected, $lang, $encoding, $rowData ) {
$this->checkPHPExtension( 'zlib' );
$this->setMwGlobals( 'wgLegacyEncoding', $encoding );
$blobStore = $this->getBlobStore();
$blobStore->setLegacyEncoding( $encoding, Language::factory( $lang ) );
$this->setService( 'BlobStore', $blobStore );
$this->testGetRevisionText( $expected, $rowData );
}
@ -460,7 +545,10 @@ class RevisionTest extends MediaWikiTestCase {
*/
public function testCompressRevisionTextUtf8Gzip() {
$this->checkPHPExtension( 'zlib' );
$this->setMwGlobals( 'wgCompressRevisions', true );
$blobStore = $this->getBlobStore();
$blobStore->setCompressBlobs( true );
$this->setService( 'BlobStore', $blobStore );
$row = new stdClass;
$row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
@ -475,20 +563,41 @@ class RevisionTest extends MediaWikiTestCase {
Revision::getRevisionText( $row ), "getRevisionText" );
}
public function provideFetchFromConds() {
yield [ 0, [] ];
yield [ Revision::READ_LOCKING, [ 'FOR UPDATE' ] ];
}
/**
* @dataProvider provideFetchFromConds
* @covers Revision::fetchFromConds
* @covers Revision::loadFromTitle
*/
public function testFetchFromConds( $flags, array $options ) {
$this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
$conditions = [ 'conditionsArray' ];
public function testLoadFromTitle() {
$title = $this->getMockTitle();
$conditions = [
'rev_id=page_latest',
'page_namespace' => $title->getNamespace(),
'page_title' => $title->getDBkey()
];
$row = (object)[
'rev_id' => '42',
'rev_page' => $title->getArticleID(),
'rev_text_id' => '2',
'rev_timestamp' => '20171017114835',
'rev_user_text' => '127.0.0.1',
'rev_user' => '0',
'rev_minor_edit' => '0',
'rev_deleted' => '0',
'rev_len' => '46',
'rev_parent_id' => '1',
'rev_sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
'rev_comment_text' => 'Goat Comment!',
'rev_comment_data' => null,
'rev_comment_cid' => null,
'rev_content_format' => 'GOATFORMAT',
'rev_content_model' => 'GOATMODEL',
];
$db = $this->getMock( IDatabase::class );
$db->expects( $this->any() )
->method( 'getDomainId' )
->will( $this->returnValue( wfWikiID() ) );
$db->expects( $this->once() )
->method( 'selectRow' )
->with(
@ -497,17 +606,24 @@ class RevisionTest extends MediaWikiTestCase {
$this->isType( 'array' ),
$this->equalTo( $conditions ),
// Method name
$this->equalTo( 'Revision::fetchFromConds' ),
$this->equalTo( $options ),
$this->stringContains( 'fetchRevisionRowFromConds' ),
// We don't really care about the options here
$this->isType( 'array' ),
// We don't really care about the join conds are they come from the joinCond methods
$this->isType( 'array' )
)
->willReturn( 'RETURNVALUE' );
->willReturn( $row );
$wrapper = TestingAccessWrapper::newFromClass( Revision::class );
$result = $wrapper->fetchFromConds( $db, $conditions, $flags );
$revision = Revision::loadFromTitle( $db, $title );
$this->assertEquals( 'RETURNVALUE', $result );
$this->assertEquals( $title->getArticleID(), $revision->getTitle()->getArticleID() );
$this->assertEquals( $row->rev_id, $revision->getId() );
$this->assertEquals( $row->rev_len, $revision->getSize() );
$this->assertEquals( $row->rev_sha1, $revision->getSha1() );
$this->assertEquals( $row->rev_parent_id, $revision->getParentId() );
$this->assertEquals( $row->rev_timestamp, $revision->getTimestamp() );
$this->assertEquals( $row->rev_comment_text, $revision->getComment() );
$this->assertEquals( $row->rev_user_text, $revision->getUserText() );
}
public function provideDecompressRevisionText() {
@ -572,8 +688,12 @@ class RevisionTest extends MediaWikiTestCase {
* @param mixed $expected
*/
public function testDecompressRevisionText( $legacyEncoding, $text, $flags, $expected ) {
$this->setMwGlobals( 'wgLegacyEncoding', $legacyEncoding );
$this->setMwGlobals( 'wgLanguageCode', 'en' );
$blobStore = $this->getBlobStore();
if ( $legacyEncoding ) {
$blobStore->setLegacyEncoding( $legacyEncoding, Language::factory( 'en' ) );
}
$this->setService( 'BlobStore', $blobStore );
$this->assertSame(
$expected,
Revision::decompressRevisionText( $text, $flags )
@ -669,14 +789,20 @@ class RevisionTest extends MediaWikiTestCase {
* @covers Revision::getRevisionText
*/
public function testGetRevisionText_external_oldId() {
$cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
$cache = $this->getWANObjectCache();
$this->setService( 'MainWANObjectCache', $cache );
$this->setService(
'ExternalStoreFactory',
new ExternalStoreFactory( [ 'ForTesting' ] )
);
$cacheKey = $cache->makeKey( 'revisiontext', 'textid', '7777' );
$lb = $this->getMockBuilder( LoadBalancer::class )
->disableOriginalConstructor()
->getMock();
$blobStore = new SqlBlobStore( $lb, $cache );
$this->setService( 'BlobStore', $blobStore );
$this->assertSame(
'AAAABBAAA',
@ -688,6 +814,8 @@ class RevisionTest extends MediaWikiTestCase {
]
)
);
$cacheKey = $cache->makeKey( 'revisiontext', 'textid', 'tt:7777' );
$this->assertSame( 'AAAABBAAA', $cache->get( $cacheKey ) );
}
@ -883,6 +1011,8 @@ class RevisionTest extends MediaWikiTestCase {
'fields' => [
'ar_id',
'ar_page_id',
'ar_namespace',
'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
@ -911,6 +1041,8 @@ class RevisionTest extends MediaWikiTestCase {
'fields' => [
'ar_id',
'ar_page_id',
'ar_namespace',
'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
@ -944,6 +1076,8 @@ class RevisionTest extends MediaWikiTestCase {
'fields' => [
'ar_id',
'ar_page_id',
'ar_namespace',
'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
@ -980,6 +1114,8 @@ class RevisionTest extends MediaWikiTestCase {
'fields' => [
'ar_id',
'ar_page_id',
'ar_namespace',
'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
@ -1016,6 +1152,8 @@ class RevisionTest extends MediaWikiTestCase {
'fields' => [
'ar_id',
'ar_page_id',
'ar_namespace',
'ar_title',
'ar_rev_id',
'ar_text',
'ar_text_id',
@ -1047,6 +1185,11 @@ class RevisionTest extends MediaWikiTestCase {
*/
public function testGetArchiveQueryInfo( $globals, $expected ) {
$this->setMwGlobals( $globals );
$revisionStore = $this->getRevisionStore();
$revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
$this->setService( 'RevisionStore', $revisionStore );
$this->assertEquals(
$expected,
Revision::getArchiveQueryInfo()
@ -1398,6 +1541,11 @@ class RevisionTest extends MediaWikiTestCase {
*/
public function testGetQueryInfo( $globals, $options, $expected ) {
$this->setMwGlobals( $globals );
$revisionStore = $this->getRevisionStore();
$revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
$this->setService( 'RevisionStore', $revisionStore );
$this->assertEquals(
$expected,
Revision::getQueryInfo( $options )