From 8454f8664181664eeceebf26a28ea8ff91d4128a Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 17:14:08 +0200 Subject: [PATCH] test: cover Lcg and worldFingerprint --- tests/testEditHistory.cpp | 161 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/testEditHistory.cpp diff --git a/tests/testEditHistory.cpp b/tests/testEditHistory.cpp new file mode 100644 index 0000000..bff1e17 --- /dev/null +++ b/tests/testEditHistory.cpp @@ -0,0 +1,161 @@ +#include "testHarness.h" +#include "vf/Codec.h" +#include "vf/EditHistory.h" + +#include +#include +#include + +using namespace vf; + +namespace { + +struct Lcg { + uint64_t state; + explicit Lcg(uint64_t seed) : state(seed) {} + uint32_t next() { + state = state * 6364136223846793005ull + 1442695040888963407ull; + return static_cast(state >> 32); + } + int below(int limit) { return static_cast(next() % static_cast(limit)); } +}; + +// Hashes the block storage itself: every non-empty chunk, in a stable +// coordinate order, over its raw byte array. Two worlds with the same +// fingerprint hold the same bytes in the same cells. +uint64_t worldFingerprint(const World& world) { + std::vector coords; + for (const auto& entry : world.allChunks()) + if (!entry.second->empty()) coords.push_back(entry.first); + std::sort(coords.begin(), coords.end(), [](const Vec3i& a, const Vec3i& b) { + if (a.x != b.x) return a.x < b.x; + if (a.y != b.y) return a.y < b.y; + return a.z < b.z; + }); + + uint64_t digest = 1469598103934665603ull; + for (const Vec3i& coord : coords) { + int32_t header[3] = {coord.x, coord.y, coord.z}; + digest ^= fnv1a64(reinterpret_cast(header), sizeof(header)); + digest *= 1099511628211ull; + const Chunk* chunk = world.chunkAt(coord); + const auto& blocks = chunk->raw(); + digest ^= fnv1a64(reinterpret_cast(blocks.data()), blocks.size()); + digest *= 1099511628211ull; + } + return digest; +} + +World layeredWorld(int span, int depth) { + World world; + for (int z = 0; z < span; ++z) + for (int x = 0; x < span; ++x) + for (int y = 0; y < depth; ++y) { + Block block = y + 1 == depth ? Block::Grass : Block::Stone; + world.setBlock(x, y, z, block); + } + return world; +} + +} // namespace + +VF_TEST(recordingAnEditCapturesThePreviousBlockAndWritesTheNewOne) { + World world; + world.setBlock(1, 2, 3, Block::Stone); + EditHistory history; + + CHECK(history.beginBatch("place", 1u)); + CHECK(history.batchOpen()); + CHECK(history.recordSet(world, {1, 2, 3}, Block::Planks)); + CHECK(world.blockAt(1, 2, 3) == Block::Planks); + CHECK_EQ(history.openBatchSize(), 1u); + CHECK(history.endBatch()); + CHECK(!history.batchOpen()); + + const EditBatch* batch = history.peekUndo(); + CHECK(batch != nullptr); + CHECK(batch->label == std::string("place")); + CHECK_EQ(batch->ops.size(), 1u); + CHECK(batch->ops[0].before == Block::Stone); + CHECK(batch->ops[0].after == Block::Planks); + CHECK(batch->ops[0].position == Vec3i(1, 2, 3)); +} + +VF_TEST(editsRecordedWithoutAnOpenBatchAreRejected) { + World world; + EditHistory history; + CHECK(!history.recordSet(world, {0, 0, 0}, Block::Stone)); + CHECK(world.blockAt(0, 0, 0) == Block::Air); + CHECK_EQ(history.undoDepth(), 0u); + CHECK(!history.endBatch()); + + CHECK(history.beginBatch("first", 0u)); + CHECK(!history.beginBatch("second", 0u)); + CHECK(history.recordSet(world, {0, 0, 0}, Block::Stone)); + CHECK(history.endBatch()); + CHECK_EQ(history.undoDepth(), 1u); +} + +VF_TEST(undoTakesBackAWholeBatchAndRedoReplaysIt) { + World world = layeredWorld(4, 3); + uint64_t original = worldFingerprint(world); + EditHistory history; + + history.beginBatch("tower", 10u); + for (int y = 3; y < 9; ++y) history.recordSet(world, {2, y, 2}, Block::Cobblestone); + CHECK(history.endBatch()); + uint64_t built = worldFingerprint(world); + CHECK(built != original); + CHECK_EQ(history.stats().opCount, 6u); + + CHECK(history.undo(world)); + CHECK_EQ(worldFingerprint(world), original); + CHECK_EQ(history.undoDepth(), 0u); + CHECK_EQ(history.redoDepth(), 1u); + CHECK(!history.undo(world)); + + CHECK(history.redo(world)); + CHECK_EQ(worldFingerprint(world), built); + CHECK_EQ(history.undoDepth(), 1u); + CHECK(!history.redo(world)); + CHECK_EQ(history.stats().undoCount, 1u); + CHECK_EQ(history.stats().redoCount, 1u); +} + +VF_TEST(undoingARandomSequenceOfTwoHundredEditsRestoresTheWorldByteForByte) { + World world = layeredWorld(8, 4); + EditHistory history(1u << 22); + const Block palette[5] = {Block::Air, Block::Stone, Block::Dirt, Block::Sand, Block::Glass}; + + std::vector marks; + marks.push_back(worldFingerprint(world)); + + Lcg rng(0xfeedfaceull); + for (int batch = 0; batch < 200; ++batch) { + history.beginBatch("stroke" + std::to_string(batch), static_cast(batch)); + int edits = 1 + rng.below(6); + for (int edit = 0; edit < edits; ++edit) { + Vec3i cell{rng.below(8), rng.below(6), rng.below(8)}; + history.recordSet(world, cell, palette[rng.below(5)]); + } + if (history.endBatch()) marks.push_back(worldFingerprint(world)); + } + + CHECK(marks.size() > 150u); + CHECK_EQ(history.undoDepth(), marks.size() - 1u); + CHECK_EQ(history.stats().droppedBatches, 0u); + + for (std::size_t step = marks.size() - 1u; step > 0u; --step) { + CHECK(history.undo(world)); + CHECK_EQ(worldFingerprint(world), marks[step - 1u]); + } + CHECK(!history.undo(world)); + CHECK_EQ(worldFingerprint(world), marks.front()); + + for (std::size_t step = 1u; step < marks.size(); ++step) { + CHECK(history.redo(world)); + CHECK_EQ(worldFingerprint(world), marks[step]); + } + CHECK(!history.redo(world)); +} +