class VariationCacheTest
Same name in other branches
- 11.x core/tests/Drupal/Tests/Core/Cache/VariationCacheTest.php \Drupal\Tests\Core\Cache\VariationCacheTest
@coversDefaultClass \Drupal\Core\Cache\VariationCache @group Cache
Hierarchy
- class \Drupal\Tests\UnitTestCase extends \PHPUnit\Framework\TestCase uses \Drupal\Tests\Traits\PhpUnitWarnings, \Drupal\Tests\PhpUnitCompatibilityTrait, \Prophecy\PhpUnit\ProphecyTrait, \Symfony\Bridge\PhpUnit\ExpectDeprecationTrait, \Drupal\Tests\RandomGeneratorTrait
- class \Drupal\Tests\Core\Cache\VariationCacheTest extends \Drupal\Tests\UnitTestCase
Expanded class hierarchy of VariationCacheTest
File
-
core/
tests/ Drupal/ Tests/ Core/ Cache/ VariationCacheTest.php, line 22
Namespace
Drupal\Tests\Core\CacheView source
class VariationCacheTest extends UnitTestCase {
/**
* The prophesized request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack|\Prophecy\Prophecy\ProphecyInterface
*/
protected $requestStack;
/**
* The backend used by the variation cache.
*
* @var \Drupal\Core\Cache\MemoryBackend
*/
protected $memoryBackend;
/**
* The prophesized cache contexts manager.
*
* @var \Drupal\Core\Cache\Context\CacheContextsManager|\Prophecy\Prophecy\ProphecyInterface
*/
protected $cacheContextsManager;
/**
* The variation cache instance.
*
* @var \Drupal\Core\Cache\VariationCacheInterface
*/
protected $variationCache;
/**
* The cache keys this test will store things under.
*
* @var string[]
*/
protected $cacheKeys = [
'your',
'housing',
'situation',
];
/**
* The cache ID for the cache keys, without taking contexts into account.
*
* @var string
*/
protected $cacheIdBase = 'your:housing:situation';
/**
* The simulated current user's housing type.
*
* For use in tests with cache contexts.
*
* @var string
*/
protected $housingType;
/**
* The cacheability for something that only varies per housing type.
*
* @var \Drupal\Core\Cache\CacheableMetadata
*/
protected $housingTypeCacheability;
/**
* The simulated current user's garden type.
*
* For use in tests with cache contexts.
*
* @var string
*/
protected $gardenType;
/**
* The cacheability for something that varies per housing and garden type.
*
* @var \Drupal\Core\Cache\CacheableMetadata
*/
protected $gardenTypeCacheability;
/**
* The simulated current user's house's orientation.
*
* For use in tests with cache contexts.
*
* @var string
*/
protected $houseOrientation;
/**
* The cacheability for varying per housing, garden and orientation.
*
* @var \Drupal\Core\Cache\CacheableMetadata
*/
protected $houseOrientationCacheability;
/**
* The simulated current user's solar panel type.
*
* For use in tests with cache contexts.
*
* @var string
*/
protected $solarType;
/**
* {@inheritdoc}
*/
protected function setUp() : void {
parent::setUp();
$this->requestStack = $this->prophesize(RequestStack::class);
$this->memoryBackend = new MemoryBackend(new Time());
$this->cacheContextsManager = $this->prophesize(CacheContextsManager::class);
$housing_type =& $this->housingType;
$garden_type =& $this->gardenType;
$house_orientation =& $this->houseOrientation;
$solar_type =& $this->solarType;
$this->cacheContextsManager
->convertTokensToKeys(Argument::any())
->will(function ($args) use (&$housing_type, &$garden_type, &$house_orientation, &$solar_type) {
$keys = [];
foreach ($args[0] as $context_id) {
switch ($context_id) {
case 'house.type':
$keys[] = "ht.{$housing_type}";
break;
case 'garden.type':
$keys[] = "gt.{$garden_type}";
break;
case 'house.orientation':
$keys[] = "ho.{$house_orientation}";
break;
case 'solar.type':
$keys[] = "st.{$solar_type}";
break;
default:
$keys[] = $context_id;
}
}
return new ContextCacheKeys($keys);
});
$this->variationCache = new VariationCache($this->requestStack
->reveal(), $this->memoryBackend, $this->cacheContextsManager
->reveal());
$this->housingTypeCacheability = (new CacheableMetadata())->setCacheTags([
'foo',
])
->setCacheContexts([
'house.type',
]);
$this->gardenTypeCacheability = (new CacheableMetadata())->setCacheTags([
'bar',
])
->setCacheContexts([
'house.type',
'garden.type',
]);
$this->houseOrientationCacheability = (new CacheableMetadata())->setCacheTags([
'baz',
])
->setCacheContexts([
'house.type',
'garden.type',
'house.orientation',
]);
}
/**
* Tests a cache item that has no variations.
*
* @covers ::get
* @covers ::set
*/
public function testNoVariations() : void {
$data = 'You have a nice house!';
$cacheability = (new CacheableMetadata())->setCacheTags([
'bar',
'foo',
]);
$initial_cacheability = (new CacheableMetadata())->setCacheTags([
'foo',
]);
$this->setVariationCacheItem($data, $cacheability, $initial_cacheability);
$this->assertVariationCacheItem($data, $cacheability, $initial_cacheability);
}
/**
* Tests a cache item that only ever varies by one context.
*
* @covers ::get
* @covers ::set
*/
public function testSingleVariation() : void {
$cacheability = $this->housingTypeCacheability;
$house_data = [
'apartment' => 'You have a nice apartment',
'house' => 'You have a nice house',
];
foreach ($house_data as $housing_type => $data) {
$this->housingType = $housing_type;
$this->assertVariationCacheMiss($cacheability);
$this->setVariationCacheItem($data, $cacheability, $cacheability);
$this->assertVariationCacheItem($data, $cacheability, $cacheability);
$this->assertCacheBackendItem("{$this->cacheIdBase}:ht.{$housing_type}", $data, $cacheability);
}
}
/**
* Tests a cache item that has nested variations.
*
* @covers ::get
* @covers ::set
*/
public function testNestedVariations() : void {
// We are running this scenario in the best possible outcome: The redirects
// are stored in expanding order, meaning the simplest one is stored first
// and the nested ones are stored in subsequent ::set() calls. This means no
// self-healing takes place where overly specific redirects are overwritten
// with simpler ones.
$possible_outcomes = [
'apartment' => 'You have a nice apartment!',
'house|no-garden' => 'You have a nice house!',
'house|garden|east' => 'You have a nice house with an east-facing garden!',
'house|garden|south' => 'You have a nice house with a south-facing garden!',
'house|garden|west' => 'You have a nice house with a west-facing garden!',
'house|garden|north' => 'You have a nice house with a north-facing garden!',
];
foreach ($possible_outcomes as $cache_context_values => $data) {
[
$this->housingType,
$this->gardenType,
$this->houseOrientation,
] = explode('|', $cache_context_values . '||');
$cacheability = $this->housingTypeCacheability;
if (!empty($this->houseOrientation)) {
$cacheability = $this->houseOrientationCacheability;
}
elseif (!empty($this->gardenType)) {
$cacheability = $this->gardenTypeCacheability;
}
$this->assertVariationCacheMiss($this->housingTypeCacheability);
$this->setVariationCacheItem($data, $cacheability, $this->housingTypeCacheability);
$this->assertVariationCacheItem($data, $cacheability, $this->housingTypeCacheability);
$cache_id_parts = [
"ht.{$this->housingType}",
];
if (!empty($this->gardenType)) {
$this->assertCacheBackendItem($this->getSortedCacheId($cache_id_parts), new CacheRedirect($this->gardenTypeCacheability));
$cache_id_parts[] = "gt.{$this->gardenType}";
}
if (!empty($this->houseOrientation)) {
$this->assertCacheBackendItem($this->getSortedCacheId($cache_id_parts), new CacheRedirect($this->houseOrientationCacheability));
$cache_id_parts[] = "ho.{$this->houseOrientation}";
}
$this->assertCacheBackendItem($this->getSortedCacheId($cache_id_parts), $data, $cacheability);
}
}
/**
* Tests a cache item that has nested variations that trigger self-healing.
*
* @covers ::get
* @covers ::set
*
* @depends testNestedVariations
*/
public function testNestedVariationsSelfHealing() : void {
// This is the worst possible scenario: A very specific item was stored
// first, followed by a less specific one. This means an overly specific
// cache redirect was stored that needs to be dumbed down. After this
// process, the first ::get() for the more specific item will fail as we
// have effectively destroyed the path to said item. Setting an item of the
// same specificity will restore the path for all items of said specificity.
$cache_id_parts = [
'ht.house',
];
$possible_outcomes = [
'house|garden|east' => 'You have a nice house with an east-facing garden!',
'house|garden|south' => 'You have a nice house with a south-facing garden!',
'house|garden|west' => 'You have a nice house with a west-facing garden!',
'house|garden|north' => 'You have a nice house with a north-facing garden!',
];
foreach ($possible_outcomes as $cache_context_values => $data) {
[
$this->housingType,
$this->gardenType,
$this->houseOrientation,
] = explode('|', $cache_context_values . '||');
$this->setVariationCacheItem($data, $this->houseOrientationCacheability, $this->housingTypeCacheability);
}
// Verify that the overly specific redirect is stored at the first possible
// redirect location, i.e.: The base cache ID.
$this->assertCacheBackendItem($this->getSortedCacheId($cache_id_parts), new CacheRedirect($this->houseOrientationCacheability));
// Store a simpler variation and verify that the first cache redirect is now
// the one redirecting to the simplest known outcome.
[
$this->housingType,
$this->gardenType,
$this->houseOrientation,
] = [
'house',
'no-garden',
NULL,
];
$this->setVariationCacheItem('You have a nice house', $this->gardenTypeCacheability, $this->housingTypeCacheability);
$this->assertCacheBackendItem($this->getSortedCacheId($cache_id_parts), new CacheRedirect($this->gardenTypeCacheability));
// Verify that the previously set outcomes are all inaccessible now.
foreach ($possible_outcomes as $cache_context_values => $data) {
[
$this->housingType,
$this->gardenType,
$this->houseOrientation,
] = explode('|', $cache_context_values . '||');
$this->assertVariationCacheMiss($this->housingTypeCacheability);
}
// Set at least one more specific item in the cache again.
$this->setVariationCacheItem($data, $this->houseOrientationCacheability, $this->housingTypeCacheability);
// Verify that the previously set outcomes are all accessible again.
foreach ($possible_outcomes as $cache_context_values => $data) {
[
$this->housingType,
$this->gardenType,
$this->houseOrientation,
] = explode('|', $cache_context_values . '||');
$this->assertVariationCacheItem($data, $this->houseOrientationCacheability, $this->housingTypeCacheability);
}
// Verify that the more specific cache redirect is now stored one step after
// the less specific one.
$cache_id_parts[] = 'gt.garden';
$this->assertCacheBackendItem($this->getSortedCacheId($cache_id_parts), new CacheRedirect($this->houseOrientationCacheability));
}
/**
* Tests self-healing for a cache item that has split variations.
*
* @covers ::get
* @covers ::set
*/
public function testSplitVariationsSelfHealing() : void {
// This is an edge case. Something varies by AB where some values of B
// trigger the whole to vary by either C, D or nothing extra. But due to an
// unfortunate series of requests, only ABC and ABD variations were cached.
//
// In this case, the cache should be smart enough to generate a redirect for
// AB, followed by redirects for ABC and ABD.
//
// For the sake of this test, we'll vary by housing and orientation, but:
// - Only vary by garden type for south-facing houses.
// - Only vary by solar panel type for north-facing houses.
$this->housingType = 'house';
$this->gardenType = 'garden';
$this->solarType = 'solar';
$initial_cacheability = (new CacheableMetadata())->setCacheTags([
'foo',
])
->setCacheContexts([
'house.type',
]);
$south_cacheability = (new CacheableMetadata())->setCacheTags([
'foo',
])
->setCacheContexts([
'house.type',
'house.orientation',
'garden.type',
]);
$north_cacheability = (new CacheableMetadata())->setCacheTags([
'foo',
])
->setCacheContexts([
'house.type',
'house.orientation',
'solar.type',
]);
$common_cacheability = (new CacheableMetadata())->setCacheContexts([
'house.type',
'house.orientation',
]);
// Calculate the cache IDs once beforehand for readability.
$cache_id = $this->getSortedCacheId([
'ht.house',
]);
$cache_id_north = $this->getSortedCacheId([
'ht.house',
'ho.north',
]);
$cache_id_south = $this->getSortedCacheId([
'ht.house',
'ho.south',
]);
// Set the first scenario.
$this->houseOrientation = 'south';
$this->setVariationCacheItem('You have a south-facing house with a garden!', $south_cacheability, $initial_cacheability);
// Verify that the overly specific redirect is stored at the first possible
// redirect location, i.e.: The base cache ID.
$this->assertCacheBackendItem($cache_id, new CacheRedirect($south_cacheability));
// Store a split variation, and verify that the common contexts are now used
// for the first cache redirect and the actual contexts for the next step of
// the redirect chain.
$this->houseOrientation = 'north';
$this->setVariationCacheItem('You have a north-facing house with solar panels!', $north_cacheability, $initial_cacheability);
$this->assertCacheBackendItem($cache_id, new CacheRedirect($common_cacheability));
$this->assertCacheBackendItem($cache_id_north, new CacheRedirect($north_cacheability));
// Verify that the initially set scenario is inaccessible now.
$this->houseOrientation = 'south';
$this->assertVariationCacheMiss($initial_cacheability);
// Reset the initial scenario and verify that its redirects are accessible.
$this->setVariationCacheItem('You have a south-facing house with a garden!', $south_cacheability, $initial_cacheability);
$this->assertCacheBackendItem($cache_id, new CacheRedirect($common_cacheability));
$this->assertCacheBackendItem($cache_id_south, new CacheRedirect($south_cacheability));
// Double-check that the split scenario redirects are left untouched.
$this->houseOrientation = 'north';
$this->assertCacheBackendItem($cache_id, new CacheRedirect($common_cacheability));
$this->assertCacheBackendItem($cache_id_north, new CacheRedirect($north_cacheability));
}
/**
* Tests exception for a cache item that has incompatible variations.
*
* @covers ::get
* @covers ::set
*/
public function testIncompatibleVariationsException() : void {
// This should never happen. When someone first stores something in the
// cache using context A and then tries to store something using context B,
// something is wrong. There should always be at least one shared context at
// the top level or else the cache cannot do its job.
$this->expectException(\LogicException::class);
$this->expectExceptionMessage("The complete set of cache contexts for a variation cache item must contain all of the initial cache contexts, missing: garden.type.");
$this->housingType = 'house';
$house_cacheability = (new CacheableMetadata())->setCacheContexts([
'house.type',
]);
$this->gardenType = 'garden';
$garden_cacheability = (new CacheableMetadata())->setCacheContexts([
'garden.type',
]);
$this->setVariationCacheItem('You have a nice garden!', $garden_cacheability, $garden_cacheability);
$this->setVariationCacheItem('You have a nice house!', $house_cacheability, $garden_cacheability);
}
/**
* Creates the sorted cache ID from cache ID parts.
*
* When core optimizes cache contexts it returns the keys alphabetically. To
* make testing easier, we replicate said sorting here.
*
* @param string[] $cache_id_parts
* The parts to add to the base cache ID, will be sorted.
*
* @return string
* The correct cache ID.
*/
protected function getSortedCacheId($cache_id_parts) {
sort($cache_id_parts);
array_unshift($cache_id_parts, $this->cacheIdBase);
return implode(':', $cache_id_parts);
}
/**
* Stores an item in the variation cache.
*
* @param mixed $data
* The data that should be stored.
* @param \Drupal\Core\Cache\CacheableMetadata $cacheability
* The cacheability that should be used.
* @param \Drupal\Core\Cache\CacheableMetadata $initial_cacheability
* The initial cacheability that should be used.
*/
protected function setVariationCacheItem($data, CacheableMetadata $cacheability, CacheableMetadata $initial_cacheability) {
$this->variationCache
->set($this->cacheKeys, $data, $cacheability, $initial_cacheability);
}
/**
* Asserts that an item was properly stored in the variation cache.
*
* @param mixed $data
* The data that should have been stored.
* @param \Drupal\Core\Cache\CacheableMetadata $cacheability
* The cacheability that should have been used.
* @param \Drupal\Core\Cache\CacheableMetadata $initial_cacheability
* The initial cacheability that should be used.
*/
protected function assertVariationCacheItem($data, CacheableMetadata $cacheability, CacheableMetadata $initial_cacheability) {
$cache_item = $this->variationCache
->get($this->cacheKeys, $initial_cacheability);
$this->assertNotFalse($cache_item, 'Variable data was stored and retrieved successfully.');
$this->assertEquals($data, $cache_item->data, 'Variable cache item contains the right data.');
$this->assertSame($cacheability->getCacheTags(), $cache_item->tags, 'Variable cache item uses the right cache tags.');
}
/**
* Asserts that an item could not be retrieved from the variation cache.
*
* @param \Drupal\Core\Cache\CacheableMetadata $initial_cacheability
* The initial cacheability that should be used.
*/
protected function assertVariationCacheMiss(CacheableMetadata $initial_cacheability) {
$this->assertFalse($this->variationCache
->get($this->cacheKeys, $initial_cacheability), 'Nothing could be retrieved for the active cache contexts.');
}
/**
* Asserts that an item was properly stored in the cache backend.
*
* @param string $cid
* The cache ID that should have been used.
* @param mixed $data
* The data that should have been stored.
* @param \Drupal\Core\Cache\CacheableMetadata|null $cacheability
* (optional) The cacheability that should have been used. Does not apply
* when checking for cache redirects.
*/
protected function assertCacheBackendItem(string $cid, $data, ?CacheableMetadata $cacheability = NULL) {
$cache_backend_item = $this->memoryBackend
->get($cid);
$this->assertNotFalse($cache_backend_item, 'The data was stored and retrieved successfully.');
$this->assertEquals($data, $cache_backend_item->data, 'Cache item contains the right data.');
if ($data instanceof CacheRedirect) {
$this->assertSame([], $cache_backend_item->tags, 'A cache redirect does not use cache tags.');
$this->assertSame(-1, $cache_backend_item->expire, 'A cache redirect is stored indefinitely.');
}
else {
$this->assertSame($cacheability->getCacheTags(), $cache_backend_item->tags, 'Cache item uses the right cache tags.');
}
}
}
Members
Title Sort descending | Deprecated | Modifiers | Object type | Summary | Overriden Title | Overrides |
---|---|---|---|---|---|---|
PhpUnitWarnings::$deprecationWarnings | private static | property | Deprecation warnings from PHPUnit to raise with @trigger_error(). | |||
PhpUnitWarnings::addWarning | public | function | Converts PHPUnit deprecation warnings to E_USER_DEPRECATED. | |||
RandomGeneratorTrait::getRandomGenerator | protected | function | Gets the random generator for the utility methods. | |||
RandomGeneratorTrait::randomMachineName | protected | function | Generates a unique random string containing letters and numbers. | |||
RandomGeneratorTrait::randomObject | public | function | Generates a random PHP object. | |||
RandomGeneratorTrait::randomString | public | function | Generates a pseudo-random string of ASCII characters of codes 32 to 126. | |||
RandomGeneratorTrait::randomStringValidate | Deprecated | public | function | Callback for random string validation. | ||
UnitTestCase::$root | protected | property | The app root. | 1 | ||
UnitTestCase::getClassResolverStub | protected | function | Returns a stub class resolver. | |||
UnitTestCase::getConfigFactoryStub | public | function | Returns a stub config factory that behaves according to the passed array. | |||
UnitTestCase::getConfigStorageStub | public | function | Returns a stub config storage that returns the supplied configuration. | |||
UnitTestCase::getContainerWithCacheTagsInvalidator | protected | function | Sets up a container with a cache tags invalidator. | |||
UnitTestCase::getStringTranslationStub | public | function | Returns a stub translation manager that just returns the passed string. | |||
UnitTestCase::setUpBeforeClass | public static | function | ||||
UnitTestCase::__get | public | function | ||||
VariationCacheTest::$cacheContextsManager | protected | property | The prophesized cache contexts manager. | |||
VariationCacheTest::$cacheIdBase | protected | property | The cache ID for the cache keys, without taking contexts into account. | |||
VariationCacheTest::$cacheKeys | protected | property | The cache keys this test will store things under. | |||
VariationCacheTest::$gardenType | protected | property | The simulated current user's garden type. | |||
VariationCacheTest::$gardenTypeCacheability | protected | property | The cacheability for something that varies per housing and garden type. | |||
VariationCacheTest::$houseOrientation | protected | property | The simulated current user's house's orientation. | |||
VariationCacheTest::$houseOrientationCacheability | protected | property | The cacheability for varying per housing, garden and orientation. | |||
VariationCacheTest::$housingType | protected | property | The simulated current user's housing type. | |||
VariationCacheTest::$housingTypeCacheability | protected | property | The cacheability for something that only varies per housing type. | |||
VariationCacheTest::$memoryBackend | protected | property | The backend used by the variation cache. | |||
VariationCacheTest::$requestStack | protected | property | The prophesized request stack. | |||
VariationCacheTest::$solarType | protected | property | The simulated current user's solar panel type. | |||
VariationCacheTest::$variationCache | protected | property | The variation cache instance. | |||
VariationCacheTest::assertCacheBackendItem | protected | function | Asserts that an item was properly stored in the cache backend. | |||
VariationCacheTest::assertVariationCacheItem | protected | function | Asserts that an item was properly stored in the variation cache. | |||
VariationCacheTest::assertVariationCacheMiss | protected | function | Asserts that an item could not be retrieved from the variation cache. | |||
VariationCacheTest::getSortedCacheId | protected | function | Creates the sorted cache ID from cache ID parts. | |||
VariationCacheTest::setUp | protected | function | Overrides UnitTestCase::setUp | |||
VariationCacheTest::setVariationCacheItem | protected | function | Stores an item in the variation cache. | |||
VariationCacheTest::testIncompatibleVariationsException | public | function | Tests exception for a cache item that has incompatible variations. | |||
VariationCacheTest::testNestedVariations | public | function | Tests a cache item that has nested variations. | |||
VariationCacheTest::testNestedVariationsSelfHealing | public | function | Tests a cache item that has nested variations that trigger self-healing. | |||
VariationCacheTest::testNoVariations | public | function | Tests a cache item that has no variations. | |||
VariationCacheTest::testSingleVariation | public | function | Tests a cache item that only ever varies by one context. | |||
VariationCacheTest::testSplitVariationsSelfHealing | public | function | Tests self-healing for a cache item that has split variations. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.