Merge "Log warning and show error on empty username"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 7 May 2019 12:14:28 +0000 (12:14 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 7 May 2019 12:14:28 +0000 (12:14 +0000)
33 files changed:
Gruntfile.js
RELEASE-NOTES-1.34
includes/ServiceWiring.php
includes/Title.php
includes/jobqueue/jobs/ActivityUpdateJob.php
includes/libs/filebackend/fsfile/TempFSFile.php
includes/search/PrefixSearch.php
includes/specials/SpecialWatchlist.php
includes/title/NamespaceInfo.php
includes/user/User.php
includes/watcheditem/NoWriteWatchedItemStore.php
includes/watcheditem/WatchedItemStore.php
includes/watcheditem/WatchedItemStoreInterface.php
maintenance/cleanupUsersWithNoId.php
package.json
resources/Resources.php
tests/parser/ParserTestPrinter.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/GlobalFunctions/GlobalTest.php
tests/phpunit/includes/TestUserRegistry.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/config/GlobalVarConfigTest.php
tests/phpunit/includes/filebackend/FileBackendTest.php
tests/phpunit/includes/jobqueue/JobQueueTest.php
tests/phpunit/includes/libs/CSSMinTest.php
tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php
tests/phpunit/includes/libs/objectcache/ReplicatedBagOStuffTest.php
tests/phpunit/includes/title/NamespaceInfoTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/includes/watcheditem/NoWriteWatchedItemStoreUnitTest.php
tests/phpunit/includes/watcheditem/WatchedItemStoreUnitTest.php
tests/phpunit/mocks/filebackend/MockFileBackend.php
tests/phpunit/mocks/filerepo/MockLocalRepo.php

index 765fe55..f3950f6 100644 (file)
@@ -9,7 +9,6 @@ module.exports = function ( grunt ) {
        grunt.loadNpmTasks( 'grunt-banana-checker' );
        grunt.loadNpmTasks( 'grunt-contrib-copy' );
        grunt.loadNpmTasks( 'grunt-eslint' );
-       grunt.loadNpmTasks( 'grunt-jsonlint' );
        grunt.loadNpmTasks( 'grunt-karma' );
        grunt.loadNpmTasks( 'grunt-stylelint' );
        grunt.loadNpmTasks( 'grunt-svgmin' );
@@ -23,10 +22,11 @@ module.exports = function ( grunt ) {
                eslint: {
                        options: {
                                reportUnusedDisableDirectives: true,
+                               extensions: [ '.js', '.json' ],
                                cache: true
                        },
                        all: [
-                               '**/*.js',
+                               '**/*.js{,on}',
                                '!docs/**',
                                '!node_modules/**',
                                '!resources/lib/**',
@@ -36,14 +36,8 @@ module.exports = function ( grunt ) {
                                '!tests/coverage/**',
                                '!vendor/**',
                                // Explicitly say "**/*.js" here in case of symlinks
-                               '!extensions/**/*.js',
-                               '!skins/**/*.js'
-                       ]
-               },
-               jsonlint: {
-                       all: [
-                               '**/*.json',
-                               '!{docs/js,extensions,node_modules,skins,vendor}/**'
+                               '!extensions/**/*.js{,on}',
+                               '!skins/**/*.js{,on}'
                        ]
                },
                banana: {
index 22b7443..9059dc6 100644 (file)
@@ -148,6 +148,8 @@ because of Phabricator reports.
 * RepoGroup::singleton(), RepoGroup::destroySingleton(),
   RepoGroup::setSingleton(), wfFindFile(), and wfLocalFile() are all
   deprecated. Use MediaWikiServices instead.
+* The getSubjectPage, getTalkPage, and getOtherPage of Title are deprecated.
+  Use NamespaceInfo's getSubjectPage, getTalkPage, and getAssociatedPage.
 
 === Other changes in 1.34 ===
 * …
index a7193d1..f74ba79 100644 (file)
@@ -708,7 +708,9 @@ return [
                        $services->getMainObjectStash(),
                        new HashBagOStuff( [ 'maxKeys' => 100 ] ),
                        $services->getReadOnlyMode(),
-                       $services->getMainConfig()->get( 'UpdateRowsPerQuery' )
+                       $services->getMainConfig()->get( 'UpdateRowsPerQuery' ),
+                       $services->getNamespaceInfo(),
+                       $services->getRevisionLookup()
                );
                $store->setStatsdDataFactory( $services->getStatsdDataFactory() );
 
index 4cac844..ad6c167 100644 (file)
@@ -1501,10 +1501,12 @@ class Title implements LinkTarget, IDBAccessObject {
        /**
         * Get a Title object associated with the talk page of this article
         *
+        * @deprecated since 1.34, use NamespaceInfo::getTalkPage
         * @return Title The object for the talk page
         */
        public function getTalkPage() {
-               return self::makeTitle( MWNamespace::getTalk( $this->mNamespace ), $this->mDbkeyform );
+               return self::castFromLinkTarget(
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) );
        }
 
        /**
@@ -1528,37 +1530,26 @@ class Title implements LinkTarget, IDBAccessObject {
         * Get a title object associated with the subject page of this
         * talk page
         *
+        * @deprecated since 1.34, use NamespaceInfo::getSubjectPage
         * @return Title The object for the subject page
         */
        public function getSubjectPage() {
-               // Is this the same title?
-               $subjectNS = MWNamespace::getSubject( $this->mNamespace );
-               if ( $this->mNamespace == $subjectNS ) {
-                       return $this;
-               }
-               return self::makeTitle( $subjectNS, $this->mDbkeyform );
+               return self::castFromLinkTarget(
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) );
        }
 
        /**
         * Get the other title for this page, if this is a subject page
         * get the talk page, if it is a subject page get the talk page
         *
+        * @deprecated since 1.34, use NamespaceInfo::getAssociatedPage
         * @since 1.25
         * @throws MWException If the page doesn't have an other page
         * @return Title
         */
        public function getOtherPage() {
-               if ( $this->isSpecialPage() ) {
-                       throw new MWException( 'Special pages cannot have other pages' );
-               }
-               if ( $this->isTalkPage() ) {
-                       return $this->getSubjectPage();
-               } else {
-                       if ( !$this->canHaveTalkPage() ) {
-                               throw new MWException( "{$this->getPrefixedText()} does not have an other page" );
-                       }
-                       return $this->getTalkPage();
-               }
+               return self::castFromLinkTarget(
+                       MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) );
        }
 
        /**
@@ -3991,14 +3982,14 @@ class Title implements LinkTarget, IDBAccessObject {
        /**
         * Compare with another title.
         *
-        * @param Title $title
+        * @param LinkTarget $title
         * @return bool
         */
-       public function equals( Title $title ) {
+       public function equals( LinkTarget $title ) {
                // Note: === is necessary for proper matching of number-like titles.
-               return $this->mInterwiki === $title->mInterwiki
-                       && $this->mNamespace == $title->mNamespace
-                       && $this->mDbkeyform === $title->mDbkeyform;
+               return $this->mInterwiki === $title->getInterwiki()
+                       && $this->mNamespace == $title->getNamespace()
+                       && $this->mDbkeyform === $title->getDBkey();
        }
 
        /**
index 8cc14e5..9b08510 100644 (file)
@@ -19,6 +19,8 @@
  * @ingroup JobQueue
  */
 
+use MediaWiki\Linker\LinkTarget;
+
 /**
  * Job for updating user activity like "last viewed" timestamps
  *
@@ -32,7 +34,9 @@
  * @since 1.26
  */
 class ActivityUpdateJob extends Job {
-       function __construct( Title $title, array $params ) {
+       function __construct( LinkTarget $title, array $params ) {
+               $title = Title::newFromLinkTarget( $title );
+
                parent::__construct( 'activityUpdateJob', $title, $params );
 
                static $required = [ 'type', 'userid', 'notifTime', 'curTime' ];
index 321424f..b993626 100644 (file)
@@ -178,7 +178,7 @@ class TempFSFile extends FSFile {
         * This method should only be called internally
         */
        public static function purgeAllOnShutdown() {
-               foreach ( self::$pathsCollect as $path ) {
+               foreach ( self::$pathsCollect as $path => $unused ) {
                        Wikimedia\suppressWarnings();
                        unlink( $path );
                        Wikimedia\restoreWarnings();
index aa429b2..a7d475e 100644 (file)
@@ -41,6 +41,7 @@ abstract class PrefixSearch {
         * @return array Array of strings
         */
        public static function titleSearch( $search, $limit, $namespaces = [], $offset = 0 ) {
+               wfDeprecated( __METHOD__, '1.34' );
                $prefixSearch = new StringPrefixSearch;
                return $prefixSearch->search( $search, $limit, $namespaces, $offset );
        }
index bac059d..812f1b0 100644 (file)
@@ -199,10 +199,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                                        'description' => 'rcfilters-filter-watchlistactivity-unseen-description',
                                        'cssClassSuffix' => 'watchedunseen',
                                        'isRowApplicableCallable' => function ( $ctx, RecentChange $rc ) {
-                                               $changeTs = $rc->getAttribute( 'rc_timestamp' );
-                                               $lastVisitTs = $this->getLatestSeenTimestamp( $rc );
-
-                                               return $lastVisitTs !== null && $changeTs >= $lastVisitTs;
+                                               return !$this->isChangeEffectivelySeen( $rc );
                                        },
                                ],
                                [
@@ -211,10 +208,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                                        'description' => 'rcfilters-filter-watchlistactivity-seen-description',
                                        'cssClassSuffix' => 'watchedseen',
                                        'isRowApplicableCallable' => function ( $ctx, RecentChange $rc ) {
-                                               $changeTs = $rc->getAttribute( 'rc_timestamp' );
-                                               $lastVisitTs = $this->getLatestSeenTimestamp( $rc );
-
-                                               return $lastVisitTs === null || $changeTs < $lastVisitTs;
+                                               return $this->isChangeEffectivelySeen( $rc );
                                        }
                                ],
                        ],
@@ -548,10 +542,9 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                        $rc->counter = $counter++;
 
                        if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) {
-                               $lastVisitTs = $this->getLatestSeenTimestamp( $rc );
-                               $updated = ( $lastVisitTs > $rc->getAttribute( 'timestamp' ) );
+                               $unseen = !$this->isChangeEffectivelySeen( $rc );
                        } else {
-                               $updated = false;
+                               $unseen = false;
                        }
 
                        if ( isset( $watchedItemStore ) ) {
@@ -561,7 +554,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                                $rc->numberofWatchingusers = 0;
                        }
 
-                       $changeLine = $list->recentChangesLine( $rc, $updated, $counter );
+                       $changeLine = $list->recentChangesLine( $rc, $unseen, $counter );
                        if ( $changeLine !== false ) {
                                $s .= $changeLine;
                        }
@@ -869,9 +862,19 @@ class SpecialWatchlist extends ChangesListSpecialPage {
 
        /**
         * @param RecentChange $rc
-        * @return string TS_MW timestamp
+        * @return bool User viewed the revision or a newer one
+        */
+       protected function isChangeEffectivelySeen( RecentChange $rc ) {
+               $lastVisitTs = $this->getLatestSeenTimestampIfHasUnseen( $rc );
+
+               return $lastVisitTs === null || $lastVisitTs > $rc->getAttribute( 'rc_timestamp' );
+       }
+
+       /**
+        * @param RecentChange $rc
+        * @return string|null TS_MW timestamp or null if all revision were seen
         */
-       protected function getLatestSeenTimestamp( RecentChange $rc ) {
+       private function getLatestSeenTimestampIfHasUnseen( RecentChange $rc ) {
                return $this->watchStore->getLatestNotificationTimestamp(
                        $rc->getAttribute( 'wl_notificationtimestamp' ),
                        $rc->getPerformer(),
index e3ab3a3..7cfadc0 100644 (file)
@@ -21,6 +21,7 @@
  */
 
 use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Linker\LinkTarget;
 
 /**
  * This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of
@@ -149,6 +150,18 @@ class NamespaceInfo {
                        : $index + 1;
        }
 
+       /**
+        * @param LinkTarget $target
+        * @return LinkTarget Talk page for $target
+        * @throws MWException if $target's namespace doesn't have talk pages (e.g., NS_SPECIAL)
+        */
+       public function getTalkPage( LinkTarget $target ) : LinkTarget {
+               if ( $this->isTalk( $target->getNamespace() ) ) {
+                       return $target;
+               }
+               return new TitleValue( $this->getTalk( $target->getNamespace() ), $target->getDbKey() );
+       }
+
        /**
         * Get the subject namespace index for a given namespace
         * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
@@ -167,6 +180,17 @@ class NamespaceInfo {
                        : $index;
        }
 
+       /**
+        * @param LinkTarget $target
+        * @return LinkTarget Subject page for $target
+        */
+       public function getSubjectPage( LinkTarget $target ) : LinkTarget {
+               if ( $this->isSubject( $target->getNamespace() ) ) {
+                       return $target;
+               }
+               return new TitleValue( $this->getSubject( $target->getNamespace() ), $target->getDbKey() );
+       }
+
        /**
         * Get the associated namespace.
         * For talk namespaces, returns the subject (non-talk) namespace
@@ -174,6 +198,7 @@ class NamespaceInfo {
         *
         * @param int $index Namespace index
         * @return int
+        * @throws MWException if called on a namespace that has no talk pages (e.g., NS_SPECIAL)
         */
        public function getAssociated( $index ) {
                $this->isMethodValidFor( $index, __METHOD__ );
@@ -184,6 +209,17 @@ class NamespaceInfo {
                return $this->getSubject( $index );
        }
 
+       /**
+        * @param LinkTarget $target
+        * @return LinkTarget Talk page for $target if it's a subject page, subject page if it's a talk
+        *   page
+        * @throws MWException if $target's namespace doesn't have talk pages (e.g., NS_SPECIAL)
+        */
+       public function getAssociatedPage( LinkTarget $target ) : LinkTarget {
+               return new TitleValue(
+                       $this->getAssociated( $target->getNamespace() ), $target->getDbKey() );
+       }
+
        /**
         * Returns whether the specified namespace exists
         *
index 57c5130..2cea712 100644 (file)
@@ -923,7 +923,7 @@ class User implements IDBAccessObject, UserIdentity {
                }
 
                if ( !( $flags & self::READ_LATEST ) && array_key_exists( $name, self::$idCacheByName ) ) {
-                       return self::$idCacheByName[$name];
+                       return is_null( self::$idCacheByName[$name] ) ? null : (int)self::$idCacheByName[$name];
                }
 
                list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
index d5d175c..72f6086 100644 (file)
@@ -134,7 +134,7 @@ class NoWriteWatchedItemStore implements WatchedItemStoreInterface {
 
        public function resetNotificationTimestamp(
                UserIdentity $user,
-               Title $title,
+               LinkTarget $title,
                $force = '',
                $oldid = 0
        ) {
index b7e5559..bd4360e 100644 (file)
@@ -2,6 +2,7 @@
 
 use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Revision\RevisionLookup;
 use MediaWiki\User\UserIdentity;
 use Wikimedia\Assert\Assert;
 use Wikimedia\Rdbms\IDatabase;
@@ -68,14 +69,19 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        private $deferredUpdatesAddCallableUpdateCallback;
 
        /**
-        * @var callable|null
+        * @var int
         */
-       private $revisionGetTimestampFromIdCallback;
+       private $updateRowsPerQuery;
 
        /**
-        * @var int
+        * @var NamespaceInfo
         */
-       private $updateRowsPerQuery;
+       private $nsInfo;
+
+       /**
+        * @var RevisionLookup
+        */
+       private $revisionLookup;
 
        /**
         * @var StatsdDataFactoryInterface
@@ -89,6 +95,8 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
         * @param HashBagOStuff $cache
         * @param ReadOnlyMode $readOnlyMode
         * @param int $updateRowsPerQuery
+        * @param NamespaceInfo $nsInfo
+        * @param RevisionLookup $revisionLookup
         */
        public function __construct(
                ILBFactory $lbFactory,
@@ -96,7 +104,9 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                BagOStuff $stash,
                HashBagOStuff $cache,
                ReadOnlyMode $readOnlyMode,
-               $updateRowsPerQuery
+               $updateRowsPerQuery,
+               NamespaceInfo $nsInfo,
+               RevisionLookup $revisionLookup
        ) {
                $this->lbFactory = $lbFactory;
                $this->loadBalancer = $lbFactory->getMainLB();
@@ -107,9 +117,9 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                $this->stats = new NullStatsdDataFactory();
                $this->deferredUpdatesAddCallableUpdateCallback =
                        [ DeferredUpdates::class, 'addCallableUpdate' ];
-               $this->revisionGetTimestampFromIdCallback =
-                       [ Revision::class, 'getTimestampFromId' ];
                $this->updateRowsPerQuery = $updateRowsPerQuery;
+               $this->nsInfo = $nsInfo;
+               $this->revisionLookup = $revisionLookup;
 
                $this->latestUpdateCache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
        }
@@ -145,29 +155,6 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                } );
        }
 
-       /**
-        * Overrides the Revision::getTimestampFromId callback
-        * This is intended for use while testing and will fail if MW_PHPUNIT_TEST is not defined.
-        *
-        * @param callable $callback
-        * @see Revision::getTimestampFromId for callback signiture
-        *
-        * @return ScopedCallback to reset the overridden value
-        * @throws MWException
-        */
-       public function overrideRevisionGetTimestampFromIdCallback( callable $callback ) {
-               if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
-                       throw new MWException(
-                               'Cannot override Revision::getTimestampFromId callback in operation.'
-                       );
-               }
-               $previousValue = $this->revisionGetTimestampFromIdCallback;
-               $this->revisionGetTimestampFromIdCallback = $callback;
-               return new ScopedCallback( function () use ( $previousValue ) {
-                       $this->revisionGetTimestampFromIdCallback = $previousValue;
-               } );
-       }
-
        private function getCacheKey( UserIdentity $user, LinkTarget $target ) {
                return $this->cache->makeKey(
                        (string)$target->getNamespace(),
@@ -985,13 +972,13 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        /**
         * @since 1.27
         * @param UserIdentity $user
-        * @param Title $title
+        * @param LinkTarget $title
         * @param string $force
         * @param int $oldid
         * @return bool
         */
        public function resetNotificationTimestamp(
-               UserIdentity $user, Title $title, $force = '', $oldid = 0
+               UserIdentity $user, LinkTarget $title, $force = '', $oldid = 0
        ) {
                $time = time();
 
@@ -1000,16 +987,20 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                        return false;
                }
 
-               // Hook expects User, not UserIdentity
+               // Hook expects User and Title, not UserIdentity and LinkTarget
                $userObj = User::newFromId( $user->getId() );
+               $titleObj = Title::castFromLinkTarget( $title );
                if ( !Hooks::run( 'BeforeResetNotificationTimestamp',
-                       [ &$userObj, &$title, $force, &$oldid ] )
+                       [ &$userObj, &$titleObj, $force, &$oldid ] )
                ) {
                        return false;
                }
                if ( !$userObj->equals( $user ) ) {
                        $user = $userObj;
                }
+               if ( !$titleObj->equals( $title ) ) {
+                       $title = $titleObj;
+               }
 
                $item = null;
                if ( $force != 'force' ) {
@@ -1020,11 +1011,19 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                }
 
                // Get the timestamp (TS_MW) of this revision to track the latest one seen
-               $seenTime = call_user_func(
-                       $this->revisionGetTimestampFromIdCallback,
-                       $title,
-                       $oldid ?: $title->getLatestRevID()
-               );
+               $id = $oldid;
+               $seenTime = null;
+               if ( !$id ) {
+                       $latestRev = $this->revisionLookup->getRevisionByTitle( $title );
+                       if ( $latestRev ) {
+                               $id = $latestRev->getId();
+                               // Save a DB query
+                               $seenTime = $latestRev->getTimestamp();
+                       }
+               }
+               if ( $seenTime === null ) {
+                       $seenTime = $this->revisionLookup->getTimestampFromId( $id );
+               }
 
                // Mark the item as read immediately in lightweight storage
                $this->stash->merge(
@@ -1105,14 +1104,15 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
        }
 
        private function getNotificationTimestamp(
-               UserIdentity $user, Title $title, $item, $force, $oldid
+               UserIdentity $user, LinkTarget $title, $item, $force, $oldid
        ) {
                if ( !$oldid ) {
                        // No oldid given, assuming latest revision; clear the timestamp.
                        return null;
                }
 
-               if ( !$title->getNextRevisionID( $oldid ) ) {
+               $oldRev = $this->revisionLookup->getRevisionById( $oldid );
+               if ( !$this->revisionLookup->getNextRevision( $oldRev, $title ) ) {
                        // Oldid given and is the latest revision for this title; clear the timestamp.
                        return null;
                }
@@ -1128,12 +1128,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
                // Oldid given and isn't the latest; update the timestamp.
                // This will result in no further notification emails being sent!
-               // Calls Revision::getTimestampFromId in normal operation
-               $notificationTimestamp = call_user_func(
-                       $this->revisionGetTimestampFromIdCallback,
-                       $title,
-                       $oldid
-               );
+               $notificationTimestamp = $this->revisionLookup->getTimestampFromId( $oldid );
 
                // We need to go one second to the future because of various strict comparisons
                // throughout the codebase
@@ -1192,11 +1187,15 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
         * @param LinkTarget $newTarget
         */
        public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) {
-               $oldTarget = Title::newFromLinkTarget( $oldTarget );
-               $newTarget = Title::newFromLinkTarget( $newTarget );
-
-               $this->duplicateEntry( $oldTarget->getSubjectPage(), $newTarget->getSubjectPage() );
-               $this->duplicateEntry( $oldTarget->getTalkPage(), $newTarget->getTalkPage() );
+               // Duplicate first the subject page, then the talk page
+               $this->duplicateEntry(
+                       $this->nsInfo->getSubjectPage( $oldTarget ),
+                       $this->nsInfo->getSubjectPage( $newTarget )
+               );
+               $this->duplicateEntry(
+                       $this->nsInfo->getTalkPage( $oldTarget ),
+                       $this->nsInfo->getTalkPage( $newTarget )
+               );
        }
 
        /**
@@ -1260,7 +1259,7 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
 
        /**
         * @param UserIdentity $user
-        * @param Title[] $titles
+        * @param LinkTarget[] $titles
         */
        private function uncacheTitlesForUser( UserIdentity $user, array $titles ) {
                foreach ( $titles as $title ) {
index 299c222..5ff29d0 100644 (file)
@@ -252,7 +252,7 @@ interface WatchedItemStoreInterface {
         * @since 1.31
         *
         * @param UserIdentity $user
-        * @param Title $title
+        * @param LinkTarget $title
         * @param string $force Whether to force the write query to be executed even if the
         *    page is not watched or the notification timestamp is already NULL.
         *    'force' in order to force
@@ -262,7 +262,7 @@ interface WatchedItemStoreInterface {
         * @return bool success Whether a job was enqueued
         */
        public function resetNotificationTimestamp(
-               UserIdentity $user, Title $title, $force = '', $oldid = 0 );
+               UserIdentity $user, LinkTarget $title, $force = '', $oldid = 0 );
 
        /**
         * @since 1.31
@@ -341,7 +341,7 @@ interface WatchedItemStoreInterface {
         * @param string|null $timestamp Value of wl_notificationtimestamp from the DB
         * @param UserIdentity $user
         * @param LinkTarget $target
-        * @return string TS_MW timestamp or null
+        * @return string|null TS_MW timestamp or null if all revision were seen
         */
        public function getLatestNotificationTimestamp(
                $timestamp, UserIdentity $user, LinkTarget $target );
index 61d1e5d..f7fd9d5 100644 (file)
@@ -166,13 +166,13 @@ class CleanupUsersWithNoId extends LoggedUpdateMaintenance {
 
                                $id = 0;
                                if ( $this->assign ) {
-                                       $id = (int)User::idFromName( $name );
+                                       $id = User::idFromName( $name );
                                        if ( !$id ) {
                                                // See if any extension wants to create it.
                                                if ( !isset( $this->triedCreations[$name] ) ) {
                                                        $this->triedCreations[$name] = true;
                                                        if ( !Hooks::run( 'ImportHandleUnknownUser', [ $name ] ) ) {
-                                                               $id = (int)User::idFromName( $name, User::READ_LATEST );
+                                                               $id = User::idFromName( $name, User::READ_LATEST );
                                                        }
                                                }
                                        }
index 2105e0f..b8ac75e 100644 (file)
     "selenium-test": "wdio ./tests/selenium/wdio.conf.js"
   },
   "devDependencies": {
-    "eslint-config-wikimedia": "0.11.0",
+    "eslint-config-wikimedia": "0.12.0",
     "grunt": "1.0.4",
     "grunt-banana-checker": "0.7.0",
     "grunt-contrib-copy": "1.0.0",
     "grunt-contrib-watch": "1.1.0",
     "grunt-eslint": "21.0.0",
-    "grunt-jsonlint": "1.1.0",
     "grunt-karma": "3.0.0",
     "grunt-stylelint": "0.10.1",
     "grunt-svgmin": "5.0.0",
index a3f51da..c28ac4a 100644 (file)
@@ -1439,6 +1439,7 @@ return [
                'dependencies' => [ 'jquery.makeCollapsible' ],
                'scripts' => 'resources/src/mediawiki.action/mediawiki.action.history.js',
                'styles' => 'resources/src/mediawiki.action/mediawiki.action.history.css',
+               'targets' => [ 'desktop', 'mobile' ]
        ],
        'mediawiki.action.history.styles' => [
                'skinStyles' => [
index fddee3d..34f8cd5 100644 (file)
@@ -168,14 +168,10 @@ class ParserTestPrinter extends TestRecorder {
                        $output = strtr( $output, $pairs );
                }
 
-               # Windows, or at least the fc utility, is retarded
-               $slash = wfIsWindows() ? '\\' : '/';
-               $prefix = wfTempDir() . "{$slash}mwParser-" . mt_rand();
-
-               $infile = "$prefix-$inFileTail";
+               $infile = tempnam( wfTempDir(), "mwParser-$inFileTail" );
                $this->dumpToFile( $input, $infile );
 
-               $outfile = "$prefix-$outFileTail";
+               $outfile = tempnam( wfTempDir(), "mwParser-$outFileTail" );
                $this->dumpToFile( $output, $outfile );
 
                global $wgDiff3;
index bdf61b3..ec61c23 100644 (file)
@@ -499,14 +499,15 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
         * @return string Absolute name of the temporary directory
         */
        protected function getNewTempDirectory() {
-               // Starting of with a temporary /file/.
+               // Starting of with a temporary *file*.
                $fileName = $this->getNewTempFile();
 
-               // Converting the temporary /file/ to a /directory/
+               // Converting the temporary file to a *directory*.
                // The following is not atomic, but at least we now have a single place,
-               // where temporary directory creation is bundled and can be improved
+               // where temporary directory creation is bundled and can be improved.
                unlink( $fileName );
-               $this->assertTrue( wfMkdirParents( $fileName ) );
+               // If this fails for some reason, PHP will warn and fail the test.
+               mkdir( $fileName, 0777, /* recursive = */ true );
 
                return $fileName;
        }
index 9443b19..1210a50 100644 (file)
@@ -74,12 +74,8 @@ class GlobalTest extends MediaWikiTestCase {
                $this->assertFalse(
                        wfRandomString() == wfRandomString()
                );
-               $this->assertEquals(
-                       strlen( wfRandomString( 10 ) ), 10
-               );
-               $this->assertTrue(
-                       preg_match( '/^[0-9a-f]+$/i', wfRandomString() ) === 1
-               );
+               $this->assertSame( 10, strlen( wfRandomString( 10 ) ), 'length' );
+               $this->assertSame( 1, preg_match( '/^[0-9a-f]+$/i', wfRandomString() ), 'pattern' );
        }
 
        /**
index 3064a3d..40a5dc5 100644 (file)
@@ -33,7 +33,7 @@ class TestUserRegistry {
         */
        public static function getMutableTestUser( $testName, $groups = [] ) {
                $id = self::getNextId();
-               $password = wfRandomString( 20 );
+               $password = "password_for_test_user_id_{$id}";
                $testUser = new TestUser(
                        "TestUser $testName $id",  // username
                        "Name $id",                // real name
@@ -75,7 +75,7 @@ class TestUserRegistry {
                                $password = 'UTSysopPassword';
                        } else {
                                $username = "TestUser $id";
-                               $password = wfRandomString( 20 );
+                               $password = "password_for_test_user_id_{$id}";
                        }
                        self::$testUsers[$key] = $testUser = new TestUser(
                                $username,            // username
index 2159a35..c46f69b 100644 (file)
@@ -786,19 +786,6 @@ class TitleTest extends MediaWikiTestCase {
                ];
        }
 
-       /**
-        * @dataProvider provideGetTalkPage_good
-        * @covers Title::getTalkPage
-        */
-       public function testGetTalkPage_good( Title $title, Title $expected ) {
-               $talk = $title->getTalkPage();
-               $this->assertSame(
-                       $expected->getPrefixedDBKey(),
-                       $talk->getPrefixedDBKey(),
-                       $title->getPrefixedDBKey()
-               );
-       }
-
        /**
         * @dataProvider provideGetTalkPage_good
         * @covers Title::getTalkPageIfDefined
index ec443e7..591f27d 100644 (file)
@@ -19,11 +19,10 @@ class GlobalVarConfigTest extends MediaWikiTestCase {
         */
        public function testConstructor( $prefix ) {
                $var = $prefix . 'GlobalVarConfigTest';
-               $rand = wfRandomString();
-               $this->setMwGlobals( $var, $rand );
+               $this->setMwGlobals( $var, 'testvalue' );
                $config = new GlobalVarConfig( $prefix );
                $this->assertInstanceOf( GlobalVarConfig::class, $config );
-               $this->assertEquals( $rand, $config->get( 'GlobalVarConfigTest' ) );
+               $this->assertEquals( 'testvalue', $config->get( 'GlobalVarConfigTest' ) );
        }
 
        public static function provideConstructor() {
@@ -41,7 +40,7 @@ class GlobalVarConfigTest extends MediaWikiTestCase {
         * @covers GlobalVarConfig::hasWithPrefix
         */
        public function testHas() {
-               $this->setMwGlobals( 'wgGlobalVarConfigTestHas', wfRandomString() );
+               $this->setMwGlobals( 'wgGlobalVarConfigTestHas', 'testvalue' );
                $config = new GlobalVarConfig();
                $this->assertTrue( $config->has( 'GlobalVarConfigTestHas' ) );
                $this->assertFalse( $config->has( 'GlobalVarConfigTestNotHas' ) );
index 50696af..8548fde 100644 (file)
@@ -98,7 +98,7 @@ class FileBackendTest extends MediaWikiTestCase {
                        'name' => 'localtesting',
                        'lockManager' => LockManagerGroup::singleton()->get( 'fsLockManager' ),
                        'parallelize' => 'implicit',
-                       'wikiId' => wfWikiID() . wfRandomString(),
+                       'wikiId' => 'testdb',
                        'backends' => [
                                [
                                        'name' => 'localmultitesting1',
@@ -2569,11 +2569,9 @@ class FileBackendTest extends MediaWikiTestCase {
                        'wikiId' => wfWikiID()
                ] ) );
 
-               $name = wfRandomString( 300 );
-
                $input = [
                        'headers' => [
-                               'content-Disposition' => FileBackend::makeContentDisposition( 'inline', $name ),
+                               'content-Disposition' => FileBackend::makeContentDisposition( 'inline', 'name' ),
                                'Content-dUration' => 25.6,
                                'X-LONG-VALUE' => str_pad( '0', 300 ),
                                'CONTENT-LENGTH' => 855055,
@@ -2581,7 +2579,7 @@ class FileBackendTest extends MediaWikiTestCase {
                ];
                $expected = [
                        'headers' => [
-                               'content-disposition' => FileBackend::makeContentDisposition( 'inline', $name ),
+                               'content-disposition' => FileBackend::makeContentDisposition( 'inline', 'name' ),
                                'content-duration' => 25.6,
                                'content-length' => 855055
                        ]
index 1baaa54..ce07f78 100644 (file)
@@ -259,8 +259,7 @@ class JobQueueTest extends MediaWikiTestCase {
                $this->assertEquals( 0, $queue->getSize(), "Queue is empty ($desc)" );
                $this->assertEquals( 0, $queue->getAcquiredCount(), "Queue is empty ($desc)" );
 
-               $id = wfRandomString( 32 );
-               $root1 = Job::newRootJobParams( "nulljobspam:$id" ); // task ID/timestamp
+               $root1 = Job::newRootJobParams( "nulljobspam:testId" ); // task ID/timestamp
                for ( $i = 0; $i < 5; ++$i ) {
                        $this->assertNull( $queue->push( $this->newJob( 0, $root1 ) ), "Push worked ($desc)" );
                }
index ef333f9..4c93789 100644 (file)
@@ -85,9 +85,9 @@ class CSSMinTest extends MediaWikiTestCase {
         * @covers CSSMin::getMimeType
         */
        public function testGetMimeType( $fileContents, $fileExtension, $expected ) {
-               $fileName = wfTempDir() . DIRECTORY_SEPARATOR . uniqid( 'MW_PHPUnit_CSSMinTest_' ) . '.'
-                       . $fileExtension;
-               $this->addTmpFiles( $fileName );
+               // Automatically removed when it falls out of scope (including if the test fails)
+               $file = TempFSFile::factory( 'PHPUnit_CSSMinTest_', $fileExtension, wfTempDir() );
+               $fileName = $file->getPath();
                file_put_contents( $fileName, $fileContents );
                $this->assertSame( $expected, CSSMin::getMimeType( $fileName ) );
        }
index 0376803..9f88474 100644 (file)
@@ -28,8 +28,8 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
         * @covers MultiWriteBagOStuff::doWrite
         */
        public function testSetImmediate() {
-               $key = wfRandomString();
-               $value = wfRandomString();
+               $key = 'key';
+               $value = 'value';
                $this->cache->set( $key, $value );
 
                // Set in tier 1
@@ -42,8 +42,8 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
         * @covers MultiWriteBagOStuff
         */
        public function testSyncMerge() {
-               $key = wfRandomString();
-               $value = wfRandomString();
+               $key = 'keyA';
+               $value = 'value';
                $func = function () use ( $value ) {
                        return $value;
                };
@@ -56,14 +56,14 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                // Set in tier 1
                $this->assertEquals( $value, $this->cache1->get( $key ), 'Written to tier 1' );
                // Not yet set in tier 2
-               $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' );
+               $this->assertFalse( $this->cache2->get( $key ), 'Not written to tier 2' );
 
                $dbw->commit();
 
                // Set in tier 2
                $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
 
-               $key = wfRandomString();
+               $key = 'keyB';
 
                $dbw->begin();
                $this->cache->merge( $key, $func, 0, 1, BagOStuff::WRITE_SYNC );
@@ -80,8 +80,8 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
         * @covers MultiWriteBagOStuff::set
         */
        public function testSetDelayed() {
-               $key = wfRandomString();
-               $value = (object)[ 'v' => wfRandomString() ];
+               $key = 'key';
+               $value = (object)[ 'v' => 'saved value' ];
                $expectValue = clone $value;
 
                // XXX: DeferredUpdates bound to transactions in CLI mode
@@ -90,12 +90,12 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                $this->cache->set( $key, $value );
 
                // Test that later changes to $value don't affect the saved value (e.g. T168040)
-               $value->v = 'bogus';
+               $value->v = 'other value';
 
                // Set in tier 1
                $this->assertEquals( $expectValue, $this->cache1->get( $key ), 'Written to tier 1' );
                // Not yet set in tier 2
-               $this->assertEquals( false, $this->cache2->get( $key ), 'Not written to tier 2' );
+               $this->assertFalse( $this->cache2->get( $key ), 'Not written to tier 2' );
 
                $dbw->commit();
 
index b7f22ec..550ec0b 100644 (file)
@@ -23,40 +23,40 @@ class ReplicatedBagOStuffTest extends MediaWikiTestCase {
         * @covers ReplicatedBagOStuff::set
         */
        public function testSet() {
-               $key = wfRandomString();
-               $value = wfRandomString();
+               $key = 'a key';
+               $value = 'a value';
                $this->cache->set( $key, $value );
 
                // Write to master.
-               $this->assertEquals( $this->writeCache->get( $key ), $value );
+               $this->assertEquals( $value, $this->writeCache->get( $key ) );
                // Don't write to replica. Replication is deferred to backend.
-               $this->assertEquals( $this->readCache->get( $key ), false );
+               $this->assertFalse( $this->readCache->get( $key ) );
        }
 
        /**
         * @covers ReplicatedBagOStuff::get
         */
        public function testGet() {
-               $key = wfRandomString();
+               $key = 'a key';
 
-               $write = wfRandomString();
+               $write = 'one value';
                $this->writeCache->set( $key, $write );
-               $read = wfRandomString();
+               $read = 'another value';
                $this->readCache->set( $key, $read );
 
                // Read from replica.
-               $this->assertEquals( $this->cache->get( $key ), $read );
+               $this->assertEquals( $read, $this->cache->get( $key ) );
        }
 
        /**
         * @covers ReplicatedBagOStuff::get
         */
        public function testGetAbsent() {
-               $key = wfRandomString();
-               $value = wfRandomString();
+               $key = 'a key';
+               $value = 'a value';
                $this->writeCache->set( $key, $value );
 
                // Don't read from master. No failover if value is absent.
-               $this->assertEquals( $this->cache->get( $key ), false );
+               $this->assertFalse( $this->cache->get( $key ) );
        }
 }
index 5e67636..556c640 100644 (file)
@@ -166,85 +166,6 @@ class NamespaceInfoTest extends MediaWikiTestCase {
                ];
        }
 
-       /**
-        * @covers NamespaceInfo::getSubject
-        */
-       public function testGetSubject() {
-               // Special namespaces are their own subjects
-               $obj = $this->newObj();
-               $this->assertEquals( NS_MEDIA, $obj->getSubject( NS_MEDIA ) );
-               $this->assertEquals( NS_SPECIAL, $obj->getSubject( NS_SPECIAL ) );
-
-               $this->assertEquals( NS_MAIN, $obj->getSubject( NS_TALK ) );
-               $this->assertEquals( NS_USER, $obj->getSubject( NS_USER_TALK ) );
-       }
-
-       /**
-        * Regular getTalk() calls
-        * Namespaces without a talk page (NS_MEDIA, NS_SPECIAL) are tested in
-        * the function testGetTalkExceptions()
-        * @covers NamespaceInfo::getTalk
-        * @covers NamespaceInfo::isMethodValidFor
-        */
-       public function testGetTalk() {
-               $obj = $this->newObj();
-               $this->assertEquals( NS_TALK, $obj->getTalk( NS_MAIN ) );
-               $this->assertEquals( NS_TALK, $obj->getTalk( NS_TALK ) );
-               $this->assertEquals( NS_USER_TALK, $obj->getTalk( NS_USER ) );
-               $this->assertEquals( NS_USER_TALK, $obj->getTalk( NS_USER_TALK ) );
-       }
-
-       /**
-        * Exceptions with getTalk()
-        * NS_MEDIA does not have talk pages. MediaWiki raise an exception for them.
-        * @expectedException MWException
-        * @covers NamespaceInfo::getTalk
-        * @covers NamespaceInfo::isMethodValidFor
-        */
-       public function testGetTalkExceptionsForNsMedia() {
-               $this->assertNull( $this->newObj()->getTalk( NS_MEDIA ) );
-       }
-
-       /**
-        * Exceptions with getTalk()
-        * NS_SPECIAL does not have talk pages. MediaWiki raise an exception for them.
-        * @expectedException MWException
-        * @covers NamespaceInfo::getTalk
-        */
-       public function testGetTalkExceptionsForNsSpecial() {
-               $this->assertNull( $this->newObj()->getTalk( NS_SPECIAL ) );
-       }
-
-       /**
-        * Regular getAssociated() calls
-        * Namespaces without an associated page (NS_MEDIA, NS_SPECIAL) are tested in
-        * the function testGetAssociatedExceptions()
-        * @covers NamespaceInfo::getAssociated
-        */
-       public function testGetAssociated() {
-               $this->assertEquals( NS_TALK, $this->newObj()->getAssociated( NS_MAIN ) );
-               $this->assertEquals( NS_MAIN, $this->newObj()->getAssociated( NS_TALK ) );
-       }
-
-       # ## Exceptions with getAssociated()
-       # ## NS_MEDIA and NS_SPECIAL do not have talk pages. MediaWiki raises
-       # ## an exception for them.
-       /**
-        * @expectedException MWException
-        * @covers NamespaceInfo::getAssociated
-        */
-       public function testGetAssociatedExceptionsForNsMedia() {
-               $this->assertNull( $this->newObj()->getAssociated( NS_MEDIA ) );
-       }
-
-       /**
-        * @expectedException MWException
-        * @covers NamespaceInfo::getAssociated
-        */
-       public function testGetAssociatedExceptionsForNsSpecial() {
-               $this->assertNull( $this->newObj()->getAssociated( NS_SPECIAL ) );
-       }
-
        /**
         * @covers NamespaceInfo::exists
         * @dataProvider provideExists
@@ -676,6 +597,210 @@ class NamespaceInfoTest extends MediaWikiTestCase {
 
        // %} End basic methods
 
+       /**********************************************************************************************
+        * getSubject/Talk/Associated
+        * %{
+        */
+       /**
+        * @dataProvider provideSubjectTalk
+        * @covers NamespaceInfo::getSubject
+        * @covers NamespaceInfo::getSubjectPage
+        * @covers NamespaceInfo::isMethodValidFor
+        * @covers Title::getSubjectPage
+        *
+        * @param int $subject
+        * @param int $talk
+        */
+       public function testGetSubject( $subject, $talk ) {
+               $obj = $this->newObj();
+               $this->assertSame( $subject, $obj->getSubject( $subject ) );
+               $this->assertSame( $subject, $obj->getSubject( $talk ) );
+
+               $subjectTitleVal = new TitleValue( $subject, 'A' );
+               $talkTitleVal = new TitleValue( $talk, 'A' );
+               // Object will be the same one passed in if it's a subject, different but equal object if
+               // it's talk
+               $this->assertSame( $subjectTitleVal, $obj->getSubjectPage( $subjectTitleVal ) );
+               $this->assertEquals( $subjectTitleVal, $obj->getSubjectPage( $talkTitleVal ) );
+
+               $subjectTitle = Title::makeTitle( $subject, 'A' );
+               $talkTitle = Title::makeTitle( $talk, 'A' );
+               $this->assertSame( $subjectTitle, $subjectTitle->getSubjectPage() );
+               $this->assertEquals( $subjectTitle, $talkTitle->getSubjectPage() );
+       }
+
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getSubject
+        * @covers NamespaceInfo::getSubjectPage
+        *
+        * @param int $ns
+        */
+       public function testGetSubject_special( $ns ) {
+               $obj = $this->newObj();
+               $this->assertSame( $ns, $obj->getSubject( $ns ) );
+
+               $title = new TitleValue( $ns, 'A' );
+               $this->assertSame( $title, $obj->getSubjectPage( $title ) );
+       }
+
+       /**
+        * @dataProvider provideSubjectTalk
+        * @covers NamespaceInfo::getTalk
+        * @covers NamespaceInfo::getTalkPage
+        * @covers NamespaceInfo::isMethodValidFor
+        * @covers Title::getTalkPage
+        *
+        * @param int $subject
+        * @param int $talk
+        */
+       public function testGetTalk( $subject, $talk ) {
+               $obj = $this->newObj();
+               $this->assertSame( $talk, $obj->getTalk( $subject ) );
+               $this->assertSame( $talk, $obj->getTalk( $talk ) );
+
+               $subjectTitleVal = new TitleValue( $subject, 'A' );
+               $talkTitleVal = new TitleValue( $talk, 'A' );
+               // Object will be the same one passed in if it's a talk, different but equal object if it's
+               // subject
+               $this->assertEquals( $talkTitleVal, $obj->getTalkPage( $subjectTitleVal ) );
+               $this->assertSame( $talkTitleVal, $obj->getTalkPage( $talkTitleVal ) );
+
+               $subjectTitle = Title::makeTitle( $subject, 'A' );
+               $talkTitle = Title::makeTitle( $talk, 'A' );
+               $this->assertEquals( $talkTitle, $subjectTitle->getTalkPage() );
+               $this->assertSame( $talkTitle, $talkTitle->getTalkPage() );
+       }
+
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getTalk
+        * @covers NamespaceInfo::isMethodValidFor
+        *
+        * @param int $ns
+        */
+       public function testGetTalk_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getTalk does not make any sense for given namespace $ns" );
+               $this->newObj()->getTalk( $ns );
+       }
+
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getTalk
+        * @covers NamespaceInfo::getTalkPage
+        * @covers NamespaceInfo::isMethodValidFor
+        *
+        * @param int $ns
+        */
+       public function testGetTalkPage_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getTalk does not make any sense for given namespace $ns" );
+               $this->newObj()->getTalkPage( new TitleValue( $ns, 'A' ) );
+       }
+
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getTalk
+        * @covers NamespaceInfo::getTalkPage
+        * @covers NamespaceInfo::isMethodValidFor
+        * @covers Title::getTalkPage
+        *
+        * @param int $ns
+        */
+       public function testTitleGetTalkPage_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getTalk does not make any sense for given namespace $ns" );
+               Title::makeTitle( $ns, 'A' )->getTalkPage();
+       }
+
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getAssociated
+        * @covers NamespaceInfo::isMethodValidFor
+        *
+        * @param int $ns
+        */
+       public function testGetAssociated_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getAssociated does not make any sense for given namespace $ns" );
+               $this->newObj()->getAssociated( $ns );
+       }
+
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getAssociated
+        * @covers NamespaceInfo::getAssociatedPage
+        * @covers NamespaceInfo::isMethodValidFor
+        *
+        * @param int $ns
+        */
+       public function testGetAssociatedPage_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getAssociated does not make any sense for given namespace $ns" );
+               $this->newObj()->getAssociatedPage( new TitleValue( $ns, 'A' ) );
+       }
+
+       /**
+        * @dataProvider provideSpecialNamespaces
+        * @covers NamespaceInfo::getAssociated
+        * @covers NamespaceInfo::getAssociatedPage
+        * @covers NamespaceInfo::isMethodValidFor
+        * @covers Title::getOtherPage
+        *
+        * @param int $ns
+        */
+       public function testTitleGetOtherPage_special( $ns ) {
+               $this->setExpectedException( MWException::class,
+                       "NamespaceInfo::getAssociated does not make any sense for given namespace $ns" );
+               Title::makeTitle( $ns, 'A' )->getOtherPage();
+       }
+
+       /**
+        * @dataProvider provideSubjectTalk
+        * @covers NamespaceInfo::getAssociated
+        * @covers NamespaceInfo::getAssociatedPage
+        * @covers Title::getOtherPage
+        *
+        * @param int $subject
+        * @param int $talk
+        */
+       public function testGetAssociated( $subject, $talk ) {
+               $obj = $this->newObj();
+               $this->assertSame( $talk, $obj->getAssociated( $subject ) );
+               $this->assertSame( $subject, $obj->getAssociated( $talk ) );
+
+               $subjectTitle = new TitleValue( $subject, 'A' );
+               $talkTitle = new TitleValue( $talk, 'A' );
+               // Object will not be the same
+               $this->assertEquals( $talkTitle, $obj->getAssociatedPage( $subjectTitle ) );
+               $this->assertEquals( $subjectTitle, $obj->getAssociatedPage( $talkTitle ) );
+
+               $subjectTitle = Title::makeTitle( $subject, 'A' );
+               $talkTitle = Title::makeTitle( $talk, 'A' );
+               $this->assertEquals( $talkTitle, $subjectTitle->getOtherPage() );
+               $this->assertEquals( $subjectTitle, $talkTitle->getOtherPage() );
+       }
+
+       public static function provideSubjectTalk() {
+               return [
+                       // Format: [ subject, talk ]
+                       'Main/talk' => [ NS_MAIN, NS_TALK ],
+                       'User/user talk' => [ NS_USER, NS_USER_TALK ],
+                       'Unknown namespaces also supported' => [ 106, 107 ],
+               ];
+       }
+
+       public static function provideSpecialNamespaces() {
+               return [
+                       'Special' => [ NS_SPECIAL ],
+                       'Media' => [ NS_MEDIA ],
+                       'Unknown negative index' => [ -613 ],
+               ];
+       }
+
+       // %} End getSubject/Talk/Associated
+
        /**********************************************************************************************
         * Canonical namespaces
         * %{
index 474decf..48c8a95 100644 (file)
@@ -1598,4 +1598,38 @@ class UserTest extends MediaWikiTestCase {
                $updater->setContent( 'main', $content );
                return $updater->saveRevision( CommentStoreComment::newUnsavedComment( $comment ) );
        }
+
+       /**
+        * @covers User::idFromName
+        */
+       public function testExistingIdFromName() {
+               $this->assertTrue(
+                       array_key_exists( $this->user->getName(), User::$idCacheByName ),
+                       'Test user should already be in the id cache.'
+               );
+               $this->assertSame(
+                       $this->user->getId(), User::idFromName( $this->user->getName() ),
+                       'Id is correctly retreived from the cache.'
+               );
+               $this->assertSame(
+                       $this->user->getId(), User::idFromName( $this->user->getName(), User::READ_LATEST ),
+                       'Id is correctly retreived from the database.'
+               );
+       }
+
+       /**
+        * @covers User::idFromName
+        */
+       public function testNonExistingIdFromName() {
+               $this->assertFalse(
+                       array_key_exists( 'NotExisitngUser', User::$idCacheByName ),
+                       'Non exisitng user should not be in the id cache.'
+               );
+               $this->assertSame( null, User::idFromName( 'NotExisitngUser' ) );
+               $this->assertTrue(
+                       array_key_exists( 'NotExisitngUser', User::$idCacheByName ),
+                       'Username will be cached when requested once.'
+               );
+               $this->assertSame( null, User::idFromName( 'NotExisitngUser' ) );
+       }
 }
index cc60899..f424b21 100644 (file)
@@ -78,7 +78,7 @@ class NoWriteWatchedItemStoreUnitTest extends MediaWikiTestCase {
                $this->setExpectedException( DBReadOnlyError::class );
                $noWriteService->resetNotificationTimestamp(
                        new UserIdentityValue( 1, 'MockUser', 0 ),
-                       Title::newFromText( 'Foo' )
+                       new TitleValue( 0, 'Foo' )
                );
        }
 
index ca5ae3e..82308de 100644 (file)
@@ -1,9 +1,10 @@
 <?php
 use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionRecord;
 use MediaWiki\User\UserIdentityValue;
 use Wikimedia\Rdbms\LBFactory;
 use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\ScopedCallback;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -109,6 +110,45 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                return $mock;
        }
 
+       /**
+        * Assumes that only getSubjectPage and getTalkPage will ever be called, and everything passed
+        * to them will have namespace 0.
+        */
+       private function getMockNsInfo() : NamespaceInfo {
+               $mock = $this->createMock( NamespaceInfo::class );
+               $mock->method( 'getSubjectPage' )->will( $this->returnArgument( 0 ) );
+               $mock->method( 'getTalkPage' )->will( $this->returnCallback(
+                               function ( $target ) {
+                                       return new TitleValue( 1, $target->getDbKey() );
+                               }
+                       ) );
+               $mock->expects( $this->never() )
+                       ->method( $this->anythingBut( 'getSubjectPage', 'getTalkPage' ) );
+               return $mock;
+       }
+
+       /**
+        * No methods may be called except provided callbacks, if any.
+        *
+        * @param array $callbacks Keys are method names, values are callbacks
+        * @param array $counts Keys are method names, values are expected number of times to be called
+        *   (default is any number is okay)
+        */
+       private function getMockRevisionLookup(
+               array $callbacks = [], array $counts = []
+       ) : RevisionLookup {
+               $mock = $this->createMock( RevisionLookup::class );
+               foreach ( $callbacks as $method => $callback ) {
+                       $count = isset( $counts[$method] ) ? $this->exactly( $counts[$method] ) : $this->any();
+                       $mock->expects( $count )
+                               ->method( $method )
+                               ->will( $this->returnCallback( $callbacks[$method] ) );
+               }
+               $mock->expects( $this->never() )
+                       ->method( $this->anythingBut( ...array_keys( $callbacks ) ) );
+               return $mock;
+       }
+
        private function getFakeRow( array $rowValues ) {
                $fakeRow = new stdClass();
                foreach ( $rowValues as $valueName => $value ) {
@@ -117,19 +157,28 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                return $fakeRow;
        }
 
-       private function newWatchedItemStore(
-               LBFactory $lbFactory,
-               JobQueueGroup $queueGroup,
-               HashBagOStuff $cache,
-               ReadOnlyMode $readOnlyMode
-       ) {
+       /**
+        * @param array $mocks Associative array providing mocks to use when constructing the
+        *   WatchedItemStore. Anything not provided will fall back to a default. Valid keys:
+        *     * lbFactory
+        *     * db
+        *     * queueGroup
+        *     * cache
+        *     * readOnlyMode
+        *     * nsInfo
+        *     * revisionLookup
+        */
+       private function newWatchedItemStore( array $mocks = [] ) : WatchedItemStore {
                return new WatchedItemStore(
-                       $lbFactory,
-                       $queueGroup,
+                       $mocks['lbFactory'] ??
+                               $this->getMockLBFactory( $mocks['db'] ?? $this->getMockDb() ),
+                       $mocks['queueGroup'] ?? $this->getMockJobQueueGroup(),
                        new HashBagOStuff(),
-                       $cache,
-                       $readOnlyMode,
-                       1000
+                       $mocks['cache'] ?? $this->getMockCache(),
+                       $mocks['readOnlyMode'] ?? $this->getMockReadOnlyMode(),
+                       1000,
+                       $mocks['nsInfo'] ?? $this->getMockNsInfo(),
+                       $mocks['revisionLookup'] ?? $this->getMockRevisionLookup()
                );
        }
 
@@ -163,12 +212,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->method( 'delete' )
                        ->with( 'RM-KEY' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
                TestingAccessWrapper::newFromObject( $store )
                        ->cacheIndex = [ 0 => [ 'F' => [ 7 => 'RM-KEY', 9 => 'KEEP-KEY' ] ] ];
 
@@ -196,12 +240,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse( $store->clearUserWatchedItems( $user ) );
        }
@@ -227,12 +266,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals( 12, $store->countWatchedItems( $user ) );
        }
@@ -259,12 +293,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals( 7, $store->countWatchers( $titleValue ) );
        }
@@ -312,12 +341,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $expected = [
                        0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
@@ -380,12 +404,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $expected = [
                        0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
@@ -430,12 +449,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals( 7, $store->countVisitingWatchers( $titleValue, '111' ) );
        }
@@ -513,12 +527,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $expected = [
                        0 => [ 'SomeDbKey' => 100, 'OtherDbKey' => 300 ],
@@ -619,12 +628,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $expected = [
                        0 => [
@@ -674,12 +678,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $expected = [
                        0 => [ 'SomeDbKey' => 0, 'OtherDbKey' => 0 ],
@@ -713,12 +712,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals( 9, $store->countUnreadNotifications( $user ) );
        }
@@ -749,12 +743,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertSame(
                        true,
@@ -788,12 +777,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        9,
@@ -820,12 +804,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        )
                        ->will( $this->returnValue( new FakeResultWrapper( [] ) ) );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb ] );
 
                $store->duplicateEntry(
                        new TitleValue( 0, 'Old_Title' ),
@@ -880,12 +859,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $store->duplicateEntry(
                        new TitleValue( 0, 'Old_Title' ),
@@ -928,12 +902,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $store->duplicateAllAssociatedEntries(
                        new TitleValue( 0, 'Old_Title' ),
@@ -1023,12 +992,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $store->duplicateAllAssociatedEntries(
                        $oldTarget,
@@ -1057,12 +1021,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->method( 'delete' )
                        ->with( '0:Some_Page:1' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $store->addWatch(
                        new UserIdentityValue( 1, 'MockUser', 0 ),
@@ -1079,12 +1038,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )
                        ->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $store->addWatch(
                        new UserIdentityValue( 0, 'AnonUser', 0 ),
@@ -1094,11 +1048,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
 
        public function testAddWatchBatchForUser_readOnlyDBReturnsFalse() {
                $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $this->getMockDb() ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode( true )
-               );
+                       [ 'readOnlyMode' => $this->getMockReadOnlyMode( true ) ] );
 
                $this->assertFalse(
                        $store->addWatchBatchForUser(
@@ -1144,12 +1094,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->method( 'delete' )
                        ->with( '1:Some_Page:1' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $mockUser = new UserIdentityValue( 1, 'MockUser', 0 );
 
@@ -1170,12 +1115,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )
                        ->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->addWatchBatchForUser(
@@ -1195,12 +1135,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )
                        ->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertTrue(
                        $store->addWatchBatchForUser( $user, [] )
@@ -1231,12 +1166,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                '0:SomeDbKey:1'
                        );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $watchedItem = $store->loadWatchedItem(
                        new UserIdentityValue( 1, 'MockUser', 0 ),
@@ -1267,12 +1197,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->loadWatchedItem(
@@ -1291,12 +1216,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->loadWatchedItem(
@@ -1341,12 +1261,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                [ '1:SomeDbKey:1' ]
                        );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertTrue(
                        $store->removeWatch(
@@ -1392,12 +1307,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                [ '1:SomeDbKey:1' ]
                        );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->removeWatch(
@@ -1417,12 +1327,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )
                        ->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->removeWatch(
@@ -1463,12 +1368,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                '0:SomeDbKey:1'
                        );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $watchedItem = $store->getWatchedItem(
                        new UserIdentityValue( 1, 'MockUser', 0 ),
@@ -1499,12 +1399,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        )
                        ->will( $this->returnValue( $cachedItem ) );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        $cachedItem,
@@ -1538,12 +1433,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' )
                        ->will( $this->returnValue( false ) );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->getWatchedItem(
@@ -1563,12 +1453,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->getWatchedItem(
@@ -1605,12 +1490,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'set' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
                $user = new UserIdentityValue( 1, 'MockUser', 0 );
 
                $watchedItems = $store->getWatchedItemsForUser( $user );
@@ -1658,11 +1538,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->will( $this->returnValue( [] ) );
 
                $store = $this->newWatchedItemStore(
-                       $mockLoadBalancer,
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+                       [ 'lbFactory' => $mockLoadBalancer, 'cache' => $mockCache ] );
 
                $watchedItems = $store->getWatchedItemsForUser(
                        $user,
@@ -1672,12 +1548,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        }
 
        public function testGetWatchedItemsForUser_badSortOptionThrowsException() {
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $this->getMockDb() ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore();
 
                $this->setExpectedException( InvalidArgumentException::class );
                $store->getWatchedItemsForUser(
@@ -1715,12 +1586,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                '0:SomeDbKey:1'
                        );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertTrue(
                        $store->isWatched(
@@ -1753,12 +1619,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' )
                        ->will( $this->returnValue( false ) );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->isWatched(
@@ -1778,12 +1639,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->isWatched(
@@ -1847,12 +1703,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [
@@ -1900,12 +1751,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [
@@ -1964,12 +1810,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [
@@ -2006,12 +1847,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [
@@ -2034,12 +1870,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache = $this->getMockCache();
                $mockCache->expects( $this->never() )->method( $this->anything() );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [
@@ -2061,17 +1892,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->resetNotificationTimestamp(
                                new UserIdentityValue( 0, 'AnonUser', 0 ),
-                               Title::newFromText( 'SomeDbKey' )
+                               new TitleValue( 0, 'SomeDbKey' )
                        )
                );
        }
@@ -2096,24 +1922,19 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'set' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertFalse(
                        $store->resetNotificationTimestamp(
                                new UserIdentityValue( 1, 'MockUser', 0 ),
-                               Title::newFromText( 'SomeDbKey' )
+                               new TitleValue( 0, 'SomeDbKey' )
                        )
                );
        }
 
        public function testResetNotificationTimestamp_item() {
                $user = new UserIdentityValue( 1, 'MockUser', 0 );
-               $title = Title::newFromText( 'SomeDbKey' );
+               $title = new TitleValue( 0, 'SomeDbKey' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -2150,12 +1971,22 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                // don't run
                        } );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               // We don't care if these methods actually do anything here
+               $mockRevisionLookup = $this->getMockRevisionLookup( [
+                       'getRevisionByTitle' => function () {
+                               return null;
+                       },
+                       'getTimestampFromId' => function () {
+                               return '00000000000000';
+                       },
+               ] );
+
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
 
                $this->assertTrue(
                        $store->resetNotificationTimestamp(
@@ -2167,7 +1998,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
 
        public function testResetNotificationTimestamp_noItemForced() {
                $user = new UserIdentityValue( 1, 'MockUser', 0 );
-               $title = Title::newFromText( 'SomeDbKey' );
+               $title = new TitleValue( 0, 'SomeDbKey' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->never() )
@@ -2181,12 +2012,23 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+
+               // We don't care if these methods actually do anything here
+               $mockRevisionLookup = $this->getMockRevisionLookup( [
+                       'getRevisionByTitle' => function () {
+                               return null;
+                       },
+                       'getTimestampFromId' => function () {
+                               return '00000000000000';
+                       },
+               ] );
+
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
 
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
@@ -2203,26 +2045,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                );
        }
 
-       /**
-        * @param string $text
-        * @param int $ns
-        *
-        * @return PHPUnit_Framework_MockObject_MockObject|Title
-        */
-       private function getMockTitle( $text, $ns = 0 ) {
-               $title = $this->createMock( Title::class );
-               $title->expects( $this->any() )
-                       ->method( 'getText' )
-                       ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) );
-               $title->expects( $this->any() )
-                       ->method( 'getDbKey' )
-                       ->will( $this->returnValue( str_replace( '_', ' ', $text ) ) );
-               $title->expects( $this->any() )
-                       ->method( 'getNamespace' )
-                       ->will( $this->returnValue( $ns ) );
-               return $title;
-       }
-
        private function verifyCallbackJob(
                ActivityUpdateJob $job,
                LinkTarget $expectedTitle,
@@ -2244,11 +2066,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        public function testResetNotificationTimestamp_oldidSpecifiedLatestRevisionForced() {
                $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $oldid = 22;
-               $title = $this->getMockTitle( 'SomeTitle' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( false ) );
+               $title = new TitleValue( 0, 'SomeTitle' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->never() )
@@ -2262,12 +2080,35 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeTitle:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+
+               $mockRevisionRecord = $this->createMock( RevisionRecord::class );
+               $mockRevisionRecord->expects( $this->never() )->method( $this->anything() );
+
+               $mockRevisionLookup = $this->getMockRevisionLookup( [
+                       'getTimestampFromId' => function () {
+                               return '00000000000000';
+                       },
+                       'getRevisionById' => function ( $id, $flags ) use ( $oldid, $mockRevisionRecord ) {
+                               $this->assertSame( $oldid, $id );
+                               $this->assertSame( 0, $flags );
+                               return $mockRevisionRecord;
+                       },
+                       'getNextRevision' =>
+                       function ( $oldRev, $titleArg ) use ( $mockRevisionRecord, $title ) {
+                               $this->assertSame( $mockRevisionRecord, $oldRev );
+                               $this->assertSame( $title, $titleArg );
+                               return false;
+                       },
+               ], [
+                       'getNextRevision' => 1,
+               ] );
+
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
 
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
@@ -2297,11 +2138,13 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        public function testResetNotificationTimestamp_oldidSpecifiedNotLatestRevisionForced() {
                $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
+               $title = new TitleValue( 0, 'SomeDbKey' );
+
+               $mockRevision = $this->createMock( RevisionRecord::class );
+               $mockRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockNextRevision = $this->createMock( RevisionRecord::class );
+               $mockNextRevision->expects( $this->never() )->method( $this->anything() );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -2329,12 +2172,34 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+
+               $mockRevisionLookup = $this->getMockRevisionLookup(
+                       [
+                               'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
+                                       $this->assertSame( $oldid, $oldidParam );
+                               },
+                               'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
+                                       $this->assertSame( $oldid, $id );
+                                       return $mockRevision;
+                               },
+                               'getNextRevision' =>
+                               function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
+                                       $this->assertSame( $mockRevision, $rev );
+                                       return $mockNextRevision;
+                               },
+                       ],
+                       [
+                               'getTimestampFromId' => 2,
+                               'getRevisionById' => 1,
+                               'getNextRevision' => 1,
+                       ]
+               );
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
 
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
@@ -2351,15 +2216,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                }
                        ) );
 
-               $getTimestampCallCounter = 0;
-               $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
-                       function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) {
-                               $getTimestampCallCounter++;
-                               $this->assertEquals( $title, $titleParam );
-                               $this->assertEquals( $oldid, $oldidParam );
-                       }
-               );
-
                $this->assertTrue(
                        $store->resetNotificationTimestamp(
                                $user,
@@ -2368,19 +2224,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                $oldid
                        )
                );
-               $this->assertEquals( 2, $getTimestampCallCounter );
-
-               ScopedCallback::consume( $scopedOverrideRevision );
        }
 
        public function testResetNotificationTimestamp_notWatchedPageForced() {
                $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
+               $title = new TitleValue( 0, 'SomeDbKey' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -2404,13 +2253,42 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
+
+               $mockRevision = $this->createMock( RevisionRecord::class );
+               $mockRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockNextRevision = $this->createMock( RevisionRecord::class );
+               $mockNextRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockRevisionLookup = $this->getMockRevisionLookup(
+                       [
+                               'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
+                                       $this->assertSame( $oldid, $oldidParam );
+                               },
+                               'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
+                                       $this->assertSame( $oldid, $id );
+                                       return $mockRevision;
+                               },
+                               'getNextRevision' =>
+                               function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
+                                       $this->assertSame( $mockRevision, $rev );
+                                       return $mockNextRevision;
+                               },
+                       ],
+                       [
+                               'getTimestampFromId' => 1,
+                               'getRevisionById' => 1,
+                               'getNextRevision' => 1,
+                       ]
                );
 
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
+
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
                        ->will( $this->returnCallback(
@@ -2439,11 +2317,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
        public function testResetNotificationTimestamp_futureNotificationTimestampForced() {
                $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
+               $title = new TitleValue( 0, 'SomeDbKey' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -2471,13 +2345,42 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
+
+               $mockRevision = $this->createMock( RevisionRecord::class );
+               $mockRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockNextRevision = $this->createMock( RevisionRecord::class );
+               $mockNextRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockRevisionLookup = $this->getMockRevisionLookup(
+                       [
+                               'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
+                                       $this->assertEquals( $oldid, $oldidParam );
+                               },
+                               'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
+                                       $this->assertSame( $oldid, $id );
+                                       return $mockRevision;
+                               },
+                               'getNextRevision' =>
+                               function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
+                                       $this->assertSame( $mockRevision, $rev );
+                                       return $mockNextRevision;
+                               },
+                       ],
+                       [
+                               'getTimestampFromId' => 2,
+                               'getRevisionById' => 1,
+                               'getNextRevision' => 1,
+                       ]
                );
 
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
+
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
                        ->will( $this->returnCallback(
@@ -2493,15 +2396,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                }
                        ) );
 
-               $getTimestampCallCounter = 0;
-               $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
-                       function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) {
-                               $getTimestampCallCounter++;
-                               $this->assertEquals( $title, $titleParam );
-                               $this->assertEquals( $oldid, $oldidParam );
-                       }
-               );
-
                $this->assertTrue(
                        $store->resetNotificationTimestamp(
                                $user,
@@ -2510,19 +2404,12 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                $oldid
                        )
                );
-               $this->assertEquals( 2, $getTimestampCallCounter );
-
-               ScopedCallback::consume( $scopedOverrideRevision );
        }
 
        public function testResetNotificationTimestamp_futureNotificationTimestampNotForced() {
                $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $oldid = 22;
-               $title = $this->getMockTitle( 'SomeDbKey' );
-               $title->expects( $this->once() )
-                       ->method( 'getNextRevisionID' )
-                       ->with( $oldid )
-                       ->will( $this->returnValue( 33 ) );
+               $title = new TitleValue( 0, 'SomeDbKey' );
 
                $mockDb = $this->getMockDb();
                $mockDb->expects( $this->once() )
@@ -2550,12 +2437,40 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->with( '0:SomeDbKey:1' );
 
                $mockQueueGroup = $this->getMockJobQueueGroup();
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $mockQueueGroup,
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+
+               $mockRevision = $this->createMock( RevisionRecord::class );
+               $mockRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockNextRevision = $this->createMock( RevisionRecord::class );
+               $mockNextRevision->expects( $this->never() )->method( $this->anything() );
+
+               $mockRevisionLookup = $this->getMockRevisionLookup(
+                       [
+                               'getTimestampFromId' => function ( $oldidParam ) use ( $oldid ) {
+                                       $this->assertEquals( $oldid, $oldidParam );
+                               },
+                               'getRevisionById' => function ( $id ) use ( $oldid, $mockRevision ) {
+                                       $this->assertSame( $oldid, $id );
+                                       return $mockRevision;
+                               },
+                               'getNextRevision' =>
+                               function ( RevisionRecord $rev ) use ( $mockRevision, $mockNextRevision ) {
+                                       $this->assertSame( $mockRevision, $rev );
+                                       return $mockNextRevision;
+                               },
+                       ],
+                       [
+                               'getTimestampFromId' => 2,
+                               'getRevisionById' => 1,
+                               'getNextRevision' => 1,
+                       ]
+               );
+               $store = $this->newWatchedItemStore( [
+                       'db' => $mockDb,
+                       'queueGroup' => $mockQueueGroup,
+                       'cache' => $mockCache,
+                       'revisionLookup' => $mockRevisionLookup,
+               ] );
 
                $mockQueueGroup->expects( $this->any() )
                        ->method( 'lazyPush' )
@@ -2572,15 +2487,6 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                }
                        ) );
 
-               $getTimestampCallCounter = 0;
-               $scopedOverrideRevision = $store->overrideRevisionGetTimestampFromIdCallback(
-                       function ( $titleParam, $oldidParam ) use ( &$getTimestampCallCounter, $title, $oldid ) {
-                               $getTimestampCallCounter++;
-                               $this->assertEquals( $title, $titleParam );
-                               $this->assertEquals( $oldid, $oldidParam );
-                       }
-               );
-
                $this->assertTrue(
                        $store->resetNotificationTimestamp(
                                $user,
@@ -2589,18 +2495,10 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                                $oldid
                        )
                );
-               $this->assertEquals( 2, $getTimestampCallCounter );
-
-               ScopedCallback::consume( $scopedOverrideRevision );
        }
 
        public function testSetNotificationTimestampsForUser_anonUser() {
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $this->getMockDb() ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore();
                $this->assertFalse( $store->setNotificationTimestampsForUser(
                        new UserIdentityValue( 0, 'AnonUser', 0 ), '' ) );
        }
@@ -2609,12 +2507,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $timestamp = '20100101010101';
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $this->getMockDb() ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore();
 
                // Note: This does not actually assert the job is correct
                $callableCallCounter = 0;
@@ -2634,12 +2527,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $user = new UserIdentityValue( 1, 'MockUser', 0 );
                $timestamp = null;
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $this->getMockDb() ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore();
 
                // Note: This does not actually assert the job is correct
                $callableCallCounter = 0;
@@ -2677,12 +2565,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->method( 'affectedRows' )
                        ->will( $this->returnValue( 2 ) );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $this->getMockCache(),
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb ] );
 
                $this->assertTrue(
                        $store->setNotificationTimestampsForUser( $user, $timestamp, $targets )
@@ -2721,12 +2604,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $this->assertEquals(
                        [ 2, 3 ],
@@ -2763,12 +2641,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                $mockCache->expects( $this->never() )->method( 'get' );
                $mockCache->expects( $this->never() )->method( 'delete' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                $watchers = $store->updateNotificationTimestamp(
                        new UserIdentityValue( 1, 'MockUser', 0 ),
@@ -2808,12 +2681,7 @@ class WatchedItemStoreUnitTest extends MediaWikiTestCase {
                        ->method( 'delete' )
                        ->with( '0:SomeDbKey:1' );
 
-               $store = $this->newWatchedItemStore(
-                       $this->getMockLBFactory( $mockDb ),
-                       $this->getMockJobQueueGroup(),
-                       $mockCache,
-                       $this->getMockReadOnlyMode()
-               );
+               $store = $this->newWatchedItemStore( [ 'db' => $mockDb, 'cache' => $mockCache ] );
 
                // This will add the item to the cache
                $store->getWatchedItem( $user, $titleValue );
index 0a04993..a1bdbad 100644 (file)
@@ -32,7 +32,7 @@ class MockFileBackend extends MemoryFileBackend {
        protected function doGetLocalCopyMulti( array $params ) {
                $tmpFiles = []; // (path => MockFSFile)
                foreach ( $params['srcs'] as $src ) {
-                       $tmpFiles[$src] = new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) );
+                       $tmpFiles[$src] = new MockFSFile( "Fake path for $src" );
                }
                return $tmpFiles;
        }
index eeaf05a..b2c51ca 100644 (file)
@@ -7,17 +7,16 @@
  * @since 1.28
  */
 class MockLocalRepo extends LocalRepo {
-       function getLocalCopy( $virtualUrl ) {
-               return new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) );
+       public function getLocalCopy( $virtualUrl ) {
+               return new MockFSFile( "Fake path for $virtualUrl" );
        }
 
-       function getLocalReference( $virtualUrl ) {
-               return new MockFSFile( wfTempDir() . '/' . wfRandomString( 32 ) );
+       public function getLocalReference( $virtualUrl ) {
+               return new MockFSFile( "Fake path for $virtualUrl" );
        }
 
-       function getFileProps( $virtualUrl ) {
+       public function getFileProps( $virtualUrl ) {
                $fsFile = $this->getLocalReference( $virtualUrl );
-
                return $fsFile->getProps();
        }
 }