diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index a5c0d6b1f57..2f76ebd63ba 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -674,12 +674,14 @@ return [ $dbLoadBalancer = $services->isServiceDisabled( 'DBLoadBalancer' ) ? null : $services->getDBLoadBalancer(); - return new LinkCache( + $linkCache = new LinkCache( $services->getTitleFormatter(), $services->getMainWANObjectCache(), $services->getNamespaceInfo(), $dbLoadBalancer ); + $linkCache->setLogger( LoggerFactory::getInstance( 'LinkCache' ) ); + return $linkCache; }, 'LinkRenderer' => static function ( MediaWikiServices $services ) : LinkRenderer { diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php index a3b1b2a841c..7c766902f6c 100644 --- a/includes/cache/LinkCache.php +++ b/includes/cache/LinkCache.php @@ -23,6 +23,11 @@ use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; +use MediaWiki\Page\PageIdentity; +use MediaWiki\Page\PageReference; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\ILoadBalancer; @@ -32,7 +37,7 @@ use Wikimedia\Rdbms\ILoadBalancer; * * @ingroup Cache */ -class LinkCache { +class LinkCache implements LoggerAwareInterface { /** @var MapCacheLRU */ private $goodLinks; /** @var MapCacheLRU */ @@ -52,6 +57,9 @@ class LinkCache { /** @var ILoadBalancer|null */ private $loadBalancer; + /** @var LoggerInterface */ + private $logger; + /** * How many Titles to store. There are two caches, so the amount actually * stored in memory can be up to twice this. @@ -80,6 +88,14 @@ class LinkCache { $this->titleFormatter = $titleFormatter; $this->nsInfo = $nsInfo; $this->loadBalancer = $loadBalancer; + $this->logger = new NullLogger(); + } + + /** + * @param LoggerInterface $logger + */ + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; } /** @@ -110,11 +126,71 @@ class LinkCache { } /** - * @param string $title Prefixed DB key - * @return int Page ID or zero + * @param LinkTarget|PageReference|string $page + * @param bool $passThrough Return $page if $page is a string + * + * @return ?string the cache key */ - public function getGoodLinkID( $title ) { - $info = $this->goodLinks->get( $title ); + private function getCacheKey( $page, $passThrough = false ) { + if ( is_string( $page ) ) { + if ( $passThrough ) { + return $page; + } else { + throw new InvalidArgumentException( 'They key may not be given as a string here' ); + } + } + + if ( $page instanceof PageReference && $page->getWikiId() !== PageReference::LOCAL ) { + // No cross-wiki support yet. Perhaps LinkCache can become wiki-aware in the future. + $this->logger->info( + 'cross-wiki page reference', + [ 'page-wiki' => $page->getWikiId(), 'page-reference' => $page ] + ); + return null; + } + + if ( $page instanceof PageIdentity && !$page->canExist() ) { + // Non-proper page, perhaps a special page or interwiki link or relative section link. + $this->logger->warning( + 'non-proper page reference', + [ 'page-reference' => $page ] + ); + return null; + } + + if ( $page instanceof LinkTarget + && ( $page->isExternal() || $page->getText() === '' || $page->getNamespace() < 0 ) + ) { + // Interwiki link or relative section link. These do not have a page ID, so they + // can neither be "good" nor "bad" in the sense of this class. + $this->logger->warning( + 'link to non-proper page', + [ 'page-link' => $page ] + ); + return null; + } + + return $this->titleFormatter->getPrefixedDBkey( $page ); + } + + /** + * Returns the ID of the given page, if information about this page has been cached. + * + * @param LinkTarget|PageReference|string $page The page to get the ID for, + * as an object or a prefixed DB key. + * In MediaWiki 1.36 and earlier, only a string was accepted. + * @return int Page ID, or zero if the page was not cached or does not exist or is not a + * proper page (e.g. a special page or an interwiki link). + */ + public function getGoodLinkID( $page ) { + $key = $this->getCacheKey( $page, true ); + + if ( $key === null ) { + return 0; + } + + $info = $this->goodLinks->get( $key ); + if ( !$info ) { return 0; } @@ -122,15 +198,22 @@ class LinkCache { } /** - * Get a field of a title object from cache. + * Get a field of a page from the cache. + * * If this link is not a cached good title, it will return NULL. - * @param LinkTarget $target - * @param string $field ('length','redirect','revision','model') - * @return string|int|null + * @param LinkTarget|PageReference $page The page to get cached info for. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. + * @param string $field ( 'id', 'length', 'redirect', 'revision', 'model', 'lang', 'restrictions' ) + * @return string|int|null The field value, or null if the page was not cached or does not exist + * or is not a proper page (e.g. a special page or interwiki link). */ - public function getGoodLinkFieldObj( LinkTarget $target, $field ) { - $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); - $info = $this->goodLinks->get( $dbkey ); + public function getGoodLinkFieldObj( $page, string $field ) { + $key = $this->getCacheKey( $page ); + if ( $key === null ) { + return null; + } + + $info = $this->goodLinks->get( $key ); if ( !$info ) { return null; } @@ -138,30 +221,43 @@ class LinkCache { } /** - * @param string $title Prefixed DB key - * @return bool + * Returns true if the fact that this page does not exist had been added to the cache. + * + * @param LinkTarget|PageReference|string $page The page to get cached info for, + * as an object or a prefixed DB key. + * In MediaWiki 1.36 and earlier, only a string was accepted. + * @return bool True if the page is known to not exist. */ - public function isBadLink( $title ) { - // Use get() to ensure it records as used for LRU. - return $this->badLinks->has( $title ); + public function isBadLink( $page ) { + $key = $this->getCacheKey( $page, true ); + + return $key !== null && $this->badLinks->has( $key ); } /** - * Add a link for the title to the link cache + * Add information about an existing page to the cache. + * + * @see addGoodLinkObjFromRow() * * @param int $id Page's ID - * @param LinkTarget $target + * @param LinkTarget|PageReference $page The page to set cached info for. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. * @param int $len Text's length * @param int|null $redir Whether the page is a redirect * @param int $revision Latest revision's ID * @param string|null $model Latest revision's content model ID * @param string|null $lang Language code of the page, if not the content language */ - public function addGoodLinkObj( $id, LinkTarget $target, $len = -1, $redir = null, + public function addGoodLinkObj( $id, $page, $len = -1, $redir = null, $revision = 0, $model = null, $lang = null ) { - $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); - $this->goodLinks->set( $dbkey, [ + $key = $this->getCacheKey( $page ); + + if ( $key === null ) { + return; + } + + $this->goodLinks->set( $key, [ 'id' => (int)$id, 'length' => (int)$len, 'redirect' => (int)$redir, @@ -170,18 +266,26 @@ class LinkCache { 'lang' => $lang ? (string)$lang : null, 'restrictions' => null ] ); + $this->badLinks->clear( $key ); } /** * Same as above with better interface. * @since 1.19 - * @param LinkTarget $target - * @param stdClass $row Object which has the fields page_id, page_is_redirect, - * page_latest and page_content_model + * + * @param LinkTarget|PageReference $page The page to set cached info for. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. + * @param stdClass $row Object which has all fields returned by getSelectFields(). + * */ - public function addGoodLinkObjFromRow( LinkTarget $target, $row ) { - $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); - $this->goodLinks->set( $dbkey, [ + public function addGoodLinkObjFromRow( $page, stdClass $row ) { + $key = $this->getCacheKey( $page ); + + if ( $key === null ) { + return; + } + + $this->goodLinks->set( $key, [ 'id' => intval( $row->page_id ), 'length' => intval( $row->page_len ), 'redirect' => intval( $row->page_is_redirect ), @@ -196,32 +300,45 @@ class LinkCache { ? strval( $row->page_restrictions ) : null ] ); + $this->badLinks->clear( $key ); } /** - * @param LinkTarget $target + * @param LinkTarget|PageReference $page The page to set cached info for. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. */ - public function addBadLinkObj( LinkTarget $target ) { - $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); - if ( !$this->isBadLink( $dbkey ) ) { - $this->badLinks->set( $dbkey, 1 ); + public function addBadLinkObj( $page ) { + $key = $this->getCacheKey( $page ); + if ( $key !== null && !$this->isBadLink( $key ) ) { + $this->badLinks->set( $key, 1 ); + $this->goodLinks->clear( $key ); } } /** - * @param string $title Prefixed DB key + * @param LinkTarget|PageReference|string $page The page to clear cached info for, + * as an object or a prefixed DB key. + * In MediaWiki 1.36 and earlier, only a string was accepted. */ - public function clearBadLink( $title ) { - $this->badLinks->clear( $title ); + public function clearBadLink( $page ) { + $key = $this->getCacheKey( $page, true ); + + if ( $key !== null ) { + $this->badLinks->clear( $key ); + } } /** - * @param LinkTarget $target + * @param LinkTarget|PageReference $page The page to clear cached info for. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. */ - public function clearLink( LinkTarget $target ) { - $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); - $this->badLinks->clear( $dbkey ); - $this->goodLinks->clear( $dbkey ); + public function clearLink( $page ) { + $key = $this->getCacheKey( $page ); + + if ( $key !== null ) { + $this->badLinks->clear( $key ); + $this->goodLinks->clear( $key ); + } } /** @@ -250,25 +367,37 @@ class LinkCache { } /** - * Add a title to the link cache, return the page_id or zero if non-existent + * Add a title to the link cache, return the page_id or zero if non-existent. + * This causes the link to be looked up in the database if it is not yet cached. + * + * @param LinkTarget|PageReference $page The page to load. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. * - * @param LinkTarget $nt LinkTarget object to add * @return int Page ID or zero */ - public function addLinkObj( LinkTarget $nt ) { - $key = $this->titleFormatter->getPrefixedDBkey( $nt ); - if ( $this->isBadLink( $key ) || $nt->isExternal() || $nt->getNamespace() < 0 ) { - return 0; - } - $id = $this->getGoodLinkID( $key ); - if ( $id != 0 ) { - return $id; + public function addLinkObj( $page ) { + if ( $page instanceof LinkTarget ) { + $nt = $page; + } else { + $nt = TitleValue::castPageToLinkTarget( $page ); } - if ( $key === '' ) { + $key = $this->getCacheKey( $nt ); + if ( $key === null ) { return 0; } + if ( !$this->mForUpdate ) { + $id = $this->getGoodLinkID( $key ); + if ( $id != 0 ) { + return $id; + } + + if ( $this->isBadLink( $key ) ) { + return 0; + } + } + // Only query database, when load balancer is provided by service wiring // This maybe not happen when running as part of the installer if ( $this->loadBalancer === null ) { @@ -312,20 +441,32 @@ class LinkCache { /** * @param WANObjectCache $cache - * @param LinkTarget $t + * @param LinkTarget|Pagereference $page + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. * @return string[] * @since 1.28 */ - public function getMutableCacheKeys( WANObjectCache $cache, LinkTarget $t ) { - if ( $this->isCacheable( $t ) ) { - return [ $cache->makeKey( 'page', $t->getNamespace(), sha1( $t->getDBkey() ) ) ]; + public function getMutableCacheKeys( WANObjectCache $cache, $page ) { + $key = $this->getCacheKey( $page ); + // if no key can be derived, the page isn't cacheable + if ( $key === null ) { + return []; + } + + if ( $this->isCacheable( $page ) ) { + return [ $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) ) ]; } return []; } - private function isCacheable( LinkTarget $title ) { - $ns = $title->getNamespace(); + /** + * @param LinkTarget|PageReference $page + * + * @return bool + */ + private function isCacheable( $page ) { + $ns = $page->getNamespace(); if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) { return true; } @@ -337,16 +478,22 @@ class LinkCache { return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) ); } - private function fetchPageRow( IDatabase $db, LinkTarget $nt ) { + /** + * @param IDatabase $db + * @param LinkTarget|PageReference $page + * + * @return stdClass|false + */ + private function fetchPageRow( IDatabase $db, $page ) { $fields = self::getSelectFields(); - if ( $this->isCacheable( $nt ) ) { + if ( $this->isCacheable( $page ) ) { $fields[] = 'page_touched'; } return $db->selectRow( 'page', $fields, - [ 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ], + [ 'page_namespace' => $page->getNamespace(), 'page_title' => $page->getDBkey() ], __METHOD__ ); } @@ -354,16 +501,19 @@ class LinkCache { /** * Purge the link cache for a title * - * @param LinkTarget $title + * @param LinkTarget|PageReference $page + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. * @since 1.28 */ - public function invalidateTitle( LinkTarget $title ) { - if ( $this->isCacheable( $title ) ) { + public function invalidateTitle( $page ) { + if ( $this->isCacheable( $page ) ) { $cache = $this->wanCache; $cache->delete( - $cache->makeKey( 'page', $title->getNamespace(), sha1( $title->getDBkey() ) ) + $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) ) ); } + + $this->clearLink( $page ); } /** @@ -373,4 +523,5 @@ class LinkCache { $this->goodLinks->clear(); $this->badLinks->clear(); } + } diff --git a/tests/phpunit/includes/cache/LinkCacheTest.php b/tests/phpunit/includes/cache/LinkCacheTest.php new file mode 100644 index 00000000000..cb34d8fdf4e --- /dev/null +++ b/tests/phpunit/includes/cache/LinkCacheTest.php @@ -0,0 +1,347 @@ + 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 providePageAndLinkAndString() { + return [ + [ new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ) ], + [ new TitleValue( NS_USER, __METHOD__ ) ], + [ 'User:' . __METHOD__ ], + ]; + } + + private function getPageRow() { + return (object)[ + 'page_id' => 8, + 'page_len' => 18, + 'page_is_redirect' => 0, + 'page_latest' => 118, + 'page_content_model' => CONTENT_MODEL_TEXT, + 'page_lang' => 'xyz', + 'page_restrictions' => 'test' + ]; + } + + /** + * @dataProvider providePageAndLink + * @covers LinkCache::addGoodLinkObjFromRow() + * @covers LinkCache::getGoodLinkID() + * @covers LinkCache::getGoodLinkFieldObj() + * @covers LinkCache::clearLink() + */ + public function testAddGoodLinkObjFromRow( $page ) { + $linkCache = $this->newLinkCache(); + + $row = $this->getPageRow(); + + $page = new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ); + $linkCache->addBadLinkObj( $page ); + $linkCache->addGoodLinkObjFromRow( $page, $row ); + + $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' ) + ); + + $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' ) ); + } + + /** + * @dataProvider providePageAndLink + * @covers LinkCache::addGoodLinkObj() + * @covers LinkCache::getGoodLinkID() + * @covers LinkCache::getGoodLinkFieldObj() + */ + public function testAddGoodLinkObjWithAllParameters( $page ) { + $linkCache = $this->newLinkCache(); + + $linkCache->addGoodLinkObj( + 8, + $page, + 18, + 0, + 118, + CONTENT_MODEL_TEXT, + 'xyz' + ); + + $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::addGoodLinkObj() + * @covers LinkCache::getGoodLinkID() + * @covers LinkCache::getGoodLinkFieldObj() + */ + public function testAddGoodLinkObjWithMinimalParameters() { + $linkCache = $this->newLinkCache(); + + $page = new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ); + $linkCache->addGoodLinkObj( + 8, + $page + ); + + $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::addGoodLinkObj() + */ + public function testAddGoodLinkObjWithInterwikiLink() { + $linkCache = $this->newLinkCache(); + + $page = new TitleValue( NS_USER, __METHOD__, '', 'acme' ); + $linkCache->addGoodLinkObj( 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->newLinkCache(); + $this->assertFalse( $linkCache->isBadLink( $key ) ); + + $linkCache->addGoodLinkObj( 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 ) ); + + // 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(); + + $row = $this->getPageRow(); + + $linkCache = $this->newLinkCache(); + + // pretend the existing page is missing, and the missing page exists + $linkCache->addGoodLinkObjFromRow( $missing, $row ); + $linkCache->addBadLinkObj( $existing ); + + // the LinkCache should use the cached info and not look into the database + $this->assertSame( (int)$row->page_id, $linkCache->addLinkObj( $missing ) ); + $this->assertSame( 0, $linkCache->addLinkObj( $existing ) ); + + // now set the "for update" flag and try again + $linkCache->forUpdate( true ); + $this->assertSame( 0, $linkCache->addLinkObj( $missing ) ); + $this->assertSame( $existing->getId(), $linkCache->addLinkObj( $existing ) ); + } + + /** + * @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(); + + $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 "for update" flag and try again + $linkCache->forUpdate( true ); + $this->assertSame( $existing->getId(), $linkCache->addLinkObj( $existing ) ); + } + + 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 ) ); + } + + 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' ) ); + } +}