diff --git a/tests/testSaveIntegrity.cpp b/tests/testSaveIntegrity.cpp index 50a392e..c9c1530 100644 --- a/tests/testSaveIntegrity.cpp +++ b/tests/testSaveIntegrity.cpp @@ -475,3 +475,122 @@ VF_TEST(repairDropsInsteadOfRebuildingWhenRegenerationIsTurnedOff) { CHECK_EQ(repaired.chunksDropped, static_cast(1)); } +VF_TEST(repairRemovesAnOrphanEntryAndKeepsTheRest) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 3)); + EntrySlot slot = slotOf(buffer, 1); + saveWriteU32At(buffer.data(), buffer.size(), slot.entryAt + 16, 0u); + refreshTableChecksum(buffer); + + SaveScanner scanner; + RepairResult repaired = scanner.scanAndRepair(buffer); + CHECK(repaired.ok); + CHECK_EQ(repaired.orphanEntriesRemoved, static_cast(1)); + CHECK_EQ(repaired.chunksPreserved, static_cast(2)); + + SaveContainer rebuilt; + CHECK(readSaveContainer(repaired.output.data(), repaired.output.size(), rebuilt)); + CHECK_EQ(rebuilt.chunks.size(), static_cast(2)); + CHECK(scanner.scan(repaired.output).clean()); +} + +VF_TEST(repairIsRefusedWhenTheHeaderItselfIsFatal) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 2)); + saveWriteU32At(buffer.data(), buffer.size(), 0, 0u); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + RepairPlan plan = scanner.planRepair(buffer.data(), buffer.size(), report); + CHECK(!plan.possible); + CHECK(plan.actions.empty()); + CHECK(plan.refusal.find("backup") != std::string::npos); + + RepairResult repaired = scanner.repair(buffer.data(), buffer.size(), plan); + CHECK(!repaired.ok); + CHECK(repaired.output.empty()); + CHECK_EQ(repaired.failure, plan.refusal); +} + +VF_TEST(repairPlanIsRefusedForABufferItWasNotBuiltFrom) { + ByteBuffer first = writeSaveContainer(buildContainer(SaveVersion::V4, 3)); + ByteBuffer second = writeSaveContainer(buildContainer(SaveVersion::V4, 2)); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(first); + RepairPlan plan = scanner.planRepair(second.data(), second.size(), report); + CHECK(!plan.possible); + CHECK(plan.refusal.find("different buffer") != std::string::npos); +} + +VF_TEST(regeneratedTerrainIsDeterministicAndFollowsTheSeed) { + Chunk first = regenerateChunkFromSeed(99u, Vec3i{1, 2, 3}); + Chunk again = regenerateChunkFromSeed(99u, Vec3i{1, 2, 3}); + Chunk elsewhere = regenerateChunkFromSeed(99u, Vec3i{4, 2, 3}); + Chunk otherSeed = regenerateChunkFromSeed(100u, Vec3i{1, 2, 3}); + + CHECK(sameBlocks(first, again)); + CHECK(!sameBlocks(first, elsewhere)); + CHECK(!sameBlocks(first, otherSeed)); + + CHECK_EQ(regeneratedSurfaceHeight(99u, 0, 0), regeneratedSurfaceHeight(99u, 0, 0)); + CHECK(regeneratedSurfaceHeight(99u, 0, 0) >= 32); + CHECK(regeneratedSurfaceHeight(99u, 0, 0) <= 64); + CHECK(regeneratedSurfaceHeight(99u, 7, 9) >= 32); + CHECK(regeneratedSurfaceHeight(99u, 7, 9) <= 64); +} + +VF_TEST(severityAndActionNamesStayReadable) { + CHECK(std::string(issueSeverityName(IssueSeverity::Info)) == "info"); + CHECK(std::string(issueSeverityName(IssueSeverity::Warning)) == "warning"); + CHECK(std::string(issueSeverityName(IssueSeverity::Recoverable)) == "recoverable"); + CHECK(std::string(issueSeverityName(IssueSeverity::Fatal)) == "fatal"); + CHECK(IssueSeverity::Info < IssueSeverity::Warning); + CHECK(IssueSeverity::Warning < IssueSeverity::Recoverable); + CHECK(IssueSeverity::Recoverable < IssueSeverity::Fatal); + + CHECK(std::string(integrityIssueName(IntegrityIssueKind::BadMagic)) == "badMagic"); + CHECK(std::string(integrityIssueName(IntegrityIssueKind::LightingInconsistent)) == + "lightingInconsistent"); + CHECK(std::string(repairActionName(RepairActionKind::RegenerateChunk)) == "regenerateChunk"); + CHECK(std::string(repairActionName(RepairActionKind::RemoveOrphanEntry)) == + "removeOrphanEntry"); +} + +VF_TEST(aContainerDamagedInSeveralPlacesIsSalvagedInOnePass) { + SaveContainer container = buildContainer(SaveVersion::V4, 5); + ByteBuffer healthy = writeSaveContainer(container); + ByteBuffer damaged = healthy; + + EntrySlot broken = slotOf(damaged, 0); + saveWriteU32At(damaged.data(), damaged.size(), broken.payloadAt + 8, 0u); + + EntrySlot orphan = slotOf(damaged, 2); + saveWriteU32At(damaged.data(), damaged.size(), orphan.entryAt + 16, 0u); + + EntrySlot stale = slotOf(damaged, 3); + std::size_t checksumAt = stale.payloadAt + stale.payloadBytes - 4; + damaged[checksumAt] = static_cast(damaged[checksumAt] ^ 0x0Fu); + refreshTableChecksum(damaged); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(damaged); + CHECK_EQ(report.countOf(IntegrityIssueKind::MalformedRecord), static_cast(1)); + CHECK_EQ(report.countOf(IntegrityIssueKind::OrphanEntry), static_cast(1)); + CHECK_EQ(report.countOf(IntegrityIssueKind::ChecksumMismatch), static_cast(1)); + + RepairResult repaired = scanner.scanAndRepair(damaged); + CHECK(repaired.ok); + CHECK_EQ(repaired.chunksDropped, static_cast(1)); + CHECK_EQ(repaired.orphanEntriesRemoved, static_cast(1)); + CHECK_EQ(repaired.checksumsRewritten, static_cast(1)); + CHECK_EQ(repaired.chunksPreserved, static_cast(3)); + CHECK(scanner.scan(repaired.output).clean()); + + const Vec3i survivors[3] = {{1, 0, 2}, {3, 0, 6}, {4, 0, 8}}; + for (const Vec3i& coord : survivors) { + ByteBuffer before; + ByteBuffer after; + CHECK(readChunkPayloadBytes(healthy.data(), healthy.size(), coord, before)); + CHECK(readChunkPayloadBytes(repaired.output.data(), repaired.output.size(), coord, after)); + CHECK(before == after); + } +}