wiki.techinc.nl/tests/phpunit/includes/deferred/LinksUpdateTest.php
Aaron Schulz 6dedffc2d7 Move category membership RC updates to CategoryMembershipChangeJob
* Recursive link updates no longer mention an category changes.
  It's hard to avoid either duplicate mentioning of changes or
  confusing explicit and automatic category changes.
* LinksUpdate no longer handles this logic, but rather WikiPage
  decides to spawn this update when needed in doEditUpdates().
* Fix race conditions with calculating category deltas. Do not
  rely on the link tables for the read used to determine these
  writes, as they may be out-of-date due to slave lag. Using the
  master would still not be good enough since that would assume
  FIFO and serialized job execution, which is not garaunteed.
  Use the parser output of the relevant revisions to determine
  the RC rows. If 3 users quickly edit a page's categories, the
  old way could misattribute who actually changed what.
* Make sure RC rows are inserted in an order that matches that
  of the corresponding revisions.
* Better avoid mentioning time-based (parser functions) category
  changes so they don't get attributed to the next editor.
* Also wait for slaves between RC row insertions if there where
  many category changes (it theory it could well over 10K rows).
* Using a separate job better separates concerns as LinksUpdate
  should not have to care about recent changes updates.
* Added more docs to $wgRCWatchCategoryMembership.

Bug: T95501
Change-Id: I5863e7d7483a4fd1fa633597af66a0088ace4c68
2015-12-03 11:28:05 +00:00

372 lines
10 KiB
PHP

