- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
201 lines
6.7 KiB
PHP
201 lines
6.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Byte;
|
|
use App\Framework\Filesystem\FileStorage;
|
|
use App\Framework\LiveComponents\Services\ChunkAssembler;
|
|
|
|
describe('ChunkAssembler Service', function () {
|
|
beforeEach(function () {
|
|
// Create temporary directory for test files
|
|
$this->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);
|
|
});
|
|
});
|