3 use MediaWiki\Cache\LinkCache
;
4 use MediaWiki\Page\PageReference
;
5 use MediaWiki\Page\PageReferenceValue
;
6 use MediaWiki\Title\Title
;
7 use MediaWiki\Title\TitleValue
;
8 use Wikimedia\ObjectCache\EmptyBagOStuff
;
9 use Wikimedia\ObjectCache\HashBagOStuff
;
10 use Wikimedia\ObjectCache\WANObjectCache
;
11 use Wikimedia\Rdbms\IDBAccessObject
;
16 * @covers \MediaWiki\Cache\LinkCache
18 class LinkCacheTest
extends MediaWikiIntegrationTestCase
{
19 use LinkCacheTestTrait
;
21 private function newLinkCache( ?WANObjectCache
$wanCache = null ) {
23 $wanCache = new WANObjectCache( [ 'cache' => new EmptyBagOStuff() ] );
27 $this->getServiceContainer()->getTitleFormatter(),
29 $this->getServiceContainer()->getNamespaceInfo(),
30 $this->getServiceContainer()->getDBLoadBalancer()
34 public static function providePageAndLink() {
36 [ new PageReferenceValue( NS_USER
, __METHOD__
, PageReference
::LOCAL
) ],
37 [ new TitleValue( NS_USER
, __METHOD__
) ]
41 public static function providePageAndLinkAndArray() {
43 [ new PageReferenceValue( NS_USER
, __METHOD__
, PageReference
::LOCAL
) ],
44 [ new TitleValue( NS_USER
, __METHOD__
) ],
45 [ [ 'page_namespace' => NS_USER
, 'page_title' => __METHOD__
] ],
49 private function getPageRow( $offset = 0 ) {
51 'page_id' => 8 +
$offset,
52 'page_namespace' => 0,
53 'page_title' => 'Test ' . $offset,
55 'page_is_redirect' => 0,
56 'page_latest' => 118 +
$offset,
57 'page_content_model' => CONTENT_MODEL_TEXT
,
60 'page_touched' => '20200202020202',
65 * @dataProvider providePageAndLinkAndArray
66 * @covers \MediaWiki\Cache\LinkCache::addGoodLinkObjFromRow()
67 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkRow()
68 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkID()
69 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkFieldObj()
70 * @covers \MediaWiki\Cache\LinkCache::clearLink()
72 public function testAddGoodLinkObjFromRow( $page ) {
73 $linkCache = $this->newLinkCache();
75 $row = $this->getPageRow();
77 $dbkey = is_array( $page ) ?
$page['page_title'] : $page->getDBkey();
78 $ns = is_array( $page ) ?
$page['page_namespace'] : $page->getNamespace();
80 $linkCache->addBadLinkObj( $page );
81 $linkCache->addGoodLinkObjFromRow( $page, $row );
85 $linkCache->getGoodLinkRow( $ns, $dbkey )
88 $this->assertSame( $row->page_id
, $linkCache->getGoodLinkID( $page ) );
89 $this->assertFalse( $linkCache->isBadLink( $page ) );
93 $linkCache->getGoodLinkFieldObj( $page, 'id' )
97 $linkCache->getGoodLinkFieldObj( $page, 'length' )
100 $row->page_is_redirect
,
101 $linkCache->getGoodLinkFieldObj( $page, 'redirect' )
105 $linkCache->getGoodLinkFieldObj( $page, 'revision' )
108 $row->page_content_model
,
109 $linkCache->getGoodLinkFieldObj( $page, 'model' )
113 $linkCache->getGoodLinkFieldObj( $page, 'lang' )
118 $linkCache->getGoodLinkRow( $ns, $dbkey )
121 $linkCache->clearBadLink( $page );
122 $this->assertNotNull( $linkCache->getGoodLinkID( $page ) );
123 $this->assertNotNull( $linkCache->getGoodLinkFieldObj( $page, 'length' ) );
125 $linkCache->clearLink( $page );
126 $this->assertSame( 0, $linkCache->getGoodLinkID( $page ) );
127 $this->assertNull( $linkCache->getGoodLinkFieldObj( $page, 'length' ) );
128 $this->assertNull( $linkCache->getGoodLinkRow( $ns, $dbkey ) );
132 * @covers \MediaWiki\Cache\LinkCache::addGoodLinkObjFromRow()
133 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkRow()
134 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkID()
135 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkFieldObj()
137 public function testAddGoodLinkObjWithAllParameters() {
138 $linkCache = $this->getServiceContainer()->getLinkCache();
140 $page = new PageReferenceValue( NS_USER
, __METHOD__
, PageReference
::LOCAL
);
141 $this->addGoodLinkObject( 8, $page, 18, 0, 118, CONTENT_MODEL_TEXT
, 'xyz' );
143 $row = $linkCache->getGoodLinkRow( $page->getNamespace(), $page->getDBkey() );
144 $this->assertEquals( 8, (int)$row->page_id
);
145 $this->assertSame( 8, $linkCache->getGoodLinkID( $page ) );
146 $this->assertSame( 8, $linkCache->getGoodLinkFieldObj( $page, 'id' ) );
150 $linkCache->getGoodLinkFieldObj( $page, 'length' )
154 $linkCache->getGoodLinkFieldObj( $page, 'redirect' )
158 $linkCache->getGoodLinkFieldObj( $page, 'revision' )
162 $linkCache->getGoodLinkFieldObj( $page, 'model' )
166 $linkCache->getGoodLinkFieldObj( $page, 'lang' )
171 * @covers \MediaWiki\Cache\LinkCache::addGoodLinkObjFromRow()
172 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkRow()
173 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkID()
174 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkFieldObj()
176 public function testAddGoodLinkObjFromRowWithMinimalParameters() {
177 $linkCache = $this->getServiceContainer()->getLinkCache();
179 $page = new PageReferenceValue( NS_USER
, __METHOD__
, PageReference
::LOCAL
);
181 $this->addGoodLinkObject( 8, $page );
185 'page_is_redirect' => 0,
187 'page_content_model' => null,
191 $actualRow = (array)$linkCache->getGoodLinkRow( $page->getNamespace(), $page->getDBkey() );
194 array_intersect_key( $actualRow, $expectedRow )
197 $this->assertSame( 8, $linkCache->getGoodLinkID( $page ) );
198 $this->assertSame( 8, $linkCache->getGoodLinkFieldObj( $page, 'id' ) );
202 $linkCache->getGoodLinkFieldObj( $page, 'length' )
206 $linkCache->getGoodLinkFieldObj( $page, 'redirect' )
210 $linkCache->getGoodLinkFieldObj( $page, 'revision' )
214 $linkCache->getGoodLinkFieldObj( $page, 'model' )
218 $linkCache->getGoodLinkFieldObj( $page, 'lang' )
223 * @covers \MediaWiki\Cache\LinkCache::addGoodLinkObjFromRow()
225 public function testAddGoodLinkObjFromRowWithInterwikiLink() {
226 $linkCache = $this->getServiceContainer()->getLinkCache();
228 $page = new TitleValue( NS_USER
, __METHOD__
, '', 'acme' );
230 $this->addGoodLinkObject( 8, $page );
232 $this->assertSame( 0, $linkCache->getGoodLinkID( $page ) );
236 * @dataProvider providePageAndLink
237 * @covers \MediaWiki\Cache\LinkCache::addBadLinkObj()
238 * @covers \MediaWiki\Cache\LinkCache::isBadLink()
239 * @covers \MediaWiki\Cache\LinkCache::clearLink()
241 public function testAddBadLinkObj( $key ) {
242 $linkCache = $this->getServiceContainer()->getLinkCache();
243 $this->assertFalse( $linkCache->isBadLink( $key ) );
245 $this->addGoodLinkObject( 17, $key );
247 $linkCache->addBadLinkObj( $key );
248 $this->assertTrue( $linkCache->isBadLink( $key ) );
249 $this->assertSame( 0, $linkCache->getGoodLinkID( $key ) );
251 $linkCache->clearLink( $key );
252 $this->assertFalse( $linkCache->isBadLink( $key ) );
256 * @covers \MediaWiki\Cache\LinkCache::addBadLinkObj()
258 public function testAddBadLinkObjWithInterwikiLink() {
259 $linkCache = $this->newLinkCache();
261 $page = new TitleValue( NS_USER
, __METHOD__
, '', 'acme' );
262 $linkCache->addBadLinkObj( $page );
264 $this->assertFalse( $linkCache->isBadLink( $page ) );
268 * @covers \MediaWiki\Cache\LinkCache::addLinkObj()
269 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkFieldObj
271 public function testAddLinkObj() {
272 $existing = $this->getExistingTestPage();
273 $missing = $this->getNonexistingTestPage();
275 $linkCache = $this->newLinkCache();
277 $linkCache->addLinkObj( $existing );
278 $linkCache->addLinkObj( $missing );
280 $this->assertTrue( $linkCache->isBadLink( $missing ) );
281 $this->assertFalse( $linkCache->isBadLink( $existing ) );
283 $this->assertSame( $existing->getId(), $linkCache->getGoodLinkID( $existing ) );
284 $this->assertTrue( $linkCache->isBadLink( $missing ) );
286 // Make sure nothing explodes when getting a field from a non-existing entry
287 $this->assertNull( $linkCache->getGoodLinkFieldObj( $missing, 'length' ) );
291 * @covers \MediaWiki\Cache\LinkCache::addLinkObj()
293 public function testAddLinkObjUsesCachedInfo() {
294 $existing = $this->getExistingTestPage();
295 $missing = $this->getNonexistingTestPage();
297 $fakeRow = $this->getPageRow( $existing->getId() +
100 );
299 $linkCache = $this->newLinkCache();
301 // pretend the existing page is missing, and the missing page exists
302 $linkCache->addGoodLinkObjFromRow( $missing, $fakeRow );
303 $linkCache->addBadLinkObj( $existing );
305 // the LinkCache should use the cached info and not look into the database
306 $this->assertSame( (int)$fakeRow->page_id
, $linkCache->addLinkObj( $missing ) );
307 $this->assertSame( 0, $linkCache->addLinkObj( $existing ) );
309 // now set the "read latest" flag and try again
310 $flags = IDBAccessObject
::READ_LATEST
;
311 $this->assertSame( 0, $linkCache->addLinkObj( $missing, $flags ) );
312 $this->assertSame( $existing->getId(), $linkCache->addLinkObj( $existing, $flags ) );
316 * @covers \MediaWiki\Cache\LinkCache::addLinkObj()
318 public function testAddLinkObjUsesWANCache() {
319 // For some namespaces we cache data (Template, File, etc)
320 $existing = $this->getExistingTestPage( Title
::makeTitle( NS_TEMPLATE
, __METHOD__
) );
321 $wanCache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
322 $linkCache = $this->newLinkCache( $wanCache );
324 // load the page row into the cache
325 $linkCache->addLinkObj( $existing );
327 // replace real row data with fake, and assert that it gets used
328 $key = $wanCache->makeKey( 'page', $existing->getNamespace(), sha1( $existing->getDBkey() ) );
329 $fakeRow = $this->getPageRow( $existing->getId() +
100 );
330 $wanCache->set( $key, $fakeRow );
331 // clear in-class cache
332 $linkCache->clearLink( $existing );
333 $this->assertSame( (int)$fakeRow->page_id
, $linkCache->addLinkObj( $existing ) );
335 // set the "read latest" flag and try again
336 $flags = IDBAccessObject
::READ_LATEST
;
337 $this->assertSame( $existing->getId(), $linkCache->addLinkObj( $existing, $flags ) );
340 public function testFalsyPageName() {
341 $linkCache = $this->newLinkCache();
343 // The stringified value is "0", which is falsy in PHP!
344 $link = new TitleValue( NS_MAIN
, '0' );
346 $linkCache->addBadLinkObj( $link );
347 $this->assertTrue( $linkCache->isBadLink( $link ) );
349 $row = $this->getPageRow();
350 $linkCache->addGoodLinkObjFromRow( $link, $row );
351 $this->assertGreaterThan( 0, $linkCache->getGoodLinkID( $link ) );
353 $this->assertSame( $row, $linkCache->getGoodLinkRow( NS_MAIN
, '0' ) );
356 public function testClearBadLinkWithString() {
357 $linkCache = $this->newLinkCache();
358 $linkCache->clearBadLink( 'Xyzzy' );
359 $this->addToAssertionCount( 1 );
362 public function testIsBadLinkWithString() {
363 $linkCache = $this->newLinkCache();
364 $this->assertFalse( $linkCache->isBadLink( 'Xyzzy' ) );
367 public function testGetGoodLinkIdWithString() {
368 $linkCache = $this->newLinkCache();
369 $this->assertSame( 0, $linkCache->getGoodLinkID( 'Xyzzy' ) );
372 public static function provideInvalidPageParams() {
374 'empty' => [ NS_MAIN
, '' ],
375 'bad chars' => [ NS_MAIN
, '_|_' ],
376 'empty in namspace' => [ NS_USER
, '' ],
377 'special' => [ NS_SPECIAL
, 'RecentChanges' ],
382 * @dataProvider provideInvalidPageParams
383 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkRow()
385 public function testGetGoodLinkRowWithBadParams( $ns, $dbkey ) {
386 $linkCache = $this->newLinkCache();
387 $this->assertNull( $linkCache->getGoodLinkRow( $ns, $dbkey ) );
390 public function getRowIfExisting( $db, $ns, $dbkey, $queryOptions ) {
391 if ( $dbkey === 'Existing' ) {
392 return $this->getPageRow();
399 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkRow()
400 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkFieldObj
402 public function testGetGoodLinkRow() {
403 $existing = new TitleValue( NS_MAIN
, 'Existing' );
404 $missing = new TitleValue( NS_MAIN
, 'Missing' );
406 $linkCache = $this->newLinkCache();
407 $callback = [ $this, 'getRowIfExisting' ];
409 $linkCache->getGoodLinkRow( $existing->getNamespace(), $existing->getDBkey(), $callback );
410 $linkCache->getGoodLinkRow( $missing->getNamespace(), $missing->getDBkey(), $callback );
412 $this->assertTrue( $linkCache->isBadLink( $missing ) );
413 $this->assertFalse( $linkCache->isBadLink( $existing ) );
415 $this->assertGreaterThan( 0, $linkCache->getGoodLinkID( $existing ) );
416 $this->assertTrue( $linkCache->isBadLink( $missing ) );
418 // Make sure nothing explodes when getting a field from a non-existing entry
419 $this->assertNull( $linkCache->getGoodLinkFieldObj( $missing, 'length' ) );
423 * @covers \MediaWiki\Cache\LinkCache::getGoodLinkRow()
425 public function testGetGoodLinkRowUsesCachedInfo() {
426 $existing = new TitleValue( NS_MAIN
, 'Existing' );
427 $missing = new TitleValue( NS_MAIN
, 'Missing' );
428 $callback = [ $this, 'getRowIfExisting' ];
430 $existingRow = $this->getPageRow( 0 );
431 $fakeRow = $this->getPageRow( 3 );
433 $linkCache = $this->newLinkCache();
435 // pretend the existing page is missing, and the missing page exists
436 $linkCache->addGoodLinkObjFromRow( $missing, $fakeRow );
437 $linkCache->addBadLinkObj( $existing );
439 // the LinkCache should use the cached info and not look into the database
442 $linkCache->getGoodLinkRow( $missing->getNamespace(), $missing->getDBkey(), $callback )
445 $linkCache->getGoodLinkRow( $existing->getNamespace(), $existing->getDBkey(), $callback )
448 // now set the "read latest" flag and try again
449 $flags = IDBAccessObject
::READ_LATEST
;
451 $linkCache->getGoodLinkRow(
452 $missing->getNamespace(),
453 $missing->getDBkey(),
460 $linkCache->getGoodLinkRow(
461 $existing->getNamespace(),
462 $existing->getDBkey(),
468 // pretend again that the missing page exists, but pretend even harder
469 $linkCache->addGoodLinkObjFromRow( $missing, $fakeRow, IDBAccessObject
::READ_LATEST
);
471 // the LinkCache should use the cached info and not look into the database
474 $linkCache->getGoodLinkRow( $missing->getNamespace(), $missing->getDBkey(), $callback )
477 // now set the "read latest" flag and try again
478 $flags = IDBAccessObject
::READ_LATEST
;
481 $linkCache->getGoodLinkRow(
482 $missing->getNamespace(),
483 $missing->getDBkey(),