#include "testHarness.h" #include "vf/Block.h" #include "vf/Region.h" #include "vf/SaveIntegrity.h" #include "vf/SaveMigration.h" #include #include using namespace vf; namespace { Chunk texturedChunk(int seed) { Chunk chunk; for (int x = 0; x < chunkSize; ++x) for (int y = 0; y < chunkSize; ++y) for (int z = 0; z < chunkSize; ++z) { int value = 1 + (x * 7 + y * 5 + z * 3 + seed) % 12; chunk.set(x, y, z, static_cast(value)); } return chunk; } SaveChunkRecord buildRecord(const Vec3i& coord, int seed, SaveVersion version) { SaveChunkRecord record; record.coord = coord; record.blocks = texturedChunk(seed); if (versionHasLight(version)) record.light = defaultChunkLight(record.blocks, saveDefaultSkyLight); return record; } SaveContainer buildContainer(SaveVersion version, int chunkTotal) { SaveContainer container; container.version = version; for (int i = 0; i < chunkTotal; ++i) { Vec3i coord{i % 4, i / 4, (i * 3) % 5}; container.chunks.push_back(buildRecord(coord, i, version)); } return container; } std::size_t distinctBlocks(const Chunk& chunk) { bool seen[256] = {false}; std::size_t total = 0; for (Block block : chunk.raw()) { std::size_t slot = static_cast(block); if (seen[slot]) continue; seen[slot] = true; ++total; } return total; } bool sameBlocks(const Chunk& a, const Chunk& b) { for (std::size_t i = 0; i < a.raw().size(); ++i) if (a.raw()[i] != b.raw()[i]) return false; return true; } } // namespace VF_TEST(containerRoundTripsAtEveryFormatVersion) { for (uint32_t v = 1; v <= 4; ++v) { SaveVersion version = static_cast(v); SaveContainer original = buildContainer(version, 3); ByteBuffer buffer = writeSaveContainer(original); SaveContainer reloaded; CHECK(readSaveContainer(buffer.data(), buffer.size(), reloaded)); CHECK(reloaded.version == version); CHECK_EQ(reloaded.chunks.size(), original.chunks.size()); for (const SaveChunkRecord& record : original.chunks) { const SaveChunkRecord* back = reloaded.find(record.coord); CHECK(back != nullptr); if (back == nullptr) continue; CHECK(sameBlocks(record.blocks, back->blocks)); CHECK(record.light == back->light); } } } VF_TEST(everyFreshContainerUsesTheCanonicalLayout) { for (uint32_t v = 1; v <= 4; ++v) { SaveContainer container = buildContainer(static_cast(v), 4); ByteBuffer buffer = writeSaveContainer(container); CHECK(isCanonicalSaveLayout(buffer.data(), buffer.size())); CHECK_EQ(buffer.size() % saveSectorBytes, static_cast(0)); } } VF_TEST(versionFeatureFlagsMatchTheDocumentedFormatTable) { CHECK(!versionHasPalette(SaveVersion::V1)); CHECK(versionHasPalette(SaveVersion::V2)); CHECK(!versionHasLight(SaveVersion::V2)); CHECK(versionHasLight(SaveVersion::V3)); CHECK(!versionHasChecksum(SaveVersion::V2)); CHECK(versionHasChecksum(SaveVersion::V3)); CHECK(!versionHasMetadata(SaveVersion::V3)); CHECK(versionHasMetadata(SaveVersion::V4)); CHECK(currentSaveVersion == SaveVersion::V4); CHECK(oldestSaveVersion == SaveVersion::V1); CHECK(std::string(saveVersionName(SaveVersion::V1)) == "v1"); CHECK(std::string(saveVersionName(SaveVersion::V4)) == "v4"); CHECK(isKnownSaveVersion(1u)); CHECK(isKnownSaveVersion(4u)); CHECK(!isKnownSaveVersion(0u)); CHECK(!isKnownSaveVersion(5u)); } VF_TEST(migrationChainFromV1ToV4KeepsEveryBlockIdentical) { SaveContainer original = buildContainer(SaveVersion::V1, 5); ByteBuffer legacy = writeSaveContainer(original); SaveMigrator migrator; MigrationResult result = migrator.migrateToCurrent(legacy); CHECK(result.ok); CHECK(result.failure.empty()); CHECK(result.from == SaveVersion::V1); CHECK(result.to == SaveVersion::V4); CHECK_EQ(result.chunksMigrated, static_cast(5)); SaveContainer lifted; CHECK(readSaveContainer(result.output.data(), result.output.size(), lifted)); CHECK(lifted.version == SaveVersion::V4); CHECK_EQ(lifted.chunks.size(), original.chunks.size()); for (const SaveChunkRecord& before : original.chunks) { const SaveChunkRecord* after = lifted.find(before.coord); CHECK(after != nullptr); if (after == nullptr) continue; CHECK(sameBlocks(before.blocks, after->blocks)); } } VF_TEST(migrationAppliesOneStepPerVersionHop) { SaveContainer original = buildContainer(SaveVersion::V1, 2); ByteBuffer legacy = writeSaveContainer(original); SaveMigrator migrator; MigrationResult result = migrator.migrateToCurrent(legacy); CHECK(result.ok); CHECK_EQ(result.applied.size(), static_cast(3)); CHECK(result.applied[0].from == SaveVersion::V1); CHECK(result.applied[0].to == SaveVersion::V2); CHECK(result.applied[1].from == SaveVersion::V2); CHECK(result.applied[1].to == SaveVersion::V3); CHECK(result.applied[2].from == SaveVersion::V3); CHECK(result.applied[2].to == SaveVersion::V4); for (const MigrationStep& step : result.applied) CHECK(!step.summary.empty()); MigrationPlan plan = SaveMigrator::planFor(SaveVersion::V2, SaveVersion::V4); CHECK(plan.possible); CHECK(plan.refusal.empty()); CHECK_EQ(plan.steps.size(), static_cast(2)); } VF_TEST(everyIntermediateVersionIsItselfAValidContainer) { SaveContainer original = buildContainer(SaveVersion::V1, 3); ByteBuffer legacy = writeSaveContainer(original); SaveMigrator migrator; SaveScanner scanner; for (uint32_t target = 2; target <= 4; ++target) { MigrationResult result = migrator.migrate(legacy.data(), legacy.size(), static_cast(target)); CHECK(result.ok); IntegrityReport report = scanner.scan(result.output); CHECK(report.clean()); CHECK(report.version == static_cast(target)); CHECK_EQ(report.readableChunks, static_cast(3)); CHECK(isCanonicalSaveLayout(result.output.data(), result.output.size())); } } VF_TEST(migrationDownwardIsRefusedWithAStatedReason) { SaveContainer modern = buildContainer(SaveVersion::V4, 2); ByteBuffer buffer = writeSaveContainer(modern); SaveMigrator migrator; MigrationResult result = migrator.migrate(buffer.data(), buffer.size(), SaveVersion::V2); CHECK(!result.ok); CHECK(result.output.empty()); CHECK(result.failure.find("downgrade") != std::string::npos); CHECK(result.failure.find("v4 -> v2") != std::string::npos); MigrationPlan plan = SaveMigrator::planFor(SaveVersion::V4, SaveVersion::V1); CHECK(!plan.possible); CHECK(plan.steps.empty()); CHECK(!plan.refusal.empty()); } VF_TEST(migrationToTheSameVersionCopiesTheBufferVerbatim) { SaveContainer modern = buildContainer(SaveVersion::V4, 3); ByteBuffer buffer = writeSaveContainer(modern); SaveMigrator migrator; MigrationResult result = migrator.migrate(buffer.data(), buffer.size(), SaveVersion::V4); CHECK(result.ok); CHECK(result.applied.empty()); CHECK(result.output == buffer); } VF_TEST(fieldsAddedByAMigrationGetTheirDocumentedDefaults) { SaveContainer original = buildContainer(SaveVersion::V2, 2); ByteBuffer buffer = writeSaveContainer(original); SaveMigrator migrator; MigrationResult toV3 = migrator.migrate(buffer.data(), buffer.size(), SaveVersion::V3); CHECK(toV3.ok); SaveContainer lit; CHECK(readSaveContainer(toV3.output.data(), toV3.output.size(), lit)); for (const SaveChunkRecord& record : lit.chunks) { CHECK_EQ(record.light.size(), static_cast(chunkVolume)); CHECK(record.light == defaultChunkLight(record.blocks, saveDefaultSkyLight)); } MigrationResult toV4 = migrator.migrate(buffer.data(), buffer.size(), SaveVersion::V4); CHECK(toV4.ok); SaveContainer stamped; CHECK(readSaveContainer(toV4.output.data(), toV4.output.size(), stamped)); for (const SaveChunkRecord& record : stamped.chunks) { CHECK_EQ(record.meta.inhabitedTicks, static_cast(0)); CHECK_EQ(record.meta.lastSavedTick, static_cast(0)); CHECK_EQ(record.meta.generationStage, saveStageComplete); } } VF_TEST(migrationCountsTheFieldsEachStepCreated) { SaveContainer original = buildContainer(SaveVersion::V1, 3); ByteBuffer legacy = writeSaveContainer(original); std::size_t expectedPaletteEntries = 0; for (const SaveChunkRecord& record : original.chunks) expectedPaletteEntries += distinctBlocks(record.blocks); SaveMigrator migrator; MigrationResult result = migrator.migrateToCurrent(legacy); CHECK(result.ok); CHECK_EQ(result.paletteEntriesCreated, expectedPaletteEntries); CHECK_EQ(result.lightCellsInitialized, static_cast(3 * chunkVolume)); CHECK_EQ(result.metadataRecordsCreated, static_cast(3)); } VF_TEST(migratorHonoursItsConfiguredDefaults) { SaveContainer original = buildContainer(SaveVersion::V2, 2); ByteBuffer buffer = writeSaveContainer(original); SaveMigrator migrator; migrator.skyLightForNewPlanes = 7; migrator.metadataDefaults.inhabitedTicks = 1234; migrator.metadataDefaults.lastSavedTick = 9999; migrator.metadataDefaults.generationStage = 1; MigrationResult result = migrator.migrateToCurrent(buffer); CHECK(result.ok); SaveContainer lifted; CHECK(readSaveContainer(result.output.data(), result.output.size(), lifted)); for (const SaveChunkRecord& record : lifted.chunks) { CHECK(record.light == defaultChunkLight(record.blocks, 7)); CHECK_EQ(record.meta.inhabitedTicks, static_cast(1234)); CHECK_EQ(record.meta.lastSavedTick, static_cast(9999)); CHECK_EQ(record.meta.generationStage, static_cast(1)); } } VF_TEST(migratorCanCompressTheFinalHopAndStillReadItBack) { SaveContainer original = buildContainer(SaveVersion::V1, 3); ByteBuffer legacy = writeSaveContainer(original); SaveMigrator plain; MigrationResult loose = plain.migrateToCurrent(legacy); CHECK(loose.ok); SaveMigrator packing; packing.compressMigratedPayloads = true; MigrationResult tight = packing.migrateToCurrent(legacy); CHECK(tight.ok); CHECK(tight.output.size() < loose.output.size()); SaveContainer reloaded; CHECK(readSaveContainer(tight.output.data(), tight.output.size(), reloaded)); CHECK(reloaded.compressPayloads); for (const SaveChunkRecord& before : original.chunks) { const SaveChunkRecord* after = reloaded.find(before.coord); CHECK(after != nullptr); if (after != nullptr) CHECK(sameBlocks(before.blocks, after->blocks)); } SaveScanner scanner; CHECK(scanner.scan(tight.output).clean()); } VF_TEST(migrationRefusesAHeaderItCannotIdentify) { SaveContainer original = buildContainer(SaveVersion::V1, 2); ByteBuffer buffer = writeSaveContainer(original); saveWriteU32At(buffer.data(), buffer.size(), 4, 7u); SaveMigrator migrator; MigrationResult result = migrator.migrateToCurrent(buffer); CHECK(!result.ok); CHECK(result.output.empty()); CHECK(result.failure.find("header") != std::string::npos); ByteBuffer stump(20, 0); MigrationResult tiny = migrator.migrateToCurrent(stump); CHECK(!tiny.ok); } VF_TEST(peekVersionReadsTheHeaderWithoutTheBody) { SaveContainer original = buildContainer(SaveVersion::V3, 2); ByteBuffer buffer = writeSaveContainer(original); ByteBuffer headerOnly(buffer.begin(), buffer.begin() + saveHeaderBytes);