mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
765 lines
24 KiB
PHP
765 lines
24 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\GraphQL;
|
|
|
|
use Appwrite\GraphQL\Cache;
|
|
use GraphQL\Type\Definition\ObjectType;
|
|
use GraphQL\Type\Definition\Type;
|
|
use GraphQL\Type\Schema as GQLSchema;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class CacheTest extends TestCase
|
|
{
|
|
private Cache $cache;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->cache = new Cache(1);
|
|
}
|
|
|
|
/**
|
|
* Create a mock schema with configurable number of types.
|
|
* Size calculation is based on type count (~4KB per type).
|
|
*/
|
|
private function createMockSchema(int $typeCount = 1, string $suffix = ''): GQLSchema
|
|
{
|
|
$types = [];
|
|
$queryFields = [];
|
|
|
|
for ($i = 0; $i < $typeCount; $i++) {
|
|
$typeName = "Type{$i}{$suffix}";
|
|
$types[$typeName] = new ObjectType([
|
|
'name' => $typeName,
|
|
'fields' => [
|
|
'id' => ['type' => Type::string()],
|
|
'name' => ['type' => Type::string()],
|
|
]
|
|
]);
|
|
$queryFields["get{$typeName}"] = ['type' => $types[$typeName]];
|
|
}
|
|
|
|
if (empty($queryFields)) {
|
|
$queryFields['dummy'] = ['type' => Type::string()];
|
|
}
|
|
|
|
return new GQLSchema([
|
|
'query' => new ObjectType([
|
|
'name' => 'Query' . $suffix,
|
|
'fields' => $queryFields
|
|
]),
|
|
'types' => \array_values($types)
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Create a large schema with many types (~400KB+).
|
|
*/
|
|
private function createLargeSchema(string $suffix = ''): GQLSchema
|
|
{
|
|
return $this->createMockSchema(100, $suffix);
|
|
}
|
|
|
|
// ============================================
|
|
// Basic Operations
|
|
// ============================================
|
|
|
|
public function testSetAndGet(): void
|
|
{
|
|
$schema = $this->createMockSchema();
|
|
$this->cache->set('project1', $schema);
|
|
|
|
$this->assertSame($schema, $this->cache->get('project1'));
|
|
$this->assertEquals(1, $this->cache->size());
|
|
}
|
|
|
|
public function testGetNonExistent(): void
|
|
{
|
|
$this->assertNull($this->cache->get('nonexistent'));
|
|
}
|
|
|
|
public function testGetAfterRemove(): void
|
|
{
|
|
$schema = $this->createMockSchema();
|
|
$this->cache->set('project1', $schema);
|
|
$this->cache->remove('project1');
|
|
|
|
$this->assertNull($this->cache->get('project1'));
|
|
}
|
|
|
|
public function testRemoveNonExistent(): void
|
|
{
|
|
// Should not throw
|
|
$this->cache->remove('nonexistent');
|
|
$this->assertEquals(0, $this->cache->size());
|
|
}
|
|
|
|
public function testRemoveMultipleTimes(): void
|
|
{
|
|
$schema = $this->createMockSchema();
|
|
$this->cache->set('project1', $schema);
|
|
|
|
$this->cache->remove('project1');
|
|
$this->cache->remove('project1'); // Second remove should be safe
|
|
$this->cache->remove('project1'); // Third remove should be safe
|
|
|
|
$this->assertEquals(0, $this->cache->size());
|
|
}
|
|
|
|
// ============================================
|
|
// Memory Tracking
|
|
// ============================================
|
|
|
|
public function testMemoryTracking(): void
|
|
{
|
|
$this->assertEquals(0, $this->cache->getCurrentBytes());
|
|
|
|
$schema = $this->createMockSchema();
|
|
$this->cache->set('project1', $schema);
|
|
|
|
$this->assertGreaterThan(0, $this->cache->getCurrentBytes());
|
|
}
|
|
|
|
public function testMemoryTrackingAccuracy(): void
|
|
{
|
|
$schema1 = $this->createMockSchema(10);
|
|
$schema2 = $this->createMockSchema(50);
|
|
|
|
$this->cache->set('project1', $schema1);
|
|
$bytes1 = $this->cache->getCurrentBytes();
|
|
|
|
$this->cache->set('project2', $schema2);
|
|
$bytes2 = $this->cache->getCurrentBytes();
|
|
|
|
// Larger schema should use more memory
|
|
$this->assertGreaterThan($bytes1, $bytes2);
|
|
$this->assertEquals(2, $this->cache->size());
|
|
}
|
|
|
|
public function testMemoryDecreasesOnRemove(): void
|
|
{
|
|
$schema = $this->createMockSchema(100);
|
|
$this->cache->set('project1', $schema);
|
|
|
|
$bytesWithSchema = $this->cache->getCurrentBytes();
|
|
$this->assertGreaterThan(0, $bytesWithSchema);
|
|
|
|
$this->cache->remove('project1');
|
|
$this->assertEquals(0, $this->cache->getCurrentBytes());
|
|
}
|
|
|
|
public function testMemoryDecreasesOnEviction(): void
|
|
{
|
|
$this->cache->setMaxSizeMB(10);
|
|
|
|
// Fill cache with enough schemas to exceed 1 MB
|
|
// Each large schema is ~83 KB, so 15+ schemas > 1 MB
|
|
for ($i = 1; $i <= 15; $i++) {
|
|
$this->cache->set("project{$i}", $this->createLargeSchema((string)$i));
|
|
}
|
|
|
|
$bytesBefore = $this->cache->getCurrentBytes();
|
|
$sizeBefore = $this->cache->size();
|
|
|
|
$this->assertGreaterThan(1024 * 1024, $bytesBefore, 'Cache should exceed 1 MB before eviction');
|
|
|
|
// Reduce limit to force eviction
|
|
$this->cache->setMaxSizeMB(1);
|
|
|
|
$bytesAfter = $this->cache->getCurrentBytes();
|
|
$sizeAfter = $this->cache->size();
|
|
|
|
$this->assertLessThan($bytesBefore, $bytesAfter);
|
|
$this->assertLessThan($sizeBefore, $sizeAfter);
|
|
$this->assertLessThanOrEqual(1024 * 1024, $bytesAfter, 'Cache should be at or under 1 MB after eviction');
|
|
}
|
|
|
|
// ============================================
|
|
// LRU Eviction
|
|
// ============================================
|
|
|
|
public function testLRUEvictionByMemory(): void
|
|
{
|
|
for ($i = 1; $i <= 20; $i++) {
|
|
$this->cache->set("project{$i}", $this->createLargeSchema());
|
|
}
|
|
|
|
$stats = $this->cache->getStats();
|
|
$this->assertLessThanOrEqual(1, $stats['memoryMB']);
|
|
}
|
|
|
|
public function testLRUEvictsOldestFirst(): void
|
|
{
|
|
$this->cache->setMaxSizeMB(1);
|
|
|
|
// Add schemas
|
|
$this->cache->set('oldest', $this->createLargeSchema('1'));
|
|
$this->cache->set('middle', $this->createLargeSchema('2'));
|
|
$this->cache->set('newest', $this->createLargeSchema('3'));
|
|
|
|
// Access middle to make it more recent than oldest
|
|
$this->cache->get('middle');
|
|
|
|
// Add more to trigger eviction
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$this->cache->set("extra{$i}", $this->createLargeSchema((string)$i));
|
|
}
|
|
|
|
// Oldest should have been evicted first (assuming it wasn't accessed)
|
|
// Middle was accessed so it should survive longer
|
|
$this->assertNull($this->cache->get('oldest'));
|
|
}
|
|
|
|
public function testLRUAccessUpdatesTimestamp(): void
|
|
{
|
|
$this->cache->setMaxSizeMB(1);
|
|
|
|
$this->cache->set('project1', $this->createLargeSchema('1'));
|
|
$this->cache->set('project2', $this->createLargeSchema('2'));
|
|
|
|
// Access project1 repeatedly to keep it fresh
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$this->cache->get('project1');
|
|
$this->cache->set("filler{$i}", $this->createLargeSchema((string)$i));
|
|
}
|
|
|
|
// project1 should still exist due to recent access
|
|
// project2 should have been evicted
|
|
$this->assertNotNull($this->cache->get('project1'));
|
|
}
|
|
|
|
public function testEvictionWithSingleEntry(): void
|
|
{
|
|
// Set very small cache
|
|
$cache = new Cache(1);
|
|
|
|
// Add a schema that's close to the limit
|
|
$cache->set('project1', $this->createLargeSchema('1'));
|
|
|
|
// Adding another large schema should evict the first
|
|
$cache->set('project2', $this->createLargeSchema('2'));
|
|
|
|
$this->assertEquals(1, $cache->getMaxSizeMB());
|
|
$stats = $cache->getStats();
|
|
$this->assertLessThanOrEqual(1, $stats['memoryMB']);
|
|
}
|
|
|
|
// ============================================
|
|
// Dirty Flag
|
|
// ============================================
|
|
|
|
public function testDirtyFlag(): void
|
|
{
|
|
$schema = $this->createMockSchema();
|
|
$this->cache->set('project1', $schema);
|
|
|
|
$this->cache->setDirty('project1');
|
|
$this->assertTrue($this->cache->isDirty('project1'));
|
|
|
|
$this->assertNull($this->cache->get('project1'));
|
|
$this->assertEquals(0, $this->cache->size());
|
|
}
|
|
|
|
public function testSetClearsDirtyFlag(): void
|
|
{
|
|
$this->cache->setDirty('project1');
|
|
$this->assertTrue($this->cache->isDirty('project1'));
|
|
|
|
$schema = $this->createMockSchema();
|
|
$this->cache->set('project1', $schema);
|
|
|
|
$this->assertFalse($this->cache->isDirty('project1'));
|
|
$this->assertNotNull($this->cache->get('project1'));
|
|
}
|
|
|
|
public function testDirtyFlagWithoutCacheEntry(): void
|
|
{
|
|
// Mark dirty without ever caching
|
|
$this->cache->setDirty('project1');
|
|
$this->assertTrue($this->cache->isDirty('project1'));
|
|
|
|
// Get should return null and clear dirty flag
|
|
$this->assertNull($this->cache->get('project1'));
|
|
$this->assertFalse($this->cache->isDirty('project1'));
|
|
}
|
|
|
|
public function testDirtyFlagClearedOnGet(): void
|
|
{
|
|
$schema = $this->createMockSchema();
|
|
$this->cache->set('project1', $schema);
|
|
$this->cache->setDirty('project1');
|
|
|
|
// First get clears dirty and removes entry
|
|
$this->assertNull($this->cache->get('project1'));
|
|
$this->assertFalse($this->cache->isDirty('project1'));
|
|
|
|
// Second get still returns null but no dirty flag
|
|
$this->assertNull($this->cache->get('project1'));
|
|
}
|
|
|
|
public function testMultipleDirtyFlags(): void
|
|
{
|
|
$this->cache->set('project1', $this->createMockSchema());
|
|
$this->cache->set('project2', $this->createMockSchema());
|
|
$this->cache->set('project3', $this->createMockSchema());
|
|
|
|
$this->cache->setDirty('project1');
|
|
$this->cache->setDirty('project3');
|
|
|
|
$this->assertTrue($this->cache->isDirty('project1'));
|
|
$this->assertFalse($this->cache->isDirty('project2'));
|
|
$this->assertTrue($this->cache->isDirty('project3'));
|
|
|
|
// Access dirty entries
|
|
$this->assertNull($this->cache->get('project1'));
|
|
$this->assertNotNull($this->cache->get('project2'));
|
|
$this->assertNull($this->cache->get('project3'));
|
|
|
|
$this->assertEquals(1, $this->cache->size());
|
|
}
|
|
|
|
public function testDirtyFlagIdempotent(): void
|
|
{
|
|
$this->cache->set('project1', $this->createMockSchema());
|
|
|
|
$this->cache->setDirty('project1');
|
|
$this->cache->setDirty('project1');
|
|
$this->cache->setDirty('project1');
|
|
|
|
$this->assertTrue($this->cache->isDirty('project1'));
|
|
$this->assertNull($this->cache->get('project1'));
|
|
}
|
|
|
|
// ============================================
|
|
// Update/Replace Operations
|
|
// ============================================
|
|
|
|
public function testUpdateExistingEntry(): void
|
|
{
|
|
$schema1 = $this->createMockSchema(10);
|
|
$schema2 = $this->createMockSchema(20);
|
|
|
|
$this->cache->set('project1', $schema1);
|
|
$bytesAfterFirst = $this->cache->getCurrentBytes();
|
|
|
|
$this->cache->set('project1', $schema2);
|
|
$bytesAfterSecond = $this->cache->getCurrentBytes();
|
|
|
|
$this->assertEquals(1, $this->cache->size());
|
|
$this->assertSame($schema2, $this->cache->get('project1'));
|
|
$this->assertGreaterThan($bytesAfterFirst, $bytesAfterSecond);
|
|
}
|
|
|
|
public function testUpdateWithSmallerSchema(): void
|
|
{
|
|
$schema1 = $this->createMockSchema(100);
|
|
$schema2 = $this->createMockSchema(10);
|
|
|
|
$this->cache->set('project1', $schema1);
|
|
$bytesAfterFirst = $this->cache->getCurrentBytes();
|
|
|
|
$this->cache->set('project1', $schema2);
|
|
$bytesAfterSecond = $this->cache->getCurrentBytes();
|
|
|
|
$this->assertEquals(1, $this->cache->size());
|
|
$this->assertLessThan($bytesAfterFirst, $bytesAfterSecond);
|
|
}
|
|
|
|
public function testRapidUpdates(): void
|
|
{
|
|
for ($i = 0; $i < 100; $i++) {
|
|
$this->cache->set('project1', $this->createMockSchema($i + 1));
|
|
}
|
|
|
|
$this->assertEquals(1, $this->cache->size());
|
|
$this->assertNotNull($this->cache->get('project1'));
|
|
}
|
|
|
|
// ============================================
|
|
// Clear Operations
|
|
// ============================================
|
|
|
|
public function testClear(): void
|
|
{
|
|
$this->cache->set('project1', $this->createMockSchema());
|
|
$this->cache->set('project2', $this->createMockSchema());
|
|
$this->cache->setDirty('project3');
|
|
|
|
$this->cache->clear();
|
|
|
|
$this->assertEquals(0, $this->cache->size());
|
|
$this->assertEquals(0, $this->cache->getCurrentBytes());
|
|
$this->assertNull($this->cache->get('project1'));
|
|
$this->assertNull($this->cache->get('project2'));
|
|
$this->assertFalse($this->cache->isDirty('project3'));
|
|
}
|
|
|
|
public function testClearEmptyCache(): void
|
|
{
|
|
$this->cache->clear();
|
|
|
|
$this->assertEquals(0, $this->cache->size());
|
|
$this->assertEquals(0, $this->cache->getCurrentBytes());
|
|
}
|
|
|
|
public function testClearAndReuse(): void
|
|
{
|
|
$this->cache->set('project1', $this->createMockSchema());
|
|
$this->cache->clear();
|
|
|
|
$schema = $this->createMockSchema();
|
|
$this->cache->set('project1', $schema);
|
|
|
|
$this->assertSame($schema, $this->cache->get('project1'));
|
|
$this->assertEquals(1, $this->cache->size());
|
|
}
|
|
|
|
// ============================================
|
|
// Stats
|
|
// ============================================
|
|
|
|
public function testGetStats(): void
|
|
{
|
|
$this->cache->set('project1', $this->createMockSchema());
|
|
$this->cache->setDirty('project2');
|
|
|
|
$stats = $this->cache->getStats();
|
|
|
|
$this->assertEquals(1, $stats['schemas']);
|
|
$this->assertArrayHasKey('memoryMB', $stats);
|
|
$this->assertGreaterThanOrEqual(0, $stats['memoryMB']);
|
|
$this->assertEquals(1, $stats['maxMemoryMB']);
|
|
$this->assertEquals(1, $stats['dirty']);
|
|
}
|
|
|
|
public function testStatsEmpty(): void
|
|
{
|
|
$stats = $this->cache->getStats();
|
|
|
|
$this->assertEquals(0, $stats['schemas']);
|
|
$this->assertEquals(0.0, $stats['memoryMB']);
|
|
$this->assertEquals(0, $stats['dirty']);
|
|
}
|
|
|
|
public function testStatsAfterEviction(): void
|
|
{
|
|
$this->cache->setMaxSizeMB(1);
|
|
|
|
for ($i = 1; $i <= 20; $i++) {
|
|
$this->cache->set("project{$i}", $this->createLargeSchema());
|
|
}
|
|
|
|
$stats = $this->cache->getStats();
|
|
$this->assertLessThanOrEqual(1, $stats['memoryMB']);
|
|
$this->assertLessThan(20, $stats['schemas']);
|
|
}
|
|
|
|
// ============================================
|
|
// Size Configuration
|
|
// ============================================
|
|
|
|
public function testMaxSizeMBChange(): void
|
|
{
|
|
$this->cache->setMaxSizeMB(10);
|
|
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
$this->cache->set("project{$i}", $this->createLargeSchema());
|
|
}
|
|
|
|
$this->cache->setMaxSizeMB(1);
|
|
|
|
$stats = $this->cache->getStats();
|
|
$this->assertLessThanOrEqual(1, $stats['memoryMB']);
|
|
}
|
|
|
|
public function testGetMaxSizeMB(): void
|
|
{
|
|
$this->cache->setMaxSizeMB(50);
|
|
$this->assertEquals(50, $this->cache->getMaxSizeMB());
|
|
}
|
|
|
|
public function testMinimumMaxSize(): void
|
|
{
|
|
$this->cache->setMaxSizeMB(0);
|
|
$this->assertEquals(1, $this->cache->getMaxSizeMB());
|
|
|
|
$this->cache->setMaxSizeMB(-5);
|
|
$this->assertEquals(1, $this->cache->getMaxSizeMB());
|
|
}
|
|
|
|
public function testSetMaxSizeNoChangeSkipsEviction(): void
|
|
{
|
|
$this->cache->setMaxSizeMB(10);
|
|
$this->cache->set('project1', $this->createMockSchema());
|
|
|
|
$sizeBefore = $this->cache->size();
|
|
|
|
// Same value should not trigger eviction
|
|
$this->cache->setMaxSizeMB(10);
|
|
|
|
$this->assertEquals($sizeBefore, $this->cache->size());
|
|
}
|
|
|
|
public function testConstructorWithCustomSize(): void
|
|
{
|
|
$cache = new Cache(100);
|
|
$this->assertEquals(100, $cache->getMaxSizeMB());
|
|
}
|
|
|
|
public function testConstructorWithZeroSize(): void
|
|
{
|
|
$cache = new Cache(0);
|
|
$this->assertEquals(1, $cache->getMaxSizeMB()); // Minimum is 1
|
|
}
|
|
|
|
public function testConstructorWithNegativeSize(): void
|
|
{
|
|
$cache = new Cache(-10);
|
|
$this->assertEquals(1, $cache->getMaxSizeMB()); // Minimum is 1
|
|
}
|
|
|
|
// ============================================
|
|
// Multiple Instances
|
|
// ============================================
|
|
|
|
public function testMultipleCacheInstances(): void
|
|
{
|
|
$cache1 = new Cache(10);
|
|
$cache2 = new Cache(20);
|
|
|
|
$schema = $this->createMockSchema();
|
|
|
|
$cache1->set('project1', $schema);
|
|
$cache2->set('project1', $schema);
|
|
|
|
$this->assertEquals(1, $cache1->size());
|
|
$this->assertEquals(1, $cache2->size());
|
|
|
|
$cache1->remove('project1');
|
|
$this->assertEquals(0, $cache1->size());
|
|
$this->assertEquals(1, $cache2->size());
|
|
}
|
|
|
|
public function testInstancesHaveIndependentDirtyFlags(): void
|
|
{
|
|
$cache1 = new Cache(10);
|
|
$cache2 = new Cache(10);
|
|
|
|
$cache1->set('project1', $this->createMockSchema());
|
|
$cache2->set('project1', $this->createMockSchema());
|
|
|
|
$cache1->setDirty('project1');
|
|
|
|
$this->assertTrue($cache1->isDirty('project1'));
|
|
$this->assertFalse($cache2->isDirty('project1'));
|
|
}
|
|
|
|
public function testInstancesHaveIndependentMemoryTracking(): void
|
|
{
|
|
$cache1 = new Cache(10);
|
|
$cache2 = new Cache(10);
|
|
|
|
$cache1->set('project1', $this->createLargeSchema());
|
|
|
|
$this->assertGreaterThan(0, $cache1->getCurrentBytes());
|
|
$this->assertEquals(0, $cache2->getCurrentBytes());
|
|
}
|
|
|
|
// ============================================
|
|
// Edge Cases - Project IDs
|
|
// ============================================
|
|
|
|
public function testEmptyProjectId(): void
|
|
{
|
|
$schema = $this->createMockSchema();
|
|
$this->cache->set('', $schema);
|
|
|
|
$this->assertSame($schema, $this->cache->get(''));
|
|
$this->assertEquals(1, $this->cache->size());
|
|
}
|
|
|
|
public function testProjectIdWithSpecialCharacters(): void
|
|
{
|
|
$schema = $this->createMockSchema();
|
|
|
|
$specialIds = [
|
|
'project-with-dashes',
|
|
'project_with_underscores',
|
|
'project.with.dots',
|
|
'project:with:colons',
|
|
'project/with/slashes',
|
|
'project@with@at',
|
|
'project#with#hash',
|
|
'project with spaces',
|
|
"project\twith\ttabs",
|
|
];
|
|
|
|
foreach ($specialIds as $id) {
|
|
$this->cache->set($id, $schema);
|
|
$this->assertSame($schema, $this->cache->get($id), "Failed for ID: {$id}");
|
|
}
|
|
|
|
$this->assertEquals(count($specialIds), $this->cache->size());
|
|
}
|
|
|
|
public function testProjectIdWithUnicode(): void
|
|
{
|
|
$schema = $this->createMockSchema();
|
|
|
|
$unicodeIds = [
|
|
'プロジェクト', // Japanese
|
|
'项目', // Chinese
|
|
'مشروع', // Arabic
|
|
'проект', // Russian
|
|
'🚀project', // Emoji
|
|
];
|
|
|
|
foreach ($unicodeIds as $id) {
|
|
$this->cache->set($id, $schema);
|
|
$this->assertSame($schema, $this->cache->get($id), "Failed for ID: {$id}");
|
|
}
|
|
}
|
|
|
|
public function testVeryLongProjectId(): void
|
|
{
|
|
$schema = $this->createMockSchema();
|
|
$longId = str_repeat('a', 10000);
|
|
|
|
$this->cache->set($longId, $schema);
|
|
$this->assertSame($schema, $this->cache->get($longId));
|
|
}
|
|
|
|
public function testNumericProjectId(): void
|
|
{
|
|
$schema = $this->createMockSchema();
|
|
|
|
$this->cache->set('123456', $schema);
|
|
$this->assertSame($schema, $this->cache->get('123456'));
|
|
}
|
|
|
|
// ============================================
|
|
// Edge Cases - Stress Tests
|
|
// ============================================
|
|
|
|
public function testManySmallSchemas(): void
|
|
{
|
|
$this->cache->setMaxSizeMB(10);
|
|
|
|
for ($i = 0; $i < 1000; $i++) {
|
|
$this->cache->set("project{$i}", $this->createMockSchema(1));
|
|
}
|
|
|
|
// Should have cached many small schemas
|
|
$this->assertGreaterThan(100, $this->cache->size());
|
|
}
|
|
|
|
public function testAlternatingSetAndGet(): void
|
|
{
|
|
for ($i = 0; $i < 100; $i++) {
|
|
$projectId = "project" . ($i % 10);
|
|
$this->cache->set($projectId, $this->createMockSchema($i + 1));
|
|
$this->assertNotNull($this->cache->get($projectId));
|
|
}
|
|
|
|
$this->assertGreaterThan(0, $this->cache->size());
|
|
$this->assertLessThanOrEqual(10, $this->cache->size());
|
|
}
|
|
|
|
public function testAlternatingDirtyAndSet(): void
|
|
{
|
|
for ($i = 0; $i < 50; $i++) {
|
|
$this->cache->set('project1', $this->createMockSchema($i + 1));
|
|
$this->cache->setDirty('project1');
|
|
$this->assertNull($this->cache->get('project1'));
|
|
}
|
|
|
|
$this->assertEquals(0, $this->cache->size());
|
|
}
|
|
|
|
// ============================================
|
|
// Edge Cases - Boundary Conditions
|
|
// ============================================
|
|
|
|
public function testSchemaExactlyAtMemoryLimit(): void
|
|
{
|
|
// This is tricky to test exactly, but we can verify behavior
|
|
$cache = new Cache(1); // 1 MB limit
|
|
|
|
// Add schemas until we hit the limit
|
|
$added = 0;
|
|
while ($cache->getCurrentBytes() < 1024 * 1024 && $added < 100) {
|
|
$cache->set("project{$added}", $this->createMockSchema(50));
|
|
$added++;
|
|
}
|
|
|
|
// Should have evicted some if over limit
|
|
$stats = $cache->getStats();
|
|
$this->assertLessThanOrEqual(1, $stats['memoryMB']);
|
|
}
|
|
|
|
public function testEvictionWhenNewSchemaLargerThanLimit(): void
|
|
{
|
|
$cache = new Cache(1); // 1 MB limit
|
|
|
|
// Add a small schema first
|
|
$cache->set('small', $this->createMockSchema(10));
|
|
|
|
// Try to add a very large schema (should evict small one)
|
|
$largeSchema = $this->createLargeSchema();
|
|
$cache->set('large', $largeSchema);
|
|
|
|
// Large schema should be cached (even if close to limit)
|
|
$this->assertNotNull($cache->get('large'));
|
|
}
|
|
|
|
// ============================================
|
|
// Regression Tests
|
|
// ============================================
|
|
|
|
public function testDirtyFlagDoesNotLeakMemory(): void
|
|
{
|
|
// Set dirty for many non-existent projects
|
|
for ($i = 0; $i < 1000; $i++) {
|
|
$this->cache->setDirty("nonexistent{$i}");
|
|
}
|
|
|
|
$stats = $this->cache->getStats();
|
|
$this->assertEquals(1000, $stats['dirty']);
|
|
|
|
// Clear should remove all dirty flags
|
|
$this->cache->clear();
|
|
$stats = $this->cache->getStats();
|
|
$this->assertEquals(0, $stats['dirty']);
|
|
}
|
|
|
|
public function testRemoveDoesNotAffectOtherEntries(): void
|
|
{
|
|
$schema1 = $this->createMockSchema(10);
|
|
$schema2 = $this->createMockSchema(20);
|
|
$schema3 = $this->createMockSchema(30);
|
|
|
|
$this->cache->set('project1', $schema1);
|
|
$this->cache->set('project2', $schema2);
|
|
$this->cache->set('project3', $schema3);
|
|
|
|
$this->cache->remove('project2');
|
|
|
|
$this->assertSame($schema1, $this->cache->get('project1'));
|
|
$this->assertNull($this->cache->get('project2'));
|
|
$this->assertSame($schema3, $this->cache->get('project3'));
|
|
}
|
|
|
|
public function testSetDirtyDoesNotAffectOtherEntries(): void
|
|
{
|
|
$schema1 = $this->createMockSchema();
|
|
$schema2 = $this->createMockSchema();
|
|
|
|
$this->cache->set('project1', $schema1);
|
|
$this->cache->set('project2', $schema2);
|
|
|
|
$this->cache->setDirty('project1');
|
|
|
|
$this->assertNull($this->cache->get('project1'));
|
|
$this->assertSame($schema2, $this->cache->get('project2'));
|
|
}
|
|
}
|