fastCache = new GeneralCache($fastDriver, $serializer, $compression); // Slow cache: Another in-memory cache to simulate slower storage $slowDriver = new InMemoryCache(); $this->slowCache = new GeneralCache($slowDriver, $serializer, $compression); // Multi-level cache combining both $this->cache = new MultiLevelCache($this->fastCache, $this->slowCache); }); describe('Basic Operations', function () { it('sets items in both fast and slow cache for small values', function () { $key = CacheKey::fromString('small-value'); $value = 'Small text'; $success = $this->cache->set(CacheItem::forSet($key, $value)); expect($success)->toBeTrue(); // Verify in fast cache $fastResult = $this->fastCache->get($key); expect($fastResult->isHit)->toBeTrue(); expect($fastResult->value)->toBe('Small text'); // Verify in slow cache $slowResult = $this->slowCache->get($key); expect($slowResult->isHit)->toBeTrue(); expect($slowResult->value)->toBe('Small text'); }); it('sets large values only in slow cache', function () { $key = CacheKey::fromString('large-value'); // Create a large value (> 1KB) $value = str_repeat('Large data content ', 100); // > 1KB $success = $this->cache->set(CacheItem::forSet($key, $value)); expect($success)->toBeTrue(); // Should NOT be in fast cache (too large) $fastResult = $this->fastCache->get($key); expect($fastResult->isHit)->toBeFalse(); // Should be in slow cache $slowResult = $this->slowCache->get($key); expect($slowResult->isHit)->toBeTrue(); }); it('retrieves from fast cache when available', function () { $key = CacheKey::fromString('fast-key'); // Set directly in fast cache $this->fastCache->set(CacheItem::forSet($key, 'Fast value')); $result = $this->cache->get($key); expect($result->isHit)->toBeTrue(); expect($result->value)->toBe('Fast value'); }); it('falls back to slow cache on fast cache miss', function () { $key = CacheKey::fromString('slow-key'); // Set only in slow cache $this->slowCache->set(CacheItem::forSet($key, 'Slow value')); $result = $this->cache->get($key); expect($result->isHit)->toBeTrue(); expect($result->value)->toBe('Slow value'); }); it('warms up fast cache when fetching from slow cache', function () { $key = CacheKey::fromString('warmup-key'); $value = 'Small value for warmup'; // Set only in slow cache $this->slowCache->set(CacheItem::forSet($key, $value)); // Fast cache should be empty initially expect($this->fastCache->get($key)->isHit)->toBeFalse(); // Get from multi-level cache (will warm up fast cache) $result = $this->cache->get($key); expect($result->isHit)->toBeTrue(); // Now fast cache should have it $fastResult = $this->fastCache->get($key); expect($fastResult->isHit)->toBeTrue(); expect($fastResult->value)->toBe($value); }); }); describe('Multi-Key Operations', function () { it('gets multiple keys from mixed sources', function () { $key1 = CacheKey::fromString('fast-1'); $key2 = CacheKey::fromString('slow-1'); $key3 = CacheKey::fromString('missing-1'); // key1 in fast cache $this->fastCache->set(CacheItem::forSet($key1, 'Value from fast')); // key2 only in slow cache $this->slowCache->set(CacheItem::forSet($key2, 'Value from slow')); // key3 nowhere $result = $this->cache->get($key1, $key2, $key3); expect($result->count())->toBe(3); expect($result->getValue($key1))->toBe('Value from fast'); expect($result->getValue($key2))->toBe('Value from slow'); expect($result->getItem($key3)->isHit)->toBeFalse(); }); it('sets multiple items with appropriate cache levels', function () { $item1 = CacheItem::forSet(CacheKey::fromString('multi-1'), 'Small'); $item2 = CacheItem::forSet(CacheKey::fromString('multi-2'), 'Another small'); $success = $this->cache->set($item1, $item2); expect($success)->toBeTrue(); // Both should be in both caches (small values) expect($this->fastCache->get(CacheKey::fromString('multi-1'))->isHit)->toBeTrue(); expect($this->fastCache->get(CacheKey::fromString('multi-2'))->isHit)->toBeTrue(); expect($this->slowCache->get(CacheKey::fromString('multi-1'))->isHit)->toBeTrue(); expect($this->slowCache->get(CacheKey::fromString('multi-2'))->isHit)->toBeTrue(); }); }); describe('Forget Operations', function () { it('forgets items from both caches', function () { $key = CacheKey::fromString('forget-me'); // Set in multi-level cache $this->cache->set(CacheItem::forSet($key, 'Temporary value')); // Verify it exists expect($this->cache->get($key)->isHit)->toBeTrue(); // Forget $success = $this->cache->forget($key); expect($success)->toBeTrue(); expect($this->fastCache->get($key)->isHit)->toBeFalse(); expect($this->slowCache->get($key)->isHit)->toBeFalse(); expect($this->cache->get($key)->isHit)->toBeFalse(); }); it('forgets multiple keys from both caches', function () { $key1 = CacheKey::fromString('forget-1'); $key2 = CacheKey::fromString('forget-2'); $this->cache->set( CacheItem::forSet($key1, 'Value 1'), CacheItem::forSet($key2, 'Value 2') ); $this->cache->forget($key1, $key2); expect($this->cache->get($key1)->isHit)->toBeFalse(); expect($this->cache->get($key2)->isHit)->toBeFalse(); }); }); describe('Clear Operations', function () { it('clears both fast and slow caches', function () { $this->cache->set( CacheItem::forSet(CacheKey::fromString('clear-1'), 'Data 1'), CacheItem::forSet(CacheKey::fromString('clear-2'), 'Data 2') ); $success = $this->cache->clear(); expect($success)->toBeTrue(); expect($this->cache->get(CacheKey::fromString('clear-1'))->isHit)->toBeFalse(); expect($this->cache->get(CacheKey::fromString('clear-2'))->isHit)->toBeFalse(); expect($this->fastCache->get(CacheKey::fromString('clear-1'))->isHit)->toBeFalse(); expect($this->slowCache->get(CacheKey::fromString('clear-1'))->isHit)->toBeFalse(); }); }); describe('Has Operations', function () { it('checks existence in fast cache first', function () { $key = CacheKey::fromString('has-fast'); $this->fastCache->set(CacheItem::forSet($key, 'Fast data')); $results = $this->cache->has($key); expect($results['has-fast'])->toBeTrue(); }); it('checks slow cache on fast cache miss', function () { $key = CacheKey::fromString('has-slow'); $this->slowCache->set(CacheItem::forSet($key, 'Slow data')); $results = $this->cache->has($key); expect($results['has-slow'])->toBeTrue(); }); it('warms up fast cache during has() check', function () { $key = CacheKey::fromString('has-warmup'); $value = 'Small value'; $this->slowCache->set(CacheItem::forSet($key, $value)); // Fast cache empty initially expect($this->fastCache->get($key)->isHit)->toBeFalse(); // Check existence (triggers warmup) $results = $this->cache->has($key); expect($results['has-warmup'])->toBeTrue(); // Fast cache should now have it expect($this->fastCache->get($key)->isHit)->toBeTrue(); }); it('returns false for non-existent keys', function () { $results = $this->cache->has(CacheKey::fromString('not-exists')); expect($results['not-exists'])->toBeFalse(); }); }); describe('Remember Pattern', function () { it('executes callback and caches result on miss', function () { $key = CacheKey::fromString('remember-key'); $callbackExecuted = false; $callback = function () use (&$callbackExecuted) { $callbackExecuted = true; return 'Computed value'; }; $result = $this->cache->remember($key, $callback); expect($callbackExecuted)->toBeTrue(); expect($result->value)->toBe('Computed value'); // Verify it's cached $cachedResult = $this->cache->get($key); expect($cachedResult->isHit)->toBeTrue(); expect($cachedResult->value)->toBe('Computed value'); }); it('returns cached value without executing callback on hit', function () { $key = CacheKey::fromString('remember-cached'); // Pre-populate cache $this->cache->set(CacheItem::forSet($key, 'Cached value')); $callbackExecuted = false; $callback = function () use (&$callbackExecuted) { $callbackExecuted = true; return 'Should not execute'; }; $result = $this->cache->remember($key, $callback); expect($callbackExecuted)->toBeFalse(); expect($result->value)->toBe('Cached value'); }); }); describe('Size-Based Cache Distribution', function () { it('caches primitives in both layers', function () { $key = CacheKey::fromString('primitive'); $this->cache->set(CacheItem::forSet($key, 42)); expect($this->fastCache->get($key)->isHit)->toBeTrue(); expect($this->slowCache->get($key)->isHit)->toBeTrue(); }); it('does not cache objects in fast layer', function () { $key = CacheKey::fromString('object'); $object = new stdClass(); $object->data = 'test'; $this->cache->set(CacheItem::forSet($key, $object)); // Object should only be in slow cache expect($this->fastCache->get($key)->isHit)->toBeFalse(); expect($this->slowCache->get($key)->isHit)->toBeTrue(); }); it('caches small arrays in both layers', function () { $key = CacheKey::fromString('small-array'); $value = ['a' => 1, 'b' => 2, 'c' => 3]; $this->cache->set(CacheItem::forSet($key, $value)); expect($this->fastCache->get($key)->isHit)->toBeTrue(); expect($this->slowCache->get($key)->isHit)->toBeTrue(); }); it('caches large arrays only in slow layer', function () { $key = CacheKey::fromString('large-array'); // Create large array (> 1KB estimated) $value = array_fill(0, 100, str_repeat('data', 10)); $this->cache->set(CacheItem::forSet($key, $value)); expect($this->fastCache->get($key)->isHit)->toBeFalse(); expect($this->slowCache->get($key)->isHit)->toBeTrue(); }); }); describe('TTL Management', function () { it('respects TTL in slow cache', function () { $key = CacheKey::fromString('ttl-test'); $item = CacheItem::forSet($key, 'TTL value', Duration::fromMinutes(10)); $this->cache->set($item); // Should be cached expect($this->slowCache->get($key)->isHit)->toBeTrue(); }); it('uses shorter TTL for fast cache', function () { $key = CacheKey::fromString('fast-ttl'); // Set with 10 minute TTL $item = CacheItem::forSet($key, 'Short value', Duration::fromMinutes(10)); $this->cache->set($item); // Fast cache should have shorter TTL (5 minutes max by default) // We can't directly verify TTL, but we verify it's cached expect($this->fastCache->get($key)->isHit)->toBeTrue(); }); }); describe('Edge Cases', function () { it('handles empty operations gracefully', function () { expect($this->cache->set())->toBeTrue(); expect($this->cache->forget())->toBeTrue(); expect($this->cache->has())->toBe([]); expect($this->cache->get())->toBeInstanceOf(CacheResult::class); }); it('handles null values correctly', function () { $key = CacheKey::fromString('null-value'); $this->cache->set(CacheItem::forSet($key, null)); $result = $this->cache->get($key); expect($result->isHit)->toBeTrue(); expect($result->value)->toBeNull(); }); it('handles false values correctly', function () { $key = CacheKey::fromString('false-value'); $this->cache->set(CacheItem::forSet($key, false)); $result = $this->cache->get($key); expect($result->isHit)->toBeTrue(); expect($result->value)->toBeFalse(); }); }); describe('Cache Layer Access', function () { it('provides access to fast cache layer', function () { $fastCache = $this->cache->getFastCache(); expect($fastCache)->toBeInstanceOf(GeneralCache::class); }); it('provides access to slow cache layer', function () { $slowCache = $this->cache->getSlowCache(); expect($slowCache)->toBeInstanceOf(GeneralCache::class); }); it('provides driver access through slow cache', function () { $driver = $this->cache->getDriver(); expect($driver)->toBeInstanceOf(InMemoryCache::class); }); }); });