Merge "resourceloader: Improve test cases for MessageBlobStore"
authorKrinkle <krinklemail@gmail.com>
Thu, 28 Mar 2019 20:37:42 +0000 (20:37 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 28 Mar 2019 20:37:42 +0000 (20:37 +0000)
autoload.php
includes/cache/MessageBlobStore.php [deleted file]
includes/resourceloader/MessageBlobStore.php [new file with mode: 0644]
tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php

index bb0457b..e9495b3 100644 (file)
@@ -974,7 +974,7 @@ $wgAutoloadLocalClasses = [
        'MergeMessageFileList' => __DIR__ . '/maintenance/mergeMessageFileList.php',
        'MergeableUpdate' => __DIR__ . '/includes/deferred/MergeableUpdate.php',
        'Message' => __DIR__ . '/includes/Message.php',
-       'MessageBlobStore' => __DIR__ . '/includes/cache/MessageBlobStore.php',
+       'MessageBlobStore' => __DIR__ . '/includes/resourceloader/MessageBlobStore.php',
        'MessageCache' => __DIR__ . '/includes/cache/MessageCache.php',
        'MessageCacheUpdate' => __DIR__ . '/includes/deferred/MessageCacheUpdate.php',
        'MessageContent' => __DIR__ . '/includes/content/MessageContent.php',
diff --git a/includes/cache/MessageBlobStore.php b/includes/cache/MessageBlobStore.php
deleted file mode 100644 (file)
index ceb51f2..0000000
+++ /dev/null
@@ -1,240 +0,0 @@
-<?php
-/**
- * Message blobs storage used by ResourceLoader.
- *
- * 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
- * @author Roan Kattouw
- * @author Trevor Parscal
- * @author Timo Tijhof
- */
-
-use MediaWiki\MediaWikiServices;
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerInterface;
-use Psr\Log\NullLogger;
-use Wikimedia\Rdbms\Database;
-
-/**
- * This class generates message blobs for use by ResourceLoader modules.
- *
- * A message blob is a JSON object containing the interface messages for a certain module in
- * a certain language.
- */
-class MessageBlobStore implements LoggerAwareInterface {
-
-       /* @var ResourceLoader */
-       private $resourceloader;
-
-       /**
-        * @var LoggerInterface
-        */
-       protected $logger;
-
-       /**
-        * @var WANObjectCache
-        */
-       protected $wanCache;
-
-       /**
-        * @param ResourceLoader $rl
-        * @param LoggerInterface|null $logger
-        */
-       public function __construct( ResourceLoader $rl, LoggerInterface $logger = null ) {
-               $this->resourceloader = $rl;
-               $this->logger = $logger ?: new NullLogger();
-               $this->wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
-       }
-
-       /**
-        * @since 1.27
-        * @param LoggerInterface $logger
-        */
-       public function setLogger( LoggerInterface $logger ) {
-               $this->logger = $logger;
-       }
-
-       /**
-        * Get the message blob for a module
-        *
-        * @since 1.27
-        * @param ResourceLoaderModule $module
-        * @param string $lang Language code
-        * @return string JSON
-        */
-       public function getBlob( ResourceLoaderModule $module, $lang ) {
-               $blobs = $this->getBlobs( [ $module->getName() => $module ], $lang );
-               return $blobs[$module->getName()];
-       }
-
-       /**
-        * Get the message blobs for a set of modules
-        *
-        * @since 1.27
-        * @param ResourceLoaderModule[] $modules Array of module objects keyed by name
-        * @param string $lang Language code
-        * @return array An array mapping module names to message blobs
-        */
-       public function getBlobs( array $modules, $lang ) {
-               // Each cache key for a message blob by module name and language code also has a generic
-               // check key without language code. This is used to invalidate any and all language subkeys
-               // that exist for a module from the updateMessage() method.
-               $cache = $this->wanCache;
-               $checkKeys = [
-                       // Global check key, see clear()
-                       $cache->makeKey( __CLASS__ )
-               ];
-               $cacheKeys = [];
-               foreach ( $modules as $name => $module ) {
-                       $cacheKey = $this->makeCacheKey( $module, $lang );
-                       $cacheKeys[$name] = $cacheKey;
-                       // Per-module check key, see updateMessage()
-                       $checkKeys[$cacheKey][] = $cache->makeKey( __CLASS__, $name );
-               }
-               $curTTLs = [];
-               $result = $cache->getMulti( array_values( $cacheKeys ), $curTTLs, $checkKeys );
-
-               $blobs = [];
-               foreach ( $modules as $name => $module ) {
-                       $key = $cacheKeys[$name];
-                       if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) {
-                               $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang );
-                       } else {
-                               // Use unexpired cache
-                               $blobs[$name] = $result[$key];
-                       }
-               }
-               return $blobs;
-       }
-
-       /**
-        * @deprecated since 1.27 Use getBlobs() instead
-        * @return array
-        */
-       public function get( ResourceLoader $resourceLoader, $modules, $lang ) {
-               return $this->getBlobs( $modules, $lang );
-       }
-
-       /**
-        * @since 1.27
-        * @param ResourceLoaderModule $module
-        * @param string $lang
-        * @return string Cache key
-        */
-       private function makeCacheKey( ResourceLoaderModule $module, $lang ) {
-               $messages = array_values( array_unique( $module->getMessages() ) );
-               sort( $messages );
-               return $this->wanCache->makeKey( __CLASS__, $module->getName(), $lang,
-                       md5( json_encode( $messages ) )
-               );
-       }
-
-       /**
-        * @since 1.27
-        * @param string $cacheKey
-        * @param ResourceLoaderModule $module
-        * @param string $lang
-        * @return string JSON blob
-        */
-       protected function recacheMessageBlob( $cacheKey, ResourceLoaderModule $module, $lang ) {
-               $blob = $this->generateMessageBlob( $module, $lang );
-               $cache = $this->wanCache;
-               $cache->set( $cacheKey, $blob,
-                       // Add part of a day to TTL to avoid all modules expiring at once
-                       $cache::TTL_WEEK + mt_rand( 0, $cache::TTL_DAY ),
-                       Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) )
-               );
-               return $blob;
-       }
-
-       /**
-        * Invalidate cache keys for modules using this message key.
-        * Called by MessageCache when a message has changed.
-        *
-        * @param string $key Message key
-        */
-       public function updateMessage( $key ) {
-               $moduleNames = $this->getResourceLoader()->getModulesByMessage( $key );
-               foreach ( $moduleNames as $moduleName ) {
-                       // Uses a holdoff to account for database replica DB lag (for MessageCache)
-                       $this->wanCache->touchCheckKey( $this->wanCache->makeKey( __CLASS__, $moduleName ) );
-               }
-       }
-
-       /**
-        * Invalidate cache keys for all known modules.
-        * Called by LocalisationCache after cache is regenerated.
-        */
-       public function clear() {
-               $cache = $this->wanCache;
-               // Disable holdoff because this invalidates all modules and also not needed since
-               // LocalisationCache is stored outside the database and doesn't have lag.
-               $cache->touchCheckKey( $cache->makeKey( __CLASS__ ), $cache::HOLDOFF_NONE );
-       }
-
-       /**
-        * @since 1.27
-        * @return ResourceLoader
-        */
-       protected function getResourceLoader() {
-               return $this->resourceloader;
-       }
-
-       /**
-        * @since 1.27
-        * @param string $key Message key
-        * @param string $lang Language code
-        * @return string
-        */
-       protected function fetchMessage( $key, $lang ) {
-               $message = wfMessage( $key )->inLanguage( $lang );
-               $value = $message->plain();
-               if ( !$message->exists() ) {
-                       $this->logger->warning( 'Failed to find {messageKey} ({lang})', [
-                               'messageKey' => $key,
-                               'lang' => $lang,
-                       ] );
-               }
-               return $value;
-       }
-
-       /**
-        * Generate the message blob for a given module in a given language.
-        *
-        * @param ResourceLoaderModule $module
-        * @param string $lang Language code
-        * @return string JSON blob
-        */
-       private function generateMessageBlob( ResourceLoaderModule $module, $lang ) {
-               $messages = [];
-               foreach ( $module->getMessages() as $key ) {
-                       $messages[$key] = $this->fetchMessage( $key, $lang );
-               }
-
-               $json = FormatJson::encode( (object)$messages );
-               // @codeCoverageIgnoreStart
-               if ( $json === false ) {
-                       $this->logger->warning( 'Failed to encode message blob for {module} ({lang})', [
-                               'module' => $module->getName(),
-                               'lang' => $lang,
-                       ] );
-                       $json = '{}';
-               }
-               // codeCoverageIgnoreEnd
-               return $json;
-       }
-}
diff --git a/includes/resourceloader/MessageBlobStore.php b/includes/resourceloader/MessageBlobStore.php
new file mode 100644 (file)
index 0000000..ceb51f2
--- /dev/null
@@ -0,0 +1,240 @@
+<?php
+/**
+ * Message blobs storage used by ResourceLoader.
+ *
+ * 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
+ * @author Roan Kattouw
+ * @author Trevor Parscal
+ * @author Timo Tijhof
+ */
+
+use MediaWiki\MediaWikiServices;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Wikimedia\Rdbms\Database;
+
+/**
+ * This class generates message blobs for use by ResourceLoader modules.
+ *
+ * A message blob is a JSON object containing the interface messages for a certain module in
+ * a certain language.
+ */
+class MessageBlobStore implements LoggerAwareInterface {
+
+       /* @var ResourceLoader */
+       private $resourceloader;
+
+       /**
+        * @var LoggerInterface
+        */
+       protected $logger;
+
+       /**
+        * @var WANObjectCache
+        */
+       protected $wanCache;
+
+       /**
+        * @param ResourceLoader $rl
+        * @param LoggerInterface|null $logger
+        */
+       public function __construct( ResourceLoader $rl, LoggerInterface $logger = null ) {
+               $this->resourceloader = $rl;
+               $this->logger = $logger ?: new NullLogger();
+               $this->wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+       }
+
+       /**
+        * @since 1.27
+        * @param LoggerInterface $logger
+        */
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
+       }
+
+       /**
+        * Get the message blob for a module
+        *
+        * @since 1.27
+        * @param ResourceLoaderModule $module
+        * @param string $lang Language code
+        * @return string JSON
+        */
+       public function getBlob( ResourceLoaderModule $module, $lang ) {
+               $blobs = $this->getBlobs( [ $module->getName() => $module ], $lang );
+               return $blobs[$module->getName()];
+       }
+
+       /**
+        * Get the message blobs for a set of modules
+        *
+        * @since 1.27
+        * @param ResourceLoaderModule[] $modules Array of module objects keyed by name
+        * @param string $lang Language code
+        * @return array An array mapping module names to message blobs
+        */
+       public function getBlobs( array $modules, $lang ) {
+               // Each cache key for a message blob by module name and language code also has a generic
+               // check key without language code. This is used to invalidate any and all language subkeys
+               // that exist for a module from the updateMessage() method.
+               $cache = $this->wanCache;
+               $checkKeys = [
+                       // Global check key, see clear()
+                       $cache->makeKey( __CLASS__ )
+               ];
+               $cacheKeys = [];
+               foreach ( $modules as $name => $module ) {
+                       $cacheKey = $this->makeCacheKey( $module, $lang );
+                       $cacheKeys[$name] = $cacheKey;
+                       // Per-module check key, see updateMessage()
+                       $checkKeys[$cacheKey][] = $cache->makeKey( __CLASS__, $name );
+               }
+               $curTTLs = [];
+               $result = $cache->getMulti( array_values( $cacheKeys ), $curTTLs, $checkKeys );
+
+               $blobs = [];
+               foreach ( $modules as $name => $module ) {
+                       $key = $cacheKeys[$name];
+                       if ( !isset( $result[$key] ) || $curTTLs[$key] === null || $curTTLs[$key] < 0 ) {
+                               $blobs[$name] = $this->recacheMessageBlob( $key, $module, $lang );
+                       } else {
+                               // Use unexpired cache
+                               $blobs[$name] = $result[$key];
+                       }
+               }
+               return $blobs;
+       }
+
+       /**
+        * @deprecated since 1.27 Use getBlobs() instead
+        * @return array
+        */
+       public function get( ResourceLoader $resourceLoader, $modules, $lang ) {
+               return $this->getBlobs( $modules, $lang );
+       }
+
+       /**
+        * @since 1.27
+        * @param ResourceLoaderModule $module
+        * @param string $lang
+        * @return string Cache key
+        */
+       private function makeCacheKey( ResourceLoaderModule $module, $lang ) {
+               $messages = array_values( array_unique( $module->getMessages() ) );
+               sort( $messages );
+               return $this->wanCache->makeKey( __CLASS__, $module->getName(), $lang,
+                       md5( json_encode( $messages ) )
+               );
+       }
+
+       /**
+        * @since 1.27
+        * @param string $cacheKey
+        * @param ResourceLoaderModule $module
+        * @param string $lang
+        * @return string JSON blob
+        */
+       protected function recacheMessageBlob( $cacheKey, ResourceLoaderModule $module, $lang ) {
+               $blob = $this->generateMessageBlob( $module, $lang );
+               $cache = $this->wanCache;
+               $cache->set( $cacheKey, $blob,
+                       // Add part of a day to TTL to avoid all modules expiring at once
+                       $cache::TTL_WEEK + mt_rand( 0, $cache::TTL_DAY ),
+                       Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) )
+               );
+               return $blob;
+       }
+
+       /**
+        * Invalidate cache keys for modules using this message key.
+        * Called by MessageCache when a message has changed.
+        *
+        * @param string $key Message key
+        */
+       public function updateMessage( $key ) {
+               $moduleNames = $this->getResourceLoader()->getModulesByMessage( $key );
+               foreach ( $moduleNames as $moduleName ) {
+                       // Uses a holdoff to account for database replica DB lag (for MessageCache)
+                       $this->wanCache->touchCheckKey( $this->wanCache->makeKey( __CLASS__, $moduleName ) );
+               }
+       }
+
+       /**
+        * Invalidate cache keys for all known modules.
+        * Called by LocalisationCache after cache is regenerated.
+        */
+       public function clear() {
+               $cache = $this->wanCache;
+               // Disable holdoff because this invalidates all modules and also not needed since
+               // LocalisationCache is stored outside the database and doesn't have lag.
+               $cache->touchCheckKey( $cache->makeKey( __CLASS__ ), $cache::HOLDOFF_NONE );
+       }
+
+       /**
+        * @since 1.27
+        * @return ResourceLoader
+        */
+       protected function getResourceLoader() {
+               return $this->resourceloader;
+       }
+
+       /**
+        * @since 1.27
+        * @param string $key Message key
+        * @param string $lang Language code
+        * @return string
+        */
+       protected function fetchMessage( $key, $lang ) {
+               $message = wfMessage( $key )->inLanguage( $lang );
+               $value = $message->plain();
+               if ( !$message->exists() ) {
+                       $this->logger->warning( 'Failed to find {messageKey} ({lang})', [
+                               'messageKey' => $key,
+                               'lang' => $lang,
+                       ] );
+               }
+               return $value;
+       }
+
+       /**
+        * Generate the message blob for a given module in a given language.
+        *
+        * @param ResourceLoaderModule $module
+        * @param string $lang Language code
+        * @return string JSON blob
+        */
+       private function generateMessageBlob( ResourceLoaderModule $module, $lang ) {
+               $messages = [];
+               foreach ( $module->getMessages() as $key ) {
+                       $messages[$key] = $this->fetchMessage( $key, $lang );
+               }
+
+               $json = FormatJson::encode( (object)$messages );
+               // @codeCoverageIgnoreStart
+               if ( $json === false ) {
+                       $this->logger->warning( 'Failed to encode message blob for {module} ({lang})', [
+                               'module' => $module->getName(),
+                               'lang' => $lang,
+                       ] );
+                       $json = '{}';
+               }
+               // codeCoverageIgnoreEnd
+               return $json;
+       }
+}
index 70bf39f..e577643 100644 (file)
@@ -3,7 +3,7 @@
 use Wikimedia\TestingAccessWrapper;
 
 /**
- * @group Cache
+ * @group ResourceLoader
  * @covers MessageBlobStore
  */
 class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
