testDir = sys_get_temp_dir() . '/cached_storage_test_' . uniqid(); mkdir($this->testDir, 0777, true); // Create wrapped storage $this->storage = new FileStorage(basePath: $this->testDir); $this->cachedStorage = new CachedFileStorage($this->storage, basePath: $this->testDir); }); afterEach(function () { // Cleanup test directory if (is_dir($this->testDir)) { deleteDirectoryRecursive($this->testDir); } }); describe('Directory Caching', function () { it('caches directory on first put', function () { $path = 'subdir/file.txt'; $content = 'test content'; // First put - directory will be created and cached $this->cachedStorage->put($path, $content); $stats = $this->cachedStorage->getCacheStats(); expect($stats['cached_directories'])->toBeGreaterThan(0); // Verify directory exists $dir = $this->testDir . '/subdir'; expect(is_dir($dir))->toBeTrue(); }); it('reuses cached directory on subsequent puts', function () { $dir = 'nested/deep/directory'; // First put - creates and caches directory $this->cachedStorage->put($dir . '/file1.txt', 'content1'); $statsBefore = $this->cachedStorage->getCacheStats(); $cacheSizeBefore = $statsBefore['cached_directories']; // Second put - reuses cached directory (no new is_dir call) $this->cachedStorage->put($dir . '/file2.txt', 'content2'); $statsAfter = $this->cachedStorage->getCacheStats(); $cacheSizeAfter = $statsAfter['cached_directories']; // Cache size should be same (directory already cached) expect($cacheSizeAfter)->toBe($cacheSizeBefore); // Both files should exist expect(file_exists($this->testDir . '/' . $dir . '/file1.txt'))->toBeTrue(); expect(file_exists($this->testDir . '/' . $dir . '/file2.txt'))->toBeTrue(); }); it('caches parent directories recursively', function () { $path = 'level1/level2/level3/file.txt'; $this->cachedStorage->put($path, 'test'); $stats = $this->cachedStorage->getCacheStats(); // Should cache level1, level1/level2, and level1/level2/level3 expect($stats['cached_directories'])->toBeGreaterThanOrEqual(3); }); it('handles absolute paths correctly', function () { $absolutePath = $this->testDir . '/absolute/file.txt'; $this->cachedStorage->put($absolutePath, 'test'); $stats = $this->cachedStorage->getCacheStats(); expect($stats['cached_directories'])->toBeGreaterThan(0); expect(file_exists($absolutePath))->toBeTrue(); }); }); describe('Read Operations', function () { it('delegates get to wrapped storage', function () { $path = 'test.txt'; $content = 'test content'; // Write file directly via wrapped storage $this->storage->put($path, $content); // Read via cached storage $result = $this->cachedStorage->get($path); expect($result)->toBe($content); }); it('throws FileNotFoundException for non-existent files', function () { expect(fn() => $this->cachedStorage->get('nonexistent.txt')) ->toThrow(FileNotFoundException::class); }); it('checks file existence correctly', function () { $path = 'exists.txt'; expect($this->cachedStorage->exists($path))->toBeFalse(); $this->cachedStorage->put($path, 'content'); expect($this->cachedStorage->exists($path))->toBeTrue(); }); }); describe('Write Operations', function () { it('creates nested directories automatically', function () { $path = 'deep/nested/structure/file.txt'; $content = 'nested content'; $this->cachedStorage->put($path, $content); expect(file_exists($this->testDir . '/' . $path))->toBeTrue(); expect(file_get_contents($this->testDir . '/' . $path))->toBe($content); }); it('overwrites existing files', function () { $path = 'overwrite.txt'; $this->cachedStorage->put($path, 'original'); expect($this->cachedStorage->get($path))->toBe('original'); $this->cachedStorage->put($path, 'updated'); expect($this->cachedStorage->get($path))->toBe('updated'); }); it('handles empty content', function () { $path = 'empty.txt'; $this->cachedStorage->put($path, ''); expect($this->cachedStorage->exists($path))->toBeTrue(); expect($this->cachedStorage->get($path))->toBe(''); }); }); describe('Copy Operations', function () { it('copies files with directory cache optimization', function () { $source = 'source.txt'; $destination = 'subdir/destination.txt'; // Create source file $this->cachedStorage->put($source, 'copy this'); // Copy to nested directory (should cache destination directory) $this->cachedStorage->copy($source, $destination); expect($this->cachedStorage->exists($destination))->toBeTrue(); expect($this->cachedStorage->get($destination))->toBe('copy this'); $stats = $this->cachedStorage->getCacheStats(); expect($stats['cached_directories'])->toBeGreaterThan(0); }); it('throws exception when copying non-existent file', function () { expect(fn() => $this->cachedStorage->copy('nonexistent.txt', 'destination.txt')) ->toThrow(FileNotFoundException::class); }); }); describe('Delete Operations', function () { it('deletes files', function () { $path = 'delete-me.txt'; $this->cachedStorage->put($path, 'content'); expect($this->cachedStorage->exists($path))->toBeTrue(); $this->cachedStorage->delete($path); expect($this->cachedStorage->exists($path))->toBeFalse(); }); it('throws exception when deleting non-existent file', function () { expect(fn() => $this->cachedStorage->delete('nonexistent.txt')) ->toThrow(FileNotFoundException::class); }); it('keeps directory in cache after file deletion', function () { $path = 'subdir/file.txt'; $this->cachedStorage->put($path, 'content'); $statsBefore = $this->cachedStorage->getCacheStats(); $cacheSizeBefore = $statsBefore['cached_directories']; $this->cachedStorage->delete($path); $statsAfter = $this->cachedStorage->getCacheStats(); $cacheSizeAfter = $statsAfter['cached_directories']; // Directory should still be cached (still exists, just empty) expect($cacheSizeAfter)->toBe($cacheSizeBefore); }); }); describe('Metadata Operations', function () { it('gets file size', function () { $path = 'sized.txt'; $content = 'test content with size'; $this->cachedStorage->put($path, $content); $size = $this->cachedStorage->size($path); expect($size)->toBe(strlen($content)); }); it('gets last modified timestamp', function () { $path = 'timestamped.txt'; $this->cachedStorage->put($path, 'content'); $lastModified = $this->cachedStorage->lastModified($path); expect($lastModified)->toBeInt(); expect($lastModified)->toBeLessThanOrEqual(time()); }); it('gets MIME type', function () { $path = 'file.txt'; $this->cachedStorage->put($path, 'text content'); $mimeType = $this->cachedStorage->getMimeType($path); expect($mimeType)->toContain('text'); }); }); describe('Batch Operations', function () { it('puts multiple files with directory cache optimization', function () { $files = [ 'batch1/file1.txt' => 'content1', 'batch1/file2.txt' => 'content2', 'batch2/file3.txt' => 'content3', ]; $this->cachedStorage->putMultiple($files); foreach ($files as $path => $content) { expect($this->cachedStorage->exists($path))->toBeTrue(); expect($this->cachedStorage->get($path))->toBe($content); } $stats = $this->cachedStorage->getCacheStats(); expect($stats['cached_directories'])->toBeGreaterThan(0); }); it('gets multiple files', function () { $files = [ 'multi1.txt' => 'content1', 'multi2.txt' => 'content2', 'multi3.txt' => 'content3', ]; // Write files $this->cachedStorage->putMultiple($files); // Read in batch $results = $this->cachedStorage->getMultiple(array_keys($files)); expect($results)->toBe($files); }); it('gets metadata for multiple files', function () { $files = [ 'meta1.txt' => 'content1', 'meta2.txt' => 'content2', ]; $this->cachedStorage->putMultiple($files); $metadata = $this->cachedStorage->getMetadataMultiple(array_keys($files)); expect($metadata)->toHaveCount(2); expect($metadata['meta1.txt']->size)->toBe(strlen('content1')); expect($metadata['meta2.txt']->size)->toBe(strlen('content2')); }); }); describe('Directory Listing', function () { it('lists directory contents', function () { $dir = 'listable'; $this->cachedStorage->put($dir . '/file1.txt', 'content1'); $this->cachedStorage->put($dir . '/file2.txt', 'content2'); $this->cachedStorage->put($dir . '/file3.txt', 'content3'); $contents = $this->cachedStorage->listDirectory($dir); // listDirectory returns array with numeric keys and relative path values (including directory prefix) expect($contents)->toHaveCount(3); // Convert to simple array values for comparison $files = array_values($contents); expect($files)->toContain('listable/file1.txt'); expect($files)->toContain('listable/file2.txt'); expect($files)->toContain('listable/file3.txt'); }); }); describe('Cache Management', function () { it('can clear directory cache', function () { $this->cachedStorage->put('cached/file.txt', 'content'); $statsBefore = $this->cachedStorage->getCacheStats(); expect($statsBefore['cached_directories'])->toBeGreaterThan(0); $this->cachedStorage->clearDirectoryCache(); $statsAfter = $this->cachedStorage->getCacheStats(); expect($statsAfter['cached_directories'])->toBe(0); }); it('provides accurate cache statistics', function () { $this->cachedStorage->put('level1/level2/file.txt', 'content'); $stats = $this->cachedStorage->getCacheStats(); expect($stats)->toHaveKeys(['cached_directories', 'cache_entries']); expect($stats['cached_directories'])->toBeGreaterThan(0); expect($stats['cache_entries'])->toBeArray(); }); it('checks if directory is cached', function () { // Use relative path that will be resolved $relativePath = 'cached_check/file.txt'; // Get the directory that will actually be cached $cachedDir = $this->testDir . '/cached_check'; expect($this->cachedStorage->isDirectoryCached($cachedDir))->toBeFalse(); $this->cachedStorage->put($relativePath, 'content'); // After put, the directory should be cached expect($this->cachedStorage->isDirectoryCached($cachedDir))->toBeTrue(); }); }); describe('Performance Characteristics', function () { it('reduces directory checks for repeated writes', function () { $dir = 'performance/test'; // First write creates and caches directory $this->cachedStorage->put($dir . '/file1.txt', 'content1'); $statsBefore = $this->cachedStorage->getCacheStats(); // Subsequent writes reuse cached directory for ($i = 2; $i <= 10; $i++) { $this->cachedStorage->put($dir . "/file{$i}.txt", "content{$i}"); } $statsAfter = $this->cachedStorage->getCacheStats(); // Cache size should not grow significantly (same directory) expect($statsAfter['cached_directories'])->toBeLessThanOrEqual( $statsBefore['cached_directories'] + 1 ); }); }); });