diff --git a/src/CachePoolTest.php b/src/CachePoolTest.php index 217a2f8..15b121e 100644 --- a/src/CachePoolTest.php +++ b/src/CachePoolTest.php @@ -34,6 +34,16 @@ abstract class CachePoolTest extends TestCase */ abstract public function createCachePool(): CacheItemPoolInterface; + /** + * A second pool backed by the same storage as createCachePool(), under a different namespace. + * + * Return null when the implementation cannot provide one, the test is then skipped. + */ + public function createCachePoolWithOtherNamespace(): ?CacheItemPoolInterface + { + return null; + } + #[Before] public function setupService() { @@ -318,6 +328,34 @@ public function testClearWithDeferredItems() $this->assertFalse($this->cache->getItem('key')->isHit(), 'Deferred items must be cleared on clear(). '); } + public function testClearDoesNotAffectOtherNamespaces() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $other = $this->createCachePoolWithOtherNamespace(); + + if (null === $other) { + $this->markTestSkipped('This implementation cannot provide a second pool on the same storage with another namespace.'); + } + + try { + $this->cache->save($this->cache->getItem('key')->set('value')); + $other->save($other->getItem('key')->set('other value')); + + $this->assertTrue($this->cache->clear(), 'clear() must return true if cache was cleared. '); + $this->assertFalse($this->cache->hasItem('key'), 'The cleared pool should be empty after it is cleared.'); + + $otherItem = $other->getItem('key'); + $this->assertTrue($otherItem->isHit(), 'Clearing a pool must not clear a pool using another namespace on the same storage.'); + $this->assertSame('other value', $otherItem->get(), 'The item stored in the other namespace must keep its own value.'); + } finally { + // The tearDownService() hook only clears $this->cache, so the other pool is cleared here. + $other->clear(); + } + } + public function testDeleteItem() { if (isset($this->skippedTests[__FUNCTION__])) { diff --git a/src/TaggableCachePoolTest.php b/src/TaggableCachePoolTest.php index 3e9216e..a82e8f7 100644 --- a/src/TaggableCachePoolTest.php +++ b/src/TaggableCachePoolTest.php @@ -34,6 +34,16 @@ abstract class TaggableCachePoolTest extends TestCase */ abstract public function createCachePool(): TaggableCacheItemPoolInterface; + /** + * A second pool backed by the same storage as createCachePool(), under a different namespace. + * + * Return null when the implementation cannot provide one, the test is then skipped. + */ + public function createCachePoolWithOtherNamespace(): ?TaggableCacheItemPoolInterface + { + return null; + } + #[Before] public function setupService() { @@ -221,7 +231,7 @@ public function testRemoveTagWhenItemIsRemoved() // Save the item and then delete it $this->cache->save($item); - $this->cache->deleteItem('key'); + $this->assertTrue($this->cache->deleteItem('key'), 'deleteItem() must return true when a tagged item is deleted'); // Create a new item (same key) (no tags) $item = $this->cache->getItem('key')->set('value'); @@ -286,16 +296,28 @@ public function testInvalidateTags() $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } + // Matched through both invalidated tags at once. $item = $this->cache->getItem('key')->set('value'); $item->setTags(['tag1', 'tag2']); $this->cache->save($item); + // Matched through tag2 only, so tag2 must be handled on its own. $item = $this->cache->getItem('key2')->set('value'); + $item->setTags(['tag2']); + $this->cache->save($item); + // Matched through tag1 only, so tag1 must be handled on its own. + $item = $this->cache->getItem('key3')->set('value'); $item->setTags(['tag1']); $this->cache->save($item); + // Matched by none of the invalidated tags. + $item = $this->cache->getItem('key4')->set('value'); + $item->setTags(['tag3']); + $this->cache->save($item); $this->cache->invalidateTags(['tag1', 'tag2']); $this->assertFalse($this->cache->hasItem('key'), 'Item should be cleared when tag is invalidated'); $this->assertFalse($this->cache->hasItem('key2'), 'Item should be cleared when tag is invalidated'); + $this->assertFalse($this->cache->hasItem('key3'), 'Item should be cleared when tag is invalidated'); + $this->assertTrue($this->cache->hasItem('key4'), 'An item tagged with none of the invalidated tags must survive'); // Create a new item (no tags) $item = $this->cache->getItem('key')->set('value'); @@ -305,6 +327,34 @@ public function testInvalidateTags() $this->assertTrue($this->cache->hasItem('key'), 'Item k list should be removed when clearing the tags'); } + public function testInvalidateTagsDoesNotAffectOtherNamespaces() + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + $other = $this->createCachePoolWithOtherNamespace(); + + if (null === $other) { + $this->markTestSkipped('This implementation cannot provide a second pool on the same storage with another namespace.'); + } + + try { + $this->cache->save($this->cache->getItem('key')->set('value')->setTags(['tag1'])); + $other->save($other->getItem('key')->set('other value')->setTags(['tag1'])); + + $this->assertTrue($this->cache->invalidateTags(['tag1'])); + $this->assertFalse($this->cache->hasItem('key'), 'The invalidated item should be gone from its own pool.'); + + $otherItem = $other->getItem('key'); + $this->assertTrue($otherItem->isHit(), 'Invalidating a tag must not reach items of a pool using another namespace on the same storage.'); + $this->assertSame('other value', $otherItem->get(), 'The item stored in the other namespace must keep its own value.'); + } finally { + // The tearDownService() hook only clears $this->cache, so the other pool is cleared here. + $other->clear(); + } + } + public function testInvalidateTagsValidatesEveryTagBeforeMutation() { if (isset($this->skippedTests[__FUNCTION__])) { diff --git a/tests/CachePoolTest.php b/tests/CachePoolTest.php index 9586745..dda4adf 100644 --- a/tests/CachePoolTest.php +++ b/tests/CachePoolTest.php @@ -25,6 +25,8 @@ final class CachePoolTest extends CachePoolContract { private ?string $namespace = null; + private ?string $otherNamespace = null; + public function createCachePool(): CacheItemPoolInterface { $this->namespace ??= bin2hex(random_bytes(16)); @@ -32,6 +34,13 @@ public function createCachePool(): CacheItemPoolInterface return new ValidatingCachePool(new FilesystemAdapter($this->namespace)); } + public function createCachePoolWithOtherNamespace(): CacheItemPoolInterface + { + $this->otherNamespace ??= bin2hex(random_bytes(16)); + + return new ValidatingCachePool(new FilesystemAdapter($this->otherNamespace)); + } + public function testDataProvidersExposeCases() { self::assertNotEmpty(self::invalidKeys()); diff --git a/tests/Fixtures/TaggableCachePool.php b/tests/Fixtures/TaggableCachePool.php index bffb7d7..093ee82 100644 --- a/tests/Fixtures/TaggableCachePool.php +++ b/tests/Fixtures/TaggableCachePool.php @@ -16,19 +16,25 @@ use Cache\TagInterop\TaggableCacheItemInterface; use Cache\TagInterop\TaggableCacheItemPoolInterface; use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\ProxyAdapter; use Symfony\Component\Cache\Exception\InvalidArgumentException; final class TaggableCachePool implements TaggableCacheItemPoolInterface { - private readonly ArrayAdapter $pool; + private readonly AdapterInterface $pool; /** @var array> */ private array $tagsByKey = []; - public function __construct() + /** + * Passing a storage and a namespace puts several pools on one key space, where the + * namespace is a real key prefix, as a shared table or collection would. + */ + public function __construct(?AdapterInterface $storage = null, string $namespace = '') { - $this->pool = new ArrayAdapter(); + $this->pool = null === $storage ? new ArrayAdapter() : new ProxyAdapter($storage, $namespace); } public function getItem(string $key): TaggableCacheItemInterface diff --git a/tests/TaggableCachePoolTest.php b/tests/TaggableCachePoolTest.php index 34fa489..539f90c 100644 --- a/tests/TaggableCachePoolTest.php +++ b/tests/TaggableCachePoolTest.php @@ -17,12 +17,25 @@ use Cache\IntegrationTests\Tests\Fixtures\TaggableCachePool; use Cache\TagInterop\TaggableCacheItemPoolInterface; use PHPUnit\Framework\SkippedTest; +use Symfony\Component\Cache\Adapter\ArrayAdapter; final class TaggableCachePoolTest extends TaggableCachePoolContract { + private ?ArrayAdapter $storage = null; + public function createCachePool(): TaggableCacheItemPoolInterface { - return new TaggableCachePool(); + return new TaggableCachePool($this->sharedStorage(), 'first'); + } + + public function createCachePoolWithOtherNamespace(): TaggableCacheItemPoolInterface + { + return new TaggableCachePool($this->sharedStorage(), 'second'); + } + + private function sharedStorage(): ArrayAdapter + { + return $this->storage ??= new ArrayAdapter(); } public function testDataProviderExposesCases()