<?php
/**
* @group LinksUpdate
* @group Database
* ^--- make sure temporary tables are used.
*/
class LinksUpdateTest extends MediaWikiTestCase {
function __construct( $name = null, array $data = array(), $dataName = '' ) {
parent::__construct( $name, $data, $dataName );
$this->tablesUsed = array_merge( $this->tablesUsed,
array(
'interwiki',
'page_props',
'pagelinks',
'categorylinks',
'langlinks',
'externallinks',
'imagelinks',
'templatelinks',
'iwlinks',
'recentchanges',
)
);
}
protected function setUp() {
parent::setUp();
$dbw = wfGetDB( DB_MASTER );
$dbw->replace(
'interwiki',
array( 'iw_prefix' ),
array(
'iw_prefix' => 'linksupdatetest',
'iw_url' => 'http://testing.com/wiki/$1',
'iw_api' => 'http://testing.com/w/api.php',
'iw_local' => 0,
'iw_trans' => 0,
'iw_wikiid' => 'linksupdatetest',
)
);
$this->setMwGlobals( 'wgRCWatchCategoryMembership', true );
}
public function addDBData() {
$this->insertPage( 'Testing' );
$this->insertPage( 'Some_other_page' );
$this->insertPage( 'Template:TestingTemplate' );
}
protected function makeTitleAndParserOutput( $name, $id ) {
$t = Title::newFromText( $name );
$t->mArticleID = $id; # XXX: this is fugly
$po = new ParserOutput();
$po->setTitleText( $t->getPrefixedText() );
return array( $t, $po );
}
/**
* @covers ParserOutput::addLink
*/
public function testUpdate_pagelinks() {
/** @var ParserOutput $po */
list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
$po->addLink( Title::newFromText( "Foo" ) );
$po->addLink( Title::newFromText( "Special:Foo" ) ); // special namespace should be ignored
$po->addLink( Title::newFromText( "linksupdatetest:Foo" ) ); // interwiki link should be ignored
$po->addLink( Title::newFromText( "#Foo" ) ); // hash link should be ignored
$update = $this->assertLinksUpdate(
$t,
$po,
'pagelinks',
'pl_namespace,
pl_title',
'pl_from = 111',
array( array( NS_MAIN, 'Foo' ) )
);
$this->assertArrayEquals( array(
Title::makeTitle( NS_MAIN, 'Foo' ), // newFromText doesn't yield the same internal state....
), $update->getAddedLinks() );
$po = new ParserOutput();
$po->setTitleText( $t->getPrefixedText() );
$po->addLink( Title::newFromText( "Bar" ) );
$po->addLink( Title::newFromText( "Talk:Bar" ) );
$update = $this->assertLinksUpdate(
$t,
$po,
'pagelinks',
'pl_namespace,
pl_title',
'pl_from = 111',
array(
array( NS_MAIN, 'Bar' ),
array( NS_TALK, 'Bar' ),
)
);
$this->assertArrayEquals( array(
Title::makeTitle( NS_MAIN, 'Bar' ),
Title::makeTitle( NS_TALK, 'Bar' ),
), $update->getAddedLinks() );
$this->assertArrayEquals( array(
Title::makeTitle( NS_MAIN, 'Foo' ),
), $update->getRemovedLinks() );
}
/**
* @covers ParserOutput::addExternalLink
*/
public function testUpdate_externallinks() {
/** @var ParserOutput $po */
list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
$po->addExternalLink( "http://testing.com/wiki/Foo" );
$this->assertLinksUpdate( $t, $po, 'externallinks', 'el_to, el_index', 'el_from = 111', array(
array( 'http://testing.com/wiki/Foo', 'http://com.testing./wiki/Foo' ),
) );
}
/**
* @covers ParserOutput::addCategory
*/
public function testUpdate_categorylinks() {
/** @var ParserOutput $po */
$this->setMwGlobals( 'wgCategoryCollation', 'uppercase' );
list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
$po->addCategory( "Foo", "FOO" );
$this->assertLinksUpdate( $t, $po, 'categorylinks', 'cl_to, cl_sortkey', 'cl_from = 111', array(
array( 'Foo', "FOO\nTESTING" ),
) );
}
public function testOnAddingAndRemovingCategory_recentChangesRowIsAdded() {
$this->setMwGlobals( 'wgCategoryCollation', 'uppercase' );
$title = Title::newFromText( 'Testing' );
$wikiPage = new WikiPage( $title );
$wikiPage->doEditContent( new WikitextContent( '[[Category:Foo]]' ), 'added category' );
$this->runAllRelatedJobs();
$this->assertRecentChangeByCategorization(
$title,
$wikiPage->getParserOutput( new ParserOptions() ),
Title::newFromText( 'Category:Foo' ),
array( array( 'Foo', '[[:Testing]] added to category' ) )
);
$wikiPage->doEditContent( new WikitextContent( '[[Category:Bar]]' ), 'replaced category' );
$this->runAllRelatedJobs();
$this->assertRecentChangeByCategorization(
$title,
$wikiPage->getParserOutput( new ParserOptions() ),
Title::newFromText( 'Category:Foo' ),
array(
array( 'Foo', '[[:Testing]] added to category' ),
array( 'Foo', '[[:Testing]] removed from category' ),
)
);
$this->assertRecentChangeByCategorization(
$title,
$wikiPage->getParserOutput( new ParserOptions() ),
Title::newFromText( 'Category:Bar' ),
array(
array( 'Bar', '[[:Testing]] added to category' ),
)
);
}
public function testOnAddingAndRemovingCategoryToTemplates_embeddingPagesAreIgnored() {
$this->setMwGlobals( 'wgCategoryCollation', 'uppercase' );
$templateTitle = Title::newFromText( 'Template:TestingTemplate' );
$templatePage = new WikiPage( $templateTitle );
$wikiPage = new WikiPage( Title::newFromText( 'Testing' ) );
$wikiPage->doEditContent( new WikitextContent( '{{TestingTemplate}}' ), 'added template' );
$this->runAllRelatedJobs();
$otherWikiPage = new WikiPage( Title::newFromText( 'Some_other_page' ) );
$otherWikiPage->doEditContent( new WikitextContent( '{{TestingTemplate}}' ), 'added template' );
$this->runAllRelatedJobs();
$this->assertRecentChangeByCategorization(
$templateTitle,
$templatePage->getParserOutput( new ParserOptions() ),
Title::newFromText( 'Baz' ),
array()
);
$templatePage->doEditContent( new WikitextContent( '[[Category:Baz]]' ), 'added category' );
$this->runAllRelatedJobs();
$this->assertRecentChangeByCategorization(
$templateTitle,
$templatePage->getParserOutput( new ParserOptions() ),
Title::newFromText( 'Baz' ),
array( array( 'Baz', '[[:Template:TestingTemplate]] and 2 pages added to category' ) )
);
}
/**
* @covers ParserOutput::addInterwikiLink
*/
public function testUpdate_iwlinks() {
/** @var ParserOutput $po */
list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
$target = Title::makeTitleSafe( NS_MAIN, "Foo", '', 'linksupdatetest' );
$po->addInterwikiLink( $target );
$this->assertLinksUpdate( $t, $po, 'iwlinks', 'iwl_prefix, iwl_title', 'iwl_from = 111', array(
array( 'linksupdatetest', 'Foo' ),
) );
}
/**
* @covers ParserOutput::addTemplate
*/
public function testUpdate_templatelinks() {
/** @var ParserOutput $po */
list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
$po->addTemplate( Title::newFromText( "Template:Foo" ), 23, 42 );
$this->assertLinksUpdate(
$t,
$po,
'templatelinks',
'tl_namespace,
tl_title',
'tl_from = 111',
array( array( NS_TEMPLATE, 'Foo' ) )
);
}
/**
* @covers ParserOutput::addImage
*/
public function testUpdate_imagelinks() {
/** @var ParserOutput $po */
list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
$po->addImage( "Foo.png" );
$this->assertLinksUpdate( $t, $po, 'imagelinks', 'il_to', 'il_from = 111', array(
array( 'Foo.png' ),
) );
}
/**
* @covers ParserOutput::addLanguageLink
*/
public function testUpdate_langlinks() {
$this->setMwGlobals( array(
'wgCapitalLinks' => true,
) );
/** @var ParserOutput $po */
list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
$po->addLanguageLink( Title::newFromText( "en:Foo" )->getFullText() );
$this->assertLinksUpdate( $t, $po, 'langlinks', 'll_lang, ll_title', 'll_from = 111', array(
array( 'En', 'Foo' ),
) );
}
/**
* @covers ParserOutput::setProperty
*/
public function testUpdate_page_props() {
global $wgPagePropsHaveSortkey;
/** @var ParserOutput $po */
list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", 111 );
$fields = array( 'pp_propname', 'pp_value' );
$expected = array();
$po->setProperty( "bool", true );
$expected[] = array( "bool", true );
$po->setProperty( "float", 4.0 + 1.0 / 4.0 );
$expected[] = array( "float", 4.0 + 1.0 / 4.0 );
$po->setProperty( "int", -7 );
$expected[] = array( "int", -7 );
$po->setProperty( "string", "33 bar" );
$expected[] = array( "string", "33 bar" );
// compute expected sortkey values
if ( $wgPagePropsHaveSortkey ) {
$fields[] = 'pp_sortkey';
foreach ( $expected as &$row ) {
$value = $row[1];
if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
$row[] = floatval( $value );
} else {
$row[] = null;
}
}
}
$this->assertLinksUpdate( $t, $po, 'page_props', $fields, 'pp_page = 111', $expected );
}
public function testUpdate_page_props_without_sortkey() {
$this->setMwGlobals( 'wgPagePropsHaveSortkey', false );
$this->testUpdate_page_props();
}
// @todo test recursive, too!
protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput,
$table, $fields, $condition, array $expectedRows
) {
$update = new LinksUpdate( $title, $parserOutput );
// NOTE: make sure LinksUpdate does not generate warnings when called inside a transaction.
$update->beginTransaction();
$update->doUpdate();
$update->commitTransaction();
$this->assertSelect( $table, $fields, $condition, $expectedRows );
return $update;
}
protected function assertRecentChangeByCategorization(
Title $pageTitle, ParserOutput $parserOutput, Title $categoryTitle, $expectedRows
) {
$this->assertSelect(
'recentchanges',
'rc_title, rc_comment',
array(
'rc_type' => RC_CATEGORIZE,
'rc_namespace' => NS_CATEGORY,
'rc_title' => $categoryTitle->getDBkey()
),
$expectedRows
);
}
private function runAllRelatedJobs() {
$queueGroup = JobQueueGroup::singleton();
while ( $job = $queueGroup->pop( 'refreshLinksPrioritized' ) ) {
$job->run();
$queueGroup->ack( $job );
}
while ( $job = $queueGroup->pop( 'categoryMembershipChange' ) ) {
$job->run();
$queueGroup->ack( $job );
}
}
}