class VariationCacheTest

Same name and namespace in other branches
  1. 11.x core/tests/Drupal/Tests/Core/Cache/VariationCacheTest.php \Drupal\Tests\Core\Cache\VariationCacheTest

@coversDefaultClass \Drupal\Core\Cache\VariationCache
@group Cache

Hierarchy

Expanded class hierarchy of VariationCacheTest

File

core/tests/Drupal/Tests/Core/Cache/VariationCacheTest.php, line 22

Namespace

Drupal\Tests\Core\Cache
View 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.