testDir = sys_get_temp_dir() . '/chunk_assembler_test_' . uniqid(); mkdir($this->testDir); $this->fileStorage = new FileStorage(); $this->assembler = new ChunkAssembler($this->fileStorage); }); afterEach(function () { // Clean up test directory if (is_dir($this->testDir)) { $files = glob($this->testDir . '/*'); foreach ($files as $file) { if (is_file($file)) { unlink($file); } } rmdir($this->testDir); } }); it('assembles multiple chunks into single file', function () { // Create 3 test chunks $chunk1Path = $this->testDir . '/chunk_0'; $chunk2Path = $this->testDir . '/chunk_1'; $chunk3Path = $this->testDir . '/chunk_2'; file_put_contents($chunk1Path, 'chunk 1 data'); file_put_contents($chunk2Path, 'chunk 2 data'); file_put_contents($chunk3Path, 'chunk 3 data'); $targetPath = $this->testDir . '/assembled.txt'; $this->assembler->assemble( [$chunk1Path, $chunk2Path, $chunk3Path], $targetPath ); expect(file_exists($targetPath))->toBeTrue(); expect(file_get_contents($targetPath))->toBe('chunk 1 datachunk 2 datachunk 3 data'); }); it('assembles single chunk', function () { $chunkPath = $this->testDir . '/chunk_0'; file_put_contents($chunkPath, 'single chunk data'); $targetPath = $this->testDir . '/assembled.txt'; $this->assembler->assemble([$chunkPath], $targetPath); expect(file_exists($targetPath))->toBeTrue(); expect(file_get_contents($targetPath))->toBe('single chunk data'); }); it('throws exception for empty chunk array', function () { $targetPath = $this->testDir . '/assembled.txt'; expect(fn() => $this->assembler->assemble([], $targetPath)) ->toThrow(InvalidArgumentException::class); }); it('throws exception for missing chunk file', function () { $existingChunk = $this->testDir . '/chunk_0'; $missingChunk = $this->testDir . '/chunk_1_missing'; file_put_contents($existingChunk, 'chunk data'); $targetPath = $this->testDir . '/assembled.txt'; expect(fn() => $this->assembler->assemble( [$existingChunk, $missingChunk], $targetPath ))->toThrow(InvalidArgumentException::class); }); it('assembles large chunks efficiently', function () { // Create 3 chunks of 1MB each $chunk1Path = $this->testDir . '/chunk_0'; $chunk2Path = $this->testDir . '/chunk_1'; $chunk3Path = $this->testDir . '/chunk_2'; file_put_contents($chunk1Path, str_repeat('A', 1024 * 1024)); file_put_contents($chunk2Path, str_repeat('B', 1024 * 1024)); file_put_contents($chunk3Path, str_repeat('C', 1024 * 1024)); $targetPath = $this->testDir . '/assembled_large.bin'; $this->assembler->assemble( [$chunk1Path, $chunk2Path, $chunk3Path], $targetPath ); expect(file_exists($targetPath))->toBeTrue(); expect(filesize($targetPath))->toBe(3 * 1024 * 1024); }); it('calculates total size correctly', function () { $chunk1Path = $this->testDir . '/chunk_0'; $chunk2Path = $this->testDir . '/chunk_1'; file_put_contents($chunk1Path, str_repeat('A', 1024)); // 1KB file_put_contents($chunk2Path, str_repeat('B', 2048)); // 2KB $totalSize = $this->assembler->calculateTotalSize([$chunk1Path, $chunk2Path]); expect($totalSize->toBytes())->toBe(3072); // 3KB expect($totalSize->toKilobytes())->toBe(3.0); }); it('handles empty chunk files', function () { $chunk1Path = $this->testDir . '/chunk_0'; $chunk2Path = $this->testDir . '/chunk_1'; file_put_contents($chunk1Path, 'data'); file_put_contents($chunk2Path, ''); // Empty chunk $targetPath = $this->testDir . '/assembled.txt'; $this->assembler->assemble([$chunk1Path, $chunk2Path], $targetPath); expect(file_exists($targetPath))->toBeTrue(); expect(file_get_contents($targetPath))->toBe('data'); }); it('preserves chunk order during assembly', function () { $paths = []; for ($i = 0; $i < 5; $i++) { $path = $this->testDir . "/chunk_$i"; file_put_contents($path, "chunk$i"); $paths[] = $path; } $targetPath = $this->testDir . '/assembled.txt'; $this->assembler->assemble($paths, $targetPath); expect(file_get_contents($targetPath))->toBe('chunk0chunk1chunk2chunk3chunk4'); }); it('assembles binary data correctly', function () { $chunk1Path = $this->testDir . '/chunk_0'; $chunk2Path = $this->testDir . '/chunk_1'; $binaryData1 = random_bytes(256); $binaryData2 = random_bytes(256); file_put_contents($chunk1Path, $binaryData1); file_put_contents($chunk2Path, $binaryData2); $targetPath = $this->testDir . '/assembled.bin'; $this->assembler->assemble([$chunk1Path, $chunk2Path], $targetPath); $assembled = file_get_contents($targetPath); expect($assembled)->toBe($binaryData1 . $binaryData2); }); it('returns zero size for no chunks', function () { $totalSize = $this->assembler->calculateTotalSize([]); expect($totalSize->toBytes())->toBe(0); expect($totalSize->isEmpty())->toBeTrue(); }); it('skips missing chunks in size calculation', function () { $chunk1Path = $this->testDir . '/chunk_0'; $missingPath = $this->testDir . '/chunk_missing'; file_put_contents($chunk1Path, str_repeat('A', 1024)); $totalSize = $this->assembler->calculateTotalSize([$chunk1Path, $missingPath]); expect($totalSize->toBytes())->toBe(1024); // Only existing chunk counted }); it('uses custom buffer size when provided', function () { $customBufferSize = Byte::fromKilobytes(16); $assembler = new ChunkAssembler($this->fileStorage, $customBufferSize); $chunkPath = $this->testDir . '/chunk_0'; file_put_contents($chunkPath, str_repeat('A', 32 * 1024)); // 32KB $targetPath = $this->testDir . '/assembled.txt'; $assembler->assemble([$chunkPath], $targetPath); expect(file_exists($targetPath))->toBeTrue(); expect(filesize($targetPath))->toBe(32 * 1024); }); });