@@ -13,64 +13,17 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
 
        protected function setUp() {
                parent::setUp();
-               // MediaWiki tests defaults $wgMainWANCache to CACHE_NONE.
-               // Use hash instead so that caching is observed
-               $this->wanCache = $this->getMockBuilder( WANObjectCache::class )
-                       ->setConstructorArgs( [ [
-                               'cache' => new HashBagOStuff(),
-                               'pool' => 'test',
-                               'relayer' => new EventRelayerNull( [] )
-                       ] ] )
-                       ->setMethods( [ 'makePurgeValue' ] )
-                       ->getMock();
-
-               $this->wanCache->expects( $this->any() )
-                       ->method( 'makePurgeValue' )
-                       ->will( $this->returnCallback( function ( $timestamp, $holdoff ) {
-                               // Disable holdoff as it messes with testing. Aside from a 0-second holdoff,
-                               // make sure that "time" passes between getMulti() check init and the set()
-                               // in recacheMessageBlob(). This especially matters for Windows clocks.
-                               $ts = (float)$timestamp - 0.0001;
-
-                               return WANObjectCache::PURGE_VAL_PREFIX . $ts . ':0';
-                       } ) );
-       }
-
-       protected function makeBlobStore( $methods = null, $rl = null ) {
-               $blobStore = $this->getMockBuilder( MessageBlobStore::class )
-                       ->setConstructorArgs( [ $rl ?? $this->createMock( ResourceLoader::class ) ] )
-                       ->setMethods( $methods )
-                       ->getMock();
-
-               $access = TestingAccessWrapper::newFromObject( $blobStore );
-               $access->wanCache = $this->wanCache;
-               return $blobStore;
-       }
-
-       protected function makeModule( array $messages ) {
-               $module = new ResourceLoaderTestModule( [ 'messages' => $messages ] );
-               $module->setName( 'test.blobstore' );
-               return $module;
-       }
-
-       /** @covers MessageBlobStore::setLogger */
-       public function testSetLogger() {
-               $blobStore = $this->makeBlobStore();
-               $this->assertSame( null, $blobStore->setLogger( new Psr\Log\NullLogger() ) );
+               // MediaWiki's test wrapper sets $wgMainWANCache to CACHE_NONE.
+               // Use HashBagOStuff here so that we can observe caching.
+               $this->wanCache = new WANObjectCache( [
+                       'cache' => new HashBagOStuff()
+               ] );
+
+               $this->clock = 1301655600.000;
+               $this->wanCache->setMockTime( $this->clock );
        }
 
