1<?php 2 3use MediaWiki\Page\PageReference; 4use MediaWiki\Page\PageReferenceValue; 5 6/** 7 * @group Database 8 * @group Cache 9 * @covers LinkCache 10 */ 11class LinkCacheTest extends MediaWikiIntegrationTestCase { 12 use LinkCacheTestTrait; 13 14 private function newLinkCache( WANObjectCache $wanCache = null ) { 15 if ( !$wanCache ) { 16 $wanCache = new WANObjectCache( [ 'cache' => new EmptyBagOStuff() ] ); 17 } 18 19 return new LinkCache( 20 $this->getServiceContainer()->getTitleFormatter(), 21 $wanCache, 22 $this->getServiceContainer()->getNamespaceInfo(), 23 $this->getServiceContainer()->getDBLoadBalancer() 24 ); 25 } 26 27 public function providePageAndLink() { 28 return [ 29 [ new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ) ], 30 [ new TitleValue( NS_USER, __METHOD__ ) ] 31 ]; 32 } 33 34 public function providePageAndLinkAndArray() { 35 return [ 36 [ new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ) ], 37 [ new TitleValue( NS_USER, __METHOD__ ) ], 38 [ [ 'page_namespace' => NS_USER, 'page_title' => __METHOD__ ] ], 39 ]; 40 } 41 42 private function getPageRow( $offset = 0 ) { 43 return (object)[ 44 'page_id' => 8 + $offset, 45 'page_len' => 18, 46 'page_is_redirect' => 0, 47 'page_latest' => 118 + $offset, 48 'page_content_model' => CONTENT_MODEL_TEXT, 49 'page_lang' => 'xyz', 50 'page_restrictions' => 'test', 51 'page_touched' => '20200202020202', 52 ]; 53 } 54 55 /** 56 * @dataProvider providePageAndLinkAndArray 57 * @covers LinkCache::addGoodLinkObjFromRow() 58 * @covers LinkCache::getGoodLinkRow() 59 * @covers LinkCache::getGoodLinkID() 60 * @covers LinkCache::getGoodLinkFieldObj() 61 * @covers LinkCache::clearLink() 62 */ 63 public function testAddGoodLinkObjFromRow( $page ) { 64 $linkCache = $this->newLinkCache(); 65 66 $row = $this->getPageRow(); 67 68 $dbkey = is_array( $page ) ? $page['page_title'] : $page->getDBkey(); 69 $ns = is_array( $page ) ? $page['page_namespace'] : $page->getNamespace(); 70 71 $linkCache->addBadLinkObj( $page ); 72 $linkCache->addGoodLinkObjFromRow( $page, $row ); 73 74 $this->assertEquals( 75 $row, 76 $linkCache->getGoodLinkRow( $ns, $dbkey ) 77 ); 78 79 $this->assertSame( $row->page_id, $linkCache->getGoodLinkID( $page ) ); 80 $this->assertFalse( $linkCache->isBadLink( $page ) ); 81 82 $this->assertSame( 83 $row->page_id, 84 $linkCache->getGoodLinkFieldObj( $page, 'id' ) 85 ); 86 $this->assertSame( 87 $row->page_len, 88 $linkCache->getGoodLinkFieldObj( $page, 'length' ) 89 ); 90 $this->assertSame( 91 $row->page_is_redirect, 92 $linkCache->getGoodLinkFieldObj( $page, 'redirect' ) 93 ); 94 $this->assertSame( 95 $row->page_latest, 96 $linkCache->getGoodLinkFieldObj( $page, 'revision' ) 97 ); 98 $this->assertSame( 99 $row->page_content_model, 100 $linkCache->getGoodLinkFieldObj( $page, 'model' ) 101 ); 102 $this->assertSame( 103 $row->page_lang, 104 $linkCache->getGoodLinkFieldObj( $page, 'lang' ) 105 ); 106 $this->assertSame( 107 $row->page_restrictions, 108 $linkCache->getGoodLinkFieldObj( $page, 'restrictions' ) 109 ); 110 111 $this->assertEquals( 112 $row, 113 $linkCache->getGoodLinkRow( $ns, $dbkey ) 114 ); 115 116 $linkCache->clearBadLink( $page ); 117 $this->assertNotNull( $linkCache->getGoodLinkID( $page ) ); 118 $this->assertNotNull( $linkCache->getGoodLinkFieldObj( $page, 'length' ) ); 119 120 $linkCache->clearLink( $page ); 121 $this->assertSame( 0, $linkCache->getGoodLinkID( $page ) ); 122 $this->assertNull( $linkCache->getGoodLinkFieldObj( $page, 'length' ) ); 123 $this->assertNull( $linkCache->getGoodLinkRow( $ns, $dbkey ) ); 124 } 125 126 /** 127 * @covers LinkCache::addGoodLinkObjFromRow() 128 * @covers LinkCache::getGoodLinkRow() 129 * @covers LinkCache::getGoodLinkID() 130 * @covers LinkCache::getGoodLinkFieldObj() 131 */ 132 public function testAddGoodLinkObjWithAllParameters() { 133 $linkCache = $this->getServiceContainer()->getLinkCache(); 134 135 $page = new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ); 136 $this->addGoodLinkObject( 8, $page, 18, 0, 118, CONTENT_MODEL_TEXT, 'xyz' ); 137 138 $row = $linkCache->getGoodLinkRow( $page->getNamespace(), $page->getDBkey() ); 139 $this->assertEquals( 8, (int)$row->page_id ); 140 $this->assertSame( 8, $linkCache->getGoodLinkID( $page ) ); 141 $this->assertSame( 8, $linkCache->getGoodLinkFieldObj( $page, 'id' ) ); 142 143 $this->assertSame( 144 18, 145 $linkCache->getGoodLinkFieldObj( $page, 'length' ) 146 ); 147 $this->assertSame( 148 0, 149 $linkCache->getGoodLinkFieldObj( $page, 'redirect' ) 150 ); 151 $this->assertSame( 152 118, 153 $linkCache->getGoodLinkFieldObj( $page, 'revision' ) 154 ); 155 $this->assertSame( 156 CONTENT_MODEL_TEXT, 157 $linkCache->getGoodLinkFieldObj( $page, 'model' ) 158 ); 159 $this->assertSame( 160 'xyz', 161 $linkCache->getGoodLinkFieldObj( $page, 'lang' ) 162 ); 163 } 164 165 /** 166 * @covers LinkCache::addGoodLinkObjFromRow() 167 * @covers LinkCache::getGoodLinkRow() 168 * @covers LinkCache::getGoodLinkID() 169 * @covers LinkCache::getGoodLinkFieldObj() 170 */ 171 public function testAddGoodLinkObjFromRowWithMinimalParameters() { 172 $linkCache = $this->getServiceContainer()->getLinkCache(); 173 174 $page = new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ); 175 176 $this->addGoodLinkObject( 8, $page ); 177 $expectedRow = [ 178 'page_id' => 8, 179 'page_len' => -1, 180 'page_is_redirect' => 0, 181 'page_latest' => 0, 182 'page_content_model' => null, 183 'page_lang' => null, 184 'page_restrictions' => null 185 ]; 186 187 $actualRow = (array)$linkCache->getGoodLinkRow( $page->getNamespace(), $page->getDBkey() ); 188 $this->assertEquals( 189 $expectedRow, 190 array_intersect_key( $actualRow, $expectedRow ) 191 ); 192 193 $this->assertSame( 8, $linkCache->getGoodLinkID( $page ) ); 194 $this->assertSame( 8, $linkCache->getGoodLinkFieldObj( $page, 'id' ) ); 195 196 $this->assertSame( 197 -1, 198 $linkCache->getGoodLinkFieldObj( $page, 'length' ) 199 ); 200 $this->assertSame( 201 0, 202 $linkCache->getGoodLinkFieldObj( $page, 'redirect' ) 203 ); 204 $this->assertSame( 205 0, 206 $linkCache->getGoodLinkFieldObj( $page, 'revision' ) 207 ); 208 $this->assertSame( 209 null, 210 $linkCache->getGoodLinkFieldObj( $page, 'model' ) 211 ); 212 $this->assertSame( 213 null, 214 $linkCache->getGoodLinkFieldObj( $page, 'lang' ) 215 ); 216 } 217 218 /** 219 * @covers LinkCache::addGoodLinkObjFromRow() 220 */ 221 public function testAddGoodLinkObjFromRowWithInterwikiLink() { 222 $linkCache = $this->getServiceContainer()->getLinkCache(); 223 224 $page = new TitleValue( NS_USER, __METHOD__, '', 'acme' ); 225 226 $this->addGoodLinkObject( 8, $page ); 227 228 $this->assertSame( 0, $linkCache->getGoodLinkID( $page ) ); 229 } 230 231 /** 232 * @dataProvider providePageAndLink 233 * @covers LinkCache::addBadLinkObj() 234 * @covers LinkCache::isBadLink() 235 * @covers LinkCache::clearLink() 236 */ 237 public function testAddBadLinkObj( $key ) { 238 $linkCache = $this->getServiceContainer()->getLinkCache(); 239 $this->assertFalse( $linkCache->isBadLink( $key ) ); 240 241 $this->addGoodLinkObject( 17, $key ); 242 243 $linkCache->addBadLinkObj( $key ); 244 $this->assertTrue( $linkCache->isBadLink( $key ) ); 245 $this->assertSame( 0, $linkCache->getGoodLinkID( $key ) ); 246 247 $linkCache->clearLink( $key ); 248 $this->assertFalse( $linkCache->isBadLink( $key ) ); 249 } 250 251 /** 252 * @covers LinkCache::addBadLinkObj() 253 */ 254 public function testAddBadLinkObjWithInterwikiLink() { 255 $linkCache = $this->newLinkCache(); 256 257 $page = new TitleValue( NS_USER, __METHOD__, '', 'acme' ); 258 $linkCache->addBadLinkObj( $page ); 259 260 $this->assertFalse( $linkCache->isBadLink( $page ) ); 261 } 262 263 /** 264 * @covers LinkCache::addLinkObj() 265 * @covers LinkCache::getGoodLinkFieldObj 266 */ 267 public function testAddLinkObj() { 268 $existing = $this->getExistingTestPage(); 269 $missing = $this->getNonexistingTestPage(); 270 271 $linkCache = $this->newLinkCache(); 272 273 $linkCache->addLinkObj( $existing ); 274 $linkCache->addLinkObj( $missing ); 275 276 $this->assertTrue( $linkCache->isBadLink( $missing ) ); 277 $this->assertFalse( $linkCache->isBadLink( $existing ) ); 278 279 $this->assertSame( $existing->getId(), $linkCache->getGoodLinkID( $existing ) ); 280 $this->assertTrue( $linkCache->isBadLink( $missing ) ); 281 282 // Make sure nothing explodes when getting a field from a non-existing entry 283 $this->assertNull( $linkCache->getGoodLinkFieldObj( $missing, 'length' ) ); 284 } 285 286 /** 287 * @covers LinkCache::addLinkObj() 288 */ 289 public function testAddLinkObjUsesCachedInfo() { 290 $existing = $this->getExistingTestPage(); 291 $missing = $this->getNonexistingTestPage(); 292 293 $fakeRow = $this->getPageRow( $existing->getId() + 100 ); 294 295 $linkCache = $this->newLinkCache(); 296 297 // pretend the existing page is missing, and the missing page exists 298 $linkCache->addGoodLinkObjFromRow( $missing, $fakeRow ); 299 $linkCache->addBadLinkObj( $existing ); 300 301 // the LinkCache should use the cached info and not look into the database 302 $this->assertSame( (int)$fakeRow->page_id, $linkCache->addLinkObj( $missing ) ); 303 $this->assertSame( 0, $linkCache->addLinkObj( $existing ) ); 304 305 // now set the "read latest" flag and try again 306 $flags = IDBAccessObject::READ_LATEST; 307 $this->assertSame( 0, $linkCache->addLinkObj( $missing, $flags ) ); 308 $this->assertSame( $existing->getId(), $linkCache->addLinkObj( $existing, $flags ) ); 309 } 310 311 /** 312 * @covers LinkCache::addLinkObj() 313 * @covers LinkCache::getMutableCacheKeys() 314 */ 315 public function testAddLinkObjUsesWANCache() { 316 // Pages in some namespaces use the WAN cache: Template, File, Category, MediaWiki 317 $existing = $this->getExistingTestPage( Title::makeTitle( NS_TEMPLATE, __METHOD__ ) ); 318 319 $fakeRow = $this->getPageRow( $existing->getId() + 100 ); 320 321 $cache = new HashBagOStuff(); 322 $wanCache = new WANObjectCache( [ 'cache' => $cache ] ); 323 $linkCache = $this->newLinkCache( $wanCache ); 324 325 // load the page row into the cache 326 $linkCache->addLinkObj( $existing ); 327 328 $keys = $linkCache->getMutableCacheKeys( $wanCache, $existing ); 329 $this->assertNotEmpty( $keys ); 330 331 foreach ( $keys as $key ) { 332 $this->assertNotFalse( $wanCache->get( $key ) ); 333 } 334 335 // replace real row data with fake, and assert that it gets used 336 $wanCache->set( $key, $fakeRow ); 337 $linkCache->clearLink( $existing ); // clear local cache 338 $this->assertSame( (int)$fakeRow->page_id, $linkCache->addLinkObj( $existing ) ); 339 340 // set the "read latest" flag and try again 341 $flags = IDBAccessObject::READ_LATEST; 342 $this->assertSame( $existing->getId(), $linkCache->addLinkObj( $existing, $flags ) ); 343 } 344 345 public function testFalsyPageName() { 346 $linkCache = $this->newLinkCache(); 347 348 // The stringified value is "0", which is falsy in PHP! 349 $link = new TitleValue( NS_MAIN, '0' ); 350 351 $linkCache->addBadLinkObj( $link ); 352 $this->assertTrue( $linkCache->isBadLink( $link ) ); 353 354 $row = $this->getPageRow(); 355 $linkCache->addGoodLinkObjFromRow( $link, $row ); 356 $this->assertGreaterThan( 0, $linkCache->getGoodLinkID( $link ) ); 357 358 $this->assertSame( $row, $linkCache->getGoodLinkRow( NS_MAIN, '0' ) ); 359 } 360 361 public function testClearBadLinkWithString() { 362 $linkCache = $this->newLinkCache(); 363 $linkCache->clearBadLink( 'Xyzzy' ); 364 $this->addToAssertionCount( 1 ); 365 } 366 367 public function testIsBadLinkWithString() { 368 $linkCache = $this->newLinkCache(); 369 $this->assertFalse( $linkCache->isBadLink( 'Xyzzy' ) ); 370 } 371 372 public function testGetGoodLinkIdWithString() { 373 $linkCache = $this->newLinkCache(); 374 $this->assertSame( 0, $linkCache->getGoodLinkID( 'Xyzzy' ) ); 375 } 376 377 public function provideInvalidPageParams() { 378 return [ 379 'empty' => [ NS_MAIN, '' ], 380 'bad chars' => [ NS_MAIN, '_|_' ], 381 'empty in namspace' => [ NS_USER, '' ], 382 'special' => [ NS_SPECIAL, 'RecentChanges' ], 383 ]; 384 } 385 386 /** 387 * @dataProvider provideInvalidPageParams 388 * @covers LinkCache::getGoodLinkRow() 389 */ 390 public function testGetGoodLinkRowWithBadParams( $ns, $dbkey ) { 391 $linkCache = $this->newLinkCache(); 392 $this->assertNull( $linkCache->getGoodLinkRow( $ns, $dbkey ) ); 393 } 394 395 public function getRowIfExisting( $db, $ns, $dbkey, $queryOptions ) { 396 if ( $dbkey === 'Existing' ) { 397 return $this->getPageRow(); 398 } 399 400 return null; 401 } 402 403 /** 404 * @covers LinkCache::getGoodLinkRow() 405 * @covers LinkCache::getGoodLinkFieldObj 406 */ 407 public function testGetGoodLinkRow() { 408 $existing = new TitleValue( NS_MAIN, 'Existing' ); 409 $missing = new TitleValue( NS_MAIN, 'Missing' ); 410 411 $linkCache = $this->newLinkCache(); 412 $callback = [ $this, 'getRowIfExisting' ]; 413 414 $linkCache->getGoodLinkRow( $existing->getNamespace(), $existing->getDBkey(), $callback ); 415 $linkCache->getGoodLinkRow( $missing->getNamespace(), $missing->getDBkey(), $callback ); 416 417 $this->assertTrue( $linkCache->isBadLink( $missing ) ); 418 $this->assertFalse( $linkCache->isBadLink( $existing ) ); 419 420 $this->assertGreaterThan( 0, $linkCache->getGoodLinkID( $existing ) ); 421 $this->assertTrue( $linkCache->isBadLink( $missing ) ); 422 423 // Make sure nothing explodes when getting a field from a non-existing entry 424 $this->assertNull( $linkCache->getGoodLinkFieldObj( $missing, 'length' ) ); 425 } 426 427 /** 428 * @covers LinkCache::getGoodLinkRow() 429 */ 430 public function testGetGoodLinkRowUsesCachedInfo() { 431 $existing = new TitleValue( NS_MAIN, 'Existing' ); 432 $missing = new TitleValue( NS_MAIN, 'Missing' ); 433 $callback = [ $this, 'getRowIfExisting' ]; 434 435 $existingRow = $this->getPageRow( 0 ); 436 $fakeRow = $this->getPageRow( 3 ); 437 438 $linkCache = $this->newLinkCache(); 439 440 // pretend the existing page is missing, and the missing page exists 441 $linkCache->addGoodLinkObjFromRow( $missing, $fakeRow ); 442 $linkCache->addBadLinkObj( $existing ); 443 444 // the LinkCache should use the cached info and not look into the database 445 $this->assertSame( 446 $fakeRow, 447 $linkCache->getGoodLinkRow( $missing->getNamespace(), $missing->getDBkey(), $callback ) 448 ); 449 $this->assertNull( 450 $linkCache->getGoodLinkRow( $existing->getNamespace(), $existing->getDBkey(), $callback ) 451 ); 452 453 // now set the "read latest" flag and try again 454 $flags = IDBAccessObject::READ_LATEST; 455 $this->assertNull( 456 $linkCache->getGoodLinkRow( 457 $missing->getNamespace(), 458 $missing->getDBkey(), 459 $callback, 460 $flags 461 ) 462 ); 463 $this->assertEquals( 464 $existingRow, 465 $linkCache->getGoodLinkRow( 466 $existing->getNamespace(), 467 $existing->getDBkey(), 468 $callback, 469 $flags 470 ) 471 ); 472 473 // pretend again that the missing page exists, but pretend even harder 474 $linkCache->addGoodLinkObjFromRow( $missing, $fakeRow, IDBAccessObject::READ_LATEST ); 475 476 // the LinkCache should use the cached info and not look into the database 477 $this->assertSame( 478 $fakeRow, 479 $linkCache->getGoodLinkRow( $missing->getNamespace(), $missing->getDBkey(), $callback ) 480 ); 481 482 // now set the "read latest" flag and try again 483 $flags = IDBAccessObject::READ_LATEST; 484 $this->assertEquals( 485 $fakeRow, 486 $linkCache->getGoodLinkRow( 487 $missing->getNamespace(), 488 $missing->getDBkey(), 489 $callback, 490 $flags 491 ) 492 ); 493 } 494 495 /** 496 * @covers LinkCache::getGoodLinkRow() 497 * @covers LinkCache::getMutableCacheKeys() 498 */ 499 public function testGetGoodLinkRowUsesWANCache() { 500 // Pages in some namespaces use the WAN cache: Template, File, Category, MediaWiki 501 $existing = new TitleValue( NS_TEMPLATE, 'Existing' ); 502 $callback = [ $this, 'getRowIfExisting' ]; 503 504 $existingRow = $this->getPageRow( 0 ); 505 $fakeRow = $this->getPageRow( 3 ); 506 507 $cache = new HashBagOStuff(); 508 $wanCache = new WANObjectCache( [ 'cache' => $cache ] ); 509 $linkCache = $this->newLinkCache( $wanCache ); 510 511 // load the page row into the cache 512 $linkCache->getGoodLinkRow( $existing->getNamespace(), $existing->getDBkey(), $callback ); 513 514 $keys = $linkCache->getMutableCacheKeys( $wanCache, $existing ); 515 $this->assertNotEmpty( $keys ); 516 517 foreach ( $keys as $key ) { 518 $this->assertNotFalse( $wanCache->get( $key ) ); 519 } 520 521 // replace real row data with fake, and assert that it gets used 522 $wanCache->set( $key, $fakeRow ); 523 $linkCache->clearLink( $existing ); // clear local cache 524 $this->assertSame( 525 $fakeRow, 526 $linkCache->getGoodLinkRow( 527 $existing->getNamespace(), 528 $existing->getDBkey(), 529 $callback 530 ) 531 ); 532 533 // set the "read latest" flag and try again 534 $flags = IDBAccessObject::READ_LATEST; 535 $this->assertEquals( 536 $existingRow, 537 $linkCache->getGoodLinkRow( 538 $existing->getNamespace(), 539 $existing->getDBkey(), 540 $callback, 541 $flags 542 ) 543 ); 544 } 545} 546