// Catch2 tests for ManifestManager // Migrated from GTest: manifest_test.cpp #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace yams; using namespace yams::manifest; using namespace yams::chunking; namespace { std::filesystem::path make_temp_dir(std::string_view prefix = "yams_manifest_test_") { namespace fs = std::filesystem; const auto base = fs::temp_directory_path(); std::uniform_int_distribution dist(0, 9999); thread_local std::mt19937_64 rng{std::random_device{}()}; for (int attempt = 0; attempt < 512; ++attempt) { const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); auto candidate = base / (std::string(prefix) + std::to_string(stamp) + "_" + std::to_string(dist(rng))); std::error_code ec; if (fs::create_directories(candidate, ec)) { return candidate; } } return base; } std::vector generateRandomBytes(std::size_t size) { static thread_local std::mt19937_64 engine{std::random_device{}()}; std::vector data(size); std::uniform_int_distribution dist(0, 255); std::generate(data.begin(), data.end(), [&] { return std::byte(dist(engine)); }); return data; } template void appendFixturePod(std::vector& output, T value) { static_assert(std::is_trivially_copyable_v); const auto bytes = std::as_bytes(std::span{&value, 1}); output.insert(output.end(), bytes.begin(), bytes.end()); } void appendFixtureString(std::vector& output, std::string_view value) { appendFixturePod(output, static_cast(value.size())); const auto bytes = std::as_bytes(std::span{value.data(), value.size()}); output.insert(output.end(), bytes.begin(), bytes.end()); } std::vector makeBinaryManifestFixture(bool legacy64BitFlags, std::uint64_t firstChunkFlags = 1) { const std::string kFileHash(64, 'f'); const std::string kFirstChunkHash(64, 'a'); const std::string kSecondChunkHash(64, 'b'); const std::string kThirdChunkHash(64, 'c'); std::vector output; const std::array magic{std::byte{'Y'}, std::byte{'A'}, std::byte{'M'}, std::byte{'S'}}; output.insert(output.end(), magic.begin(), magic.end()); appendFixturePod(output, Manifest::CURRENT_VERSION); appendFixtureString(output, kFileHash); appendFixturePod(output, std::uint64_t{18}); appendFixtureString(output, "legacy.bin"); appendFixtureString(output, "application/octet-stream"); appendFixturePod(output, std::uint32_t{3}); const auto appendChunk = [&](std::string_view hash, std::uint64_t offset, std::uint32_t size, std::uint64_t flags) { appendFixtureString(output, hash); appendFixturePod(output, offset); appendFixturePod(output, size); if (legacy64BitFlags) { appendFixturePod(output, flags); } else { appendFixturePod(output, static_cast(flags)); } }; appendChunk(kFirstChunkHash, 0, 5, firstChunkFlags); appendChunk(kSecondChunkHash, 5, 7, 2); appendChunk(kThirdChunkHash, 12, 6, 3); return output; } struct ManifestTestFixture { ManifestTestFixture() { testDir = make_temp_dir(); ManifestManager::Config config{.enableCompression = false, // Disable for predictable tests .enableChecksums = true, .enableCaching = true, .cacheSize = 100}; manager = std::make_unique(config); } ~ManifestTestFixture() { manager.reset(); std::error_code ec; std::filesystem::remove_all(testDir, ec); } // Helper to create test chunks std::vector createTestChunks(size_t count, size_t chunkSize = 1024) { std::vector chunks; chunks.reserve(count); uint64_t offset = 0; auto hasher = crypto::createSHA256Hasher(); for (size_t i = 0; i < count; ++i) { auto data = generateRandomBytes(chunkSize); auto hash = hasher->hash(data); chunks.emplace_back( Chunk{.data = data, .hash = hash, .offset = offset, .size = data.size()}); offset += data.size(); } return chunks; } // Helper to create test file info FileInfo createTestFileInfo(const std::vector& chunks) { uint64_t totalSize = 0; std::vector allData; for (const auto& chunk : chunks) { totalSize += chunk.size; allData.insert(allData.end(), chunk.data.begin(), chunk.data.end()); } auto hasher = crypto::createSHA256Hasher(); auto fileHash = hasher->hash(allData); return FileInfo{.hash = fileHash, .size = totalSize, .mimeType = "text/plain", .createdAt = std::chrono::system_clock::now(), .originalName = "test.txt"}; } std::filesystem::path testDir; std::unique_ptr manager; }; } // namespace TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager create simple manifest", "[manifest][catch2]") { auto chunks = createTestChunks(5, 512); auto fileInfo = createTestFileInfo(chunks); auto result = manager->createManifest(fileInfo, chunks); REQUIRE(result.has_value()); const auto& manifest = result.value(); CHECK((manifest.version == Manifest::CURRENT_VERSION)); CHECK((manifest.fileHash == fileInfo.hash)); CHECK((manifest.fileSize == fileInfo.size)); CHECK((manifest.originalName == fileInfo.originalName)); CHECK((manifest.mimeType == fileInfo.mimeType)); CHECK((manifest.chunks.size() == chunks.size())); // Verify chunk mapping for (size_t i = 0; i < chunks.size(); ++i) { CHECK((manifest.chunks[i].hash == chunks[i].hash)); CHECK((manifest.chunks[i].offset == chunks[i].offset)); CHECK((manifest.chunks[i].size == chunks[i].size)); } CHECK(manifest.isValid()); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager create empty manifest fails", "[manifest][catch2]") { std::vector emptyChunks; FileInfo fileInfo{.hash = "empty", .size = 0, .mimeType = "text/plain", .createdAt = std::chrono::system_clock::now(), .originalName = "empty.txt"}; auto result = manager->createManifest(fileInfo, emptyChunks); REQUIRE_FALSE(result.has_value()); CHECK((result.error() == ErrorCode::InvalidArgument)); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager create large manifest", "[manifest][catch2]") { // Test with many chunks auto chunks = createTestChunks(1000, 64); auto fileInfo = createTestFileInfo(chunks); auto result = manager->createManifest(fileInfo, chunks); REQUIRE(result.has_value()); const auto& manifest = result.value(); CHECK((manifest.chunks.size() == 1000u)); CHECK(manifest.isValid()); // Verify total size calculation CHECK((manifest.calculateTotalSize() == manifest.fileSize)); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager serialize deserialize roundtrip", "[manifest][catch2]") { auto chunks = createTestChunks(10, 256); auto fileInfo = createTestFileInfo(chunks); // Create manifest auto createResult = manager->createManifest(fileInfo, chunks); REQUIRE(createResult.has_value()); const auto& originalManifest = createResult.value(); // Serialize auto serializeResult = manager->serialize(originalManifest); REQUIRE(serializeResult.has_value()); const auto& serializedData = serializeResult.value(); // Verify serialized size is reasonable CHECK((serializedData.size() > 100u)); // Should have substantial content CHECK((serializedData.size() < originalManifest.fileSize)); // Should be less than original file size // Deserialize auto deserializeResult = manager->deserialize(serializedData); REQUIRE(deserializeResult.has_value()); const auto& deserializedManifest = deserializeResult.value(); // Verify roundtrip accuracy CHECK((deserializedManifest.version == originalManifest.version)); CHECK((deserializedManifest.fileHash == originalManifest.fileHash)); CHECK((deserializedManifest.fileSize == originalManifest.fileSize)); CHECK((deserializedManifest.originalName == originalManifest.originalName)); CHECK((deserializedManifest.mimeType == originalManifest.mimeType)); CHECK((deserializedManifest.chunks.size() == originalManifest.chunks.size())); // Verify all chunks match for (size_t i = 0; i < originalManifest.chunks.size(); ++i) { CHECK((deserializedManifest.chunks[i].hash == originalManifest.chunks[i].hash)); CHECK((deserializedManifest.chunks[i].offset == originalManifest.chunks[i].offset)); CHECK((deserializedManifest.chunks[i].size == originalManifest.chunks[i].size)); } } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager reads canonical 32-bit chunk flags", "[manifest][catch2][compatibility]") { const auto bytes = makeBinaryManifestFixture(false); auto result = manager->deserialize(bytes); REQUIRE(result.has_value()); REQUIRE((result.value().chunks.size() == 3)); CHECK((result.value().chunks[0].flags == 1)); CHECK((result.value().chunks[1].flags == 2)); CHECK((result.value().chunks[2].flags == 3)); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager reads legacy 64-bit chunk flags", "[manifest][catch2][compatibility]") { const auto bytes = makeBinaryManifestFixture(true); auto result = manager->deserialize(bytes); REQUIRE(result.has_value()); REQUIRE((result.value().chunks.size() == 3)); CHECK((result.value().chunks[0].flags == 1)); CHECK((result.value().chunks[1].flags == 2)); CHECK((result.value().chunks[2].flags == 3)); CHECK((result.value().calculateTotalSize() == result.value().fileSize)); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager rejects oversized legacy chunk flags", "[manifest][catch2][compatibility]") { const auto bytes = makeBinaryManifestFixture( true, static_cast(std::numeric_limits::max()) + 1); auto result = manager->deserialize(bytes); REQUIRE_FALSE(result.has_value()); CHECK((result.error() == ErrorCode::CorruptedData)); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager rejects trailing binary manifest bytes", "[manifest][catch2][compatibility]") { auto bytes = makeBinaryManifestFixture(false); appendFixturePod(bytes, std::uint32_t{0}); auto result = manager->deserialize(bytes); REQUIRE_FALSE(result.has_value()); CHECK((result.error() == ErrorCode::CorruptedData)); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager deserialize corrupted data fails", "[manifest][catch2]") { std::vector corruptedData{std::byte{0xFF}, std::byte{0xFE}, std::byte{0xFD}}; auto result = manager->deserialize(corruptedData); REQUIRE_FALSE(result.has_value()); CHECK((result.error() == ErrorCode::CorruptedData)); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager validate manifest", "[manifest][catch2]") { auto chunks = createTestChunks(5, 512); auto fileInfo = createTestFileInfo(chunks); auto createResult = manager->createManifest(fileInfo, chunks); REQUIRE(createResult.has_value()); auto manifest = createResult.value(); // Valid manifest auto validationResult = manager->validateManifest(manifest); REQUIRE(validationResult.has_value()); CHECK(validationResult.value()); // Test invalid manifest - wrong file size manifest.fileSize = 12345; validationResult = manager->validateManifest(manifest); REQUIRE(validationResult.has_value()); CHECK_FALSE(validationResult.value()); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager validate manifest with gaps", "[manifest][catch2]") { auto chunks = createTestChunks(3, 512); auto fileInfo = createTestFileInfo(chunks); auto createResult = manager->createManifest(fileInfo, chunks); REQUIRE(createResult.has_value()); auto manifest = createResult.value(); // Create gap in chunk offsets manifest.chunks[1].offset = manifest.chunks[0].offset + manifest.chunks[0].size + 100; auto validationResult = manager->validateManifest(manifest); REQUIRE(validationResult.has_value()); CHECK_FALSE(validationResult.value()); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager file reconstruction", "[manifest][catch2]") { auto chunks = createTestChunks(5, 1024); auto fileInfo = createTestFileInfo(chunks); // Create manifest auto createResult = manager->createManifest(fileInfo, chunks); REQUIRE(createResult.has_value()); const auto& manifest = createResult.value(); // Mock chunk provider struct MockChunkProvider : public IChunkProvider { const std::vector& chunks; explicit MockChunkProvider(const std::vector& c) : chunks(c) {} Result> getChunk(const std::string& hash) const override { for (const auto& chunk : chunks) { if (chunk.hash == hash) { return Result>(chunk.data); } } return Result>(ErrorCode::ChunkNotFound); } bool hasChunk(const std::string& hash) const { return std::ranges::any_of(chunks, [&hash](const auto& chunk) { return chunk.hash == hash; }); } }; MockChunkProvider provider(chunks); auto outputPath = testDir / "reconstructed.txt"; // Reconstruct file auto result = manager->reconstructFile(manifest, outputPath, provider); REQUIRE(result.has_value()); // Verify reconstructed file REQUIRE(std::filesystem::exists(outputPath)); CHECK((std::filesystem::file_size(outputPath) == manifest.fileSize)); // Verify file content matches original std::ifstream file(outputPath, std::ios::binary); REQUIRE(file.is_open()); for (const auto& chunk : chunks) { std::vector buffer(chunk.size); file.read(buffer.data(), static_cast(chunk.size)); REQUIRE(file.good()); // Compare chunk data auto chunkBytes = reinterpret_cast(buffer.data()); CHECK(std::equal(chunk.data.begin(), chunk.data.end(), chunkBytes)); } } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager file reconstruction missing chunk", "[manifest][catch2]") { auto chunks = createTestChunks(3, 512); auto fileInfo = createTestFileInfo(chunks); auto createResult = manager->createManifest(fileInfo, chunks); REQUIRE(createResult.has_value()); const auto& manifest = createResult.value(); // Mock provider missing the second chunk struct IncompleteChunkProvider : public IChunkProvider { const std::vector& chunks; explicit IncompleteChunkProvider(const std::vector& c) : chunks(c) {} Result> getChunk(const std::string& hash) const override { // Skip the second chunk for (size_t i = 0; i < chunks.size(); ++i) { if (i == 1) continue; // Skip second chunk if (chunks[i].hash == hash) { return Result>(chunks[i].data); } } return Result>(ErrorCode::ChunkNotFound); } bool hasChunk(const std::string& hash) const { for (size_t i = 0; i < chunks.size(); ++i) { if (i == 1) continue; if (chunks[i].hash == hash) return true; } return false; } }; IncompleteChunkProvider provider(chunks); auto outputPath = testDir / "failed_reconstruction.txt"; auto result = manager->reconstructFile(manifest, outputPath, provider); REQUIRE_FALSE(result.has_value()); CHECK((result.error() == ErrorCode::ChunkNotFound)); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager async file reconstruction", "[manifest][catch2]") { auto chunks = createTestChunks(3, 256); auto fileInfo = createTestFileInfo(chunks); auto createResult = manager->createManifest(fileInfo, chunks); REQUIRE(createResult.has_value()); const auto& manifest = createResult.value(); // Mock chunk provider with delays struct SlowChunkProvider : public IChunkProvider { const std::vector& chunks; explicit SlowChunkProvider(const std::vector& c) : chunks(c) {} Result> getChunk(const std::string& hash) const override { // Simulate network delay std::this_thread::sleep_for(std::chrono::milliseconds(10)); for (const auto& chunk : chunks) { if (chunk.hash == hash) { return Result>(chunk.data); } } return Result>(ErrorCode::ChunkNotFound); } bool hasChunk(const std::string& hash) const { return std::ranges::any_of(chunks, [&hash](const auto& chunk) { return chunk.hash == hash; }); } }; SlowChunkProvider provider(chunks); auto outputPath = testDir / "async_reconstructed.txt"; // Start async reconstruction auto future = manager->reconstructFileAsync(manifest, outputPath, provider); // Should complete within reasonable time auto status = future.wait_for(std::chrono::seconds(5)); REQUIRE((status == std::future_status::ready)); auto result = future.get(); REQUIRE(result.has_value()); // Verify file was created REQUIRE(std::filesystem::exists(outputPath)); CHECK((std::filesystem::file_size(outputPath) == manifest.fileSize)); } TEST_CASE("ManifestManager mime type detection", "[manifest][catch2]") { // Test various file extensions CHECK((ManifestManager::detectMimeType("test.txt") == "text/plain")); CHECK((ManifestManager::detectMimeType("config.json") == "application/json")); CHECK((ManifestManager::detectMimeType("image.jpg") == "image/jpeg")); CHECK((ManifestManager::detectMimeType("image.JPEG") == "image/jpeg")); // Case insensitive CHECK((ManifestManager::detectMimeType("document.pdf") == "application/pdf")); CHECK((ManifestManager::detectMimeType("unknown.xyz") == "application/octet-stream")); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager statistics", "[manifest][catch2]") { auto chunks = createTestChunks(5, 512); auto fileInfo = createTestFileInfo(chunks); // Create and serialize multiple manifests for (int i = 0; i < 3; ++i) { auto createResult = manager->createManifest(fileInfo, chunks); REQUIRE(createResult.has_value()); auto serializeResult = manager->serialize(createResult.value()); REQUIRE(serializeResult.has_value()); auto deserializeResult = manager->deserialize(serializeResult.value()); REQUIRE(deserializeResult.has_value()); } auto stats = manager->getStats(); CHECK((stats.totalManifests >= 3u)); // At least 3 manifests CHECK((stats.avgSerializationTime.count() > 0)); CHECK((stats.avgDeserializationTime.count() > 0)); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager concurrent operations", "[manifest][catch2]") { const int numThreads = 10; const int operationsPerThread = 5; std::vector threads; std::atomic successCount{0}; auto chunks = createTestChunks(3, 256); auto fileInfo = createTestFileInfo(chunks); for (int i = 0; i < numThreads; ++i) { threads.emplace_back([this, &chunks, &fileInfo, &successCount]() { for (int j = 0; j < operationsPerThread; ++j) { // Create manifest auto createResult = manager->createManifest(fileInfo, chunks); if (!createResult.has_value()) continue; // Serialize auto serializeResult = manager->serialize(createResult.value()); if (!serializeResult.has_value()) continue; // Deserialize auto deserializeResult = manager->deserialize(serializeResult.value()); if (!deserializeResult.has_value()) continue; // Validate auto validateResult = manager->validateManifest(deserializeResult.value()); if (!validateResult.has_value() || !validateResult.value()) continue; successCount++; } }); } for (auto& thread : threads) { thread.join(); } CHECK((successCount.load() == numThreads * operationsPerThread)); } TEST_CASE_METHOD(ManifestTestFixture, "ManifestManager performance benchmark", "[manifest][catch2][!benchmark]") { // Test serialization performance auto chunks = createTestChunks(100, 1024); // 100KB file auto fileInfo = createTestFileInfo(chunks); auto createResult = manager->createManifest(fileInfo, chunks); REQUIRE(createResult.has_value()); const auto& manifest = createResult.value(); // Benchmark serialization auto start = std::chrono::steady_clock::now(); for (int i = 0; i < 10; ++i) { auto result = manager->serialize(manifest); REQUIRE(result.has_value()); } auto end = std::chrono::steady_clock::now(); auto avgSerializationTime = std::chrono::duration_cast(end - start).count() / 10; // Should serialize 100KB file manifest in < 50ms CHECK((avgSerializationTime < 50)); // Verify manifest size is < 10% of file size auto serializeResult = manager->serialize(manifest); REQUIRE(serializeResult.has_value()); double manifestRatio = static_cast(serializeResult.value().size()) / manifest.fileSize; CHECK((manifestRatio < 0.1)); // < 10% }