* 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
372 lines
10 KiB
PHP
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 );
|
|
}
|
|
}
|
|
}
|