Files
michaelschiemer/tests/Unit/Framework/Cache/MultiLevelCacheTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

397 lines
15 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheResult;
use App\Framework\Cache\Compression\NoCompression;
use App\Framework\Cache\Driver\InMemoryCache;
use App\Framework\Cache\GeneralCache;
use App\Framework\Cache\MultiLevelCache;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Serializer\Php\PhpSerializer;
describe('MultiLevelCache', function () {
beforeEach(function () {
// Fast cache: In-memory for quick access
$fastDriver = new InMemoryCache();
$serializer = new PhpSerializer();
$compression = new NoCompression();
$this->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);
});
});
});