diff --git a/tests/testRegionCompact.cpp b/tests/testRegionCompact.cpp new file mode 100644 index 0000000..fcc3162 --- /dev/null +++ b/tests/testRegionCompact.cpp @@ -0,0 +1,156 @@ +#include "testHarness.h" + +#include "vf/Block.h" +#include "vf/RegionCompact.h" +#include "vf/SaveIntegrity.h" +#include "vf/SaveMigration.h" + +#include +#include + +using namespace vf; + +namespace { + +Chunk uniformChunk(Block block) { + Chunk chunk; + for (int x = 0; x < chunkSize; ++x) + for (int y = 0; y < chunkSize; ++y) + for (int z = 0; z < chunkSize; ++z) chunk.set(x, y, z, block); + return chunk; +} + +Chunk busyChunk(int seed) { + Chunk chunk; + int cell = 0; + for (int x = 0; x < chunkSize; ++x) + for (int y = 0; y < chunkSize; ++y) + for (int z = 0; z < chunkSize; ++z) { + int value = (cell + seed) % static_cast(Block::Count); + chunk.set(x, y, z, static_cast(value)); + ++cell; + } + return chunk; +} + +SaveChunkRecord buildRecord(const Vec3i& coord, const Chunk& blocks) { + SaveChunkRecord record; + record.coord = coord; + record.blocks = blocks; + record.light = defaultChunkLight(blocks, saveDefaultSkyLight); + return record; +} + +SaveContainer uniformContainer(int chunkTotal) { + SaveContainer container; + container.version = SaveVersion::V4; + for (int i = 0; i < chunkTotal; ++i) + container.chunks.push_back(buildRecord(Vec3i{i, 0, 0}, uniformChunk(Block::Stone))); + return container; +} + +std::vector coordsOf(const ByteBuffer& buffer) { + std::vector coords; + SaveHeader header; + if (!readSaveHeader(buffer.data(), buffer.size(), header)) return coords; + std::vector entries; + if (!readSaveTable(buffer.data(), buffer.size(), header, entries)) return coords; + for (const SaveTableEntry& entry : entries) coords.push_back(entry.coord); + return coords; +} + +// A layout written by hand so the fragmentation arithmetic is known before the +// code runs: 13 sectors of 64 bytes, 2 header sectors, runs of 4 and 3 sectors +// at sector 2 and sector 10, so 4 sectors are holes and 1 sector inside the +// second run is reserved for nothing. +ByteBuffer handBuiltLayout() { + const uint32_t sectorBytes = 64; + ByteBuffer buffer(13 * static_cast(sectorBytes), 0); + saveWriteU32At(buffer.data(), buffer.size(), 0, saveMagic); + saveWriteU32At(buffer.data(), buffer.size(), 4, static_cast(SaveVersion::V4)); + saveWriteU32At(buffer.data(), buffer.size(), 8, 0u); + saveWriteU32At(buffer.data(), buffer.size(), 12, 0u); + saveWriteU32At(buffer.data(), buffer.size(), 16, 2u); + saveWriteU32At(buffer.data(), buffer.size(), 20, sectorBytes); + + std::size_t at = saveHeaderBytes; + saveWriteU32At(buffer.data(), buffer.size(), at + 0, 0u); + saveWriteU32At(buffer.data(), buffer.size(), at + 4, 0u); + saveWriteU32At(buffer.data(), buffer.size(), at + 8, 0u); + saveWriteU32At(buffer.data(), buffer.size(), at + 12, 2u); + saveWriteU32At(buffer.data(), buffer.size(), at + 16, 4u); + saveWriteU32At(buffer.data(), buffer.size(), at + 20, 200u); + + at += saveEntryBytes; + saveWriteU32At(buffer.data(), buffer.size(), at + 0, 0u); + saveWriteU32At(buffer.data(), buffer.size(), at + 4, 0u); + saveWriteU32At(buffer.data(), buffer.size(), at + 8, 1u); + saveWriteU32At(buffer.data(), buffer.size(), at + 12, 10u); + saveWriteU32At(buffer.data(), buffer.size(), at + 16, 3u); + saveWriteU32At(buffer.data(), buffer.size(), at + 20, 100u); + + uint32_t checksum = fnv1a32(buffer.data() + saveHeaderBytes, 2 * saveEntryBytes); + saveWriteU32At(buffer.data(), buffer.size(), 24, checksum); + return buffer; +} + +} // namespace + +VF_TEST(fragmentationOfAHandBuiltLayoutMatchesTheArithmetic) { + ByteBuffer buffer = handBuiltLayout(); + RegionCompactor compactor; + + CHECK_EQ(compactor.fragmentationPercent(buffer), 38); + + std::vector fragments = compactor.survey(buffer); + CHECK_EQ(fragments.size(), static_cast(2)); + if (fragments.size() != 2) return; + + CHECK_EQ(fragments[0].sectorOffset, static_cast(2)); + CHECK_EQ(fragments[0].sectorCount, static_cast(4)); + CHECK_EQ(fragments[0].payloadBytes, static_cast(200)); + CHECK_EQ(fragments[0].neededSectors, static_cast(4)); + CHECK_EQ(fragments[0].excessSectors, static_cast(0)); + CHECK_EQ(fragments[0].slackBytes, static_cast(56)); + CHECK(fragments[0].reachable); + + CHECK_EQ(fragments[1].sectorOffset, static_cast(10)); + CHECK_EQ(fragments[1].sectorCount, static_cast(3)); + CHECK_EQ(fragments[1].neededSectors, static_cast(2)); + CHECK_EQ(fragments[1].excessSectors, static_cast(1)); + CHECK_EQ(fragments[1].slackBytes, static_cast(92)); + CHECK(fragments[1].reachable); +} + +VF_TEST(aFreshContainerIsCanonicalAndUnfragmented) { + ByteBuffer buffer = writeSaveContainer(uniformContainer(4)); + RegionCompactor compactor; + CHECK_EQ(compactor.fragmentationPercent(buffer), 0); + CHECK(isCanonicalSaveLayout(buffer.data(), buffer.size())); + + ByteBuffer out; + CompactionStats stats; + CHECK(compactor.compact(buffer, out, stats)); + CHECK(stats.ok); + CHECK(out == buffer); + CHECK_EQ(stats.chunksMoved, static_cast(0)); + CHECK_EQ(stats.chunksDropped, static_cast(0)); + CHECK_EQ(stats.chunksKept, static_cast(4)); + CHECK_EQ(stats.bytesReclaimed, static_cast(0)); + CHECK_EQ(stats.holeSectors, static_cast(0)); + CHECK_EQ(stats.excessSectors, static_cast(0)); + CHECK_EQ(stats.fragmentationBefore, 0); + CHECK_EQ(stats.fragmentationAfter, 0); + CHECK_EQ(stats.operations, static_cast(5)); +} + +VF_TEST(aChunkThatOutgrowsItsRunMovesToTheEndAndLeavesAHole) { + ByteBuffer buffer = writeSaveContainer(uniformContainer(3)); + RegionCompactor compactor; + std::vector before = compactor.survey(buffer); + uint32_t abandoned = before[1].sectorCount; + std::size_t sizeBefore = buffer.size(); + + CHECK(updateChunkInPlace(buffer, buildRecord(Vec3i{1, 0, 0}, busyChunk(3)), false)); + CHECK(buffer.size() > sizeBefore); +