cache = new ClassCache($methodCache, $attributeCache); } /** * Test that ClassCache doesn't leak memory with repeated class access */ public function test_class_cache_memory_stability(): void { $initialMemory = memory_get_usage(true); $testClasses = [stdClass::class, TestCase::class, \Exception::class, \DateTime::class]; // Simulate heavy usage for ($iteration = 0; $iteration < 100; $iteration++) { foreach ($testClasses as $className) { $classNameObj = ClassName::create($className); // Access different cached properties $this->cache->getNativeClass($classNameObj); $this->cache->getWrappedClass($classNameObj); $this->cache->isInstantiable($classNameObj); $this->cache->implementsInterface($classNameObj, 'JsonSerializable'); } if ($iteration % 25 === 0) { gc_collect_cycles(); } } $finalMemory = memory_get_usage(true); $memoryGrowth = $finalMemory - $initialMemory; $stats = $this->cache->getStats(); // Memory growth should be bounded $this->assertLessThan( 2 * 1024 * 1024, $memoryGrowth, "ClassCache grew by " . number_format($memoryGrowth) . " bytes" ); // Statistics should show reasonable counts $counters = $stats->getCounters(); $this->assertArrayHasKey('classes', $counters); $this->assertLessThanOrEqual( count($testClasses), $counters['classes'], "ClassCache should not cache more classes than accessed" ); echo "\nClassCache Memory Growth: " . number_format($memoryGrowth) . " bytes\n"; echo "Cached Classes: " . $counters['classes'] . "\n"; echo "Total Cache Items: " . $stats->getTotalCount() . "\n"; } /** * Test that cache eviction works if implemented */ public function test_cache_eviction_prevents_unbounded_growth(): void { $initialStats = $this->cache->getStats(); $testClasses = $this->generateManyTestClasses(500); // Fill cache beyond reasonable limits foreach ($testClasses as $className) { if (class_exists($className)) { $classNameObj = ClassName::create($className); $this->cache->getNativeClass($classNameObj); } } $finalStats = $this->cache->getStats(); $cacheGrowth = $finalStats->getTotalCount() - $initialStats->getTotalCount(); // If cache has size limits, it should not grow indefinitely if ($cacheGrowth > 100) { $this->addWarning("Cache grew by {$cacheGrowth} items. Consider implementing cache size limits."); } echo "\nCache growth: {$cacheGrowth} items\n"; } /** * Test that cache flush completely clears memory */ public function test_cache_flush_clears_all_data(): void { $testClasses = [stdClass::class, TestCase::class, \Exception::class]; // Fill cache foreach ($testClasses as $className) { $classNameObj = ClassName::create($className); $this->cache->getNativeClass($classNameObj); $this->cache->getWrappedClass($classNameObj); } $statsBeforeFlush = $this->cache->getStats(); $this->assertGreaterThan(0, $statsBeforeFlush->getTotalCount()); // Flush cache $this->cache->flush(); $statsAfterFlush = $this->cache->getStats(); $this->assertEquals( 0, $statsAfterFlush->getTotalCount(), "Cache should be empty after flush" ); // Verify all cache types are cleared $counters = $statsAfterFlush->getCounters(); foreach ($counters as $cacheType => $count) { $this->assertEquals(0, $count, "Cache type '{$cacheType}' should be empty after flush"); } } /** * Test individual class cache forget functionality */ public function test_individual_class_forget(): void { $className1 = stdClass::class; $className2 = TestCase::class; $classNameObj1 = ClassName::create($className1); $classNameObj2 = ClassName::create($className2); // Cache both classes $this->cache->getNativeClass($classNameObj1); $this->cache->getNativeClass($classNameObj2); $statsAfterCaching = $this->cache->getStats(); $this->assertGreaterThanOrEqual(2, $statsAfterCaching->getCounter('classes')); // Forget one class $this->cache->forget($classNameObj1); $statsAfterForget = $this->cache->getStats(); $this->assertLessThan( $statsAfterCaching->getTotalCount(), $statsAfterForget->getTotalCount(), "Total cache count should decrease after forgetting a class" ); } /** * Test cache performance with many lookups */ public function test_cache_performance_stability(): void { $testClasses = [stdClass::class, TestCase::class, \Exception::class, \DateTime::class, \SplFileInfo::class]; // Pre-fill cache foreach ($testClasses as $className) { $classNameObj = ClassName::create($className); $this->cache->getNativeClass($classNameObj); } $startTime = microtime(true); $memoryBefore = memory_get_usage(true); // Many cache hits for ($i = 0; $i < 1000; $i++) { $randomClass = $testClasses[array_rand($testClasses)]; $classNameObj = ClassName::create($randomClass); $class = $this->cache->getNativeClass($classNameObj); $this->assertInstanceOf(\ReflectionClass::class, $class); } $endTime = microtime(true); $memoryAfter = memory_get_usage(true); $duration = $endTime - $startTime; $memoryGrowth = $memoryAfter - $memoryBefore; // Performance should be good $this->assertLessThan(0.1, $duration, "1000 cache hits took {$duration}s"); // Memory should not grow significantly with cache hits $this->assertLessThan( 100 * 1024, $memoryGrowth, "Memory grew by " . number_format($memoryGrowth) . " bytes during cache hits" ); echo "\nCache hit performance: " . number_format(1000 / $duration, 0) . " hits/second\n"; echo "Memory growth during hits: " . number_format($memoryGrowth) . " bytes\n"; } /** * Generate many test class names (some may not exist) * @return array */ private function generateManyTestClasses(int $count): array { $classes = []; $baseClasses = [ stdClass::class, TestCase::class, \Exception::class, \DateTime::class, \SplFileInfo::class, \ArrayObject::class, \RuntimeException::class, \InvalidArgumentException::class, ]; for ($i = 0; $i < $count; $i++) { $classes[] = $baseClasses[$i % count($baseClasses)]; } return $classes; } }