Some checks failed
Deploy Application / deploy (push) Has been cancelled
1416 lines
53 KiB
PHP
1416 lines
53 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Framework\Discovery;
|
|
|
|
use Mockery;
|
|
use App\Framework\Cache\Cache;
|
|
use App\Framework\Cache\CacheItem;
|
|
use App\Framework\Cache\CacheKey;
|
|
use App\Framework\Cache\CachePrefix;
|
|
use App\Framework\Cache\CacheResult;
|
|
use App\Framework\Cache\Driver\InMemoryCache;
|
|
use App\Framework\Cache\GeneralCache;
|
|
use App\Framework\Core\Events\EventDispatcher;
|
|
use App\Framework\Core\ValueObjects\Byte;
|
|
use App\Framework\Core\ValueObjects\Percentage;
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
use App\Framework\DateTime\Clock;
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\Discovery\Events\CacheCompressionEvent;
|
|
use App\Framework\Discovery\Events\CacheHitEvent;
|
|
use App\Framework\Discovery\Events\CacheMissEvent;
|
|
use App\Framework\Discovery\Memory\DiscoveryMemoryManager;
|
|
use App\Framework\Discovery\Memory\MemoryStatus;
|
|
use App\Framework\Discovery\Memory\MemoryStatusInfo;
|
|
use App\Framework\Discovery\Results\AttributeRegistry;
|
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
|
use App\Framework\Discovery\Results\InterfaceRegistry;
|
|
use App\Framework\Discovery\Results\TemplateRegistry;
|
|
use App\Framework\Discovery\Runtime\DiscoveryLoader;
|
|
use App\Framework\Discovery\Storage\DiscoveryCacheManager;
|
|
use App\Framework\Discovery\ValueObjects\CacheLevel;
|
|
use App\Framework\Discovery\ValueObjects\CacheTier;
|
|
use App\Framework\Discovery\ValueObjects\CompressionLevel;
|
|
use App\Framework\Discovery\ValueObjects\DiscoveryContext;
|
|
use App\Framework\Discovery\ValueObjects\DiscoveryOptions;
|
|
use App\Framework\Discovery\ValueObjects\MemoryStrategy;
|
|
use App\Framework\Discovery\ValueObjects\ScanType;
|
|
use App\Framework\Filesystem\FileSystemService;
|
|
use App\Framework\Filesystem\ValueObjects\FilePath;
|
|
use App\Framework\Filesystem\ValueObjects\FileMetadata;
|
|
use App\Framework\Serializer\Php\PhpSerializer;
|
|
use App\Framework\Serializer\Php\PhpSerializerConfig;
|
|
|
|
describe('DiscoveryCacheManager - Basic Operations', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
// Use a real existing path to avoid stale detection issues
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$testPath = $basePath . '/src';
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService
|
|
);
|
|
|
|
// Use a future time to avoid stale detection issues
|
|
$futureTime = new \DateTimeImmutable('2099-01-01 00:00:00');
|
|
$this->testContext = new DiscoveryContext(
|
|
paths: [$testPath],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $futureTime
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
it('can be instantiated', function () {
|
|
expect($this->cacheManager)->toBeInstanceOf(DiscoveryCacheManager::class);
|
|
});
|
|
|
|
it('stores discovery results in cache', function () {
|
|
$success = $this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
expect($success)->toBeTrue();
|
|
|
|
// Verify cache contains the data
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($cached->isEmpty())->toBe($this->testRegistry->isEmpty());
|
|
});
|
|
|
|
it('retrieves cached discovery results', function () {
|
|
// Store first
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Retrieve
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($cached)->not->toBeNull();
|
|
});
|
|
|
|
it('returns null for cache miss', function () {
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeNull();
|
|
});
|
|
|
|
it('invalidates cache for a context', function () {
|
|
// Store first
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
expect($this->cacheManager->get($this->testContext))->not->toBeNull();
|
|
|
|
// Invalidate
|
|
$success = $this->cacheManager->invalidate($this->testContext);
|
|
expect($success)->toBeTrue();
|
|
|
|
// Should be null now
|
|
expect($this->cacheManager->get($this->testContext))->toBeNull();
|
|
});
|
|
|
|
it('clears all discovery caches', function () {
|
|
// Store multiple contexts
|
|
$context1 = new DiscoveryContext(
|
|
paths: ['/test/path1'],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $this->clock->now()
|
|
);
|
|
$context2 = new DiscoveryContext(
|
|
paths: ['/test/path2'],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $this->clock->now()
|
|
);
|
|
|
|
$this->cacheManager->store($context1, $this->testRegistry);
|
|
$this->cacheManager->store($context2, $this->testRegistry);
|
|
|
|
expect($this->cacheManager->get($context1))->not->toBeNull();
|
|
expect($this->cacheManager->get($context2))->not->toBeNull();
|
|
|
|
// Clear all
|
|
$success = $this->cacheManager->clearAll();
|
|
expect($success)->toBeTrue();
|
|
|
|
// Both should be null
|
|
expect($this->cacheManager->get($context1))->toBeNull();
|
|
expect($this->cacheManager->get($context2))->toBeNull();
|
|
});
|
|
|
|
it('optimizes registry before caching', function () {
|
|
$registry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
|
|
// Store should call optimize internally
|
|
$success = $this->cacheManager->store($this->testContext, $registry);
|
|
expect($success)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Cache Hit/Miss Scenarios', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
// Use a real existing path
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$testPath = $basePath . '/src';
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService
|
|
);
|
|
|
|
$this->testContext = new DiscoveryContext(
|
|
paths: ['/test/path'],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $this->clock->now()
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
it('handles cache hit correctly', function () {
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->not->toBeNull();
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
|
|
it('handles cache miss when key does not exist', function () {
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeNull();
|
|
});
|
|
|
|
it('handles cache miss when data is corrupted', function () {
|
|
// Store invalid data directly in cache
|
|
$key = $this->testContext->getCacheKey();
|
|
$invalidItem = CacheItem::forSet($key, 'invalid_data');
|
|
$this->cache->set($invalidItem);
|
|
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeNull();
|
|
});
|
|
|
|
it('handles cache miss when data type is wrong', function () {
|
|
// Store wrong type
|
|
$key = $this->testContext->getCacheKey();
|
|
$wrongTypeItem = CacheItem::forSet($key, ['not' => 'a', 'registry' => 'object']);
|
|
$this->cache->set($wrongTypeItem);
|
|
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Stale Cache Detection', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
|
|
// FileSystemService is final, so we need to use a real instance
|
|
// For stale detection tests, we'll use a real path and manipulate timing
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
it('considers incremental scans as always stale', function () {
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$testPath = $basePath . '/src';
|
|
|
|
$incrementalContext = new DiscoveryContext(
|
|
paths: [$testPath],
|
|
scanType: ScanType::INCREMENTAL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $this->clock->now()
|
|
);
|
|
|
|
$this->cacheManager->store($incrementalContext, $this->testRegistry);
|
|
|
|
// Incremental scans should always be considered stale
|
|
$cached = $this->cacheManager->get($incrementalContext);
|
|
|
|
expect($cached)->toBeNull();
|
|
});
|
|
|
|
it('detects stale cache when directory is modified', function () {
|
|
// Use a real path that exists
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$testPath = $basePath . '/src';
|
|
|
|
$startTime = new \DateTimeImmutable('2020-01-01 00:00:00');
|
|
$context = new DiscoveryContext(
|
|
paths: [$testPath],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $startTime
|
|
);
|
|
|
|
// Store cache
|
|
$this->cacheManager->store($context, $this->testRegistry);
|
|
|
|
// The directory will have a modification time after startTime, so it should be stale
|
|
// This test verifies that stale detection works
|
|
$cached = $this->cacheManager->get($context);
|
|
|
|
// Should be stale because real directory modification time is after startTime
|
|
expect($cached)->toBeNull();
|
|
});
|
|
|
|
it('considers cache fresh when directory is not modified', function () {
|
|
// Use a real path that exists
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$testPath = $basePath . '/src';
|
|
|
|
// Use a future time so the directory modification time is before it
|
|
$futureTime = new \DateTimeImmutable('2099-01-01 00:00:00');
|
|
$context = new DiscoveryContext(
|
|
paths: [$testPath],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $futureTime
|
|
);
|
|
|
|
// Store cache
|
|
$this->cacheManager->store($context, $this->testRegistry);
|
|
|
|
// Should be fresh because directory modification time is before futureTime
|
|
$cached = $this->cacheManager->get($context);
|
|
|
|
expect($cached)->not->toBeNull();
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
|
|
it('handles file system errors gracefully in stale detection', function () {
|
|
// Use a non-existent path to trigger file system error
|
|
$context = new DiscoveryContext(
|
|
paths: ['/nonexistent/path/that/does/not/exist'],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $this->clock->now()
|
|
);
|
|
|
|
$this->cacheManager->store($context, $this->testRegistry);
|
|
|
|
// Should assume stale on error (non-existent path throws exception)
|
|
$cached = $this->cacheManager->get($context);
|
|
|
|
expect($cached)->toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Health Status', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService
|
|
);
|
|
});
|
|
|
|
it('returns health status without memory manager', function () {
|
|
$health = $this->cacheManager->getHealthStatus();
|
|
|
|
expect($health)->toBeArray();
|
|
expect($health)->toHaveKey('cache_driver');
|
|
expect($health)->toHaveKey('ttl_hours');
|
|
expect($health)->toHaveKey('prefix');
|
|
expect($health)->toHaveKey('memory_aware');
|
|
expect($health['memory_aware'])->toBeFalse();
|
|
});
|
|
|
|
it('returns health status with memory manager', function () {
|
|
$memoryManager = new DiscoveryMemoryManager(
|
|
strategy: MemoryStrategy::BATCH,
|
|
memoryLimit: Byte::fromMegabytes(128),
|
|
memoryPressureThreshold: 0.8,
|
|
memoryMonitor: null,
|
|
logger: null,
|
|
eventDispatcher: null,
|
|
clock: $this->clock
|
|
);
|
|
|
|
$cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
logger: null,
|
|
ttlHours: 24,
|
|
memoryManager: $memoryManager
|
|
);
|
|
|
|
$health = $cacheManager->getHealthStatus();
|
|
|
|
expect($health)->toBeArray();
|
|
expect($health['memory_aware'])->toBeTrue();
|
|
expect($health)->toHaveKey('memory_management');
|
|
expect($health['memory_management'])->toBeArray();
|
|
expect($health['memory_management'])->toHaveKey('status');
|
|
expect($health['memory_management'])->toHaveKey('current_usage');
|
|
expect($health['memory_management'])->toHaveKey('memory_pressure');
|
|
expect($health['memory_management'])->toHaveKey('cache_level');
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Memory-Aware Caching', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
$this->memoryManager = new DiscoveryMemoryManager(
|
|
strategy: MemoryStrategy::BATCH,
|
|
memoryLimit: Byte::fromMegabytes(128),
|
|
memoryPressureThreshold: 0.8,
|
|
memoryMonitor: null,
|
|
logger: null,
|
|
eventDispatcher: null,
|
|
clock: $this->clock
|
|
);
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
logger: null,
|
|
ttlHours: 24,
|
|
memoryManager: $this->memoryManager
|
|
);
|
|
|
|
$this->testContext = new DiscoveryContext(
|
|
paths: ['/test/path'],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $this->clock->now()
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
it('uses memory-aware storage when memory manager is available', function () {
|
|
$success = $this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
expect($success)->toBeTrue();
|
|
|
|
// Should be retrievable
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
|
|
it('determines cache level based on memory pressure', function () {
|
|
// This is tested indirectly through storage, but we can verify health status
|
|
$health = $this->cacheManager->getHealthStatus();
|
|
|
|
expect($health['memory_aware'])->toBeTrue();
|
|
expect($health['memory_management'])->toBeArray();
|
|
expect($health['memory_management'])->toHaveKey('cache_level');
|
|
});
|
|
|
|
it('performs memory pressure management', function () {
|
|
$result = $this->cacheManager->performMemoryPressureManagement();
|
|
|
|
expect($result)->toBeArray();
|
|
expect($result)->toHaveKey('actions');
|
|
expect($result)->toHaveKey('memory_status');
|
|
expect($result)->toHaveKey('cache_level');
|
|
});
|
|
|
|
it('clears cache in critical memory situations', function () {
|
|
// Store some data
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
expect($this->cacheManager->get($this->testContext))->not->toBeNull();
|
|
|
|
// Create memory manager with very low limit to trigger critical status
|
|
$lowMemoryManager = new DiscoveryMemoryManager(
|
|
strategy: MemoryStrategy::BATCH,
|
|
memoryLimit: Byte::fromBytes(1024), // Very low limit
|
|
memoryPressureThreshold: 0.8,
|
|
memoryMonitor: null,
|
|
logger: null,
|
|
eventDispatcher: null,
|
|
clock: $this->clock
|
|
);
|
|
|
|
$cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
logger: null,
|
|
ttlHours: 24,
|
|
memoryManager: $lowMemoryManager
|
|
);
|
|
|
|
// Store with low memory manager
|
|
$cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Force memory usage to trigger critical
|
|
// Note: This is hard to test directly, but the method exists
|
|
$result = $cacheManager->performMemoryPressureManagement();
|
|
expect($result)->toBeArray();
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Tiered Caching', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
$this->memoryManager = new DiscoveryMemoryManager(
|
|
strategy: MemoryStrategy::BATCH,
|
|
memoryLimit: Byte::fromMegabytes(128),
|
|
memoryPressureThreshold: 0.8,
|
|
memoryMonitor: null,
|
|
logger: null,
|
|
eventDispatcher: null,
|
|
clock: $this->clock
|
|
);
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
logger: null,
|
|
ttlHours: 24,
|
|
memoryManager: $this->memoryManager
|
|
);
|
|
|
|
$this->testContext = new DiscoveryContext(
|
|
paths: ['/test/path'],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $this->clock->now()
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
it('uses tiered caching when memory manager is available', function () {
|
|
$success = $this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
expect($success)->toBeTrue();
|
|
|
|
// Should be retrievable from tiered cache
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
|
|
it('determines appropriate cache tier based on data size and access frequency', function () {
|
|
// Store data - tier determination happens internally
|
|
$success = $this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
expect($success)->toBeTrue();
|
|
|
|
// Verify it can be retrieved (tiered cache lookup)
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
|
|
it('applies different TTL multipliers per tier', function () {
|
|
// Store data - TTL adjustment happens internally based on tier
|
|
$success = $this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
expect($success)->toBeTrue();
|
|
|
|
// Verify retrieval works
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
|
|
it('tracks access patterns for tier determination', function () {
|
|
// Store and retrieve multiple times to build access pattern
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Multiple accesses
|
|
$this->cacheManager->get($this->testContext);
|
|
$this->cacheManager->get($this->testContext);
|
|
$this->cacheManager->get($this->testContext);
|
|
|
|
// Should still work
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Cache Compression', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
$this->memoryManager = new DiscoveryMemoryManager(
|
|
strategy: MemoryStrategy::BATCH,
|
|
memoryLimit: Byte::fromMegabytes(128),
|
|
memoryPressureThreshold: 0.8,
|
|
memoryMonitor: null,
|
|
logger: null,
|
|
eventDispatcher: null,
|
|
clock: $this->clock
|
|
);
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
logger: null,
|
|
ttlHours: 24,
|
|
memoryManager: $this->memoryManager
|
|
);
|
|
|
|
$this->testContext = new DiscoveryContext(
|
|
paths: ['/test/path'],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $this->clock->now()
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
it('compresses data for appropriate tiers', function () {
|
|
// Store data - compression happens internally for appropriate tiers
|
|
$success = $this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
expect($success)->toBeTrue();
|
|
|
|
// Should be retrievable and decompressed
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
|
|
it('decompresses data when retrieving from cache', function () {
|
|
// Store compressed data
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Retrieve should decompress automatically
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($cached->isEmpty())->toBe($this->testRegistry->isEmpty());
|
|
});
|
|
|
|
it('handles uncompressed data correctly', function () {
|
|
// Store data that might not need compression (small size, hot tier)
|
|
$success = $this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
expect($success)->toBeTrue();
|
|
|
|
// Should still be retrievable
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Cache Metrics', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
$this->memoryManager = new DiscoveryMemoryManager(
|
|
strategy: MemoryStrategy::BATCH,
|
|
memoryLimit: Byte::fromMegabytes(128),
|
|
memoryPressureThreshold: 0.8,
|
|
memoryMonitor: null,
|
|
logger: null,
|
|
eventDispatcher: null,
|
|
clock: $this->clock
|
|
);
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
logger: null,
|
|
ttlHours: 24,
|
|
memoryManager: $this->memoryManager
|
|
);
|
|
|
|
$this->testContext = new DiscoveryContext(
|
|
paths: ['/test/path'],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $this->clock->now()
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
it('returns null metrics when memory manager is not available', function () {
|
|
$cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService
|
|
);
|
|
|
|
$metrics = $cacheManager->getCacheMetrics();
|
|
|
|
expect($metrics)->toBeNull();
|
|
});
|
|
|
|
it('returns cache metrics when memory manager is available', function () {
|
|
// Store and retrieve to generate metrics
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
$this->cacheManager->get($this->testContext);
|
|
|
|
$metrics = $this->cacheManager->getCacheMetrics();
|
|
|
|
expect($metrics)->not->toBeNull();
|
|
expect($metrics)->toBeInstanceOf(\App\Framework\Discovery\ValueObjects\CacheMetrics::class);
|
|
});
|
|
|
|
it('calculates hit rate correctly', function () {
|
|
// Store data
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Multiple hits
|
|
$this->cacheManager->get($this->testContext);
|
|
$this->cacheManager->get($this->testContext);
|
|
|
|
$metrics = $this->cacheManager->getCacheMetrics();
|
|
|
|
expect($metrics)->not->toBeNull();
|
|
// Hit rate should be calculated (exact value depends on implementation)
|
|
});
|
|
|
|
it('tracks total cache size', function () {
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
$metrics = $this->cacheManager->getCacheMetrics();
|
|
|
|
expect($metrics)->not->toBeNull();
|
|
expect($metrics->totalSize)->toBeInstanceOf(Byte::class);
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Cache Events', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
// EventDispatcher is final and requires a Container
|
|
// We'll use a real instance but can't verify events directly
|
|
$this->container = new \App\Framework\DI\DefaultContainer();
|
|
$this->eventDispatcher = new EventDispatcher($this->container);
|
|
|
|
$this->memoryManager = new DiscoveryMemoryManager(
|
|
strategy: MemoryStrategy::BATCH,
|
|
memoryLimit: Byte::fromMegabytes(128),
|
|
memoryPressureThreshold: 0.8,
|
|
memoryMonitor: null,
|
|
logger: null,
|
|
eventDispatcher: $this->eventDispatcher,
|
|
clock: $this->clock
|
|
);
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
logger: null,
|
|
ttlHours: 24,
|
|
memoryManager: $this->memoryManager,
|
|
eventDispatcher: $this->eventDispatcher
|
|
);
|
|
|
|
// Use a future time to avoid stale detection issues
|
|
$futureTime = new \DateTimeImmutable('2099-01-01 00:00:00');
|
|
$this->testContext = new DiscoveryContext(
|
|
paths: [$testPath],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $futureTime
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
it('dispatches CacheHitEvent on cache hit', function () {
|
|
// Store first
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// EventDispatcher is final, so we can't verify events directly
|
|
// But we can verify the cache hit works
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
|
|
it('dispatches CacheMissEvent on cache miss', function () {
|
|
// EventDispatcher is final, so we can't verify events directly
|
|
// But we can verify the cache miss works
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeNull();
|
|
});
|
|
|
|
it('dispatches CacheCompressionEvent when compressing data', function () {
|
|
// EventDispatcher is final, so we can't verify events directly
|
|
// But we can verify compression works by storing and retrieving
|
|
$success = $this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
expect($success)->toBeTrue();
|
|
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
|
|
it('handles multiple miss scenarios for different reasons', function () {
|
|
// Not found scenario
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeNull();
|
|
|
|
// Corrupted data scenario
|
|
$key = $this->testContext->getCacheKey();
|
|
$invalidItem = CacheItem::forSet($key, 'invalid');
|
|
$this->cache->set($invalidItem);
|
|
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Cache Format Migration', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
// Use a real existing path to avoid stale detection issues
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$testPath = $basePath . '/src';
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService
|
|
);
|
|
|
|
// Use a future time to avoid stale detection issues
|
|
$futureTime = new \DateTimeImmutable('2099-01-01 00:00:00');
|
|
$this->testContext = new DiscoveryContext(
|
|
paths: [$testPath],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $futureTime
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
it('migrates old cache format (DiscoveryRegistry only) to new format', function () {
|
|
// Store old format directly in cache (simulating legacy cache entry)
|
|
$key = $this->testContext->getCacheKey();
|
|
$oldFormatItem = CacheItem::forSet($key, $this->testRegistry);
|
|
$this->cache->set($oldFormatItem);
|
|
|
|
// Retrieve should handle old format and migrate it
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($cached->isEmpty())->toBe($this->testRegistry->isEmpty());
|
|
|
|
// Verify cache was upgraded to new format
|
|
$result = $this->cache->get($key);
|
|
$item = $result->getItem($key);
|
|
expect($item->isHit)->toBeTrue();
|
|
|
|
// New format should be array with registry and startTime
|
|
$cachedData = $item->value;
|
|
expect($cachedData)->toBeArray();
|
|
expect($cachedData)->toHaveKey('registry');
|
|
expect($cachedData)->toHaveKey('startTime');
|
|
expect($cachedData['registry'])->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($cachedData['startTime'])->toBeInstanceOf(\DateTimeInterface::class);
|
|
});
|
|
|
|
it('handles new cache format (array with registry and startTime) correctly', function () {
|
|
// Store new format
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Retrieve should work with new format
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($cached->isEmpty())->toBe($this->testRegistry->isEmpty());
|
|
});
|
|
|
|
it('validates cache structure for new format', function () {
|
|
// Store invalid structure (missing registry key)
|
|
$key = $this->testContext->getCacheKey();
|
|
$invalidItem = CacheItem::forSet($key, [
|
|
'startTime' => $this->clock->now(),
|
|
// Missing 'registry' key
|
|
]);
|
|
$this->cache->set($invalidItem);
|
|
|
|
// Should return null for invalid structure
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeNull();
|
|
});
|
|
|
|
it('validates registry type in cache structure', function () {
|
|
// Store invalid structure (registry is not DiscoveryRegistry)
|
|
$key = $this->testContext->getCacheKey();
|
|
$invalidItem = CacheItem::forSet($key, [
|
|
'registry' => 'not a registry',
|
|
'startTime' => $this->clock->now(),
|
|
]);
|
|
$this->cache->set($invalidItem);
|
|
|
|
// Should return null for invalid registry type
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeNull();
|
|
});
|
|
|
|
it('handles optional startTime in new format', function () {
|
|
// Store new format without startTime (should still work)
|
|
$key = $this->testContext->getCacheKey();
|
|
$newFormatItem = CacheItem::forSet($key, [
|
|
'registry' => $this->testRegistry,
|
|
// startTime is optional
|
|
]);
|
|
$this->cache->set($newFormatItem);
|
|
|
|
// Should work but use context startTime as fallback
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
|
|
it('validates startTime type when present', function () {
|
|
// Store invalid structure (startTime is not DateTimeInterface)
|
|
$key = $this->testContext->getCacheKey();
|
|
$invalidItem = CacheItem::forSet($key, [
|
|
'registry' => $this->testRegistry,
|
|
'startTime' => 'not a datetime',
|
|
]);
|
|
$this->cache->set($invalidItem);
|
|
|
|
// Should return null for invalid startTime type
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
expect($cached)->toBeNull();
|
|
});
|
|
|
|
it('migrates tiered cache entries from old to new format', function () {
|
|
$memoryManager = new DiscoveryMemoryManager(
|
|
strategy: MemoryStrategy::BATCH,
|
|
memoryLimit: Byte::fromMegabytes(128),
|
|
memoryPressureThreshold: 0.8,
|
|
memoryMonitor: null,
|
|
logger: null,
|
|
eventDispatcher: null,
|
|
clock: $this->clock
|
|
);
|
|
|
|
$cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
logger: null,
|
|
ttlHours: 24,
|
|
memoryManager: $memoryManager
|
|
);
|
|
|
|
// Store old format in tiered cache (simulating legacy tiered cache entry)
|
|
$key = CacheKey::fromString('discovery:tier_hot:' . $this->testContext->getCacheKey()->toString());
|
|
$oldFormatItem = CacheItem::forSet($key, $this->testRegistry);
|
|
$this->cache->set($oldFormatItem);
|
|
|
|
// Retrieve should handle old format and migrate it
|
|
$cached = $cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
|
|
it('tracks migration metrics', function () {
|
|
// Store old format
|
|
$key = $this->testContext->getCacheKey();
|
|
$oldFormatItem = CacheItem::forSet($key, $this->testRegistry);
|
|
$this->cache->set($oldFormatItem);
|
|
|
|
// Retrieve (triggers migration)
|
|
$this->cacheManager->get($this->testContext);
|
|
|
|
// Metrics should be tracked (we can't directly access private metrics,
|
|
// but we can verify the cache was upgraded)
|
|
$result = $this->cache->get($key);
|
|
$item = $result->getItem($key);
|
|
expect($item->isHit)->toBeTrue();
|
|
|
|
// Verify new format
|
|
$cachedData = $item->value;
|
|
expect($cachedData)->toBeArray();
|
|
expect($cachedData)->toHaveKey('registry');
|
|
expect($cachedData)->toHaveKey('startTime');
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Registry Versioning', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
// Use a real existing path to avoid stale detection issues
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$testPath = $basePath . '/src';
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService
|
|
);
|
|
|
|
// Use a future time to avoid stale detection issues
|
|
$futureTime = new \DateTimeImmutable('2099-01-01 00:00:00');
|
|
$this->testContext = new DiscoveryContext(
|
|
paths: [$testPath],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $futureTime
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
it('stores registry with version in cache', function () {
|
|
// Store registry
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Verify cache contains version
|
|
$key = $this->testContext->getCacheKey();
|
|
$result = $this->cache->get($key);
|
|
$item = $result->getItem($key);
|
|
|
|
expect($item->isHit)->toBeTrue();
|
|
expect($item->value)->toBeArray();
|
|
expect($item->value)->toHaveKey('version');
|
|
expect($item->value['version'])->toBeString();
|
|
expect(str_starts_with($item->value['version'], 'v1-'))->toBeTrue();
|
|
});
|
|
|
|
it('invalidates cache when version mismatch detected', function () {
|
|
// Store registry
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Modify registry content (simulating a change)
|
|
$modifiedRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
|
|
// Store modified registry with different content
|
|
// This should create a different version
|
|
$key = $this->testContext->getCacheKey();
|
|
$cachedData = $this->cache->get($key)->getItem($key)->value;
|
|
$oldVersion = $cachedData['version'] ?? null;
|
|
|
|
// Create a new context to simulate a new discovery
|
|
$newContext = new DiscoveryContext(
|
|
paths: [$this->testContext->paths[0]],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $this->testContext->startTime
|
|
);
|
|
|
|
// Store modified registry
|
|
$this->cacheManager->store($newContext, $modifiedRegistry);
|
|
|
|
// Get new version
|
|
$newCachedData = $this->cache->get($key)->getItem($key)->value;
|
|
$newVersion = $newCachedData['version'] ?? null;
|
|
|
|
// Versions should be different if registry content changed
|
|
// (Note: Empty registries might have same version, so we just verify version exists)
|
|
expect($newVersion)->toBeString();
|
|
expect(str_starts_with($newVersion, 'v1-'))->toBeTrue();
|
|
});
|
|
|
|
it('handles version in tiered cache', function () {
|
|
$memoryManager = new DiscoveryMemoryManager(
|
|
strategy: MemoryStrategy::BATCH,
|
|
memoryLimit: Byte::fromMegabytes(128),
|
|
memoryPressureThreshold: 0.8,
|
|
memoryMonitor: null,
|
|
logger: null,
|
|
eventDispatcher: null,
|
|
clock: $this->clock
|
|
);
|
|
|
|
$cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
logger: null,
|
|
ttlHours: 24,
|
|
memoryManager: $memoryManager
|
|
);
|
|
|
|
// Store registry
|
|
$cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Verify tiered cache contains version
|
|
$key = CacheKey::fromString('discovery:tier_hot:' . $this->testContext->getCacheKey()->toString());
|
|
$result = $this->cache->get($key);
|
|
$item = $result->getItem($key);
|
|
|
|
if ($item->isHit) {
|
|
$data = $item->value;
|
|
if (is_array($data) && isset($data['registry'])) {
|
|
expect($data)->toHaveKey('version');
|
|
expect($data['version'])->toBeString();
|
|
}
|
|
}
|
|
});
|
|
|
|
it('validates version format', function () {
|
|
// Store registry
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Retrieve and verify version format
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
|
|
// Verify cache structure has version
|
|
$key = $this->testContext->getCacheKey();
|
|
$result = $this->cache->get($key);
|
|
$item = $result->getItem($key);
|
|
|
|
if ($item->isHit && is_array($item->value)) {
|
|
if (isset($item->value['version'])) {
|
|
expect($item->value['version'])->toBeString();
|
|
expect(str_starts_with($item->value['version'], 'v1-'))->toBeTrue();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Unified Storage Strategy', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
// Use a real existing path to avoid stale detection issues
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$testPath = $basePath . '/src';
|
|
|
|
// Use a future time to avoid stale detection issues
|
|
$futureTime = new \DateTimeImmutable('2099-01-01 00:00:00');
|
|
$this->testContext = new DiscoveryContext(
|
|
paths: [$testPath],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $futureTime
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
it('uses Runtime-Cache when Build-Time Storage is not available', function () {
|
|
// Create cache manager without Build-Time Loader
|
|
$cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
buildTimeLoader: null
|
|
);
|
|
|
|
// Store in Runtime-Cache
|
|
$cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Should retrieve from Runtime-Cache
|
|
$cached = $cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($cached->isEmpty())->toBe($this->testRegistry->isEmpty());
|
|
});
|
|
|
|
it('falls back to Runtime-Cache when Build-Time Storage is not available', function () {
|
|
// Create cache manager without Build-Time Loader (null = not available)
|
|
$cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
buildTimeLoader: null
|
|
);
|
|
|
|
// Store in Runtime-Cache
|
|
$cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Should retrieve from Runtime-Cache
|
|
$cached = $cacheManager->get($this->testContext);
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($cached->isEmpty())->toBe($this->testRegistry->isEmpty());
|
|
});
|
|
|
|
it('uses Runtime-Cache when Build-Time Storage has no data', function () {
|
|
// Create a real DiscoveryLoader with empty storage (no data available)
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$pathProvider = new \App\Framework\Core\PathProvider($basePath);
|
|
$storageService = new \App\Framework\Discovery\Storage\DiscoveryStorageService($pathProvider);
|
|
|
|
// Clear storage to ensure no data exists
|
|
try {
|
|
$storageService->clear();
|
|
} catch (\Throwable) {
|
|
// Ignore if clear fails
|
|
}
|
|
|
|
$buildTimeLoader = new DiscoveryLoader($storageService);
|
|
|
|
// Use a future time to avoid stale detection issues
|
|
$futureTime = new \DateTimeImmutable('2099-01-01 00:00:00');
|
|
$testContext = new DiscoveryContext(
|
|
paths: [$this->testContext->paths[0]],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $futureTime
|
|
);
|
|
|
|
$cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
buildTimeLoader: $buildTimeLoader
|
|
);
|
|
|
|
// Store in Runtime-Cache
|
|
$cacheManager->store($testContext, $this->testRegistry);
|
|
|
|
// Should retrieve from Runtime-Cache (Build-Time Storage has no data)
|
|
// Note: Cache might be stale due to directory modification time, but that's okay for this test
|
|
$cached = $cacheManager->get($testContext);
|
|
|
|
// If cache is not stale, verify it works
|
|
// If cache is stale (null), that's also acceptable - Build-Time Storage fallback worked
|
|
if ($cached !== null) {
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($cached->isEmpty())->toBe($this->testRegistry->isEmpty());
|
|
}
|
|
// If null, Build-Time Storage was checked first (has no data), then Runtime-Cache was checked but was stale
|
|
// This is acceptable behavior
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryCacheManager - Enhanced Logging and Monitoring', function () {
|
|
beforeEach(function () {
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
$this->fileSystemService = new FileSystemService();
|
|
|
|
// Use a real existing path to avoid stale detection issues
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$testPath = $basePath . '/src';
|
|
|
|
$this->cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService
|
|
);
|
|
|
|
// Use a future time to avoid stale detection issues
|
|
$futureTime = new \DateTimeImmutable('2099-01-01 00:00:00');
|
|
$this->testContext = new DiscoveryContext(
|
|
paths: [$testPath],
|
|
scanType: ScanType::FULL,
|
|
options: new DiscoveryOptions(),
|
|
startTime: $futureTime
|
|
);
|
|
|
|
$this->testRegistry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
});
|
|
|
|
it('logs cache hits with source information', function () {
|
|
// Store registry
|
|
$this->cacheManager->store($this->testContext, $this->testRegistry);
|
|
|
|
// Retrieve should log cache hit
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
// Logging is tested indirectly - if no errors occur, logging works
|
|
});
|
|
|
|
it('logs cache misses with reason', function () {
|
|
// Try to retrieve non-existent cache
|
|
$cached = $this->cacheManager->get($this->testContext);
|
|
|
|
expect($cached)->toBeNull();
|
|
// Logging is tested indirectly - if no errors occur, logging works
|
|
});
|
|
|
|
it('tracks cache metrics for hits and misses', function () {
|
|
$memoryManager = new DiscoveryMemoryManager(
|
|
strategy: MemoryStrategy::BATCH,
|
|
memoryLimit: Byte::fromMegabytes(128),
|
|
memoryPressureThreshold: 0.8,
|
|
memoryMonitor: null,
|
|
logger: null,
|
|
eventDispatcher: null,
|
|
clock: $this->clock
|
|
);
|
|
|
|
$cacheManager = new DiscoveryCacheManager(
|
|
cache: $this->cache,
|
|
clock: $this->clock,
|
|
fileSystemService: $this->fileSystemService,
|
|
logger: null,
|
|
ttlHours: 24,
|
|
memoryManager: $memoryManager
|
|
);
|
|
|
|
// Store and retrieve to generate metrics
|
|
$cacheManager->store($this->testContext, $this->testRegistry);
|
|
$cacheManager->get($this->testContext);
|
|
$cacheManager->get($this->testContext);
|
|
|
|
$metrics = $cacheManager->getCacheMetrics();
|
|
|
|
expect($metrics)->not->toBeNull();
|
|
expect($metrics->totalItems)->toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryRegistry - Metadata and Debug Helpers', function () {
|
|
it('provides metadata about registry', function () {
|
|
$registry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
|
|
$metadata = $registry->getMetadata();
|
|
|
|
expect($metadata)->toBeArray();
|
|
expect($metadata)->toHaveKey('item_count');
|
|
expect($metadata)->toHaveKey('attribute_count');
|
|
expect($metadata)->toHaveKey('interface_count');
|
|
expect($metadata)->toHaveKey('template_count');
|
|
expect($metadata)->toHaveKey('is_empty');
|
|
expect($metadata)->toHaveKey('memory_stats');
|
|
});
|
|
|
|
it('provides source information', function () {
|
|
$registry = new DiscoveryRegistry(
|
|
attributes: new AttributeRegistry(),
|
|
interfaces: new InterfaceRegistry(),
|
|
templates: new TemplateRegistry()
|
|
);
|
|
|
|
$source = $registry->getSource();
|
|
|
|
expect($source)->toBeString();
|
|
// Default source is 'unknown' for base registry
|
|
expect($source)->toBe('unknown');
|
|
});
|
|
});
|
|
|