pax_global_header00006660000000000000000000000064152414575240014523gustar00rootroot0000000000000052 comment=0ba62616715bc2ca859a60b91390daab526af37b integration-tests-1.0.5/000077500000000000000000000000001524145752400152115ustar00rootroot00000000000000integration-tests-1.0.5/.github/000077500000000000000000000000001524145752400165515ustar00rootroot00000000000000integration-tests-1.0.5/.github/workflows/000077500000000000000000000000001524145752400206065ustar00rootroot00000000000000integration-tests-1.0.5/.github/workflows/unit-tests.yaml000066400000000000000000000030301524145752400236050ustar00rootroot00000000000000name: CI on: push: pull_request: permissions: contents: read jobs: tests: name: PHP ${{ matrix.php }}, PSR-16 ${{ matrix.psr_simple_cache }} strategy: fail-fast: false matrix: include: - php: '8.2' psr_simple_cache: '2.0.0' - php: '8.2' psr_simple_cache: '3.0.0' - php: '8.3' psr_simple_cache: '3.0.0' - php: '8.4' psr_simple_cache: '3.0.0' - php: '8.5' psr_simple_cache: '3.0.0' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: shivammathur/setup-php@2.37.2 with: php-version: ${{ matrix.php }} coverage: none - run: composer update --with "psr/simple-cache:${{ matrix.psr_simple_cache }}" --no-interaction --prefer-dist --no-progress - run: composer test quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: shivammathur/setup-php@2.37.2 with: php-version: '8.2' coverage: none - run: composer install --no-interaction --prefer-dist --no-progress - run: composer validate --strict - run: composer analyse - run: composer cs-check coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: shivammathur/setup-php@2.37.2 with: php-version: '8.5' coverage: xdebug - run: composer install --no-interaction --prefer-dist --no-progress - run: composer coverage integration-tests-1.0.5/.gitignore000077500000000000000000000001241524145752400172010ustar00rootroot00000000000000.phpunit.result.cache .phpunit.cache .php-cs-fixer.cache build composer.lock vendor integration-tests-1.0.5/.php-cs-fixer.dist.php000066400000000000000000000021651524145752400212530ustar00rootroot00000000000000, Tobias Nyholm This source file is subject to the MIT license that is bundled with this source code in the file LICENSE. HEADER; $finder = PhpCsFixer\Finder::create() ->in([ __DIR__.'/src', __DIR__.'/tests', __DIR__.'/tools', ]); return (new PhpCsFixer\Config()) ->setRules([ '@Symfony' => true, '@Symfony:risky' => true, 'declare_strict_types' => false, 'header_comment' => ['header' => $header], 'modern_serialization_methods' => false, 'no_php4_constructor' => false, 'no_trailing_whitespace_in_string' => true, 'php_unit_construct' => false, 'php_unit_mock_short_will_return' => false, 'php_unit_set_up_tear_down_visibility' => false, 'php_unit_test_annotation' => false, 'protected_to_private' => false, 'static_lambda' => false, 'void_return' => false, ]) ->setRiskyAllowed(true) ->setFinder($finder); integration-tests-1.0.5/LICENSE000077500000000000000000000020711524145752400162210ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Aaron Scherer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. integration-tests-1.0.5/README.md000066400000000000000000000052131524145752400164710ustar00rootroot00000000000000# PSR-6 and PSR-16 integration tests [![CI](https://github.com/php-cache/integration-tests/actions/workflows/unit-tests.yaml/badge.svg)](https://github.com/php-cache/integration-tests/actions/workflows/unit-tests.yaml) [![Latest Stable Version](https://poser.pugx.org/cache/integration-tests/v/stable)](https://packagist.org/packages/cache/integration-tests) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) This package tests whether cache implementations follow PSR-6 or PSR-16. It also includes suites for PHP Cache tags and hierarchical keys. Version 1 requires PHP 8.2, PHPUnit 11.5 or 12, `psr/cache` 3, `psr/simple-cache` 2 or 3, and `cache/tag-interop` 2. ## Installation ```bash composer require --dev cache/integration-tests:^1.0 ``` ## Testing a PSR-6 pool Extend `CachePoolTest` and return a fresh pool from `createCachePool()`: ```php use Cache\IntegrationTests\CachePoolTest; use Psr\Cache\CacheItemPoolInterface; final class PoolIntegrationTest extends CachePoolTest { public function createCachePool(): CacheItemPoolInterface { return new MyCachePool(); } } ``` Use `HierarchicalCachePoolTest` for pools that support hierarchical keys. ## Testing tags ```php use Cache\IntegrationTests\TaggableCachePoolTest; use Cache\TagInterop\TaggableCacheItemPoolInterface; final class TagIntegrationTest extends TaggableCachePoolTest { public function createCachePool(): TaggableCacheItemPoolInterface { return new MyTaggableCachePool(); } } ``` ## Testing a PSR-16 cache ```php use Cache\IntegrationTests\SimpleCacheTest; use Psr\SimpleCache\CacheInterface; final class SimpleCacheIntegrationTest extends SimpleCacheTest { public function createSimpleCache(): CacheInterface { return new MySimpleCache(); } } ``` ## Skipping unsupported behavior The test bases expose a typed `$skippedTests` map. Skip a test only when the storage backend cannot provide that behavior. ```php use Cache\IntegrationTests\CachePoolTest; use Psr\Cache\CacheItemPoolInterface; final class PoolWithSkippedExpirationTest extends CachePoolTest { /** @var array */ protected array $skippedTests = [ 'testExpiration' => 'This backend does not support expiration.', ]; public function createCachePool(): CacheItemPoolInterface { return new MyCachePool(); } } ``` ## Contributing Run the complete local checks before opening a pull request: ```bash composer test composer coverage composer analyse composer cs-check ``` Report problems on the [GitHub issue tracker](https://github.com/php-cache/integration-tests/issues). integration-tests-1.0.5/composer.json000077500000000000000000000034011524145752400177340ustar00rootroot00000000000000{ "name": "cache/integration-tests", "description": "Integration tests for PSR-6 and PSR-16 cache implementations", "license": "MIT", "type": "library", "keywords": [ "cache", "psr6", "psr16", "test" ], "authors": [ { "name": "Aaron Scherer", "email": "aequasi@gmail.com", "homepage": "https://github.com/aequasi" }, { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com", "homepage": "https://github.com/nyholm" } ], "homepage": "https://github.com/php-cache/integration-tests", "require": { "php": "^8.2", "cache/tag-interop": "^2.0", "psr/cache": "^3.0", "psr/simple-cache": "^2.0 || ^3.0" }, "require-dev": { "php-cs-fixer/shim": "3.95.19", "phpstan/phpstan": "2.2.8", "phpunit/phpunit": "^11.5 || ^12.0", "symfony/cache": "^7.4 || ^8.0" }, "autoload": { "psr-4": { "Cache\\IntegrationTests\\": "src/" } }, "autoload-dev": { "psr-4": { "Cache\\IntegrationTests\\Tests\\": "tests/" } }, "extra": { "branch-alias": { "dev-master": "1.0-dev" } }, "scripts": { "analyse": "phpstan analyse --configuration=phpstan.neon --debug --no-progress --memory-limit=1G", "coverage": [ "@php -d xdebug.mode=coverage vendor/bin/phpunit --coverage-clover build/coverage.xml", "@php tools/check-coverage.php build/coverage.xml 90" ], "cs-check": "php-cs-fixer fix --dry-run --diff --sequential", "cs-fix": "php-cs-fixer fix --sequential", "test": "phpunit" } } integration-tests-1.0.5/phpstan.neon000066400000000000000000000007001524145752400175440ustar00rootroot00000000000000parameters: level: 9 phpVersion: 80200 treatPhpDocTypesAsCertain: false ignoreErrors: - message: '#^Method Cache\\IntegrationTests\\(?:Tests\\)?(?:CachePoolTest|HierarchicalCachePoolTest|SimpleCacheTest|TaggableCachePoolTest)::(?:advanceTime|setupService|tearDownService|test[A-Za-z0-9_]*)\(\) has no return type specified\.$#' identifier: missingType.return paths: - src - tests integration-tests-1.0.5/phpunit.xml.dist000066400000000000000000000010371524145752400203650ustar00rootroot00000000000000 tests src integration-tests-1.0.5/src/000077500000000000000000000000001524145752400160005ustar00rootroot00000000000000integration-tests-1.0.5/src/CachePoolTest.php000066400000000000000000000774121524145752400212210ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\InvalidArgumentException; abstract class CachePoolTest extends TestCase { /** @var array */ protected array $skippedTests = []; protected CacheItemPoolInterface $cache; /** * @return CacheItemPoolInterface that is used in the tests */ abstract public function createCachePool(): CacheItemPoolInterface; #[Before] public function setupService() { $this->cache = $this->createCachePool(); } #[After] public function tearDownService() { if (isset($this->cache)) { $this->cache->clear(); } } /** @param callable(): mixed $callable */ private function assertCacheExceptionOrTypeError(callable $callable): void { try { $callable(); } catch (\TypeError|InvalidArgumentException) { $this->addToAssertionCount(1); return; } $this->fail('Expected TypeError or PSR InvalidArgumentException.'); } private function callWithInvalidArguments(callable $callable, mixed ...$arguments): mixed { return $callable(...$arguments); } /** * Data provider for invalid keys. * * @return list */ public static function invalidKeys(): array { return [ ['{str'], ['rand{'], ['rand{str'], ['rand}str'], ['rand(str'], ['rand)str'], ['rand/str'], ['rand\\str'], ['rand@str'], ['rand:str'], ]; } /** @return list */ public static function invalidKeyTypes(): array { return [ [true], [false], [null], [2], [2.5], [new \stdClass()], [['array']], ]; } public function testBasicUsage() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('4711'); $this->cache->save($item); $item = $this->cache->getItem('key2'); $item->set('4712'); $this->cache->save($item); $fooItem = $this->cache->getItem('key'); $this->assertTrue($fooItem->isHit()); $this->assertEquals('4711', $fooItem->get()); $barItem = $this->cache->getItem('key2'); $this->assertTrue($barItem->isHit()); $this->assertEquals('4712', $barItem->get()); // Remove 'key' and make sure 'key2' is still there $this->cache->deleteItem('key'); $this->assertFalse($this->cache->getItem('key')->isHit()); $this->assertTrue($this->cache->getItem('key2')->isHit()); // Remove everything $this->cache->clear(); $this->assertFalse($this->cache->getItem('key')->isHit()); $this->assertFalse($this->cache->getItem('key2')->isHit()); } public function testBasicUsageWithLongKey() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $pool = $this->createCachePool(); $key = str_repeat('a', 300); $item = $pool->getItem($key); $this->assertFalse($item->isHit()); $this->assertSame($key, $item->getKey()); $item->set('value'); $this->assertTrue($pool->save($item)); $item = $pool->getItem($key); $this->assertTrue($item->isHit()); $this->assertSame($key, $item->getKey()); $this->assertSame('value', $item->get()); $this->assertTrue($pool->deleteItem($key)); $item = $pool->getItem($key); $this->assertFalse($item->isHit()); } public function testItemModifiersReturnsStatic() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $this->assertSame($item, $item->set('4711')); $this->assertSame($item, $item->expiresAfter(2)); $this->assertSame($item, $item->expiresAt(new \DateTime('+2hours'))); } public function testGetItem() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $this->cache->save($item); // get existing item $item = $this->cache->getItem('key'); $this->assertEquals('value', $item->get(), 'A stored item must be returned from cached.'); $this->assertEquals('key', $item->getKey(), 'Cache key can not change.'); // get non-existent item $item = $this->cache->getItem('key2'); $this->assertFalse($item->isHit()); $this->assertNull($item->get(), "Item's value must be null when isHit is false."); } public function testGetItems() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $keys = ['foo', 'bar', 'baz']; $items = $this->cache->getItems($keys); $count = 0; /** @var CacheItemInterface $item */ foreach ($items as $i => $item) { $item->set($i); $this->cache->save($item); ++$count; } $this->assertSame(3, $count); $keys[] = 'biz'; /** @var CacheItemInterface[] $items */ $items = $this->cache->getItems($keys); $count = 0; foreach ($items as $key => $item) { $itemKey = $item->getKey(); $this->assertEquals($itemKey, $key, 'Keys must be preserved when fetching multiple items'); $this->assertEquals('biz' !== $key, $item->isHit()); $this->assertTrue(\in_array($key, $keys), 'Cache key can not change.'); // Remove $key for $keys foreach ($keys as $k => $v) { if ($v === $key) { unset($keys[$k]); } } ++$count; } $this->assertSame(4, $count); } public function testGetItemsPreservesNumericStringKeys() { $count = 0; foreach ($this->cache->getItems(['123']) as $key => $item) { $this->assertSame('123', $key); $this->assertSame('123', $item->getKey()); ++$count; } $this->assertSame(1, $count); } public function testGetItemsEmpty() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $items = $this->cache->getItems([]); $count = 0; foreach ($items as $item) { ++$count; } $this->assertSame(0, $count); } public function testHasItem() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $this->cache->save($item); // has existing item $this->assertTrue($this->cache->hasItem('key')); // has non-existent item $this->assertFalse($this->cache->hasItem('key2')); } public function testClear() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $this->cache->save($item); $return = $this->cache->clear(); $this->assertTrue($return, 'clear() must return true if cache was cleared. '); $this->assertFalse($this->cache->getItem('key')->isHit(), 'No item should be a hit after the cache is cleared. '); $this->assertFalse($this->cache->hasItem('key2'), 'The cache pool should be empty after it is cleared.'); } public function testClearWithDeferredItems() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $this->cache->saveDeferred($item); $this->cache->clear(); $this->cache->commit(); $this->assertFalse($this->cache->getItem('key')->isHit(), 'Deferred items must be cleared on clear(). '); } public function testDeleteItem() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $this->cache->save($item); $this->assertTrue($this->cache->deleteItem('key')); $this->assertFalse($this->cache->getItem('key')->isHit(), 'A deleted item should not be a hit.'); $this->assertFalse($this->cache->hasItem('key'), 'A deleted item should not be a in cache.'); $this->assertTrue($this->cache->deleteItem('key2'), 'Deleting an item that does not exist should return true.'); } public function testDeleteItems() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $items = $this->cache->getItems(['foo', 'bar', 'baz']); /** @var CacheItemInterface $item */ foreach ($items as $idx => $item) { $item->set($idx); $this->cache->save($item); } // All should be a hit but 'biz' $this->assertTrue($this->cache->getItem('foo')->isHit()); $this->assertTrue($this->cache->getItem('bar')->isHit()); $this->assertTrue($this->cache->getItem('baz')->isHit()); $this->assertFalse($this->cache->getItem('biz')->isHit()); $return = $this->cache->deleteItems(['foo', 'bar', 'biz']); $this->assertTrue($return); $this->assertFalse($this->cache->getItem('foo')->isHit()); $this->assertFalse($this->cache->getItem('bar')->isHit()); $this->assertTrue($this->cache->getItem('baz')->isHit()); $this->assertFalse($this->cache->getItem('biz')->isHit()); } public function testSave() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $return = $this->cache->save($item); $this->assertTrue($return, 'save() should return true when items are saved.'); $this->assertEquals('value', $this->cache->getItem('key')->get()); } public function testSaveExpired() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $item->expiresAt(new \DateTimeImmutable('@'.(time() + 10))); $this->cache->save($item); $item->expiresAt(new \DateTimeImmutable('@'.(time() - 1))); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertFalse($item->isHit(), 'Cache should not save expired items'); } public function testSaveWithoutExpire() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('test_ttl_null'); $item->set('data'); $this->cache->save($item); // Use a new pool instance to ensure that we don't hit any caches $pool = $this->createCachePool(); $item = $pool->getItem('test_ttl_null'); $this->assertTrue($item->isHit(), 'Cache should have retrieved the items'); $this->assertEquals('data', $item->get()); } public function testDeferredSave() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('4711'); $return = $this->cache->saveDeferred($item); $this->assertTrue($return, 'save() should return true when items are saved.'); $item = $this->cache->getItem('key2'); $item->set('4712'); $this->cache->saveDeferred($item); // They are not saved yet but should be a hit $this->assertTrue($this->cache->hasItem('key'), 'Deferred items should be considered as a part of the cache even before they are committed'); $this->assertTrue($this->cache->getItem('key')->isHit(), 'Deferred items should be a hit even before they are committed'); $this->assertTrue($this->cache->getItem('key2')->isHit()); $this->cache->commit(); // They should be a hit after the commit as well $this->assertTrue($this->cache->getItem('key')->isHit()); $this->assertTrue($this->cache->getItem('key2')->isHit()); } public function testDeferredExpired() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('4711'); $item->expiresAt(new \DateTimeImmutable('@'.(time() - 1))); $this->cache->saveDeferred($item); $this->assertFalse($this->cache->hasItem('key'), 'Cache should not have expired deferred item'); $this->cache->commit(); $item = $this->cache->getItem('key'); $this->assertFalse($item->isHit(), 'Cache should not save expired items'); } public function testDeleteDeferredItem() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('4711'); $this->cache->saveDeferred($item); $this->assertTrue($this->cache->getItem('key')->isHit()); $this->cache->deleteItem('key'); $this->assertFalse($this->cache->hasItem('key'), 'You must be able to delete a deferred item before committed. '); $this->assertFalse($this->cache->getItem('key')->isHit(), 'You must be able to delete a deferred item before committed. '); $this->cache->commit(); $this->assertFalse($this->cache->hasItem('key'), 'A deleted item should not reappear after commit. '); $this->assertFalse($this->cache->getItem('key')->isHit(), 'A deleted item should not reappear after commit. '); } public function testDeferredSaveWithoutCommit() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->prepareDeferredSaveWithoutCommit(); gc_collect_cycles(); $this->cache = $this->createCachePool(); $this->assertTrue($this->cache->getItem('key')->isHit(), 'A deferred item should automatically be committed on CachePool::__destruct().'); } private function prepareDeferredSaveWithoutCommit(): void { $cache = $this->cache; unset($this->cache); $item = $cache->getItem('key'); $item->set('4711'); $cache->saveDeferred($item); } public function testCommit() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $this->cache->saveDeferred($item); $return = $this->cache->commit(); $this->assertTrue($return, 'commit() should return true on successful commit. '); $this->assertEquals('value', $this->cache->getItem('key')->get()); $return = $this->cache->commit(); $this->assertTrue($return, 'commit() should return true even if no items were deferred. '); } #[Group('slow')] public function testExpiration() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $item->expiresAfter(2); $this->cache->save($item); sleep(3); $item = $this->cache->getItem('key'); $this->assertFalse($item->isHit()); $this->assertNull($item->get(), "Item's value must be null when isHit() is false."); } public function testExpiresAt() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $item->expiresAt(new \DateTime('+2hours')); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertTrue($item->isHit()); } public function testExpiresAtWithNull() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $item->expiresAt(null); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertTrue($item->isHit()); } public function testExpiresAfterWithNull() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $item->expiresAfter(null); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertTrue($item->isHit()); } public function testKeyLength() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $key = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.'; $item = $this->cache->getItem($key); $item->set('value'); $this->assertTrue($this->cache->save($item), 'The implementation does not support a valid cache key'); $this->assertTrue($this->cache->hasItem($key)); } #[DataProvider('invalidKeys')] public function testGetItemInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->expectException('Psr\Cache\InvalidArgumentException'); $this->cache->getItem($key); } #[DataProvider('invalidKeyTypes')] public function testGetItemInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'getItem'], $key) ); } #[DataProvider('invalidKeys')] public function testGetItemsInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->expectException('Psr\Cache\InvalidArgumentException'); $this->cache->getItems(['key1', $key, 'key2']); } #[DataProvider('invalidKeyTypes')] public function testGetItemsInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'getItems'], ['key1', $key, 'key2']) ); } #[DataProvider('invalidKeys')] public function testHasItemInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->expectException('Psr\Cache\InvalidArgumentException'); $this->cache->hasItem($key); } #[DataProvider('invalidKeyTypes')] public function testHasItemInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'hasItem'], $key) ); } #[DataProvider('invalidKeys')] public function testDeleteItemInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->expectException('Psr\Cache\InvalidArgumentException'); $this->cache->deleteItem($key); } #[DataProvider('invalidKeyTypes')] public function testDeleteItemInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'deleteItem'], $key) ); } #[DataProvider('invalidKeys')] public function testDeleteItemsInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->expectException('Psr\Cache\InvalidArgumentException'); $this->cache->deleteItems(['key1', $key, 'key2']); } public function testDeleteItemsValidatesEveryKeyBeforeMutation() { $item = $this->cache->getItem('key1'); $item->set('value'); if (!$this->cache->save($item) || !$this->cache->hasItem('key1')) { $this->markTestSkipped('The pool does not store cache items.'); } try { $this->cache->deleteItems(['key1', 'invalid{key']); $this->fail('Expected PSR InvalidArgumentException.'); } catch (InvalidArgumentException) { $this->addToAssertionCount(1); } $this->assertTrue($this->cache->hasItem('key1')); } #[DataProvider('invalidKeyTypes')] public function testDeleteItemsInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'deleteItems'], ['key1', $key, 'key2']) ); } public function testDataTypeString() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('5'); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertTrue('5' === $item->get(), 'Wrong data type. If we store a string we must get an string back.'); $this->assertTrue(\is_string($item->get()), 'Wrong data type. If we store a string we must get an string back.'); } public function testDataTypeInteger() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set(5); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertTrue(5 === $item->get(), 'Wrong data type. If we store an int we must get an int back.'); $this->assertTrue(\is_int($item->get()), 'Wrong data type. If we store an int we must get an int back.'); } public function testDataTypeNull() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set(null); $this->cache->save($item); $this->assertTrue($this->cache->hasItem('key'), 'Null is a perfectly fine cache value. hasItem() should return true when null are stored. '); $item = $this->cache->getItem('key'); $this->assertNull($item->get(), 'Wrong data type. If we store null we must get a null back.'); $this->assertTrue($item->isHit(), 'isHit() should return true when null are stored. '); } public function testDataTypeFloat() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $float = 1.23456789; $item = $this->cache->getItem('key'); $item->set($float); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertTrue(\is_float($item->get()), 'Wrong data type. If we store float we must get an float back.'); $this->assertEquals($float, $item->get()); $this->assertTrue($item->isHit(), 'isHit() should return true when float are stored. '); } public function testDataTypeBoolean() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set(true); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertTrue(\is_bool($item->get()), 'Wrong data type. If we store boolean we must get an boolean back.'); $this->assertTrue($item->get()); $this->assertTrue($item->isHit(), 'isHit() should return true when true are stored. '); } public function testDataTypeArray() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $array = ['a' => 'foo', 2 => 'bar']; $item = $this->cache->getItem('key'); $item->set($array); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertTrue(\is_array($item->get()), 'Wrong data type. If we store array we must get an array back.'); $this->assertEquals($array, $item->get()); $this->assertTrue($item->isHit(), 'isHit() should return true when array are stored. '); } public function testDataTypeObject() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $object = new \stdClass(); $object->a = 'foo'; $item = $this->cache->getItem('key'); $item->set($object); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertTrue(\is_object($item->get()), 'Wrong data type. If we store object we must get an object back.'); $this->assertEquals($object, $item->get()); $this->assertTrue($item->isHit(), 'isHit() should return true when object are stored. '); } public function testBinaryData() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $data = ''; for ($i = 0; $i < 256; ++$i) { $data .= \chr($i); } $item = $this->cache->getItem('key'); $item->set($data); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertTrue($data === $item->get(), 'Binary data must survive a round trip.'); } public function testIsHit() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertTrue($item->isHit()); } public function testIsHitDeferred() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $this->cache->saveDeferred($item); // Test accessing the value before it is committed $item = $this->cache->getItem('key'); $this->assertTrue($item->isHit()); $this->cache->commit(); $item = $this->cache->getItem('key'); $this->assertTrue($item->isHit()); } public function testSaveDeferredWhenChangingValues() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $this->cache->saveDeferred($item); $item = $this->cache->getItem('key'); $item->set('new value'); $item = $this->cache->getItem('key'); $this->assertEquals('value', $item->get(), 'Items that is put in the deferred queue should not get their values changed'); $this->cache->commit(); $item = $this->cache->getItem('key'); $this->assertEquals('value', $item->get(), 'Items that is put in the deferred queue should not get their values changed'); } public function testSaveDeferredOverwrite() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $this->cache->saveDeferred($item); $item = $this->cache->getItem('key'); $item->set('new value'); $this->cache->saveDeferred($item); $item = $this->cache->getItem('key'); $this->assertEquals('new value', $item->get()); $this->cache->commit(); $item = $this->cache->getItem('key'); $this->assertEquals('new value', $item->get()); } public function testSaveOverridesDeferredItem() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('deferred'); $this->cache->saveDeferred($item); $item = $this->cache->getItem('key')->set('immediate'); $this->cache->save($item); $this->cache->commit(); $this->assertSame('immediate', $this->cache->getItem('key')->get()); } public function testSavingObject() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set(new \DateTime()); $this->cache->save($item); $item = $this->cache->getItem('key'); $value = $item->get(); $this->assertInstanceOf('DateTime', $value, 'You must be able to store objects in cache.'); } #[Group('slow')] public function testHasItemReturnsFalseWhenDeferredItemIsExpired() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key'); $item->set('value'); $item->expiresAfter(2); $this->cache->saveDeferred($item); sleep(3); $this->assertFalse($this->cache->hasItem('key')); } } integration-tests-1.0.5/src/HierarchicalCachePoolTest.php000066400000000000000000000113461524145752400235120ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemPoolInterface; /** * @author Tobias Nyholm */ abstract class HierarchicalCachePoolTest extends TestCase { /** @var array */ protected array $skippedTests = []; protected CacheItemPoolInterface $cache; /** * @return CacheItemPoolInterface that is used in the tests */ abstract public function createCachePool(): CacheItemPoolInterface; #[Before] public function setupService() { $this->cache = $this->createCachePool(); } #[After] public function tearDownService() { if (isset($this->cache)) { $this->cache->clear(); } } public function testBasicUsage() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $user = 4711; for ($i = 0; $i < 10; ++$i) { $item = $this->cache->getItem(\sprintf('|users|%d|followers|%d|likes', $user, $i)); $item->set('Justin Bieber'); $this->cache->save($item); } $this->assertTrue($this->cache->hasItem('|users|4711|followers|4|likes')); $this->cache->deleteItem('|users|4711|followers'); $this->assertFalse($this->cache->hasItem('|users|4711|followers|4|likes')); } public function testChain() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('|aaa|bbb|ccc|ddd'); $item->set('value'); $this->cache->save($item); $item = $this->cache->getItem('|aaa|bbb|ccc|xxx'); $item->set('value'); $this->cache->save($item); $item = $this->cache->getItem('|aaa|bbb|zzz|ddd'); $item->set('value'); $this->cache->save($item); $this->assertTrue($this->cache->hasItem('|aaa|bbb|ccc|ddd')); $this->assertTrue($this->cache->hasItem('|aaa|bbb|ccc|xxx')); $this->assertTrue($this->cache->hasItem('|aaa|bbb|zzz|ddd')); $this->assertFalse($this->cache->hasItem('|aaa|bbb|ccc')); $this->assertFalse($this->cache->hasItem('|aaa|bbb|zzz')); $this->assertFalse($this->cache->hasItem('|aaa|bbb')); $this->assertFalse($this->cache->hasItem('|aaa')); $this->assertFalse($this->cache->hasItem('|')); // This is a different thing $this->cache->deleteItem('|aaa|bbb|cc'); $this->assertTrue($this->cache->hasItem('|aaa|bbb|ccc|ddd')); $this->assertTrue($this->cache->hasItem('|aaa|bbb|ccc|xxx')); $this->assertTrue($this->cache->hasItem('|aaa|bbb|zzz|ddd')); $this->cache->deleteItem('|aaa|bbb|ccc'); $this->assertFalse($this->cache->hasItem('|aaa|bbb|ccc|ddd')); $this->assertFalse($this->cache->hasItem('|aaa|bbb|ccc|xxx')); $this->assertTrue($this->cache->hasItem('|aaa|bbb|zzz|ddd')); $this->cache->deleteItem('|aaa'); $this->assertFalse($this->cache->hasItem('|aaa|bbb|zzz|ddd')); } public function testRemoval() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('foo'); $item->set('value'); $this->cache->save($item); $item = $this->cache->getItem('|aaa|bbb'); $item->set('value'); $this->cache->save($item); $this->cache->deleteItem('|'); $this->assertFalse($this->cache->hasItem('|aaa|bbb'), 'Hierarchy items should be removed when deleting root'); $this->assertTrue($this->cache->hasItem('foo'), 'All cache should not be cleared when deleting root'); } public function testRemovalWhenDeferred() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('|aaa|bbb'); $item->set('value'); $this->cache->saveDeferred($item); $this->cache->deleteItem('|'); $this->assertFalse($this->cache->hasItem('|aaa|bbb'), 'Deferred hierarchy items should be removed'); $this->cache->commit(); $this->assertFalse($this->cache->hasItem('|aaa|bbb'), 'Deferred hierarchy items should be removed'); } } integration-tests-1.0.5/src/SimpleCacheTest.php000066400000000000000000000706301524145752400215340ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\InvalidArgumentException; abstract class SimpleCacheTest extends TestCase { /** @var array */ protected array $skippedTests = []; protected CacheInterface $cache; /** * @return CacheInterface that is used in the tests */ abstract public function createSimpleCache(): CacheInterface; /** * Advance time perceived by the cache for the purposes of testing TTL. * * The default implementation sleeps for the specified duration, * but subclasses are encouraged to override this, * adjusting a mocked time possibly set up in {@link createSimpleCache()}, * to speed up the tests. */ public function advanceTime(int $seconds) { sleep($seconds); } #[Before] public function setupService() { $this->cache = $this->createSimpleCache(); } #[After] public function tearDownService() { if (isset($this->cache)) { $this->cache->clear(); } } /** @param callable(): mixed $callable */ private function assertCacheExceptionOrTypeError(callable $callable): void { try { $callable(); } catch (\TypeError|InvalidArgumentException) { $this->addToAssertionCount(1); return; } $this->fail('Expected TypeError or PSR InvalidArgumentException.'); } private function callWithInvalidArguments(callable $callable, mixed ...$arguments): mixed { return $callable(...$arguments); } /** * Data provider for invalid cache keys. * * @return list */ public static function invalidKeys(): array { return self::invalidArrayKeys(); } /** * Data provider for invalid array keys. * * @return list */ public static function invalidArrayKeys(): array { return [ [''], ['{str'], ['rand{'], ['rand{str'], ['rand}str'], ['rand(str'], ['rand)str'], ['rand/str'], ['rand\\str'], ['rand@str'], ['rand:str'], ]; } /** @return list */ public static function invalidKeyTypes(): array { return [ [true], [false], [null], [2], [2.5], [new \stdClass()], [['array']], ]; } /** @return list */ public static function nonIterableValues(): array { return [ ['key'], [true], [false], [null], [2], [2.5], [new \stdClass()], ]; } /** @return list */ public static function invalidTtlValues(): array { return [ [''], [true], [false], ['abc'], [2.5], [' 1'], ['12foo'], ['025'], [new \stdClass()], [['array']], ]; } /** @return list */ public static function invalidSetMultipleKeyTypes(): array { return [ [true], [false], [null], [2.5], [new \stdClass()], [['array']], ]; } /** * Data provider for valid keys. * * @return list */ public static function validKeys(): array { return [ ['AbC19_.'], ['1234567890123456789012345678901234567890123456789012345678901234'], ]; } /** * Data provider for valid data to store. * * @return list */ public static function validData(): array { return [ ['AbC19_.'], [4711], [47.11], [true], [null], [['key' => 'value']], [new \stdClass()], ]; } public function testSet() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $result = $this->cache->set('key', 'value'); $this->assertTrue($result, 'set() must return true if success'); $this->assertEquals('value', $this->cache->get('key')); } #[Group('slow')] public function testSetTtl() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $result = $this->cache->set('key1', 'value', 2); $this->assertTrue($result, 'set() must return true if success'); $this->assertEquals('value', $this->cache->get('key1')); $this->cache->set('key2', 'value', new \DateInterval('PT2S')); $this->assertEquals('value', $this->cache->get('key2')); $this->advanceTime(3); $this->assertNull($this->cache->get('key1'), 'Value must expire after ttl.'); $this->assertNull($this->cache->get('key2'), 'Value must expire after ttl.'); } public function testSetExpiredTtl() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->set('key0', 'value'); $this->cache->set('key0', 'value', 0); $this->assertNull($this->cache->get('key0')); $this->assertFalse($this->cache->has('key0')); $this->cache->set('key1', 'value', -1); $this->assertNull($this->cache->get('key1')); $this->assertFalse($this->cache->has('key1')); } public function testGet() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertNull($this->cache->get('key')); $this->assertEquals('foo', $this->cache->get('key', 'foo')); $this->cache->set('key', 'value'); $this->assertEquals('value', $this->cache->get('key', 'foo')); } public function testDelete() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertTrue($this->cache->delete('key'), 'Deleting a value that does not exist should return true'); $this->cache->set('key', 'value'); $this->assertTrue($this->cache->delete('key'), 'Delete must return true on success'); $this->assertNull($this->cache->get('key'), 'Values must be deleted on delete()'); } public function testClear() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertTrue($this->cache->clear(), 'Clearing an empty cache should return true'); $this->cache->set('key', 'value'); $this->assertTrue($this->cache->clear(), 'Delete must return true on success'); $this->assertNull($this->cache->get('key'), 'Values must be deleted on clear()'); } public function testSetMultiple() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $result = $this->cache->setMultiple(['key0' => 'value0', 'key1' => 'value1']); $this->assertTrue($result, 'setMultiple() must return true if success'); $this->assertEquals('value0', $this->cache->get('key0')); $this->assertEquals('value1', $this->cache->get('key1')); } public function testSetMultipleWithIntegerArrayKey() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $result = $this->cache->setMultiple(['0' => 'value0']); $this->assertTrue($result, 'setMultiple() must return true if success'); $this->assertEquals('value0', $this->cache->get('0')); } #[Group('slow')] public function testSetMultipleTtl() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->setMultiple(['key2' => 'value2', 'key3' => 'value3'], 2); $this->assertEquals('value2', $this->cache->get('key2')); $this->assertEquals('value3', $this->cache->get('key3')); $this->cache->setMultiple(['key4' => 'value4'], new \DateInterval('PT2S')); $this->assertEquals('value4', $this->cache->get('key4')); $this->advanceTime(3); $this->assertNull($this->cache->get('key2'), 'Value must expire after ttl.'); $this->assertNull($this->cache->get('key3'), 'Value must expire after ttl.'); $this->assertNull($this->cache->get('key4'), 'Value must expire after ttl.'); } public function testSetMultipleExpiredTtl() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->setMultiple(['key0' => 'value0', 'key1' => 'value1'], 0); $this->assertNull($this->cache->get('key0')); $this->assertNull($this->cache->get('key1')); } public function testSetMultipleWithGenerator() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $gen = function () { yield 'key0' => 'value0'; yield 'key1' => 'value1'; }; $this->cache->setMultiple($gen()); $this->assertEquals('value0', $this->cache->get('key0')); $this->assertEquals('value1', $this->cache->get('key1')); } public function testGetMultiple() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $result = $this->cache->getMultiple(['key0', 'key1']); $keys = []; foreach ($result as $i => $r) { $keys[] = $i; $this->assertNull($r); } sort($keys); $this->assertSame(['key0', 'key1'], $keys); $this->cache->set('key3', 'value'); $result = $this->cache->getMultiple(['key2', 'key3', 'key4'], 'foo'); $keys = []; foreach ($result as $key => $r) { $keys[] = $key; if ('key3' === $key) { $this->assertEquals('value', $r); } else { $this->assertEquals('foo', $r); } } sort($keys); $this->assertSame(['key2', 'key3', 'key4'], $keys); } public function testGetMultiplePreservesNumericStringKeys() { $count = 0; foreach ($this->cache->getMultiple(['123']) as $key => $value) { $this->assertSame('123', $key); $this->assertNull($value); ++$count; } $this->assertSame(1, $count); } public function testGetMultipleWithGenerator() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $gen = function () { yield 1 => 'key0'; yield 1 => 'key1'; }; $this->cache->set('key0', 'value0'); $result = $this->cache->getMultiple($gen()); $keys = []; foreach ($result as $key => $r) { $keys[] = $key; if ('key0' === $key) { $this->assertEquals('value0', $r); } elseif ('key1' === $key) { $this->assertNull($r); } else { $this->fail('Unexpected cache key returned.'); } } sort($keys); $this->assertSame(['key0', 'key1'], $keys); $this->assertEquals('value0', $this->cache->get('key0')); $this->assertNull($this->cache->get('key1')); } public function testDeleteMultiple() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertTrue($this->cache->deleteMultiple([]), 'Deleting a empty array should return true'); $this->assertTrue($this->cache->deleteMultiple(['key']), 'Deleting a value that does not exist should return true'); $this->cache->set('key0', 'value0'); $this->cache->set('key1', 'value1'); $this->assertTrue($this->cache->deleteMultiple(['key0', 'key1']), 'Delete must return true on success'); $this->assertNull($this->cache->get('key0'), 'Values must be deleted on deleteMultiple()'); $this->assertNull($this->cache->get('key1'), 'Values must be deleted on deleteMultiple()'); } public function testDeleteMultipleGenerator() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $gen = function () { yield 1 => 'key0'; yield 1 => 'key1'; }; $this->cache->set('key0', 'value0'); $this->assertTrue($this->cache->deleteMultiple($gen()), 'Deleting a generator should return true'); $this->assertNull($this->cache->get('key0'), 'Values must be deleted on deleteMultiple()'); $this->assertNull($this->cache->get('key1'), 'Values must be deleted on deleteMultiple()'); } public function testHas() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertFalse($this->cache->has('key0')); $this->cache->set('key0', 'value0'); $this->assertTrue($this->cache->has('key0')); } public function testBasicUsageWithLongKey() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $key = str_repeat('a', 300); $this->assertFalse($this->cache->has($key)); $this->assertTrue($this->cache->set($key, 'value')); $this->assertTrue($this->cache->has($key)); $this->assertSame('value', $this->cache->get($key)); $this->assertTrue($this->cache->delete($key)); $this->assertFalse($this->cache->has($key)); } #[DataProvider('invalidKeys')] public function testGetInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->expectException('Psr\SimpleCache\InvalidArgumentException'); $this->cache->get($key); } #[DataProvider('invalidKeyTypes')] public function testGetInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'get'], $key) ); } #[DataProvider('invalidKeys')] public function testGetMultipleInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->expectException('Psr\SimpleCache\InvalidArgumentException'); $this->cache->getMultiple(['key1', $key, 'key2']); } #[DataProvider('invalidKeyTypes')] public function testGetMultipleInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'getMultiple'], ['key1', $key, 'key2']) ); } #[DataProvider('nonIterableValues')] public function testGetMultipleNonIterable(mixed $keys) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'getMultiple'], $keys) ); } #[DataProvider('invalidKeys')] public function testSetInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->expectException('Psr\SimpleCache\InvalidArgumentException'); $this->cache->set($key, 'foobar'); } #[DataProvider('invalidKeyTypes')] public function testSetInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'set'], $key, 'foobar') ); } #[DataProvider('invalidArrayKeys')] public function testSetMultipleInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $values = function () use ($key) { yield 'key1' => 'foo'; yield $key => 'bar'; yield 'key2' => 'baz'; }; $this->expectException('Psr\SimpleCache\InvalidArgumentException'); $this->cache->setMultiple($values()); } #[DataProvider('invalidSetMultipleKeyTypes')] public function testSetMultipleInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $values = static function () use ($key): \Generator { yield 'key1' => 'foo'; yield $key => 'bar'; yield 'key2' => 'baz'; }; $this->assertCacheExceptionOrTypeError(fn (): bool => $this->cache->setMultiple($values())); } #[DataProvider('nonIterableValues')] public function testSetMultipleNonIterable(mixed $values) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'setMultiple'], $values) ); } #[DataProvider('invalidKeys')] public function testHasInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->expectException('Psr\SimpleCache\InvalidArgumentException'); $this->cache->has($key); } #[DataProvider('invalidKeyTypes')] public function testHasInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'has'], $key) ); } #[DataProvider('invalidKeys')] public function testDeleteInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->expectException('Psr\SimpleCache\InvalidArgumentException'); $this->cache->delete($key); } #[DataProvider('invalidKeyTypes')] public function testDeleteInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'delete'], $key) ); } #[DataProvider('invalidKeys')] public function testDeleteMultipleInvalidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->expectException('Psr\SimpleCache\InvalidArgumentException'); $this->cache->deleteMultiple(['key1', $key, 'key2']); } #[DataProvider('invalidKeyTypes')] public function testDeleteMultipleInvalidKeyTypes(mixed $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'deleteMultiple'], ['key1', $key, 'key2']) ); } #[DataProvider('nonIterableValues')] public function testDeleteMultipleNonIterable(mixed $keys) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'deleteMultiple'], $keys) ); } #[DataProvider('invalidTtlValues')] public function testSetInvalidTtl(mixed $ttl) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'set'], 'key', 'value', $ttl) ); } #[DataProvider('invalidTtlValues')] public function testSetMultipleInvalidTtl(mixed $ttl) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->assertCacheExceptionOrTypeError( fn (): mixed => $this->callWithInvalidArguments([$this->cache, 'setMultiple'], ['key' => 'value'], $ttl) ); } public function testNullOverwrite() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->set('key', 5); $this->cache->set('key', null); $this->assertNull($this->cache->get('key'), 'Setting null to a key must overwrite previous value'); } public function testDataTypeString() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->set('key', '5'); $result = $this->cache->get('key'); $this->assertTrue('5' === $result, 'Wrong data type. If we store a string we must get an string back.'); } public function testDataTypeInteger() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->set('key', 5); $result = $this->cache->get('key'); $this->assertTrue(5 === $result, 'Wrong data type. If we store an int we must get an int back.'); } public function testDataTypeFloat() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $float = 1.23456789; $this->cache->set('key', $float); $result = $this->cache->get('key'); $this->assertTrue(\is_float($result), 'Wrong data type. If we store float we must get an float back.'); $this->assertEquals($float, $result); } public function testDataTypeBoolean() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->set('key', false); $result = $this->cache->get('key'); $this->assertTrue(\is_bool($result), 'Wrong data type. If we store boolean we must get an boolean back.'); $this->assertFalse($result); $this->assertTrue($this->cache->has('key'), 'has() should return true when true are stored. '); } public function testDataTypeArray() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $array = ['a' => 'foo', 2 => 'bar']; $this->cache->set('key', $array); $result = $this->cache->get('key'); $this->assertTrue(\is_array($result), 'Wrong data type. If we store array we must get an array back.'); $this->assertEquals($array, $result); } public function testDataTypeObject() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $object = new \stdClass(); $object->a = 'foo'; $this->cache->set('key', $object); $result = $this->cache->get('key'); $this->assertTrue(\is_object($result), 'Wrong data type. If we store object we must get an object back.'); $this->assertEquals($object, $result); } public function testBinaryData() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $data = ''; for ($i = 0; $i < 256; ++$i) { $data .= \chr($i); } $array = ['a' => 'foo', 2 => 'bar']; $this->cache->set('key', $data); $result = $this->cache->get('key'); $this->assertTrue($data === $result, 'Binary data must survive a round trip.'); } #[DataProvider('validKeys')] public function testSetValidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->set($key, 'foobar'); $this->assertEquals('foobar', $this->cache->get($key)); } #[DataProvider('validKeys')] public function testSetMultipleValidKeys(string $key) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->setMultiple([$key => 'foobar']); $result = $this->cache->getMultiple([$key]); $keys = []; foreach ($result as $i => $r) { $keys[] = $i; $this->assertEquals($key, $i); $this->assertEquals('foobar', $r); } $this->assertSame([$key], $keys); } #[DataProvider('validData')] public function testSetValidData(mixed $data) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->set('key', $data); $this->assertEquals($data, $this->cache->get('key')); } #[DataProvider('validData')] public function testSetMultipleValidData(mixed $data) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->setMultiple(['key' => $data]); $result = $this->cache->getMultiple(['key']); $keys = []; foreach ($result as $i => $r) { $keys[] = $i; $this->assertEquals($data, $r); } $this->assertSame(['key'], $keys); } public function testObjectAsDefaultValue() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $obj = new \stdClass(); $obj->foo = 'value'; $this->assertEquals($obj, $this->cache->get('key', $obj)); } public function testObjectDoesNotChangeInCache() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $obj = new \stdClass(); $obj->foo = 'value'; $this->cache->set('key', $obj); $obj->foo = 'changed'; $cacheObject = $this->cache->get('key'); $this->assertInstanceOf(\stdClass::class, $cacheObject); $this->assertEquals('value', $cacheObject->foo, 'Object in cache should not have their values changed.'); } } integration-tests-1.0.5/src/TaggableCachePoolTest.php000066400000000000000000000365451524145752400226520ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests; use Cache\TagInterop\TaggableCacheItemPoolInterface; use PHPUnit\Framework\Attributes\After; use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; /** * @author Tobias Nyholm */ abstract class TaggableCachePoolTest extends TestCase { /** @var array */ protected array $skippedTests = []; protected TaggableCacheItemPoolInterface $cache; /** * @return TaggableCacheItemPoolInterface that is used in the tests */ abstract public function createCachePool(): TaggableCacheItemPoolInterface; #[Before] public function setupService() { $this->cache = $this->createCachePool(); } #[After] public function tearDownService() { if (isset($this->cache)) { $this->cache->clear(); } } /** @return list */ public static function invalidTags(): array { return array_merge( CachePoolTest::invalidKeys(), [ [true], [false], [null], [2], [2.5], [new \stdClass()], [['array']], ] ); } public function testMultipleTags() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->save($this->cache->getItem('key1')->set('value')->setTags(['tag1', 'tag2'])); $this->cache->save($this->cache->getItem('key2')->set('value')->setTags(['tag1', 'tag3'])); $this->cache->save($this->cache->getItem('key3')->set('value')->setTags(['tag2', 'tag3'])); $this->cache->save($this->cache->getItem('key4')->set('value')->setTags(['tag4', 'tag3'])); $this->cache->invalidateTags(['tag1']); $this->assertFalse($this->cache->hasItem('key1')); $this->assertFalse($this->cache->hasItem('key2')); $this->assertTrue($this->cache->hasItem('key3')); $this->assertTrue($this->cache->hasItem('key4')); $this->cache->invalidateTags(['tag2']); $this->assertFalse($this->cache->hasItem('key1')); $this->assertFalse($this->cache->hasItem('key2')); $this->assertFalse($this->cache->hasItem('key3')); $this->assertTrue($this->cache->hasItem('key4')); } public function testPreviousTag() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $tags = $item->getPreviousTags(); $this->assertCount(0, $tags); $item->setTags(['tag0']); $this->assertCount(0, $item->getPreviousTags()); $this->cache->save($item); $this->assertCount(0, $item->getPreviousTags()); $item = $this->cache->getItem('key'); $this->assertCount(1, $item->getPreviousTags()); } public function testPreviousTagDeferred() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $item->setTags(['tag0']); $this->assertCount(0, $item->getPreviousTags()); $this->cache->saveDeferred($item); $this->assertCount(0, $item->getPreviousTags()); $item = $this->cache->getItem('key'); $this->assertCount(1, $item->getPreviousTags()); } public function testPreviousTagsPreserveIdentityMap() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $item->setTags(['tag1', 'tag2']); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertSame(['tag1' => 'tag1', 'tag2' => 'tag2'], $item->getPreviousTags()); } public function testTagAccessorWithEmptyTag() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $this->expectException('Psr\Cache\InvalidArgumentException'); $item->setTags(['']); } #[DataProvider('invalidTags')] public function testTagAccessorWithInvalidTag(mixed $tag) { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $this->expectException('Psr\Cache\InvalidArgumentException'); (new \ReflectionMethod($item, 'setTags'))->invoke($item, [$tag]); } public function testTagAccessorDuplicateTags() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $item->setTags(['tag', 'tag', 'tag']); $this->cache->save($item); $item = $this->cache->getItem('key'); $this->assertCount(1, $item->getPreviousTags()); } public function testNumericStringTagsRemainUsable() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } foreach (['0', '123', '-1'] as $tag) { $key = 'key_'.str_replace('-', 'minus_', $tag); $this->assertTrue($this->cache->save($this->cache->getItem($key)->set('value')->setTags([$tag]))); $this->assertTrue($this->cache->hasItem($key)); $this->assertTrue($this->cache->invalidateTag($tag)); $this->assertFalse($this->cache->hasItem($key)); } } public function testTagNamesOutsidePortableKeyAlphabetRemainUsable() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $tag = 'tag with spaces![]?'; self::assertTrue($this->cache->save($this->cache->getItem('key_with_special_tag')->set('value')->setTags([$tag]))); self::assertTrue($this->cache->hasItem('key_with_special_tag')); self::assertTrue($this->cache->invalidateTag($tag)); self::assertFalse($this->cache->hasItem('key_with_special_tag')); } /** * The tag must be removed whenever we remove an item. If not, when creating a new item * with the same key will get the same tags. */ public function testRemoveTagWhenItemIsRemoved() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $item->setTags(['tag1']); // Save the item and then delete it $this->cache->save($item); $this->cache->deleteItem('key'); // Create a new item (same key) (no tags) $item = $this->cache->getItem('key')->set('value'); $this->cache->save($item); // Clear the tag, The new item should not be cleared $this->cache->invalidateTags(['tag1']); $this->assertTrue($this->cache->hasItem('key'), 'Item key should be removed from the tag list when the item is removed'); } public function testClearPool() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $item->setTags(['tag1']); $this->cache->save($item); // Clear the pool $this->cache->clear(); // Create a new item (no tags) $item = $this->cache->getItem('key')->set('value'); $this->cache->save($item); $this->cache->invalidateTags(['tag1']); $this->assertTrue($this->cache->hasItem('key'), 'Tags should be removed when the pool was cleared.'); } public function testInvalidateTag() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $item->setTags(['tag1', 'tag2']); $this->cache->save($item); $item = $this->cache->getItem('key2')->set('value'); $item->setTags(['tag1']); $this->cache->save($item); $this->cache->invalidateTag('tag2'); $this->assertFalse($this->cache->hasItem('key'), 'Item should be cleared when tag is invalidated'); $this->assertTrue($this->cache->hasItem('key2'), 'Item should be cleared when tag is invalidated'); // Create a new item (no tags) $item = $this->cache->getItem('key')->set('value'); $this->cache->save($item); $this->cache->invalidateTags(['tag2']); $this->assertTrue($this->cache->hasItem('key'), 'Item key list should be removed when clearing the tags'); $this->cache->invalidateTags(['tag1']); $this->assertTrue($this->cache->hasItem('key'), 'Item key list should be removed when clearing the tags'); } public function testInvalidateTags() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $item->setTags(['tag1', 'tag2']); $this->cache->save($item); $item = $this->cache->getItem('key2')->set('value'); $item->setTags(['tag1']); $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'); // Create a new item (no tags) $item = $this->cache->getItem('key')->set('value'); $this->cache->save($item); $this->cache->invalidateTags(['tag1']); $this->assertTrue($this->cache->hasItem('key'), 'Item k list should be removed when clearing the tags'); } public function testInvalidateTagsValidatesEveryTagBeforeMutation() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } self::assertTrue($this->cache->save($this->cache->getItem('key')->set('value')->setTags(['valid']))); try { $this->cache->invalidateTags(['valid', '']); self::fail('invalidateTags accepted an invalid tag'); } catch (\Psr\Cache\InvalidArgumentException) { } self::assertTrue($this->cache->hasItem('key')); } public function testInvalidateTagRemovesDeferredItem() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } self::assertTrue($this->cache->saveDeferred($this->cache->getItem('key')->set('value')->setTags(['tag']))); self::assertTrue($this->cache->invalidateTag('tag')); self::assertFalse($this->cache->hasItem('key')); } /** * When an item is overwritten we need to clear tags for original item. */ public function testTagsAreCleanedOnSave() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $pool = $this->cache; $i = $pool->getItem('key')->set('value'); $pool->save($i->setTags(['foo'])); $i = $pool->getItem('key'); $pool->save($i->setTags(['bar'])); $pool->invalidateTags(['foo']); $this->assertTrue($pool->getItem('key')->isHit()); } public function testTagsAreCleanedWhenSameItemIsSavedAgain() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $this->cache->save($item->setTags(['foo'])); $this->cache->save($item->setTags(['bar'])); $this->cache->invalidateTag('foo'); $this->assertTrue($this->cache->getItem('key')->isHit()); $this->cache->invalidateTag('bar'); $this->assertFalse($this->cache->getItem('key')->isHit()); } public function testExpiredSaveDoesNotLeaveTagIndex() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $expired = $this->cache->getItem('key')->set('expired')->setTags(['foo']); $expired->expiresAfter(-1); $this->cache->save($expired); $this->cache->save($this->cache->getItem('key')->set('replacement')); $this->cache->invalidateTag('foo'); $this->assertTrue($this->cache->getItem('key')->isHit()); } public function testTagsAreCleanedWhenSameItemIsSavedDeferredAgain() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $item = $this->cache->getItem('key')->set('value'); $this->cache->saveDeferred($item->setTags(['foo'])); $this->cache->commit(); $this->cache->saveDeferred($item->setTags(['bar'])); $this->cache->commit(); $this->cache->invalidateTag('foo'); $this->assertTrue($this->cache->getItem('key')->isHit()); $this->cache->invalidateTag('bar'); $this->assertFalse($this->cache->getItem('key')->isHit()); } public function testExpiredDeferredSaveDoesNotLeaveTagIndex() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $expired = $this->cache->getItem('key')->set('expired')->setTags(['foo']); $expired->expiresAfter(-1); $this->cache->saveDeferred($expired); $this->cache->commit(); $this->cache->save($this->cache->getItem('key')->set('replacement')); $this->cache->invalidateTag('foo'); $this->assertTrue($this->cache->getItem('key')->isHit()); } public function testImmediateSaveOverridesDeferredTags() { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); } $this->cache->save($this->cache->getItem('key')->set('first')->setTags(['foo'])); $this->cache->saveDeferred($this->cache->getItem('key')->set('deferred')->setTags(['bar'])); $this->cache->save($this->cache->getItem('key')->set('immediate')->setTags(['baz'])); $this->cache->invalidateTag('foo'); $this->cache->invalidateTag('bar'); $this->assertSame('immediate', $this->cache->getItem('key')->get()); $this->cache->commit(); $this->assertSame('immediate', $this->cache->getItem('key')->get()); $this->cache->invalidateTag('baz'); $this->assertFalse($this->cache->getItem('key')->isHit()); } } integration-tests-1.0.5/tests/000077500000000000000000000000001524145752400163535ustar00rootroot00000000000000integration-tests-1.0.5/tests/CachePoolTest.php000066400000000000000000000045011524145752400215610ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests\Tests; use Cache\IntegrationTests\CachePoolTest as CachePoolContract; use Cache\IntegrationTests\HierarchicalCachePoolTest as HierarchicalCachePoolContract; use Cache\IntegrationTests\SimpleCacheTest as SimpleCacheContract; use Cache\IntegrationTests\TaggableCachePoolTest as TaggableCachePoolContract; use Cache\IntegrationTests\Tests\Fixtures\ValidatingCachePool; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; final class CachePoolTest extends CachePoolContract { private ?string $namespace = null; public function createCachePool(): CacheItemPoolInterface { $this->namespace ??= bin2hex(random_bytes(16)); return new ValidatingCachePool(new FilesystemAdapter($this->namespace)); } public function testDataProvidersExposeCases() { self::assertNotEmpty(self::invalidKeys()); self::assertNotEmpty(self::invalidKeyTypes()); } public function testExtensionPointsRemainCompatibleWithUntypedOverrides() { $hooks = ['advanceTime', 'setupService', 'tearDownService']; $contracts = [ CachePoolContract::class, HierarchicalCachePoolContract::class, SimpleCacheContract::class, TaggableCachePoolContract::class, ]; foreach ($contracts as $contract) { $reflection = new \ReflectionClass($contract); foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { if ($method->getDeclaringClass()->getName() !== $contract) { continue; } if (!str_starts_with($method->getName(), 'test') && !\in_array($method->getName(), $hooks, true)) { continue; } self::assertNull( $method->getReturnType(), \sprintf('%s::%s() must remain compatible with untyped consumer overrides.', $contract, $method->getName()) ); } } } } integration-tests-1.0.5/tests/Fixtures/000077500000000000000000000000001524145752400201645ustar00rootroot00000000000000integration-tests-1.0.5/tests/Fixtures/HierarchicalCachePool.php000066400000000000000000000054711524145752400250400ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests\Tests\Fixtures; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; final class HierarchicalCachePool implements CacheItemPoolInterface { private readonly CacheItemPoolInterface $pool; /** @var array */ private array $keys = []; public function __construct() { $this->pool = new ArrayAdapter(); } public function getItem(string $key): CacheItemInterface { return $this->pool->getItem($key); } /** @return iterable */ public function getItems(array $keys = []): iterable { return $this->pool->getItems($keys); } public function hasItem(string $key): bool { return $this->pool->hasItem($key); } public function clear(): bool { $this->keys = []; return $this->pool->clear(); } public function deleteItem(string $key): bool { $key = ValidatingCachePool::validateKey($key); if (!str_starts_with($key, '|')) { unset($this->keys[$key]); return $this->pool->deleteItem($key); } $keys = array_values(array_filter( array_keys($this->keys), static fn (string $storedKey): bool => '|' === $key ? str_starts_with($storedKey, '|') : $storedKey === $key || str_starts_with($storedKey, $key.'|'), )); foreach ($keys as $storedKey) { unset($this->keys[$storedKey]); } return [] === $keys ? $this->pool->deleteItem($key) : $this->pool->deleteItems($keys); } public function deleteItems(array $keys): bool { $success = true; foreach ($keys as $key) { $success = $this->deleteItem($key) && $success; } return $success; } public function save(CacheItemInterface $item): bool { $saved = $this->pool->save($item); if ($saved) { $key = ValidatingCachePool::validateKey($item->getKey()); $this->keys[$key] = true; } return $saved; } public function saveDeferred(CacheItemInterface $item): bool { $saved = $this->pool->saveDeferred($item); if ($saved) { $key = ValidatingCachePool::validateKey($item->getKey()); $this->keys[$key] = true; } return $saved; } public function commit(): bool { return $this->pool->commit(); } } integration-tests-1.0.5/tests/Fixtures/TaggableCacheItem.php000066400000000000000000000040461524145752400241520ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests\Tests\Fixtures; use Cache\TagInterop\TaggableCacheItemInterface; use Psr\Cache\CacheItemInterface; final class TaggableCacheItem implements TaggableCacheItemInterface { /** @var array */ private array $tags; /** * @param array $previousTags */ public function __construct( private readonly CacheItemInterface $item, private readonly array $previousTags, ) { $this->tags = $previousTags; } public function getKey(): string { return $this->item->getKey(); } public function get(): mixed { return $this->item->get(); } public function isHit(): bool { return $this->item->isHit(); } public function set(mixed $value): static { $this->item->set($value); return $this; } public function expiresAt(?\DateTimeInterface $expiration): static { $this->item->expiresAt($expiration); return $this; } public function expiresAfter(\DateInterval|int|null $time): static { $this->item->expiresAfter($time); return $this; } public function getPreviousTags(): array { return $this->previousTags; } public function setTags(array $tags): static { $validatedTags = []; foreach ($tags as $tag) { $tag = ValidatingCachePool::validateKey($tag); $validatedTags[$tag] = $tag; } $this->tags = $validatedTags; return $this; } public function getInnerItem(): CacheItemInterface { return $this->item; } /** @return array */ public function getTags(): array { return $this->tags; } } integration-tests-1.0.5/tests/Fixtures/TaggableCachePool.php000066400000000000000000000073021524145752400241630ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests\Tests\Fixtures; use Cache\TagInterop\TaggableCacheItemInterface; use Cache\TagInterop\TaggableCacheItemPoolInterface; use Psr\Cache\CacheItemInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Exception\InvalidArgumentException; final class TaggableCachePool implements TaggableCacheItemPoolInterface { private readonly ArrayAdapter $pool; /** @var array> */ private array $tagsByKey = []; public function __construct() { $this->pool = new ArrayAdapter(); } public function getItem(string $key): TaggableCacheItemInterface { $item = $this->pool->getItem($key); $previousTags = $item->isHit() ? ($this->tagsByKey[$key] ?? []) : []; return new TaggableCacheItem($item, $previousTags); } /** @return iterable */ public function getItems(array $keys = []): iterable { foreach ($keys as $key) { $item = $this->getItem($key); yield $item->getKey() => $item; } } public function hasItem(string $key): bool { return $this->pool->hasItem($key); } public function clear(): bool { $this->tagsByKey = []; return $this->pool->clear(); } public function deleteItem(string $key): bool { unset($this->tagsByKey[$key]); return $this->pool->deleteItem($key); } public function deleteItems(array $keys): bool { foreach ($keys as $key) { unset($this->tagsByKey[$key]); } return $this->pool->deleteItems($keys); } public function save(CacheItemInterface $item): bool { $item = $this->requireTaggableItem($item); $saved = $this->pool->save($item->getInnerItem()); $this->syncTags($item); return $saved; } public function saveDeferred(CacheItemInterface $item): bool { $item = $this->requireTaggableItem($item); $saved = $this->pool->saveDeferred($item->getInnerItem()); if ($saved) { $this->tagsByKey[$item->getKey()] = $item->getTags(); } return $saved; } public function commit(): bool { return $this->pool->commit(); } public function invalidateTag(string $tag): bool { return $this->invalidateTags([$tag]); } public function invalidateTags(array $tags): bool { $validatedTags = []; foreach ($tags as $tag) { $tag = ValidatingCachePool::validateKey($tag); $validatedTags[$tag] = true; } $keys = []; foreach ($this->tagsByKey as $key => $itemTags) { if ([] !== array_intersect_key($itemTags, $validatedTags)) { $keys[] = $key; } } return $this->deleteItems($keys); } private function requireTaggableItem(CacheItemInterface $item): TaggableCacheItem { if (!$item instanceof TaggableCacheItem) { throw new InvalidArgumentException('Items must come from this cache pool.'); } return $item; } private function syncTags(TaggableCacheItem $item): void { if ($this->pool->hasItem($item->getKey())) { $this->tagsByKey[$item->getKey()] = $item->getTags(); return; } unset($this->tagsByKey[$item->getKey()]); } } integration-tests-1.0.5/tests/Fixtures/ValidatingCachePool.php000066400000000000000000000045111524145752400245360ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests\Tests\Fixtures; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Exception\InvalidArgumentException; final class ValidatingCachePool implements CacheItemPoolInterface { public function __construct(private readonly CacheItemPoolInterface $pool) { } public function getItem(string $key): CacheItemInterface { self::validateKey($key); return $this->pool->getItem($key); } /** @return iterable */ public function getItems(array $keys = []): iterable { self::validateKeys($keys); return $this->pool->getItems($keys); } public function hasItem(string $key): bool { self::validateKey($key); return $this->pool->hasItem($key); } public function clear(): bool { return $this->pool->clear(); } public function deleteItem(string $key): bool { self::validateKey($key); return $this->pool->deleteItem($key); } public function deleteItems(array $keys): bool { self::validateKeys($keys); return $this->pool->deleteItems($keys); } public function save(CacheItemInterface $item): bool { return $this->pool->save($item); } public function saveDeferred(CacheItemInterface $item): bool { return $this->pool->saveDeferred($item); } public function commit(): bool { return $this->pool->commit(); } /** @return non-empty-string */ public static function validateKey(mixed $key): string { if (!\is_string($key) || '' === $key || 1 === preg_match('~[{}()/\\\\@:]~', $key)) { throw new InvalidArgumentException('Cache keys must be non-empty strings without reserved characters.'); } return $key; } /** @param array $keys */ private static function validateKeys(array $keys): void { foreach ($keys as $key) { self::validateKey($key); } } } integration-tests-1.0.5/tests/Fixtures/ValidatingSimpleCache.php000066400000000000000000000064631524145752400250660ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests\Tests\Fixtures; use Psr\SimpleCache\CacheInterface; final class ValidatingSimpleCache implements CacheInterface { public function __construct(private readonly CacheInterface $cache) { } public function get(string $key, mixed $default = null): mixed { ValidatingCachePool::validateKey($key); return $this->cache->get($key, $default); } public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool { ValidatingCachePool::validateKey($key); return $this->cache->set($key, $value, $ttl); } public function delete(string $key): bool { ValidatingCachePool::validateKey($key); return $this->cache->delete($key); } public function clear(): bool { return $this->cache->clear(); } /** * @param iterable $keys * * @return iterable */ public function getMultiple(iterable $keys, mixed $default = null): iterable { $validatedKeys = iterator_to_array($this->validateKeys($keys), false); return $this->restoreKeys($this->cache->getMultiple($validatedKeys, $default), $validatedKeys, $default); } /** @param iterable $values */ public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool { return $this->cache->setMultiple($this->validateValues($values), $ttl); } /** @param iterable $keys */ public function deleteMultiple(iterable $keys): bool { return $this->cache->deleteMultiple($this->validateKeys($keys)); } public function has(string $key): bool { ValidatingCachePool::validateKey($key); return $this->cache->has($key); } /** * @param iterable $keys * * @return iterable */ private function validateKeys(iterable $keys): iterable { foreach ($keys as $key) { yield ValidatingCachePool::validateKey($key); } } /** * @param iterable $values * * @return iterable */ private function validateValues(iterable $values): iterable { foreach ($values as $key => $value) { $validatedKey = \is_int($key) ? (string) $key : ValidatingCachePool::validateKey($key); yield $validatedKey => $value; } } /** * @param iterable $values * @param list $keys * * @return iterable */ private function restoreKeys(iterable $values, array $keys, mixed $default): iterable { $valuesByKey = []; foreach ($values as $key => $value) { $valuesByKey["\0".(string) $key] = $value; } foreach ($keys as $key) { $lookupKey = "\0".$key; yield $key => \array_key_exists($lookupKey, $valuesByKey) ? $valuesByKey[$lookupKey] : $default; } } } integration-tests-1.0.5/tests/HierarchicalCachePoolTest.php000066400000000000000000000013241524145752400240600ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests\Tests; use Cache\IntegrationTests\HierarchicalCachePoolTest as HierarchicalCachePoolContract; use Cache\IntegrationTests\Tests\Fixtures\HierarchicalCachePool; use Psr\Cache\CacheItemPoolInterface; final class HierarchicalCachePoolTest extends HierarchicalCachePoolContract { public function createCachePool(): CacheItemPoolInterface { return new HierarchicalCachePool(); } } integration-tests-1.0.5/tests/SimpleCacheTest.php000066400000000000000000000030021524145752400220740ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests\Tests; use Cache\IntegrationTests\SimpleCacheTest as SimpleCacheContract; use Cache\IntegrationTests\Tests\Fixtures\ValidatingCachePool; use Cache\IntegrationTests\Tests\Fixtures\ValidatingSimpleCache; use Psr\SimpleCache\CacheInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Psr16Cache; final class SimpleCacheTest extends SimpleCacheContract { private ?string $namespace = null; public function createSimpleCache(): CacheInterface { $this->namespace ??= bin2hex(random_bytes(16)); $pool = new ValidatingCachePool(new FilesystemAdapter($this->namespace)); return new ValidatingSimpleCache(new Psr16Cache($pool)); } public function testDataProvidersExposeCases() { self::assertNotEmpty(self::invalidKeys()); self::assertNotEmpty(self::invalidArrayKeys()); self::assertNotEmpty(self::invalidKeyTypes()); self::assertNotEmpty(self::nonIterableValues()); self::assertNotEmpty(self::invalidTtlValues()); self::assertNotEmpty(self::invalidSetMultipleKeyTypes()); self::assertNotEmpty(self::validKeys()); self::assertNotEmpty(self::validData()); } } integration-tests-1.0.5/tests/TaggableCachePoolTest.php000066400000000000000000000032731524145752400232150ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace Cache\IntegrationTests\Tests; use Cache\IntegrationTests\TaggableCachePoolTest as TaggableCachePoolContract; use Cache\IntegrationTests\Tests\Fixtures\TaggableCachePool; use Cache\TagInterop\TaggableCacheItemPoolInterface; use PHPUnit\Framework\SkippedTest; final class TaggableCachePoolTest extends TaggableCachePoolContract { public function createCachePool(): TaggableCacheItemPoolInterface { return new TaggableCachePool(); } public function testDataProviderExposesCases() { self::assertNotEmpty(self::invalidTags()); } public function testSkippedTestsMapStopsConfiguredTests() { $tested = 0; foreach (get_class_methods(TaggableCachePoolContract::class) as $method) { if (!str_starts_with($method, 'test') || (new \ReflectionMethod(TaggableCachePoolContract::class, $method))->getNumberOfRequiredParameters() > 0) { continue; } $this->skippedTests[$method] = 'expected skip'; try { $this->{$method}(); self::fail(\sprintf('%s did not honor the skipped tests map.', $method)); } catch (SkippedTest $exception) { self::assertSame('expected skip', $exception->getMessage()); } unset($this->skippedTests[$method]); ++$tested; } self::assertGreaterThan(0, $tested); } } integration-tests-1.0.5/tools/000077500000000000000000000000001524145752400163515ustar00rootroot00000000000000integration-tests-1.0.5/tools/check-coverage.php000066400000000000000000000030721524145752400217320ustar00rootroot00000000000000, Tobias Nyholm * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ $arguments = $_SERVER['argv'] ?? []; if (!is_array($arguments) || !array_is_list($arguments) || 3 !== count($arguments) || !is_string($arguments[1]) || !is_string($arguments[2]) || !is_numeric($arguments[2])) { fwrite(\STDERR, "Usage: php tools/check-coverage.php \n"); exit(2); } $document = new DOMDocument(); if (!$document->load($arguments[1])) { fwrite(\STDERR, sprintf("Could not read coverage report %s.\n", $arguments[1])); exit(2); } $metricNodes = (new DOMXPath($document))->query('/coverage/project/metrics'); $metrics = false === $metricNodes ? null : $metricNodes->item(0); if (!$metrics instanceof DOMElement) { fwrite(\STDERR, "The coverage report does not contain project metrics.\n"); exit(2); } $statements = (int) $metrics->getAttribute('statements'); $coveredStatements = (int) $metrics->getAttribute('coveredstatements'); $minimum = (float) $arguments[2]; if (0 === $statements) { fwrite(\STDERR, "The coverage report does not contain executable statements.\n"); exit(2); } $percentage = 100 * $coveredStatements / $statements; printf( "Line coverage: %.2f%% (%d/%d); required: %.2f%%.\n", $percentage, $coveredStatements, $statements, $minimum, ); exit($percentage >= $minimum ? 0 : 1);