123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935 |
- <?php
- namespace MediaWiki\Tests\Storage;
- use CommentStoreComment;
- use Content;
- use ContentHandler;
- use LinksUpdate;
- use MediaWiki\MediaWikiServices;
- use MediaWiki\Storage\DerivedPageDataUpdater;
- use MediaWiki\Storage\MutableRevisionRecord;
- use MediaWiki\Storage\MutableRevisionSlots;
- use MediaWiki\Storage\RevisionRecord;
- use MediaWiki\Storage\RevisionSlotsUpdate;
- use MediaWiki\Storage\SlotRecord;
- use MediaWikiTestCase;
- use MWCallableUpdate;
- use PHPUnit\Framework\MockObject\MockObject;
- use TextContent;
- use TextContentHandler;
- use Title;
- use User;
- use Wikimedia\TestingAccessWrapper;
- use WikiPage;
- use WikitextContent;
- /**
- * @group Database
- *
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater
- */
- class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
- /**
- * @param string $title
- *
- * @return Title
- */
- private function getTitle( $title ) {
- return Title::makeTitleSafe( $this->getDefaultWikitextNS(), $title );
- }
- /**
- * @param string|Title $title
- *
- * @return WikiPage
- */
- private function getPage( $title ) {
- $title = ( $title instanceof Title ) ? $title : $this->getTitle( $title );
- return WikiPage::factory( $title );
- }
- /**
- * @param string|Title|WikiPage $page
- *
- * @return DerivedPageDataUpdater
- */
- private function getDerivedPageDataUpdater( $page, RevisionRecord $rec = null ) {
- if ( is_string( $page ) || $page instanceof Title ) {
- $page = $this->getPage( $page );
- }
- $page = TestingAccessWrapper::newFromObject( $page );
- return $page->getDerivedDataUpdater( null, $rec );
- }
- /**
- * Creates a revision in the database.
- *
- * @param WikiPage $page
- * @param $summary
- * @param null|string|Content $content
- *
- * @return RevisionRecord|null
- */
- private function createRevision( WikiPage $page, $summary, $content = null ) {
- $user = $this->getTestUser()->getUser();
- $comment = CommentStoreComment::newUnsavedComment( $summary );
- if ( $content === null || is_string( $content ) ) {
- $content = new WikitextContent( $content ?? $summary );
- }
- if ( !is_array( $content ) ) {
- $content = [ 'main' => $content ];
- }
- $this->getDerivedPageDataUpdater( $page ); // flush cached instance before.
- $updater = $page->newPageUpdater( $user );
- foreach ( $content as $role => $c ) {
- $updater->setContent( $role, $c );
- }
- $rev = $updater->saveRevision( $comment );
- $this->getDerivedPageDataUpdater( $page ); // flush cached instance after.
- return $rev;
- }
- // TODO: test setArticleCountMethod() and isCountable();
- // TODO: test isRedirect() and wasRedirect()
- /**
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOptions()
- */
- public function testGetCanonicalParserOptions() {
- $user = $this->getTestUser()->getUser();
- $page = $this->getPage( __METHOD__ );
- $parentRev = $this->createRevision( $page, 'first' );
- $mainContent = new WikitextContent( 'Lorem ipsum' );
- $update = new RevisionSlotsUpdate();
- $update->modifyContent( 'main', $mainContent );
- $updater = $this->getDerivedPageDataUpdater( $page );
- $updater->prepareContent( $user, $update, false );
- $options1 = $updater->getCanonicalParserOptions();
- $this->assertSame( MediaWikiServices::getInstance()->getContentLanguage(),
- $options1->getUserLangObj() );
- $speculativeId = $options1->getSpeculativeRevId();
- $this->assertSame( $parentRev->getId() + 1, $speculativeId );
- $rev = $this->makeRevision(
- $page->getTitle(),
- $update,
- $user,
- $parentRev->getId() + 7,
- $parentRev->getId()
- );
- $updater->prepareUpdate( $rev );
- $options2 = $updater->getCanonicalParserOptions();
- $currentRev = call_user_func( $options2->getCurrentRevisionCallback(), $page->getTitle() );
- $this->assertSame( $rev->getId(), $currentRev->getId() );
- }
- /**
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::grabCurrentRevision()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
- */
- public function testGrabCurrentRevision() {
- $page = $this->getPage( __METHOD__ );
- $updater0 = $this->getDerivedPageDataUpdater( $page );
- $this->assertNull( $updater0->grabCurrentRevision() );
- $this->assertFalse( $updater0->pageExisted() );
- $rev1 = $this->createRevision( $page, 'first' );
- $updater1 = $this->getDerivedPageDataUpdater( $page );
- $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
- $this->assertFalse( $updater0->pageExisted() );
- $this->assertTrue( $updater1->pageExisted() );
- $rev2 = $this->createRevision( $page, 'second' );
- $updater2 = $this->getDerivedPageDataUpdater( $page );
- $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
- $this->assertSame( $rev2->getId(), $updater2->grabCurrentRevision()->getId() );
- }
- /**
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isContentPrepared()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput()
- */
- public function testPrepareContent() {
- $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
- $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
- $this->assertFalse( $updater->isContentPrepared() );
- // TODO: test stash
- // TODO: MCR: Test multiple slots. Test slot removal.
- $mainContent = new WikitextContent( 'first [[main]] ~~~' );
- $auxContent = new WikitextContent( 'inherited ~~~ content' );
- $auxSlot = SlotRecord::newSaved(
- 10, 7, 'tt:7',
- SlotRecord::newUnsaved( 'aux', $auxContent )
- );
- $update = new RevisionSlotsUpdate();
- $update->modifyContent( 'main', $mainContent );
- $update->modifySlot( SlotRecord::newInherited( $auxSlot ) );
- // TODO: MCR: test removing slots!
- $updater->prepareContent( $sysop, $update, false );
- // second be ok to call again with the same params
- $updater->prepareContent( $sysop, $update, false );
- $this->assertNull( $updater->grabCurrentRevision() );
- $this->assertTrue( $updater->isContentPrepared() );
- $this->assertFalse( $updater->isUpdatePrepared() );
- $this->assertFalse( $updater->pageExisted() );
- $this->assertTrue( $updater->isCreation() );
- $this->assertTrue( $updater->isChange() );
- $this->assertFalse( $updater->isContentDeleted() );
- $this->assertNotNull( $updater->getRevision() );
- $this->assertNotNull( $updater->getRenderedRevision() );
- $this->assertEquals( [ 'main', 'aux' ], $updater->getSlots()->getSlotRoles() );
- $this->assertEquals( [ 'main' ], array_keys( $updater->getSlots()->getOriginalSlots() ) );
- $this->assertEquals( [ 'aux' ], array_keys( $updater->getSlots()->getInheritedSlots() ) );
- $this->assertEquals( [ 'main', 'aux' ], $updater->getModifiedSlotRoles() );
- $this->assertEquals( [ 'main', 'aux' ], $updater->getTouchedSlotRoles() );
- $mainSlot = $updater->getRawSlot( 'main' );
- $this->assertInstanceOf( SlotRecord::class, $mainSlot );
- $this->assertNotContains( '~~~', $mainSlot->getContent()->serialize(), 'PST should apply.' );
- $this->assertContains( $sysop->getName(), $mainSlot->getContent()->serialize() );
- $auxSlot = $updater->getRawSlot( 'aux' );
- $this->assertInstanceOf( SlotRecord::class, $auxSlot );
- $this->assertContains( '~~~', $auxSlot->getContent()->serialize(), 'No PST should apply.' );
- $mainOutput = $updater->getCanonicalParserOutput();
- $this->assertContains( 'first', $mainOutput->getText() );
- $this->assertContains( '<a ', $mainOutput->getText() );
- $this->assertNotEmpty( $mainOutput->getLinks() );
- $canonicalOutput = $updater->getCanonicalParserOutput();
- $this->assertContains( 'first', $canonicalOutput->getText() );
- $this->assertContains( '<a ', $canonicalOutput->getText() );
- $this->assertContains( 'inherited ', $canonicalOutput->getText() );
- $this->assertNotEmpty( $canonicalOutput->getLinks() );
- }
- /**
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange()
- */
- public function testPrepareContentInherit() {
- $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
- $page = $this->getPage( __METHOD__ );
- $mainContent1 = new WikitextContent( 'first [[main]] ({{REVISIONUSER}}) #~~~#' );
- $mainContent2 = new WikitextContent( 'second ({{subst:REVISIONUSER}}) #~~~#' );
- $rev = $this->createRevision( $page, 'first', $mainContent1 );
- $mainContent1 = $rev->getContent( 'main' ); // get post-pst content
- $userName = $rev->getUser()->getName();
- $sysopName = $sysop->getName();
- $update = new RevisionSlotsUpdate();
- $update->modifyContent( 'main', $mainContent1 );
- $updater1 = $this->getDerivedPageDataUpdater( $page );
- $updater1->prepareContent( $sysop, $update, false );
- $this->assertNotNull( $updater1->grabCurrentRevision() );
- $this->assertTrue( $updater1->isContentPrepared() );
- $this->assertTrue( $updater1->pageExisted() );
- $this->assertFalse( $updater1->isCreation() );
- $this->assertFalse( $updater1->isChange() );
- $this->assertNotNull( $updater1->getRevision() );
- $this->assertNotNull( $updater1->getRenderedRevision() );
- // parser-output for null-edit uses the original author's name
- $html = $updater1->getRenderedRevision()->getRevisionParserOutput()->getText();
- $this->assertNotContains( $sysopName, $html, '{{REVISIONUSER}}' );
- $this->assertNotContains( '{{REVISIONUSER}}', $html, '{{REVISIONUSER}}' );
- $this->assertNotContains( '~~~', $html, 'signature ~~~' );
- $this->assertContains( '(' . $userName . ')', $html, '{{REVISIONUSER}}' );
- $this->assertContains( '>' . $userName . '<', $html, 'signature ~~~' );
- // TODO: MCR: test inheritance from parent
- $update = new RevisionSlotsUpdate();
- $update->modifyContent( 'main', $mainContent2 );
- $updater2 = $this->getDerivedPageDataUpdater( $page );
- $updater2->prepareContent( $sysop, $update, false );
- // non-null edit use the new user name in PST
- $pstText = $updater2->getSlots()->getContent( 'main' )->serialize();
- $this->assertNotContains( '{{subst:REVISIONUSER}}', $pstText, '{{subst:REVISIONUSER}}' );
- $this->assertNotContains( '~~~', $pstText, 'signature ~~~' );
- $this->assertContains( '(' . $sysopName . ')', $pstText, '{{subst:REVISIONUSER}}' );
- $this->assertContains( ':' . $sysopName . '|', $pstText, 'signature ~~~' );
- $this->assertFalse( $updater2->isCreation() );
- $this->assertTrue( $updater2->isChange() );
- }
- // TODO: test failure of prepareContent() when called again...
- // - with different user
- // - with different update
- // - after calling prepareUpdate()
- /**
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isUpdatePrepared()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput()
- */
- public function testPrepareUpdate() {
- $page = $this->getPage( __METHOD__ );
- $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
- $rev1 = $this->createRevision( $page, 'first', $mainContent1 );
- $updater1 = $this->getDerivedPageDataUpdater( $page, $rev1 );
- $options = []; // TODO: test *all* the options...
- $updater1->prepareUpdate( $rev1, $options );
- $this->assertTrue( $updater1->isUpdatePrepared() );
- $this->assertTrue( $updater1->isContentPrepared() );
- $this->assertTrue( $updater1->isCreation() );
- $this->assertTrue( $updater1->isChange() );
- $this->assertFalse( $updater1->isContentDeleted() );
- $this->assertNotNull( $updater1->getRevision() );
- $this->assertNotNull( $updater1->getRenderedRevision() );
- $this->assertEquals( [ 'main' ], $updater1->getSlots()->getSlotRoles() );
- $this->assertEquals( [ 'main' ], array_keys( $updater1->getSlots()->getOriginalSlots() ) );
- $this->assertEquals( [], array_keys( $updater1->getSlots()->getInheritedSlots() ) );
- $this->assertEquals( [ 'main' ], $updater1->getModifiedSlotRoles() );
- $this->assertEquals( [ 'main' ], $updater1->getTouchedSlotRoles() );
- // TODO: MCR: test multiple slots, test slot removal!
- $this->assertInstanceOf( SlotRecord::class, $updater1->getRawSlot( 'main' ) );
- $this->assertNotContains( '~~~~', $updater1->getRawContent( 'main' )->serialize() );
- $mainOutput = $updater1->getCanonicalParserOutput();
- $this->assertContains( 'first', $mainOutput->getText() );
- $this->assertContains( '<a ', $mainOutput->getText() );
- $this->assertNotEmpty( $mainOutput->getLinks() );
- $canonicalOutput = $updater1->getCanonicalParserOutput();
- $this->assertContains( 'first', $canonicalOutput->getText() );
- $this->assertContains( '<a ', $canonicalOutput->getText() );
- $this->assertNotEmpty( $canonicalOutput->getLinks() );
- $mainContent2 = new WikitextContent( 'second' );
- $rev2 = $this->createRevision( $page, 'second', $mainContent2 );
- $updater2 = $this->getDerivedPageDataUpdater( $page, $rev2 );
- $options = []; // TODO: test *all* the options...
- $updater2->prepareUpdate( $rev2, $options );
- $this->assertFalse( $updater2->isCreation() );
- $this->assertTrue( $updater2->isChange() );
- $canonicalOutput = $updater2->getCanonicalParserOutput();
- $this->assertContains( 'second', $canonicalOutput->getText() );
- }
- /**
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
- */
- public function testPrepareUpdateReusesParserOutput() {
- $user = $this->getTestUser()->getUser();
- $page = $this->getPage( __METHOD__ );
- $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
- $update = new RevisionSlotsUpdate();
- $update->modifyContent( 'main', $mainContent1 );
- $updater = $this->getDerivedPageDataUpdater( $page );
- $updater->prepareContent( $user, $update, false );
- $mainOutput = $updater->getSlotParserOutput( 'main' );
- $canonicalOutput = $updater->getCanonicalParserOutput();
- $rev = $this->createRevision( $page, 'first', $mainContent1 );
- $options = []; // TODO: test *all* the options...
- $updater->prepareUpdate( $rev, $options );
- $this->assertTrue( $updater->isUpdatePrepared() );
- $this->assertTrue( $updater->isContentPrepared() );
- $this->assertSame( $mainOutput, $updater->getSlotParserOutput( 'main' ) );
- $this->assertSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
- }
- /**
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
- */
- public function testPrepareUpdateOutputReset() {
- $user = $this->getTestUser()->getUser();
- $page = $this->getPage( __METHOD__ );
- $mainContent1 = new WikitextContent( 'first --{{REVISIONID}}--' );
- $update = new RevisionSlotsUpdate();
- $update->modifyContent( 'main', $mainContent1 );
- $updater = $this->getDerivedPageDataUpdater( $page );
- $updater->prepareContent( $user, $update, false );
- $mainOutput = $updater->getSlotParserOutput( 'main' );
- $canonicalOutput = $updater->getCanonicalParserOutput();
- // prevent optimization on matching speculative ID
- $mainOutput->setSpeculativeRevIdUsed( 0 );
- $canonicalOutput->setSpeculativeRevIdUsed( 0 );
- $rev = $this->createRevision( $page, 'first', $mainContent1 );
- $options = []; // TODO: test *all* the options...
- $updater->prepareUpdate( $rev, $options );
- $this->assertTrue( $updater->isUpdatePrepared() );
- $this->assertTrue( $updater->isContentPrepared() );
- // ParserOutput objects should have been flushed.
- $this->assertNotSame( $mainOutput, $updater->getSlotParserOutput( 'main' ) );
- $this->assertNotSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
- $html = $updater->getCanonicalParserOutput()->getText();
- $this->assertContains( '--' . $rev->getId() . '--', $html );
- // TODO: MCR: ensure that when the main slot uses {{REVISIONID}} but another slot is
- // updated, the main slot is still re-rendered!
- }
- // TODO: test failure of prepareUpdate() when called again with a different revision
- // TODO: test failure of prepareUpdate() on inconsistency with prepareContent.
- /**
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
- */
- public function testGetPreparedEditAfterPrepareContent() {
- $user = $this->getTestUser()->getUser();
- $mainContent = new WikitextContent( 'first [[main]] ~~~' );
- $update = new RevisionSlotsUpdate();
- $update->modifyContent( 'main', $mainContent );
- $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
- $updater->prepareContent( $user, $update, false );
- $canonicalOutput = $updater->getCanonicalParserOutput();
- $preparedEdit = $updater->getPreparedEdit();
- $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
- $this->assertSame( $canonicalOutput, $preparedEdit->output );
- $this->assertSame( $mainContent, $preparedEdit->newContent );
- $this->assertSame( $updater->getRawContent( 'main' ), $preparedEdit->pstContent );
- $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
- $this->assertSame( null, $preparedEdit->revid );
- }
- /**
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
- */
- public function testGetPreparedEditAfterPrepareUpdate() {
- $page = $this->getPage( __METHOD__ );
- $mainContent = new WikitextContent( 'first [[main]] ~~~' );
- $update = new MutableRevisionSlots();
- $update->setContent( 'main', $mainContent );
- $rev = $this->createRevision( $page, __METHOD__ );
- $updater = $this->getDerivedPageDataUpdater( $page );
- $updater->prepareUpdate( $rev );
- $canonicalOutput = $updater->getCanonicalParserOutput();
- $preparedEdit = $updater->getPreparedEdit();
- $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
- $this->assertSame( $canonicalOutput, $preparedEdit->output );
- $this->assertSame( $updater->getRawContent( 'main' ), $preparedEdit->pstContent );
- $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
- $this->assertSame( $rev->getId(), $preparedEdit->revid );
- }
- public function testGetSecondaryDataUpdatesAfterPrepareContent() {
- $user = $this->getTestUser()->getUser();
- $page = $this->getPage( __METHOD__ );
- $this->createRevision( $page, __METHOD__ );
- $mainContent1 = new WikitextContent( 'first' );
- $update = new RevisionSlotsUpdate();
- $update->modifyContent( 'main', $mainContent1 );
- $updater = $this->getDerivedPageDataUpdater( $page );
- $updater->prepareContent( $user, $update, false );
- $dataUpdates = $updater->getSecondaryDataUpdates();
- $this->assertNotEmpty( $dataUpdates );
- $linksUpdates = array_filter( $dataUpdates, function ( $du ) {
- return $du instanceof LinksUpdate;
- } );
- $this->assertCount( 1, $linksUpdates );
- }
- /**
- * @param string $name
- *
- * @return ContentHandler
- */
- private function defineMockContentModelForUpdateTesting( $name ) {
- /** @var ContentHandler|MockObject $handler */
- $handler = $this->getMockBuilder( TextContentHandler::class )
- ->setConstructorArgs( [ $name ] )
- ->setMethods(
- [ 'getSecondaryDataUpdates', 'getDeletionUpdates', 'unserializeContent' ]
- )
- ->getMock();
- $dataUpdate = new MWCallableUpdate( 'time' );
- $dataUpdate->_name = "$name data update";
- $deletionUpdate = new MWCallableUpdate( 'time' );
- $deletionUpdate->_name = "$name deletion update";
- $handler->method( 'getSecondaryDataUpdates' )->willReturn( [ $dataUpdate ] );
- $handler->method( 'getDeletionUpdates' )->willReturn( [ $deletionUpdate ] );
- $handler->method( 'unserializeContent' )->willReturnCallback(
- function ( $text ) use ( $handler ) {
- return $this->createMockContent( $handler, $text );
- }
- );
- $this->mergeMwGlobalArrayValue(
- 'wgContentHandlers', [
- $name => function () use ( $handler ){
- return $handler;
- }
- ]
- );
- return $handler;
- }
- /**
- * @param ContentHandler $handler
- * @param string $text
- *
- * @return Content
- */
- private function createMockContent( ContentHandler $handler, $text ) {
- /** @var Content|MockObject $content */
- $content = $this->getMockBuilder( TextContent::class )
- ->setConstructorArgs( [ $text ] )
- ->setMethods( [ 'getModel', 'getContentHandler' ] )
- ->getMock();
- $content->method( 'getModel' )->willReturn( $handler->getModelID() );
- $content->method( 'getContentHandler' )->willReturn( $handler );
- return $content;
- }
- public function testGetSecondaryDataUpdatesWithSlotRemoval() {
- global $wgMultiContentRevisionSchemaMigrationStage;
- if ( ! ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) ) {
- $this->markTestSkipped( 'Slot removal cannot happen with MCR being enabled' );
- }
- $m1 = $this->defineMockContentModelForUpdateTesting( 'M1' );
- $a1 = $this->defineMockContentModelForUpdateTesting( 'A1' );
- $m2 = $this->defineMockContentModelForUpdateTesting( 'M2' );
- $mainContent1 = $this->createMockContent( $m1, 'main 1' );
- $auxContent1 = $this->createMockContent( $a1, 'aux 1' );
- $mainContent2 = $this->createMockContent( $m2, 'main 2' );
- $user = $this->getTestUser()->getUser();
- $page = $this->getPage( __METHOD__ );
- $this->createRevision(
- $page,
- __METHOD__,
- [ 'main' => $mainContent1, 'aux' => $auxContent1 ]
- );
- $update = new RevisionSlotsUpdate();
- $update->modifyContent( 'main', $mainContent2 );
- $update->removeSlot( 'aux' );
- $page = $this->getPage( __METHOD__ );
- $updater = $this->getDerivedPageDataUpdater( $page );
- $updater->prepareContent( $user, $update, false );
- $dataUpdates = $updater->getSecondaryDataUpdates();
- $this->assertNotEmpty( $dataUpdates );
- $updateNames = array_map( function ( $du ) {
- return isset( $du->_name ) ? $du->_name : get_class( $du );
- }, $dataUpdates );
- $this->assertContains( LinksUpdate::class, $updateNames );
- $this->assertContains( 'A1 deletion update', $updateNames );
- $this->assertContains( 'M2 data update', $updateNames );
- $this->assertNotContains( 'M1 data update', $updateNames );
- }
- /**
- * Creates a dummy revision object without touching the database.
- *
- * @param Title $title
- * @param RevisionSlotsUpdate $update
- * @param User $user
- * @param string $comment
- * @param int $id
- * @param int $parentId
- *
- * @return MutableRevisionRecord
- */
- private function makeRevision(
- Title $title,
- RevisionSlotsUpdate $update,
- User $user,
- $comment,
- $id,
- $parentId = 0
- ) {
- $rev = new MutableRevisionRecord( $title );
- $rev->applyUpdate( $update );
- $rev->setUser( $user );
- $rev->setComment( CommentStoreComment::newUnsavedComment( $comment ) );
- $rev->setId( $id );
- $rev->setPageId( $title->getArticleID() );
- $rev->setParentId( $parentId );
- return $rev;
- }
- /**
- * @param int $id
- * @return Title
- */
- private function getMockTitle( $id = 23 ) {
- $mock = $this->getMockBuilder( Title::class )
- ->disableOriginalConstructor()
- ->getMock();
- $mock->expects( $this->any() )
- ->method( 'getDBkey' )
- ->will( $this->returnValue( __CLASS__ ) );
- $mock->expects( $this->any() )
- ->method( 'getArticleID' )
- ->will( $this->returnValue( $id ) );
- return $mock;
- }
- public function provideIsReusableFor() {
- $title = $this->getMockTitle();
- $user1 = User::newFromName( 'Alice' );
- $user2 = User::newFromName( 'Bob' );
- $content1 = new WikitextContent( 'one' );
- $content2 = new WikitextContent( 'two' );
- $update1 = new RevisionSlotsUpdate();
- $update1->modifyContent( 'main', $content1 );
- $update1b = new RevisionSlotsUpdate();
- $update1b->modifyContent( 'xyz', $content1 );
- $update2 = new RevisionSlotsUpdate();
- $update2->modifyContent( 'main', $content2 );
- $rev1 = $this->makeRevision( $title, $update1, $user1, 'rev1', 11 );
- $rev1b = $this->makeRevision( $title, $update1b, $user1, 'rev1', 11 );
- $rev2 = $this->makeRevision( $title, $update2, $user1, 'rev2', 12 );
- $rev2x = $this->makeRevision( $title, $update2, $user2, 'rev2', 12 );
- $rev2y = $this->makeRevision( $title, $update2, $user1, 'rev2', 122 );
- yield 'any' => [
- '$prepUser' => null,
- '$prepRevision' => null,
- '$prepUpdate' => null,
- '$forUser' => null,
- '$forRevision' => null,
- '$forUpdate' => null,
- '$forParent' => null,
- '$isReusable' => true,
- ];
- yield 'for any' => [
- '$prepUser' => $user1,
- '$prepRevision' => $rev1,
- '$prepUpdate' => $update1,
- '$forUser' => null,
- '$forRevision' => null,
- '$forUpdate' => null,
- '$forParent' => null,
- '$isReusable' => true,
- ];
- yield 'unprepared' => [
- '$prepUser' => null,
- '$prepRevision' => null,
- '$prepUpdate' => null,
- '$forUser' => $user1,
- '$forRevision' => $rev1,
- '$forUpdate' => $update1,
- '$forParent' => 0,
- '$isReusable' => true,
- ];
- yield 'match prepareContent' => [
- '$prepUser' => $user1,
- '$prepRevision' => null,
- '$prepUpdate' => $update1,
- '$forUser' => $user1,
- '$forRevision' => null,
- '$forUpdate' => $update1,
- '$forParent' => 0,
- '$isReusable' => true,
- ];
- yield 'match prepareUpdate' => [
- '$prepUser' => null,
- '$prepRevision' => $rev1,
- '$prepUpdate' => null,
- '$forUser' => $user1,
- '$forRevision' => $rev1,
- '$forUpdate' => null,
- '$forParent' => 0,
- '$isReusable' => true,
- ];
- yield 'match all' => [
- '$prepUser' => $user1,
- '$prepRevision' => $rev1,
- '$prepUpdate' => $update1,
- '$forUser' => $user1,
- '$forRevision' => $rev1,
- '$forUpdate' => $update1,
- '$forParent' => 0,
- '$isReusable' => true,
- ];
- yield 'mismatch prepareContent update' => [
- '$prepUser' => $user1,
- '$prepRevision' => null,
- '$prepUpdate' => $update1,
- '$forUser' => $user1,
- '$forRevision' => null,
- '$forUpdate' => $update1b,
- '$forParent' => 0,
- '$isReusable' => false,
- ];
- yield 'mismatch prepareContent user' => [
- '$prepUser' => $user1,
- '$prepRevision' => null,
- '$prepUpdate' => $update1,
- '$forUser' => $user2,
- '$forRevision' => null,
- '$forUpdate' => $update1,
- '$forParent' => 0,
- '$isReusable' => false,
- ];
- yield 'mismatch prepareContent parent' => [
- '$prepUser' => $user1,
- '$prepRevision' => null,
- '$prepUpdate' => $update1,
- '$forUser' => $user1,
- '$forRevision' => null,
- '$forUpdate' => $update1,
- '$forParent' => 7,
- '$isReusable' => false,
- ];
- yield 'mismatch prepareUpdate revision update' => [
- '$prepUser' => null,
- '$prepRevision' => $rev1,
- '$prepUpdate' => null,
- '$forUser' => null,
- '$forRevision' => $rev1b,
- '$forUpdate' => null,
- '$forParent' => 0,
- '$isReusable' => false,
- ];
- yield 'mismatch prepareUpdate revision user' => [
- '$prepUser' => null,
- '$prepRevision' => $rev2,
- '$prepUpdate' => null,
- '$forUser' => null,
- '$forRevision' => $rev2x,
- '$forUpdate' => null,
- '$forParent' => 0,
- '$isReusable' => false,
- ];
- yield 'mismatch prepareUpdate revision id' => [
- '$prepUser' => null,
- '$prepRevision' => $rev2,
- '$prepUpdate' => null,
- '$forUser' => null,
- '$forRevision' => $rev2y,
- '$forUpdate' => null,
- '$forParent' => 0,
- '$isReusable' => false,
- ];
- }
- /**
- * @dataProvider provideIsReusableFor
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isReusableFor()
- *
- * @param User|null $prepUser
- * @param RevisionRecord|null $prepRevision
- * @param RevisionSlotsUpdate|null $prepUpdate
- * @param User|null $forUser
- * @param RevisionRecord|null $forRevision
- * @param RevisionSlotsUpdate|null $forUpdate
- * @param int|null $forParent
- * @param bool $isReusable
- */
- public function testIsReusableFor(
- User $prepUser = null,
- RevisionRecord $prepRevision = null,
- RevisionSlotsUpdate $prepUpdate = null,
- User $forUser = null,
- RevisionRecord $forRevision = null,
- RevisionSlotsUpdate $forUpdate = null,
- $forParent = null,
- $isReusable = null
- ) {
- $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
- if ( $prepUpdate ) {
- $updater->prepareContent( $prepUser, $prepUpdate, false );
- }
- if ( $prepRevision ) {
- $updater->prepareUpdate( $prepRevision );
- }
- $this->assertSame(
- $isReusable,
- $updater->isReusableFor( $forUser, $forRevision, $forUpdate, $forParent )
- );
- }
- /**
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doSecondaryDataUpdates()
- * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
- */
- public function testDoUpdates() {
- $page = $this->getPage( __METHOD__ );
- $content = [ 'main' => new WikitextContent( 'first [[main]]' ) ];
- if ( $this->hasMultiSlotSupport() ) {
- $content['aux'] = new WikitextContent( 'Aux [[Nix]]' );
- }
- $rev = $this->createRevision( $page, 'first', $content );
- $pageId = $page->getId();
- $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
- $this->db->delete( 'pagelinks', '*' );
- $pcache = MediaWikiServices::getInstance()->getParserCache();
- $pcache->deleteOptionsKey( $page );
- $updater = $this->getDerivedPageDataUpdater( $page, $rev );
- $updater->setArticleCountMethod( 'link' );
- $options = []; // TODO: test *all* the options...
- $updater->prepareUpdate( $rev, $options );
- $updater->doUpdates();
- // links table update
- $pageLinks = $this->db->select(
- 'pagelinks',
- '*',
- [ 'pl_from' => $pageId ],
- __METHOD__,
- [ 'ORDER BY' => 'pl_namespace, pl_title' ]
- );
- $pageLinksRow = $pageLinks->fetchObject();
- $this->assertInternalType( 'object', $pageLinksRow );
- $this->assertSame( 'Main', $pageLinksRow->pl_title );
- if ( $this->hasMultiSlotSupport() ) {
- $pageLinksRow = $pageLinks->fetchObject();
- $this->assertInternalType( 'object', $pageLinksRow );
- $this->assertSame( 'Nix', $pageLinksRow->pl_title );
- }
- // parser cache update
- $cached = $pcache->get( $page, $updater->getCanonicalParserOptions() );
- $this->assertInternalType( 'object', $cached );
- $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
- // site stats
- $stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
- $this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages );
- $this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits );
- $this->assertSame( $oldStats->ss_good_articles + 1, (int)$stats->ss_good_articles );
- // TODO: MCR: test data updates for additional slots!
- // TODO: test update for edit without page creation
- // TODO: test message cache purge
- // TODO: test module cache purge
- // TODO: test CDN purge
- // TODO: test newtalk update
- // TODO: test search update
- // TODO: test site stats good_articles while turning the page into (or back from) a redir.
- // TODO: test category membership update (with setRcWatchCategoryMembership())
- }
- private function hasMultiSlotSupport() {
- global $wgMultiContentRevisionSchemaMigrationStage;
- return ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW )
- && ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW );
- }
- }
|