diff --git a/core/lib/Drupal/Core/Cache/CacheCollector.php b/core/lib/Drupal/Core/Cache/CacheCollector.php index ccf0618..c335da9 100644 --- a/core/lib/Drupal/Core/Cache/CacheCollector.php +++ b/core/lib/Drupal/Core/Cache/CacheCollector.php @@ -27,14 +27,14 @@ abstract class CacheCollector implements CacheCollectorInterface, DestructableInterface { /** - * A cid to pass to cache()->set() and cache()->get(). + * The cache id that is used for the cache entry. * * @var string */ protected $cid; /** - * A tags array to pass to cache()->set(). + * A list of tags that are used for the cache entry. * * @var array */ @@ -206,7 +206,7 @@ protected function updateCache($lock = TRUE) { $data[$offset] = $this->storage[$offset]; } } - if (empty($data)) { + if (empty($data) && empty($this->keysToRemove)) { return; } diff --git a/core/lib/Drupal/Core/Path/AliasWhitelist.php b/core/lib/Drupal/Core/Path/AliasWhitelist.php index fc8a107..dd0f389 100644 --- a/core/lib/Drupal/Core/Path/AliasWhitelist.php +++ b/core/lib/Drupal/Core/Path/AliasWhitelist.php @@ -44,8 +44,8 @@ class AliasWhitelist extends CacheCollector { * The cache backend. * @param \Drupal\Core\Lock\LockBackendInterface $lock * The lock backend. - * @param \Drupal\Core\KeyValueStore\KeyValueFactory $keyvalue - * The keyvalue factory to get the state cache from. + * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state + * The state keyvalue store. * @param \Drupal\Core\Database\Connection $connection * The database connection. */ diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheCollectorHelper.php b/core/tests/Drupal/Tests/Core/Cache/CacheCollectorHelper.php new file mode 100644 index 0000000..b54c9e0 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Cache/CacheCollectorHelper.php @@ -0,0 +1,72 @@ +persist($key); + } + + /** + * {@inheritdoc} + */ + public function resolveCacheMiss($key) { + $this->cacheMisses++; + if (isset($this->cacheMissData[$key])) { + $this->storage[$key] = $this->cacheMissData[$key]; + $this->persist($key); + return $this->cacheMissData[$key]; + } + } + + /** + * Sets data to return from a cache miss resolve. + * + * @param string $key + * The key being looked for. + * @param mixed $value + * The value to return. + */ + public function setCacheMissData($key, $value) { + $this->cacheMissData[$key] = $value; + } + + /** + * Returns the number of cache misses. + * + * @return int + * Number of calls to the resolve cache miss method. + */ + public function getCacheMisses() { + return $this->cacheMisses; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Cache/CacheCollectorTest.php b/core/tests/Drupal/Tests/Core/Cache/CacheCollectorTest.php new file mode 100644 index 0000000..faab56e --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Cache/CacheCollectorTest.php @@ -0,0 +1,405 @@ + 'CacheCollector test', + 'description' => 'Tests the cache collector base implementaion', + 'group' => 'Cache', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + $this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface'); + $this->cid = $this->randomName(); + } + + + /** + * Tests the resolve cache miss function. + */ + function testResolveCacheMiss() { + $this->constructCollector(); + $key = $this->randomName(); + $value = $this->randomName(); + $this->collector->setCacheMissData($key, $value); + + $this->assertEquals($value, $this->collector->get($key)); + } + + /** + * Tests setting and getting values when the cache is empty. + */ + function testSetAndGet() { + $this->constructCollector(); + $key = $this->randomName(); + $value = $this->randomName(); + + $this->assertNull($this->collector->get($key)); + + $this->collector->set($key, $value); + $this->assertTrue($this->collector->has($key)); + $this->assertEquals($value, $this->collector->get($key)); + } + + + /** + * Makes sure that NULL is a valid value and is collected. + */ + function testSetAndGetFalse() { + $this->constructCollector(); + $key = $this->randomName(); + $value = NULL; + + $this->constructCollector(); + $key = $this->randomName(); + $value = $this->randomName(); + + $this->cache->expects($this->once()) + ->method('invalidate') + ->with($this->cid); + $this->collector->set($key, $value); + $this->assertTrue($this->collector->has($key)); + $this->assertEquals($value, $this->collector->get($key)); + } + + /** + * Tests returning value from the collected cache. + */ + function testGetFromCache() { + $key = $this->randomName(); + $value = $this->randomName(); + + $cache = (object) array( + 'data' => array($key => $value), + 'created' => REQUEST_TIME, + ); + $this->cache->expects($this->once()) + ->method('get') + ->with($this->cid) + ->will($this->returnValue($cache)); + $this->constructCollector(); + $this->assertTrue($this->collector->has($key)); + $this->assertEquals($value, $this->collector->get($key)); + $this->assertEquals(0, $this->collector->getCacheMisses()); + } + + /** + * Tests setting and deleting values. + */ + function testDelete() { + $this->constructCollector(); + $key = $this->randomName(); + $value = $this->randomName(); + + $this->assertNull($this->collector->get($key)); + + $this->collector->set($key, $value); + $this->assertTrue($this->collector->has($key)); + $this->assertEquals($value, $this->collector->get($key)); + + $this->cache->expects($this->once()) + ->method('invalidate') + ->with($this->cid); + $this->collector->delete($key); + $this->assertFalse($this->collector->has($key)); + $this->assertEquals(NULL, $this->collector->get($key)); + } + + /** + * Tests updating the cache when no changes were made. + */ + function testUpdateCacheNoChanges() { + $this->constructCollector(); + + $this->lock->expects($this->never()) + ->method('acquire'); + $this->cache->expects($this->never()) + ->method('set'); + + $this->collector->destruct(); + } + + /** + * Tests updating the cache after a set. + */ + function testUpdateCache() { + $this->constructCollector(); + $key = $this->randomName(); + $value = $this->randomName(); + + $this->collector->setCacheMissData($key, $value); + $this->collector->get($key); + + // Set up mock objects for the expected calls, first a lock acquire, then + // cache get to look for conflicting cache entries, then a cache set and + // finally the lock is released again. + $this->lock->expects($this->once()) + ->method('acquire') + ->with($this->cid . ':Drupal\Core\Cache\CacheCollector') + ->will($this->returnValue(TRUE)); + $this->cache->expects($this->once()) + ->method('get') + ->with($this->cid, FALSE); + $this->cache->expects($this->once()) + ->method('set') + ->with($this->cid, array($key => $value), CacheBackendInterface::CACHE_PERMANENT, array()); + $this->lock->expects($this->once()) + ->method('release') + ->with($this->cid . ':Drupal\Core\Cache\CacheCollector'); + + $this->collector->destruct(); + } + + + /** + * Tests updating the cache when the lock acquire fails. + */ + function testUpdateCacheLockFail() { + $this->constructCollector(); + $key = $this->randomName(); + $value = $this->randomName(); + + $this->collector->setCacheMissData($key, $value); + $this->collector->get($key); + + // The lock acquire returns false, so the method should abort. + $this->lock->expects($this->once()) + ->method('acquire') + ->with($this->cid . ':Drupal\Core\Cache\CacheCollector') + ->will($this->returnValue(FALSE)); + $this->cache->expects($this->never()) + ->method('set'); + + $this->collector->destruct(); + } + + /** + * Tests updating the cache when there is a + */ + function testUpdateCacheInvalidatedConflict() { + $key = $this->randomName(); + $value = $this->randomName(); + + $cache = (object) array( + 'data' => array($key => $value), + 'created' => REQUEST_TIME, + ); + $this->cache->expects($this->at(0)) + ->method('get') + ->with($this->cid) + ->will($this->returnValue($cache)); + $this->constructCollector(); + + $this->cache->expects($this->at(0)) + ->method('invalidate') + ->with($this->cid); + $this->collector->set($key, 'new value'); + + // Set up mock objects for the expected calls, first a lock acquire, then + // cache get to look for conflicting cache entries, which does find + // and then it deletes the cache and aborts. + $this->lock->expects($this->once()) + ->method('acquire') + ->with($this->cid . ':Drupal\Core\Cache\CacheCollector') + ->will($this->returnValue(TRUE)); + $cache = (object) array( + 'data' => array($key => $value), + 'created' => REQUEST_TIME + 1, + ); + $this->cache->expects($this->at(0)) + ->method('get') + ->with($this->cid) + ->will($this->returnValue($cache)); + $this->cache->expects($this->once()) + ->method('delete') + ->with($this->cid); + $this->lock->expects($this->once()) + ->method('release') + ->with($this->cid . ':Drupal\Core\Cache\CacheCollector'); + + $this->collector->destruct(); + } + + /** + * Tests updating the cache when a different request + */ + function testUpdateCacheMerge() { + $this->constructCollector(); + $key = $this->randomName(); + $value = $this->randomName(); + + $this->collector->setCacheMissData($key, $value); + $this->collector->get($key); + + // Set up mock objects for the expected calls, first a lock acquire, then + // cache get to look for existing cache entries, which does find + // and then it merges them. + $this->lock->expects($this->once()) + ->method('acquire') + ->with($this->cid . ':Drupal\Core\Cache\CacheCollector') + ->will($this->returnValue(TRUE)); + $cache = (object) array( + 'data' => array('other key' => 'other value'), + 'created' => REQUEST_TIME + 1, + ); + $this->cache->expects($this->at(0)) + ->method('get') + ->with($this->cid) + ->will($this->returnValue($cache)); + $this->cache->expects($this->once()) + ->method('set') + ->with($this->cid, array('other key' => 'other value', $key => $value), CacheBackendInterface::CACHE_PERMANENT, array()); + $this->lock->expects($this->once()) + ->method('release') + ->with($this->cid . ':Drupal\Core\Cache\CacheCollector'); + + $this->collector->destruct(); + } + + /** + * Tests updating the cache after a delete. + */ + function testUpdateCacheDelete() { + $key = $this->randomName(); + $value = $this->randomName(); + + $cache = (object) array( + 'data' => array($key => $value), + 'created' => REQUEST_TIME, + ); + $this->cache->expects($this->at(0)) + ->method('get') + ->with($this->cid) + ->will($this->returnValue($cache)); + $this->constructCollector(); + + $this->collector->delete($key); + + // Set up mock objects for the expected calls, first a lock acquire, then + // cache get to look for conflicting cache entries, then a cache set and + // finally the lock is released again. + $this->lock->expects($this->once()) + ->method('acquire') + ->with($this->cid . ':Drupal\Core\Cache\CacheCollector') + ->will($this->returnValue(TRUE)); + // The second argument is set to TRUE because we triggered a cache + // invalidation. + $this->cache->expects($this->at(0)) + ->method('get') + ->with($this->cid, TRUE); + $this->cache->expects($this->once()) + ->method('set') + ->with($this->cid, array(), CacheBackendInterface::CACHE_PERMANENT, array()); + $this->lock->expects($this->once()) + ->method('release') + ->with($this->cid . ':Drupal\Core\Cache\CacheCollector'); + + $this->collector->destruct(); + } + + /** + * Tests a reset of the cache collector. + */ + function testUpdateCacheReset() { + $this->constructCollector(); + $key = $this->randomName(); + $value = $this->randomName(); + + // Set the data and request it. + $this->collector->setCacheMissData($key, $value); + $this->assertEquals($value, $this->collector->get($key)); + $this->assertEquals($value, $this->collector->get($key)); + + // Should have been added to the storage and only be requested once. + $this->assertEquals(1, $this->collector->getCacheMisses()); + + // Reset the collected cache, should call it again. + $this->collector->reset(); + $this->assertEquals($value, $this->collector->get($key)); + $this->assertEquals(2, $this->collector->getCacheMisses()); + } + + /** + * Tests a clear of the cache collector. + */ + function testUpdateCacheClear() { + $this->constructCollector(); + $key = $this->randomName(); + $value = $this->randomName(); + + // Set the data and request it. + $this->collector->setCacheMissData($key, $value); + $this->assertEquals($value, $this->collector->get($key)); + $this->assertEquals($value, $this->collector->get($key)); + + // Should have been added to the storage and only be requested once. + $this->assertEquals(1, $this->collector->getCacheMisses()); + + // Clear the collected cache, should call it again. + $this->cache->expects($this->once()) + ->method('delete') + ->with($this->cid); + $this->collector->clear(); + $this->assertEquals($value, $this->collector->get($key)); + $this->assertEquals(2, $this->collector->getCacheMisses()); + } + + /** + * Constructs the collector. + * + * Separate methods so that the cache mock can be set up before calling the + * constructor. + */ + protected function constructCollector() { + $this->collector = new CacheCollectorHelper($this->cid, $this->cache, $this->lock); + } +}