Merge "Remove some unused variables"
authorAaron Schulz <aschulz@wikimedia.org>
Thu, 12 Jul 2012 20:47:18 +0000 (20:47 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 12 Jul 2012 20:47:18 +0000 (20:47 +0000)
includes/AutoLoader.php
includes/Exception.php
includes/cache/ProcessCacheLRU.php [new file with mode: 0644]
includes/filerepo/backend/FileBackendStore.php
maintenance/copyFileBackend.php
tests/phpunit/includes/cache/ProcessCacheLRUTest.php [new file with mode: 0644]

index 11cf616..6c37d1a 100644 (file)
@@ -419,6 +419,7 @@ $wgAutoloadLocalClasses = array(
        'LinkCache' => 'includes/cache/LinkCache.php',
        'MessageCache' => 'includes/cache/MessageCache.php',
        'ObjectFileCache' => 'includes/cache/ObjectFileCache.php',
+       'ProcessCacheLRU' => 'includes/cache/ProcessCacheLRU.php',
        'ResourceFileCache' => 'includes/cache/ResourceFileCache.php',
        'SquidUpdate' => 'includes/cache/SquidUpdate.php',
        'TitleDependency' => 'includes/cache/CacheDependency.php',
index 6e1325f..0fc5cd7 100644 (file)
@@ -33,7 +33,8 @@ class MWException extends Exception {
        var $logId;
 
        /**
-        * Should the exception use $wgOut to output the error ?
+        * Should the exception use $wgOut to output the error?
+        *
         * @return bool
         */
        function useOutputPage() {
@@ -44,7 +45,8 @@ class MWException extends Exception {
        }
 
        /**
-        * Can the extension use wfMsg() to get i18n messages ?
+        * Can the extension use wfMsg() to get i18n messages?
+        *
         * @return bool
         */
        function useMessageCache() {
@@ -62,9 +64,9 @@ class MWException extends Exception {
        /**
         * Run hook to allow extensions to modify the text of the exception
         *
-        * @param $name String: class name of the exception
-        * @param $args Array: arguments to pass to the callback functions
-        * @return Mixed: string to output or null if any hook has been called
+        * @param $name string: class name of the exception
+        * @param $args array: arguments to pass to the callback functions
+        * @return string|null string to output or null if any hook has been called
         */
        function runHooks( $name, $args = array() ) {
                global $wgExceptionHooks;
@@ -97,11 +99,11 @@ class MWException extends Exception {
        /**
         * Get a message from i18n
         *
-        * @param $key String: message name
-        * @param $fallback String: default message if the message cache can't be
+        * @param $key string: message name
+        * @param $fallback string: default message if the message cache can't be
         *                  called by the exception
         * The function also has other parameters that are arguments for the message
-        * @return String message with arguments replaced
+        * @return string message with arguments replaced
         */
        function msg( $key, $fallback /*[, params...] */ ) {
                $args = array_slice( func_get_args(), 2 );
@@ -118,7 +120,7 @@ class MWException extends Exception {
         * backtrace to the error, otherwise show a message to ask to set it to true
         * to show that information.
         *
-        * @return String html to output
+        * @return string html to output
         */
        function getHTML() {
                global $wgShowExceptionDetails;
@@ -140,8 +142,10 @@ class MWException extends Exception {
        }
 
        /**
+        * Get the text to display when reporting the error on the command line.
         * If $wgShowExceptionDetails is true, return a text message with a
         * backtrace to the error.
+        *
         * @return string
         */
        function getText() {
@@ -157,13 +161,21 @@ class MWException extends Exception {
        }
 
        /**
-        * Return titles of this error page
-        * @return String
+        * Return the title of the page when reporting this error in a HTTP response.
+        *
+        * @return string
         */
        function getPageTitle() {
                return $this->msg( 'internalerror', "Internal error" );
        }
 
+       /**
+        * Get a random ID for this error.
+        * This allows to link the exception to its correspoding log entry when
+        * $wgShowExceptionDetails is set to false.
+        *
+        * @return string
+        */
        function getLogId() {
                if ( $this->logId === null ) {
                        $this->logId = wfRandomString( 8 );
@@ -175,7 +187,7 @@ class MWException extends Exception {
         * Return the requested URL and point to file and line number from which the
         * exception occured
         *
-        * @return String
+        * @return string
         */
        function getLogMessage() {
                global $wgRequest;
@@ -197,7 +209,9 @@ class MWException extends Exception {
                return "[$id] $url   Exception from line $line of $file: $message";
        }
 
-       /** Output the exception report using HTML */
+       /**
+        * Output the exception report using HTML.
+        */
        function reportHTML() {
                global $wgOut;
                if ( $this->useOutputPage() ) {
@@ -261,7 +275,9 @@ class MWException extends Exception {
        }
 
        /**
-        * @static
+        * Check whether we are in command line mode or not to report the exception
+        * in the correct format.
+        *
         * @return bool
         */
        static function isCommandLine() {
@@ -272,6 +288,7 @@ class MWException extends Exception {
 /**
  * Exception class which takes an HTML error message, and does not
  * produce a backtrace. Replacement for OutputPage::fatalError().
+ *
  * @ingroup Exception
  */
 class FatalError extends MWException {
@@ -292,7 +309,8 @@ class FatalError extends MWException {
 }
 
 /**
- * An error page which can definitely be safely rendered using the OutputPage
+ * An error page which can definitely be safely rendered using the OutputPage.
+ *
  * @ingroup Exception
  */
 class ErrorPageError extends MWException {
@@ -331,6 +349,8 @@ class ErrorPageError extends MWException {
  * Show an error page on a badtitle.
  * Similar to ErrorPage, but emit a 400 HTTP error code to let mobile
  * browser it is not really a valid content.
+ *
+ * @ingroup Exception
  */
 class BadTitleError extends ErrorPageError {
 
@@ -360,6 +380,7 @@ class BadTitleError extends ErrorPageError {
 /**
  * Show an error when a user tries to do something they do not have the necessary
  * permissions for.
+ *
  * @ingroup Exception
  */
 class PermissionsError extends ErrorPageError {
@@ -396,7 +417,8 @@ class PermissionsError extends ErrorPageError {
 
 /**
  * Show an error when the wiki is locked/read-only and the user tries to do
- * something that requires write access
+ * something that requires write access.
+ *
  * @ingroup Exception
  */
 class ReadOnlyError extends ErrorPageError {
@@ -410,7 +432,8 @@ class ReadOnlyError extends ErrorPageError {
 }
 
 /**
- * Show an error when the user hits a rate limit
+ * Show an error when the user hits a rate limit.
+ *
  * @ingroup Exception
  */
 class ThrottledError extends ErrorPageError {
@@ -429,7 +452,8 @@ class ThrottledError extends ErrorPageError {
 }
 
 /**
- * Show an error when the user tries to do something whilst blocked
+ * Show an error when the user tries to do something whilst blocked.
+ *
  * @ingroup Exception
  */
 class UserBlockedError extends ErrorPageError {
@@ -495,15 +519,18 @@ class UserBlockedError extends ErrorPageError {
  * }
  * @endcode
  *
- * @param $reasonMsg A message key containing the reason for the error.
- *        Optional, default: 'exception-nologin-text'
- * @param $titleMsg A message key to set the page title.
- *        Optional, default: 'exception-nologin'
- * @param $params Parameters to wfMsg().
- *        Optiona, default: null
+ * @ingroup Exception
  */
 class UserNotLoggedIn extends ErrorPageError {
 
+       /**
+        * @param $reasonMsg A message key containing the reason for the error.
+        *        Optional, default: 'exception-nologin-text'
+        * @param $titleMsg A message key to set the page title.
+        *        Optional, default: 'exception-nologin'
+        * @param $params Parameters to wfMsg().
+        *        Optiona, default: null
+        */
        public function __construct(
                $reasonMsg = 'exception-nologin-text',
                $titleMsg  = 'exception-nologin',
@@ -628,7 +655,8 @@ class MWExceptionHandler {
        /**
         * Print a message, if possible to STDERR.
         * Use this in command line mode only (see isCommandLine)
-        * @param $message String Failure text
+        *
+        * @param $message string Failure text
         */
        public static function printError( $message ) {
                # NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602).
@@ -642,8 +670,9 @@ class MWExceptionHandler {
 
        /**
         * Print a message after escaping it and converting newlines to <br>
-        * Use this for non-command line failures
-        * @param $message String Failure text
+        * Use this for non-command line failures.
+        *
+        * @param $message string Failure text
         */
        private static function escapeEchoAndDie( $message ) {
                echo nl2br( htmlspecialchars( $message ) ) . "\n";
diff --git a/includes/cache/ProcessCacheLRU.php b/includes/cache/ProcessCacheLRU.php
new file mode 100644 (file)
index 0000000..a9bcd9b
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Per-process memory cache for storing items.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Cache
+ */
+
+/**
+ * Handles per process caching of items
+ * @ingroup Cache
+ */
+class ProcessCacheLRU {
+       /** @var Array */
+       protected $cache = array(); // (key => prop => value)
+
+       protected $maxCacheKeys; // integer; max entries
+
+       /**
+        * @param $maxEntries integer Maximum number of entries allowed (min 1).
+        * @throws MWException When $maxCacheKeys is not an int or =< 0.
+        */
+       public function __construct( $maxKeys ) {
+               if ( !is_int( $maxKeys ) || $maxKeys < 1 ) {
+                       throw new MWException( __METHOD__ . " must be given an integer and >= 1" );
+               }
+               $this->maxCacheKeys = $maxKeys;
+       }
+
+       /**
+        * Set a property field for a cache entry.
+        * This will prune the cache if it gets too large.
+        * If the item is already set, it will be pushed to the top of the cache.
+        *
+        * @param $key string
+        * @param $prop string
+        * @param $value mixed
+        * @return void
+        */
+       public function set( $key, $prop, $value ) {
+               if ( isset( $this->cache[$key] ) ) {
+                       $this->ping( $key ); // push to top
+               } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) {
+                       reset( $this->cache );
+                       unset( $this->cache[key( $this->cache )] );
+               }
+               $this->cache[$key][$prop] = $value;
+       }
+
+       /**
+        * Check if a property field exists for a cache entry.
+        *
+        * @param $key string
+        * @param $prop string
+        * @return bool
+        */
+       public function has( $key, $prop ) {
+               return isset( $this->cache[$key][$prop] );
+       }
+
+       /**
+        * Get a property field for a cache entry.
+        * This returns null if the property is not set.
+        * If the item is already set, it will be pushed to the top of the cache.
+        *
+        * @param $key string
+        * @param $prop string
+        * @return mixed
+        */
+       public function get( $key, $prop ) {
+               if ( isset( $this->cache[$key][$prop] ) ) {
+                       $this->ping( $key ); // push to top
+                       return $this->cache[$key][$prop];
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Clear one or several cache entries, or all cache entries
+        *
+        * @param $keys string|Array
+        * @return void
+        */
+       public function clear( $keys = null ) {
+               if ( $keys === null ) {
+                       $this->cache = array();
+               } else {
+                       foreach ( (array)$keys as $key ) {
+                               unset( $this->cache[$key] );
+                       }
+               }
+       }
+
+       /**
+        * Push an entry to the top of the cache
+        *
+        * @param $key string
+        */
+       protected function ping( $key ) {
+               $item = $this->cache[$key];
+               unset( $this->cache[$key] );
+               $this->cache[$key] = $item;
+       }
+}
index 9c713d3..49bb039 100644 (file)
 abstract class FileBackendStore extends FileBackend {
        /** @var BagOStuff */
        protected $memCache;
-
-       /** @var Array Map of paths to small (RAM/disk) cache items */
-       protected $cache = array(); // (storage path => key => value)
-       protected $maxCacheSize = 300; // integer; max paths with entries
-       /** @var Array Map of paths to large (RAM/disk) cache items */
-       protected $expensiveCache = array(); // (storage path => key => value)
-       protected $maxExpensiveCacheSize = 5; // integer; max paths with entries
+       /** @var ProcessCacheLRU */
+       protected $cheapCache; // Map of paths to small (RAM/disk) cache items
+       /** @var ProcessCacheLRU */
+       protected $expensiveCache; // Map of paths to large (RAM/disk) cache items
 
        /** @var Array Map of container names to sharding settings */
        protected $shardViaHashLevels = array(); // (container name => config array)
@@ -58,7 +55,9 @@ abstract class FileBackendStore extends FileBackend {
         */
        public function __construct( array $config ) {
                parent::__construct( $config );
-               $this->memCache = new EmptyBagOStuff(); // disabled by default
+               $this->memCache       = new EmptyBagOStuff(); // disabled by default
+               $this->cheapCache     = new ProcessCacheLRU( 300 );
+               $this->expensiveCache = new ProcessCacheLRU( 5 );
        }
 
        /**
@@ -579,17 +578,17 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $latest = !empty( $params['latest'] ); // use latest data?
-               if ( !isset( $this->cache[$path]['stat'] ) ) {
+               if ( !$this->cheapCache->has( $path, 'stat' ) ) {
                        $this->primeFileCache( array( $path ) ); // check persistent cache
                }
-               if ( isset( $this->cache[$path]['stat'] ) ) {
+               if ( $this->cheapCache->has( $path, 'stat' ) ) {
+                       $stat = $this->cheapCache->get( $path, 'stat' );
                        // If we want the latest data, check that this cached
                        // value was in fact fetched with the latest available data.
-                       if ( !$latest || $this->cache[$path]['stat']['latest'] ) {
-                               $this->pingCache( $path ); // LRU
+                       if ( !$latest || $stat['latest'] ) {
                                wfProfileOut( __METHOD__ . '-' . $this->name );
                                wfProfileOut( __METHOD__ );
-                               return $this->cache[$path]['stat'];
+                               return $stat;
                        }
                }
                wfProfileIn( __METHOD__ . '-miss' );
@@ -599,13 +598,11 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileOut( __METHOD__ . '-miss' );
                if ( is_array( $stat ) ) { // don't cache negatives
                        $stat['latest'] = $latest;
-                       $this->trimCache(); // limit memory
-                       $this->cache[$path]['stat'] = $stat;
+                       $this->cheapCache->set( $path, 'stat', $stat );
                        $this->setFileCache( $path, $stat ); // update persistent cache
                        if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
-                               $this->trimCache(); // limit memory
-                               $this->cache[$path]['sha1'] =
-                                       array( 'hash' => $stat['sha1'], 'latest' => $latest );
+                               $this->cheapCache->set( $path, 'sha1',
+                                       array( 'hash' => $stat['sha1'], 'latest' => $latest ) );
                        }
                } else {
                        wfDebug( __METHOD__ . ": File $path does not exist.\n" );
@@ -653,14 +650,14 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $latest = !empty( $params['latest'] ); // use latest data?
-               if ( isset( $this->cache[$path]['sha1'] ) ) {
+               if ( $this->cheapCache->has( $path, 'sha1' ) ) {
+                       $stat = $this->cheapCache->get( $path, 'sha1' );
                        // If we want the latest data, check that this cached
                        // value was in fact fetched with the latest available data.
-                       if ( !$latest || $this->cache[$path]['sha1']['latest'] ) {
-                               $this->pingCache( $path ); // LRU
+                       if ( !$latest || $stat['latest'] ) {
                                wfProfileOut( __METHOD__ . '-' . $this->name );
                                wfProfileOut( __METHOD__ );
-                               return $this->cache[$path]['sha1']['hash'];
+                               return $stat['hash'];
                        }
                }
                wfProfileIn( __METHOD__ . '-miss' );
@@ -669,8 +666,8 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileOut( __METHOD__ . '-miss-' . $this->name );
                wfProfileOut( __METHOD__ . '-miss' );
                if ( $hash ) { // don't cache negatives
-                       $this->trimCache(); // limit memory
-                       $this->cache[$path]['sha1'] = array( 'hash' => $hash, 'latest' => $latest );
+                       $this->cheapCache->set( $path, 'sha1',
+                               array( 'hash' => $hash, 'latest' => $latest ) );
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -716,21 +713,20 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $latest = !empty( $params['latest'] ); // use latest data?
-               if ( isset( $this->expensiveCache[$path]['localRef'] ) ) {
+               if ( $this->expensiveCache->has( $path, 'localRef' ) ) {
+                       $val = $this->expensiveCache->get( $path, 'localRef' );
                        // If we want the latest data, check that this cached
                        // value was in fact fetched with the latest available data.
-                       if ( !$latest || $this->expensiveCache[$path]['localRef']['latest'] ) {
-                               $this->pingExpensiveCache( $path );
+                       if ( !$latest || $val['latest'] ) {
                                wfProfileOut( __METHOD__ . '-' . $this->name );
                                wfProfileOut( __METHOD__ );
-                               return $this->expensiveCache[$path]['localRef']['object'];
+                               return $val['object'];
                        }
                }
                $tmpFile = $this->getLocalCopy( $params );
                if ( $tmpFile ) { // don't cache negatives
-                       $this->trimExpensiveCache(); // limit memory
-                       $this->expensiveCache[$path]['localRef'] =
-                               array( 'object' => $tmpFile, 'latest' => $latest );
+                       $this->expensiveCache->set( $path, 'localRef',
+                               array( 'object' => $tmpFile, 'latest' => $latest ) );
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -1119,12 +1115,12 @@ abstract class FileBackendStore extends FileBackend {
                        $paths = array_filter( $paths, 'strlen' ); // remove nulls
                }
                if ( $paths === null ) {
-                       $this->cache = array();
-                       $this->expensiveCache = array();
+                       $this->cheapCache->clear();
+                       $this->expensiveCache->clear();
                } else {
                        foreach ( $paths as $path ) {
-                               unset( $this->cache[$path] );
-                               unset( $this->expensiveCache[$path] );
+                               $this->cheapCache->clear( $path );
+                               $this->expensiveCache->clear( $path );
                        }
                }
                $this->doClearCache( $paths );
@@ -1149,58 +1145,6 @@ abstract class FileBackendStore extends FileBackend {
         */
        abstract protected function directoriesAreVirtual();
 
-       /**
-        * Move a cache entry to the top (such as when accessed)
-        *
-        * @param $path string Storage path
-        * @return void
-        */
-       protected function pingCache( $path ) {
-               if ( isset( $this->cache[$path] ) ) {
-                       $tmp = $this->cache[$path];
-                       unset( $this->cache[$path] );
-                       $this->cache[$path] = $tmp;
-               }
-       }
-
-       /**
-        * Prune the inexpensive cache if it is too big to add an item
-        *
-        * @return void
-        */
-       protected function trimCache() {
-               if ( count( $this->cache ) >= $this->maxCacheSize ) {
-                       reset( $this->cache );
-                       unset( $this->cache[key( $this->cache )] );
-               }
-       }
-
-       /**
-        * Move a cache entry to the top (such as when accessed)
-        *
-        * @param $path string Storage path
-        * @return void
-        */
-       protected function pingExpensiveCache( $path ) {
-               if ( isset( $this->expensiveCache[$path] ) ) {
-                       $tmp = $this->expensiveCache[$path];
-                       unset( $this->expensiveCache[$path] );
-                       $this->expensiveCache[$path] = $tmp;
-               }
-       }
-
-       /**
-        * Prune the expensive cache if it is too big to add an item
-        *
-        * @return void
-        */
-       protected function trimExpensiveCache() {
-               if ( count( $this->expensiveCache ) >= $this->maxExpensiveCacheSize ) {
-                       reset( $this->expensiveCache );
-                       unset( $this->expensiveCache[key( $this->expensiveCache )] );
-               }
-       }
-
        /**
         * Check if a container name is valid.
         * This checks for for length and illegal characters.
@@ -1555,12 +1499,10 @@ abstract class FileBackendStore extends FileBackend {
                foreach ( $values as $cacheKey => $val ) {
                        if ( is_array( $val ) ) {
                                $path = $pathNames[$cacheKey];
-                               $this->trimCache(); // limit memory
-                               $this->cache[$path]['stat'] = $val;
+                               $this->cheapCache->set( $path, 'stat', $val );
                                if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
-                                       $this->trimCache(); // limit memory
-                                       $this->cache[$path]['sha1'] =
-                                               array( 'hash' => $val['sha1'], 'latest' => $val['latest'] );
+                                       $this->cheapCache->set( $path, 'sha1',
+                                               array( 'hash' => $val['sha1'], 'latest' => $val['latest'] ) );
                                }
                        }
                }
index 8db0a7f..184bf3d 100644 (file)
@@ -44,6 +44,7 @@ class CopyFileBackend extends Maintenance {
                $this->addOption( 'subdir', 'Only do items in this child directory', false, true );
                $this->addOption( 'ratefile', 'File to check periodically for batch size', false, true );
                $this->addOption( 'skiphash', 'Skip SHA-1 sync checks for files' );
+               $this->addOption( 'missingonly', 'Only copy files missing from destination listing' );
                $this->setBatchSize( 50 );
        }
 
@@ -65,12 +66,32 @@ class CopyFileBackend extends Maintenance {
                                $this->output( "Doing container '$container'...\n" );
                        }
 
-                       $dir = $src->getRootStoragePath() . "/$backendRel";
-                       $srcPathsRel = $src->getFileList( array( 'dir' => $dir ) );
+                       $srcPathsRel = $src->getFileList( array(
+                               'dir' => $src->getRootStoragePath() . "/$backendRel" ) );
                        if ( $srcPathsRel === null ) {
                                $this->error( "Could not list files in $container.", 1 ); // die
                        }
 
+                       // Do a listing comparison if specified
+                       if ( $this->hasOption( 'missingonly' ) ) {
+                               $relFilesSrc = array();
+                               $relFilesDst = array();
+                               foreach ( $srcPathsRel as $srcPathRel ) {
+                                       $relFilesSrc[] = $srcPathRel;
+                               }
+                               $dstPathsRel = $dst->getFileList( array(
+                                       'dir' => $dst->getRootStoragePath() . "/$backendRel" ) );
+                               if ( $dstPathsRel === null ) {
+                                       $this->error( "Could not list files in $container.", 1 ); // die
+                               }
+                               foreach ( $dstPathsRel as $dstPathRel ) {
+                                       $relFilesDst[] = $dstPathRel;
+                               }
+                               // Only copy the missing files over in the next loop
+                               $srcPathsRel = array_diff( $relFilesSrc, $relFilesDst );
+                               $this->output( count( $srcPathsRel ) . " file(s) need to be copied.\n" );
+                       }
+
                        $batchPaths = array();
                        foreach ( $srcPathsRel as $srcPathRel ) {
                                // Check up on the rate file periodically to adjust the concurrency
@@ -131,7 +152,7 @@ class CopyFileBackend extends Maintenance {
                }
 
                $t_start = microtime( true );
-               $status = $dst->doOperations( $ops, array( 'nonJournaled' => 1 ) );
+               $status = $dst->doQuickOperations( $ops );
                $ellapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
                if ( !$status->isOK() ) {
                        $this->error( print_r( $status->getErrorsArray(), true ) );
diff --git a/tests/phpunit/includes/cache/ProcessCacheLRUTest.php b/tests/phpunit/includes/cache/ProcessCacheLRUTest.php
new file mode 100644 (file)
index 0000000..30bfb12
--- /dev/null
@@ -0,0 +1,239 @@
+<?php
+
+/**
+ * Test for ProcessCacheLRU class.
+ *
+ * Note that it uses the ProcessCacheLRUTestable class which extends some
+ * properties and methods visibility. That class is defined at the end of the
+ * file containing this class.
+ *
+ * @group Cache
+ */
+class ProcessCacheLRUTest extends MediaWikiTestCase {
+
+       /**
+        * Helper to verify emptiness of a cache object.
+        * Compare against an array so we get the cache content difference.
+        */
+       function assertCacheEmpty( $cache, $msg = 'Cache should be empty' ) {
+               $this->assertAttributeEquals( array(), 'cache', $cache, $msg );
+       }
+
+       /**
+        * Helper to fill a cache object passed by reference
+        */
+       function fillCache( &$cache, $numEntries ) {
+               // Fill cache with three values
+               for( $i=1; $i<=$numEntries; $i++) {
+                       $cache->set( "cache-key-$i", "prop-$i", "value-$i" );
+               }
+       }
+
+       /**
+        * Generates an array of what would be expected in cache for a given cache
+        * size and a number of entries filled in sequentially
+        */
+       function getExpectedCache( $cacheMaxEntries, $entryToFill ) {
+               $expected = array();
+
+               if( $entryToFill === 0 ) {
+                       # The cache is empty!
+                       return array();
+               } elseif( $entryToFill <= $cacheMaxEntries ) {
+                       # Cache is not fully filled
+                       $firstKey = 1;
+               } else {
+                       # Cache overflowed
+                       $firstKey = 1 + $entryToFill - $cacheMaxEntries;
+               }
+
+               $lastKey  = $entryToFill;
+
+               for( $i=$firstKey; $i<=$lastKey; $i++ ) {
+                       $expected["cache-key-$i"] = array( "prop-$i" => "value-$i" );
+               }
+               return $expected;
+       }
+
+       /**
+        * Highlight diff between assertEquals and assertNotSame
+        */
+       function testPhpUnitArrayEquality() {
+               $one = array( 'A' => 1, 'B' => 2 );
+               $two = array( 'B' => 2, 'A' => 1 );
+               $this->assertEquals( $one, $two );  // ==
+               $this->assertNotSame( $one, $two ); // ===
+       }
+
+       /**
+        * @dataProvider provideInvalidConstructorArg
+        * @expectedException MWException
+        */
+       function testConstructorGivenInvalidValue( $maxSize ) {
+               $c = new ProcessCacheLRUTestable( $maxSize );
+       }
+
+       /**
+        * Value which are forbidden by the constructor
+        */
+       function provideInvalidConstructorArg() {
+               return array(
+                       array( null ),
+                       array( array() ),
+                       array( new stdClass() ),
+                       array( 0 ),
+                       array( '5' ),
+                       array( -1 ),
+               );
+       }
+
+       function testAddAndGetAKey() {
+               $oneCache = new ProcessCacheLRUTestable( 1 );
+               $this->assertCacheEmpty( $oneCache );
+
+               // First set just one value
+               $oneCache->set( 'cache-key', 'prop1', 'value1' );
+               $this->assertEquals( 1, $oneCache->getEntriesCount() );
+               $this->assertTrue( $oneCache->has( 'cache-key', 'prop1' ) );
+               $this->assertEquals( 'value1', $oneCache->get( 'cache-key', 'prop1' ) );
+       }
+
+       function testDeleteOldKey() {
+               $oneCache = new ProcessCacheLRUTestable( 1 );
+               $this->assertCacheEmpty( $oneCache );
+
+               $oneCache->set( 'cache-key', 'prop1', 'value1' );
+               $oneCache->set( 'cache-key', 'prop1', 'value2' );
+               $this->assertEquals( 'value2', $oneCache->get( 'cache-key', 'prop1' ) );
+       }
+
+       /**
+        * This test that we properly overflow when filling a cache with
+        * a sequence of always different cache-keys. Meant to verify we correclty
+        * delete the older key.
+        *
+        * @dataProvider provideCacheFilling
+        * @param $cacheMaxEntries Maximum entry the created cache will hold
+        * @param $entryToFill Number of entries to insert in the created cache.
+        */
+       function testFillingCache( $cacheMaxEntries, $entryToFill, $msg = '' ) {
+               $cache = new ProcessCacheLRUTestable( $cacheMaxEntries );
+               $this->fillCache( $cache, $entryToFill);
+
+               $this->assertSame(
+                       $this->getExpectedCache( $cacheMaxEntries, $entryToFill ),
+                       $cache->getCache(),
+                       "Filling a $cacheMaxEntries entries cache with $entryToFill entries"
+               );
+
+       }
+
+       /**
+        * Provider for testFillingCache
+        */
+       function provideCacheFilling() {
+               // ($cacheMaxEntries, $entryToFill, $msg='')
+               return array(
+                       array( 1,  0 ),
+                       array( 1,  1 ),
+                       array( 1,  2 ), # overflow
+                       array( 5, 33 ), # overflow
+               );
+
+       }
+
+       /**
+        * Create a cache with only one remaining entry then update
+        * the first inserted entry. Should bump it to the top.
+        */
+       function testReplaceExistingKeyShouldBumpEntryToTop() {
+               $maxEntries = 3;
+
+               $cache = new ProcessCacheLRUTestable( $maxEntries );
+               // Fill cache leaving just one remaining slot
+               $this->fillCache( $cache, $maxEntries - 1 );
+
+               // Set an existing cache key
+               $cache->set( "cache-key-1", "prop-1", "new-value-for-1" );
+
+               $this->assertSame(
+                       array(
+                               'cache-key-2' => array( 'prop-2' => 'value-2' ),
+                               'cache-key-1' => array( 'prop-1' => 'new-value-for-1' ),
+                       ),
+                       $cache->getCache()
+               );
+       }
+
+       function testRecentlyAccessedKeyStickIn() {
+               $cache = new ProcessCacheLRUTestable( 2 );
+               $cache->set( 'first' , 'prop1', 'value1' );
+               $cache->set( 'second', 'prop2', 'value2' );
+
+               // Get first
+               $cache->get( 'first', 'prop1' );
+               // Cache a third value, should invalidate the least used one
+               $cache->set( 'third', 'prop3', 'value3' );
+
+               $this->assertFalse( $cache->has( 'second', 'prop2' ) );
+       }
+
+       /**
+        * This first create a full cache then update the value for the 2nd
+        * filled entry.
+        * Given a cache having 1,2,3 as key, updating 2 should bump 2 to
+        * the top of the queue with the new value: 1,3,2* (* = updated).
+        */
+       function testReplaceExistingKeyInAFullCacheShouldBumpToTop() {
+               $maxEntries = 3;
+
+               $cache = new ProcessCacheLRUTestable( $maxEntries );
+               $this->fillCache( $cache, $maxEntries );
+
+               // Set an existing cache key
+               $cache->set( "cache-key-2", "prop-2", "new-value-for-2" );
+               $this->assertSame(
+                       array(
+                               'cache-key-1' => array( 'prop-1' => 'value-1' ),
+                               'cache-key-3' => array( 'prop-3' => 'value-3' ),
+                               'cache-key-2' => array( 'prop-2' => 'new-value-for-2' ),
+                       ),
+                       $cache->getCache()
+               );
+               $this->assertEquals( 'new-value-for-2',
+                       $cache->get( 'cache-key-2', 'prop-2' )
+               );
+       }
+
+       function testBumpExistingKeyToTop() {
+               $cache = new ProcessCacheLRUTestable( 3 );
+               $this->fillCache( $cache, 3 );
+
+               // Set the very first cache key to a new value
+               $cache->set( "cache-key-1", "prop-1", "new value for 1" );
+               $this->assertEquals(
+                       array(
+                               'cache-key-2' => array( 'prop-2' => 'value-2' ),
+                               'cache-key-3' => array( 'prop-3' => 'value-3' ),
+                               'cache-key-1' => array( 'prop-1' => 'new value for 1' ),
+                       ),
+                       $cache->getCache()
+               );
+
+       }
+
+}
+
+/**
+ * Overrides some ProcessCacheLRU methods and properties accessibility.
+ */
+class ProcessCacheLRUTestable extends ProcessCacheLRU {
+       public $cache = array();
+
+       public function getCache() {
+               return $this->cache;
+       }
+       public function getEntriesCount() {
+               return count( $this->cache );
+       }
+}