wiki.techinc.nl/tests/phpunit/includes/cache/LinkCacheTest.php
daniel 855988fd0e LinkCache: soft deprecate addGoodLinkObj()
addGoodLinkObj() has many optional arguments, but omitting them actually
means corrupting the cache.

Nearly all existing callers are in tests.
So LinkCacheTestTrait::addGoodLinkObject() was created only
for testing. It is better to have this method in the
trait, because building the row directly in each test
would make these tests brittle against schema changes.

The only usage in WMF production code was in WikiPage and has been
fixed.

Bug: T284955
Change-Id: I03a2bd9ed64fcc0281ee29a286c8db395a9e03d9
2021-09-10 16:00:02 +02:00

545 lines
16 KiB
PHP

<?php
use MediaWiki\Page\PageReference;
use MediaWiki\Page\PageReferenceValue;
/**
* @group Database
* @group Cache
* @covers LinkCache
*/
class LinkCacheTest extends MediaWikiIntegrationTestCase {
use LinkCacheTestTrait;
private function newLinkCache( WANObjectCache $wanCache = null ) {
if ( !$wanCache ) {
$wanCache = new WANObjectCache( [ 'cache' => new EmptyBagOStuff() ] );
}
return new LinkCache(
$this->getServiceContainer()->getTitleFormatter(),
$wanCache,
$this->getServiceContainer()->getNamespaceInfo(),
$this->getServiceContainer()->getDBLoadBalancer()
);
}
public function providePageAndLink() {
return [
[ new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ) ],
[ new TitleValue( NS_USER, __METHOD__ ) ]
];
}
public function providePageAndLinkAndArray() {
return [
[ new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ) ],
[ new TitleValue( NS_USER, __METHOD__ ) ],
[ [ 'page_namespace' => NS_USER, 'page_title' => __METHOD__ ] ],
];
}
private function getPageRow( $offset = 0 ) {
return (object)[
'page_id' => 8 + $offset,
'page_len' => 18,
'page_is_redirect' => 0,
'page_latest' => 118 + $offset,
'page_content_model' => CONTENT_MODEL_TEXT,
'page_lang' => 'xyz',
'page_restrictions' => 'test',
'page_touched' => '20200202020202',
];
}
/**
* @dataProvider providePageAndLinkAndArray
* @covers LinkCache::addGoodLinkObjFromRow()
* @covers LinkCache::getGoodLinkRow()
* @covers LinkCache::getGoodLinkID()
* @covers LinkCache::getGoodLinkFieldObj()
* @covers LinkCache::clearLink()
*/
public function testAddGoodLinkObjFromRow( $page ) {
$linkCache = $this->newLinkCache();
$row = $this->getPageRow();
$dbkey = is_array( $page ) ? $page['page_title'] : $page->getDBkey();
$ns = is_array( $page ) ? $page['page_namespace'] : $page->getNamespace();
$linkCache->addBadLinkObj( $page );
$linkCache->addGoodLinkObjFromRow( $page, $row );
$this->assertEquals(
$row,
$linkCache->getGoodLinkRow( $ns, $dbkey )
);
$this->assertSame( $row->page_id, $linkCache->getGoodLinkID( $page ) );
$this->assertFalse( $linkCache->isBadLink( $page ) );
$this->assertSame(
$row->page_id,
$linkCache->getGoodLinkFieldObj( $page, 'id' )
);
$this->assertSame(
$row->page_len,
$linkCache->getGoodLinkFieldObj( $page, 'length' )
);
$this->assertSame(
$row->page_is_redirect,
$linkCache->getGoodLinkFieldObj( $page, 'redirect' )
);
$this->assertSame(
$row->page_latest,
$linkCache->getGoodLinkFieldObj( $page, 'revision' )
);
$this->assertSame(
$row->page_content_model,
$linkCache->getGoodLinkFieldObj( $page, 'model' )
);
$this->assertSame(
$row->page_lang,
$linkCache->getGoodLinkFieldObj( $page, 'lang' )
);
$this->assertSame(
$row->page_restrictions,
$linkCache->getGoodLinkFieldObj( $page, 'restrictions' )
);
$this->assertEquals(
$row,
$linkCache->getGoodLinkRow( $ns, $dbkey )
);
$linkCache->clearBadLink( $page );
$this->assertNotNull( $linkCache->getGoodLinkID( $page ) );
$this->assertNotNull( $linkCache->getGoodLinkFieldObj( $page, 'length' ) );
$linkCache->clearLink( $page );
$this->assertSame( 0, $linkCache->getGoodLinkID( $page ) );
$this->assertNull( $linkCache->getGoodLinkFieldObj( $page, 'length' ) );
$this->assertNull( $linkCache->getGoodLinkRow( $ns, $dbkey ) );
}
/**
* @covers LinkCache::addGoodLinkObjFromRow()
* @covers LinkCache::getGoodLinkRow()
* @covers LinkCache::getGoodLinkID()
* @covers LinkCache::getGoodLinkFieldObj()
*/
public function testAddGoodLinkObjWithAllParameters() {
$linkCache = $this->getServiceContainer()->getLinkCache();
$page = new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL );
$this->addGoodLinkObject( 8, $page, 18, 0, 118, CONTENT_MODEL_TEXT, 'xyz' );
$row = $linkCache->getGoodLinkRow( $page->getNamespace(), $page->getDBkey() );
$this->assertEquals( 8, (int)$row->page_id );
$this->assertSame( 8, $linkCache->getGoodLinkID( $page ) );
$this->assertSame( 8, $linkCache->getGoodLinkFieldObj( $page, 'id' ) );
$this->assertSame(
18,
$linkCache->getGoodLinkFieldObj( $page, 'length' )
);
$this->assertSame(
0,
$linkCache->getGoodLinkFieldObj( $page, 'redirect' )
);
$this->assertSame(
118,
$linkCache->getGoodLinkFieldObj( $page, 'revision' )
);
$this->assertSame(
CONTENT_MODEL_TEXT,
$linkCache->getGoodLinkFieldObj( $page, 'model' )
);
$this->assertSame(
'xyz',
$linkCache->getGoodLinkFieldObj( $page, 'lang' )
);
}
/**
* @covers LinkCache::addGoodLinkObjFromRow()
* @covers LinkCache::getGoodLinkRow()
* @covers LinkCache::getGoodLinkID()
* @covers LinkCache::getGoodLinkFieldObj()
*/
public function testAddGoodLinkObjFromRowWithMinimalParameters() {
$linkCache = $this->getServiceContainer()->getLinkCache();
$page = new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL );
$this->addGoodLinkObject( 8, $page );
$expectedRow = [
'page_id' => 8,
'page_len' => -1,
'page_is_redirect' => 0,
'page_latest' => 0,
'page_content_model' => null,
'page_lang' => null,
'page_restrictions' => null
];
$actualRow = (array)$linkCache->getGoodLinkRow( $page->getNamespace(), $page->getDBkey() );
$this->assertEquals(
$expectedRow,
array_intersect_key( $actualRow, $expectedRow )
);
$this->assertSame( 8, $linkCache->getGoodLinkID( $page ) );
$this->assertSame( 8, $linkCache->getGoodLinkFieldObj( $page, 'id' ) );
$this->assertSame(
-1,
$linkCache->getGoodLinkFieldObj( $page, 'length' )
);
$this->assertSame(
0,
$linkCache->getGoodLinkFieldObj( $page, 'redirect' )
);
$this->assertSame(
0,
$linkCache->getGoodLinkFieldObj( $page, 'revision' )
);
$this->assertSame(
null,
$linkCache->getGoodLinkFieldObj( $page, 'model' )
);
$this->assertSame(
null,
$linkCache->getGoodLinkFieldObj( $page, 'lang' )
);
}
/**
* @covers LinkCache::addGoodLinkObjFromRow()
*/
public function testAddGoodLinkObjFromRowWithInterwikiLink() {
$linkCache = $this->getServiceContainer()->getLinkCache();
$page = new TitleValue( NS_USER, __METHOD__, '', 'acme' );
$this->addGoodLinkObject( 8, $page );
$this->assertSame( 0, $linkCache->getGoodLinkID( $page ) );
}
/**
* @dataProvider providePageAndLink
* @covers LinkCache::addBadLinkObj()
* @covers LinkCache::isBadLink()
* @covers LinkCache::clearLink()
*/
public function testAddBadLinkObj( $key ) {
$linkCache = $this->getServiceContainer()->getLinkCache();
$this->assertFalse( $linkCache->isBadLink( $key ) );
$this->addGoodLinkObject( 17, $key );
$linkCache->addBadLinkObj( $key );
$this->assertTrue( $linkCache->isBadLink( $key ) );
$this->assertSame( 0, $linkCache->getGoodLinkID( $key ) );
$linkCache->clearLink( $key );
$this->assertFalse( $linkCache->isBadLink( $key ) );
}
/**
* @covers LinkCache::addBadLinkObj()
*/
public function testAddBadLinkObjWithInterwikiLink() {
$linkCache = $this->newLinkCache();
$page = new TitleValue( NS_USER, __METHOD__, '', 'acme' );
$linkCache->addBadLinkObj( $page );
$this->assertFalse( $linkCache->isBadLink( $page ) );
}
/**
* @covers LinkCache::addLinkObj()
* @covers LinkCache::getGoodLinkFieldObj
*/
public function testAddLinkObj() {
$existing = $this->getExistingTestPage();
$missing = $this->getNonexistingTestPage();
$linkCache = $this->newLinkCache();
$linkCache->addLinkObj( $existing );
$linkCache->addLinkObj( $missing );
$this->assertTrue( $linkCache->isBadLink( $missing ) );
$this->assertFalse( $linkCache->isBadLink( $existing ) );
$this->assertSame( $existing->getId(), $linkCache->getGoodLinkID( $existing ) );
$this->assertTrue( $linkCache->isBadLink( $missing ) );
// Make sure nothing explodes when getting a field from a non-existing entry
$this->assertNull( $linkCache->getGoodLinkFieldObj( $missing, 'length' ) );
}
/**
* @covers LinkCache::addLinkObj()
*/
public function testAddLinkObjUsesCachedInfo() {
$existing = $this->getExistingTestPage();
$missing = $this->getNonexistingTestPage();
$fakeRow = $this->getPageRow( $existing->getId() + 100 );
$linkCache = $this->newLinkCache();
// pretend the existing page is missing, and the missing page exists
$linkCache->addGoodLinkObjFromRow( $missing, $fakeRow );
$linkCache->addBadLinkObj( $existing );
// the LinkCache should use the cached info and not look into the database
$this->assertSame( (int)$fakeRow->page_id, $linkCache->addLinkObj( $missing ) );
$this->assertSame( 0, $linkCache->addLinkObj( $existing ) );
// now set the "read latest" flag and try again
$flags = IDBAccessObject::READ_LATEST;
$this->assertSame( 0, $linkCache->addLinkObj( $missing, $flags ) );
$this->assertSame( $existing->getId(), $linkCache->addLinkObj( $existing, $flags ) );
}
/**
* @covers LinkCache::addLinkObj()
* @covers LinkCache::getMutableCacheKeys()
*/
public function testAddLinkObjUsesWANCache() {
// Pages in some namespaces use the WAN cache: Template, File, Category, MediaWiki
$existing = $this->getExistingTestPage( Title::makeTitle( NS_TEMPLATE, __METHOD__ ) );
$fakeRow = $this->getPageRow( $existing->getId() + 100 );
$cache = new HashBagOStuff();
$wanCache = new WANObjectCache( [ 'cache' => $cache ] );
$linkCache = $this->newLinkCache( $wanCache );
// load the page row into the cache
$linkCache->addLinkObj( $existing );
$keys = $linkCache->getMutableCacheKeys( $wanCache, $existing );
$this->assertNotEmpty( $keys );
foreach ( $keys as $key ) {
$this->assertNotFalse( $wanCache->get( $key ) );
}
// replace real row data with fake, and assert that it gets used
$wanCache->set( $key, $fakeRow );
$linkCache->clearLink( $existing ); // clear local cache
$this->assertSame( (int)$fakeRow->page_id, $linkCache->addLinkObj( $existing ) );
// set the "read latest" flag and try again
$flags = IDBAccessObject::READ_LATEST;
$this->assertSame( $existing->getId(), $linkCache->addLinkObj( $existing, $flags ) );
}
public function testFalsyPageName() {
$linkCache = $this->newLinkCache();
// The stringified value is "0", which is falsy in PHP!
$link = new TitleValue( NS_MAIN, '0' );
$linkCache->addBadLinkObj( $link );
$this->assertTrue( $linkCache->isBadLink( $link ) );
$row = $this->getPageRow();
$linkCache->addGoodLinkObjFromRow( $link, $row );
$this->assertGreaterThan( 0, $linkCache->getGoodLinkID( $link ) );
$this->assertSame( $row, $linkCache->getGoodLinkRow( NS_MAIN, '0' ) );
}
public function testClearBadLinkWithString() {
$linkCache = $this->newLinkCache();
$linkCache->clearBadLink( 'Xyzzy' );
$this->addToAssertionCount( 1 );
}
public function testIsBadLinkWithString() {
$linkCache = $this->newLinkCache();
$this->assertFalse( $linkCache->isBadLink( 'Xyzzy' ) );
}
public function testGetGoodLinkIdWithString() {
$linkCache = $this->newLinkCache();
$this->assertSame( 0, $linkCache->getGoodLinkID( 'Xyzzy' ) );
}
public function provideInvalidPageParams() {
return [
'empty' => [ NS_MAIN, '' ],
'bad chars' => [ NS_MAIN, '_|_' ],
'empty in namspace' => [ NS_USER, '' ],
'special' => [ NS_SPECIAL, 'RecentChanges' ],
];
}
/**
* @dataProvider provideInvalidPageParams
* @covers LinkCache::getGoodLinkRow()
*/
public function testGetGoodLinkRowWithBadParams( $ns, $dbkey ) {
$linkCache = $this->newLinkCache();
$this->assertNull( $linkCache->getGoodLinkRow( $ns, $dbkey ) );
}
public function getRowIfExisting( $db, $ns, $dbkey, $queryOptions ) {
if ( $dbkey === 'Existing' ) {
return $this->getPageRow();
}
return null;
}
/**
* @covers LinkCache::getGoodLinkRow()
* @covers LinkCache::getGoodLinkFieldObj
*/
public function testGetGoodLinkRow() {
$existing = new TitleValue( NS_MAIN, 'Existing' );
$missing = new TitleValue( NS_MAIN, 'Missing' );
$linkCache = $this->newLinkCache();
$callback = [ $this, 'getRowIfExisting' ];
$linkCache->getGoodLinkRow( $existing->getNamespace(), $existing->getDBkey(), $callback );
$linkCache->getGoodLinkRow( $missing->getNamespace(), $missing->getDBkey(), $callback );
$this->assertTrue( $linkCache->isBadLink( $missing ) );
$this->assertFalse( $linkCache->isBadLink( $existing ) );
$this->assertGreaterThan( 0, $linkCache->getGoodLinkID( $existing ) );
$this->assertTrue( $linkCache->isBadLink( $missing ) );
// Make sure nothing explodes when getting a field from a non-existing entry
$this->assertNull( $linkCache->getGoodLinkFieldObj( $missing, 'length' ) );
}
/**
* @covers LinkCache::getGoodLinkRow()
*/
public function testGetGoodLinkRowUsesCachedInfo() {
$existing = new TitleValue( NS_MAIN, 'Existing' );
$missing = new TitleValue( NS_MAIN, 'Missing' );
$callback = [ $this, 'getRowIfExisting' ];
$existingRow = $this->getPageRow( 0 );
$fakeRow = $this->getPageRow( 3 );
$linkCache = $this->newLinkCache();
// pretend the existing page is missing, and the missing page exists
$linkCache->addGoodLinkObjFromRow( $missing, $fakeRow );
$linkCache->addBadLinkObj( $existing );
// the LinkCache should use the cached info and not look into the database
$this->assertSame(
$fakeRow,
$linkCache->getGoodLinkRow( $missing->getNamespace(), $missing->getDBkey(), $callback )
);
$this->assertNull(
$linkCache->getGoodLinkRow( $existing->getNamespace(), $existing->getDBkey(), $callback )
);
// now set the "read latest" flag and try again
$flags = IDBAccessObject::READ_LATEST;
$this->assertNull(
$linkCache->getGoodLinkRow(
$missing->getNamespace(),
$missing->getDBkey(),
$callback,
$flags
)
);
$this->assertEquals(
$existingRow,
$linkCache->getGoodLinkRow(
$existing->getNamespace(),
$existing->getDBkey(),
$callback,
$flags
)
);
// pretend again that the missing page exists, but pretend even harder
$linkCache->addGoodLinkObjFromRow( $missing, $fakeRow, IDBAccessObject::READ_LATEST );
// the LinkCache should use the cached info and not look into the database
$this->assertSame(
$fakeRow,
$linkCache->getGoodLinkRow( $missing->getNamespace(), $missing->getDBkey(), $callback )
);
// now set the "read latest" flag and try again
$flags = IDBAccessObject::READ_LATEST;
$this->assertEquals(
$fakeRow,
$linkCache->getGoodLinkRow(
$missing->getNamespace(),
$missing->getDBkey(),
$callback,
$flags
)
);
}
/**
* @covers LinkCache::getGoodLinkRow()
* @covers LinkCache::getMutableCacheKeys()
*/
public function testGetGoodLinkRowUsesWANCache() {
// Pages in some namespaces use the WAN cache: Template, File, Category, MediaWiki
$existing = new TitleValue( NS_TEMPLATE, 'Existing' );
$callback = [ $this, 'getRowIfExisting' ];
$existingRow = $this->getPageRow( 0 );
$fakeRow = $this->getPageRow( 3 );
$cache = new HashBagOStuff();
$wanCache = new WANObjectCache( [ 'cache' => $cache ] );
$linkCache = $this->newLinkCache( $wanCache );
// load the page row into the cache
$linkCache->getGoodLinkRow( $existing->getNamespace(), $existing->getDBkey(), $callback );
$keys = $linkCache->getMutableCacheKeys( $wanCache, $existing );
$this->assertNotEmpty( $keys );
foreach ( $keys as $key ) {
$this->assertNotFalse( $wanCache->get( $key ) );
}
// replace real row data with fake, and assert that it gets used
$wanCache->set( $key, $fakeRow );
$linkCache->clearLink( $existing ); // clear local cache
$this->assertSame(
$fakeRow,
$linkCache->getGoodLinkRow(
$existing->getNamespace(),
$existing->getDBkey(),
$callback
)
);
// set the "read latest" flag and try again
$flags = IDBAccessObject::READ_LATEST;
$this->assertEquals(
$existingRow,
$linkCache->getGoodLinkRow(
$existing->getNamespace(),
$existing->getDBkey(),
$callback,
$flags
)
);
}
}