-       /** @covers MessageBlobStore::getResourceLoader */
-       public function testGetResourceLoader() {
-               // Call protected method
-               $blobStore = TestingAccessWrapper::newFromObject( $this->makeBlobStore() );
-               $this->assertInstanceOf(
-                       ResourceLoader::class,
-                       $blobStore->getResourceLoader()
-               );
-       }
-
-       /** @covers MessageBlobStore::fetchMessage */
-       public function testFetchMessage() {
+       public function testBlobCreation() {
                $module = $this->makeModule( [ 'mainpage' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
@@ -81,140 +34,153 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
                $this->assertEquals( '{"mainpage":"Main Page"}', $blob, 'Generated blob' );
        }
 
-       /** @covers MessageBlobStore::fetchMessage */
-       public function testFetchMessageFail() {
+       public function testBlobCreation_unknownMessage() {
                $module = $this->makeModule( [ 'i-dont-exist' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
-
                $blobStore = $this->makeBlobStore( null, $rl );
-               $blob = $blobStore->getBlob( $module, 'en' );
 
+               // Generating a blob should succeed without errors,
+               // even if a message is unknown.
+               $blob = $blobStore->getBlob( $module, 'en' );
                $this->assertEquals( '{"i-dont-exist":"\u29fci-dont-exist\u29fd"}', $blob, 'Generated blob' );
        }
 
-       public function testGetBlob() {
-               $module = $this->makeModule( [ 'foo' ] );
+       public function testMessageCachingAndPurging() {
+               $module = $this->makeModule( [ 'example' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
-
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
+
+               // Advance this new WANObjectCache instance to a normal state,
+               // by doing one "get" and letting its hold off period expire.
+               // Without this, the first real "get" would lazy-initialise the
+               // checkKey and thus reject the first "set".
+               $blobStore->getBlob( $module, 'en' );
+               $this->clock += 20;
+
+               // Arrange version 1 of a message
                $blobStore->expects( $this->once() )
                        ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'Example' ) );
+                       ->will( $this->returnValue( 'First version' ) );
 
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"example":"First version"}', $blob, 'Blob for v1' );
 
-               $this->assertEquals( '{"foo":"Example"}', $blob, 'Generated blob' );
-       }
-
-       public function testGetBlobCached() {
-               $module = $this->makeModule( [ 'example' ] );
-               $rl = new ResourceLoader();
-               $rl->register( $module->getName(), $module );
-
+               // Arrange version 2
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                $blobStore->expects( $this->once() )
                        ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'First' ) );
+                       ->will( $this->returnValue( 'Second version' ) );
+               $this->clock += 20;
 
-               $module = $this->makeModule( [ 'example' ] );
+               // Assert
+               // We do not validate whether a cached message is up-to-date.
+               // Instead, changes to messages will send us a purge.
+               // When cache is not purged or expired, it must be used.
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' );
+               $this->assertEquals( '{"example":"First version"}', $blob, 'Reuse cached v1 blob' );
 
-               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               $blobStore->expects( $this->never() )
-                       ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'Second' ) );
+               // Purge cache
+               $blobStore->updateMessage( 'example' );
+               $this->clock += 20;
 
-               $module = $this->makeModule( [ 'example' ] );
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Cache hit' );
+               $this->assertEquals( '{"example":"Second version"}', $blob, 'Updated blob for v2' );
        }
 
-       public function testUpdateMessage() {
+       public function testPurgeEverything() {
                $module = $this->makeModule( [ 'example' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               $blobStore->expects( $this->once() )
+               // Advance this new WANObjectCache instance to a normal state.
+               $blobStore->getBlob( $module, 'en' );
+               $this->clock += 20;
+
+               // Arrange version 1 and 2
+               $blobStore->expects( $this->exactly( 2 ) )
                        ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'First' ) );
+                       ->will( $this->onConsecutiveCalls( 'First', 'Second' ) );
 
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' );
+               $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1' );
 
-               $blobStore->updateMessage( 'example' );
+               $this->clock += 20;
 
-               $module = $this->makeModule( [ 'example' ] );
-               $rl = new ResourceLoader();
-               $rl->register( $module->getName(), $module );
-               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               $blobStore->expects( $this->once() )
-                       ->method( 'fetchMessage' )
-                       ->will( $this->returnValue( 'Second' ) );
+               // Assert
+               $blob = $blobStore->getBlob( $module, 'en' );
+               $this->assertEquals( '{"example":"First"}', $blob, 'Blob for v1 again' );
+
+               // Purge everything
+               $blobStore->clear();
+               $this->clock += 20;
 
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"Second"}', $blob, 'Updated blob' );
+               $this->assertEquals( '{"example":"Second"}', $blob, 'Blob for v2' );
        }
 
-       public function testValidation() {
+       public function testValidateAgainstModuleRegistry() {
+               // Arrange version 1 of a module
                $module = $this->makeModule( [ 'foo' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
-
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                $blobStore->expects( $this->once() )
                        ->method( 'fetchMessage' )
                        ->will( $this->returnValueMap( [
+                               // message key, language code, message value
                                [ 'foo', 'en', 'Hello' ],
                        ] ) );
 
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"foo":"Hello"}', $blob, 'Generated blob' );
+               $this->assertEquals( '{"foo":"Hello"}', $blob, 'Blob for v1' );
 
-               // Now, imagine a change to the module is deployed. The module now contains
-               // message 'foo' and 'bar'. While updateMessage() was not called (since no
-               // message values were changed) it should detect the change in list of
-               // message keys.
+               // Arrange version 2 of module
+               // While message values may be out of date, the set of messages returned
+               // must always match the set of message keys required by the module.
+               // We do not receive purges for this because no messages were changed.
                $module = $this->makeModule( [ 'foo', 'bar' ] );
                $rl = new ResourceLoader();
                $rl->register( $module->getName(), $module );
-
                $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
                $blobStore->expects( $this->exactly( 2 ) )
                        ->method( 'fetchMessage' )
                        ->will( $this->returnValueMap( [
+                               // message key, language code, message value
                                [ 'foo', 'en', 'Hello' ],
                                [ 'bar', 'en', 'World' ],
                        ] ) );
 
+               // Assert
                $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"foo":"Hello","bar":"World"}', $blob, 'Updated blob' );
+               $this->assertEquals( '{"foo":"Hello","bar":"World"}', $blob, 'Blob for v2' );
        }
 
-       public function testClear() {
-               $module = $this->makeModule( [ 'example' ] );
-               $rl = new ResourceLoader();
-               $rl->register( $module->getName(), $module );
-               $blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
-               $blobStore->expects( $this->exactly( 2 ) )
-                       ->method( 'fetchMessage' )
-                       ->will( $this->onConsecutiveCalls( 'First', 'Second' ) );
-
-               $now = microtime( true );
-               $this->wanCache->setMockTime( $now );
-
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' );
+       public function testSetLoggedIsVoid() {
+               $blobStore = $this->makeBlobStore();
+               $this->assertSame( null, $blobStore->setLogger( new Psr\Log\NullLogger() ) );
+       }
 
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"First"}', $blob, 'Cache-hit' );
+       private function makeBlobStore( $methods = null, $rl = null ) {
+               $blobStore = $this->getMockBuilder( MessageBlobStore::class )
+                       ->setConstructorArgs( [ $rl ?? $this->createMock( ResourceLoader::class ) ] )
+                       ->setMethods( $methods )
+                       ->getMock();
 
-               $now += 1;
-               $blobStore->clear();
+               $access = TestingAccessWrapper::newFromObject( $blobStore );
+               $access->wanCache = $this->wanCache;
+               return $blobStore;
+       }
 
-               $blob = $blobStore->getBlob( $module, 'en' );
-               $this->assertEquals( '{"example":"Second"}', $blob, 'Updated blob' );
+       private function makeModule( array $messages ) {
+               $module = new ResourceLoaderTestModule( [ 'messages' => $messages ] );
+               $module->setName( 'test.blobstore' );
+               return $module;
        }
 }