From: Timo Tijhof Date: Wed, 27 Mar 2019 20:23:02 +0000 (+0000) Subject: resourceloader: Improve test cases for MessageBlobStore X-Git-Tag: 1.34.0-rc.0~2269^2 X-Git-Url: http://git.cyclocoop.org/%7B%24admin_url%7Dcompta/comptes/journal.php?a=commitdiff_plain;h=853a291903dc6d21c3d1e92400c2d205a5537958;p=lhc%2Fweb%2Fwiklou.git resourceloader: Improve test cases for MessageBlobStore Move source code to includes/resourceloader to match test case. This is part of ResourceLoader and not meant to be used elsewhere. Merge two similar test cases for getting blobs and fetching messages which were doing the same thing. Rewrite the test names to be a better reflection of the stories they test, add comments for why, and re-order them to put related tests together. Move test-utilities to the bottom and make them actually private. Change-Id: I7a437eebf3ba6a722e286dfe77c2f9fe49ad222f --- diff --git a/autoload.php b/autoload.php index 528b7fe372..16236ed2a5 100644 --- a/autoload.php +++ b/autoload.php @@ -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 index ceb51f2d3f..0000000000 --- a/includes/cache/MessageBlobStore.php +++ /dev/null @@ -1,240 +0,0 @@ -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 index 0000000000..ceb51f2d3f --- /dev/null +++ b/includes/resourceloader/MessageBlobStore.php @@ -0,0 +1,240 @@ +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/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php b/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php index 70bf39f75d..e57764306e 100644 --- a/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php +++ b/tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php @@ -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; } }