From bb2a850641499e0e42db30706621950feaea0784 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 17:18:42 +0200 Subject: [PATCH] test: cover testEditHistory --- tests/testEditHistory.cpp | 217 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/tests/testEditHistory.cpp b/tests/testEditHistory.cpp index bff1e17..9522539 100644 --- a/tests/testEditHistory.cpp +++ b/tests/testEditHistory.cpp @@ -159,3 +159,220 @@ VF_TEST(undoingARandomSequenceOfTwoHundredEditsRestoresTheWorldByteForByte) { CHECK(!history.redo(world)); } +VF_TEST(aNewEditThrowsAwayTheRedoStack) { + World world; + EditHistory history; + + history.beginBatch("one", 1u); + history.recordSet(world, {0, 0, 0}, Block::Stone); + history.endBatch(); + CHECK(history.undo(world)); + CHECK_EQ(history.redoDepth(), 1u); + + history.beginBatch("two", 2u); + history.recordSet(world, {5, 0, 0}, Block::Dirt); + history.endBatch(); + CHECK_EQ(history.redoDepth(), 0u); + CHECK(!history.redo(world)); + CHECK(world.blockAt(0, 0, 0) == Block::Air); + CHECK(world.blockAt(5, 0, 0) == Block::Dirt); +} + +VF_TEST(theMemoryBudgetDropsTheOldestBatchesFirst) { + World probe; + EditHistory sizer; + sizer.beginBatch("probe", 0u); + for (int z = 0; z < 10; ++z) sizer.recordSet(probe, {0, 0, z}, Block::Stone); + sizer.endBatch(); + std::size_t batchBytes = sizer.peekUndo()->byteSize(); + CHECK(batchBytes > 0u); + + World world; + EditHistory history(batchBytes * 3u); + for (int batch = 0; batch < 10; ++batch) { + history.beginBatch("probe", static_cast(batch)); + for (int z = 0; z < 10; ++z) history.recordSet(world, {batch, 0, z}, Block::Stone); + CHECK(history.endBatch()); + } + + CHECK_EQ(history.undoDepth(), 3u); + CHECK_EQ(history.stats().droppedBatches, 7u); + CHECK(history.stats().bytes <= batchBytes * 3u); + + while (history.undo(world)) { + } + // The seven oldest batches were evicted, so their blocks stay in the world. + for (int batch = 0; batch < 7; ++batch) CHECK(world.blockAt(batch, 0, 5) == Block::Stone); + for (int batch = 7; batch < 10; ++batch) CHECK(world.blockAt(batch, 0, 5) == Block::Air); +} + +VF_TEST(shrinkingTheBudgetLaterAlsoEvictsTheOldestBatches) { + World world; + EditHistory history(1u << 20); + for (int batch = 0; batch < 6; ++batch) { + history.beginBatch("fill", static_cast(batch)); + for (int z = 0; z < 4; ++z) history.recordSet(world, {batch, 0, z}, Block::Sand); + history.endBatch(); + } + CHECK_EQ(history.undoDepth(), 6u); + + std::size_t oneBatch = history.peekUndo()->byteSize(); + history.setMemoryBudget(oneBatch * 2u); + CHECK_EQ(history.undoDepth(), 2u); + CHECK_EQ(history.stats().droppedBatches, 4u); + CHECK_EQ(history.memoryBudget(), oneBatch * 2u); +} + +VF_TEST(batchesTouchingTheSameBlocksCloseInTimeMergeIntoOne) { + World world; + EditHistory history; + history.setMergeWindow(5u); + CHECK_EQ(history.mergeWindowTicks(), 5u); + + history.beginBatch("dab", 100u); + history.recordSet(world, {0, 0, 0}, Block::Stone); + history.endBatch(); + + history.beginBatch("dab", 102u); + history.recordSet(world, {0, 0, 0}, Block::Dirt); + history.recordSet(world, {1, 0, 0}, Block::Dirt); + history.endBatch(); + + CHECK_EQ(history.undoDepth(), 1u); + CHECK_EQ(history.stats().mergedBatches, 1u); + CHECK_EQ(history.stats().opCount, 2u); + + const EditBatch* merged = history.peekUndo(); + CHECK(merged->touches({0, 0, 0})); + CHECK(merged->touches({1, 0, 0})); + for (const EditOp& op : merged->ops) { + if (op.position == Vec3i(0, 0, 0)) { + CHECK(op.before == Block::Air); + CHECK(op.after == Block::Dirt); + } + } + + CHECK(history.undo(world)); + CHECK(world.blockAt(0, 0, 0) == Block::Air); + CHECK(world.blockAt(1, 0, 0) == Block::Air); + CHECK_EQ(history.undoDepth(), 0u); +} + +VF_TEST(batchesStaySeparateWhenTheyMissInSpaceOrInTime) { + World world; + EditHistory apart; + apart.setMergeWindow(5u); + apart.beginBatch("a", 100u); + apart.recordSet(world, {0, 0, 0}, Block::Stone); + apart.endBatch(); + apart.beginBatch("b", 101u); + apart.recordSet(world, {9, 0, 0}, Block::Stone); + apart.endBatch(); + CHECK_EQ(apart.undoDepth(), 2u); + CHECK_EQ(apart.stats().mergedBatches, 0u); + + World other; + EditHistory late; + late.setMergeWindow(5u); + late.beginBatch("a", 100u); + late.recordSet(other, {0, 0, 0}, Block::Stone); + late.endBatch(); + late.beginBatch("b", 400u); + late.recordSet(other, {0, 0, 0}, Block::Dirt); + late.endBatch(); + CHECK_EQ(late.undoDepth(), 2u); + CHECK_EQ(late.stats().mergedBatches, 0u); + + CHECK(late.undo(other)); + CHECK(other.blockAt(0, 0, 0) == Block::Stone); + CHECK(late.undo(other)); + CHECK(other.blockAt(0, 0, 0) == Block::Air); +} + +VF_TEST(aBatchThatChangesNothingIsNotRecorded) { + World world; + world.setBlock(0, 0, 0, Block::Stone); + EditHistory history; + + history.beginBatch("same", 1u); + CHECK(!history.recordSet(world, {0, 0, 0}, Block::Stone)); + CHECK(!history.endBatch()); + CHECK_EQ(history.undoDepth(), 0u); + + history.beginBatch("thereAndBack", 2u); + CHECK(history.recordSet(world, {0, 0, 0}, Block::Dirt)); + CHECK(history.recordSet(world, {0, 0, 0}, Block::Stone)); + CHECK(!history.endBatch()); + CHECK_EQ(history.undoDepth(), 0u); + CHECK(world.blockAt(0, 0, 0) == Block::Stone); +} + +VF_TEST(repaintingOneBlockInsideABatchKeepsASingleOpAndUndoesToTheOriginal) { + World world; + world.setBlock(4, 4, 4, Block::Stone); + EditHistory history; + + history.beginBatch("repaint", 1u); + history.recordSet(world, {4, 4, 4}, Block::Dirt); + history.recordSet(world, {4, 4, 4}, Block::Sand); + history.recordSet(world, {4, 4, 4}, Block::Glass); + CHECK_EQ(history.openBatchSize(), 1u); + CHECK(history.endBatch()); + + CHECK_EQ(history.stats().opCount, 1u); + CHECK(world.blockAt(4, 4, 4) == Block::Glass); + CHECK(history.undo(world)); + CHECK(world.blockAt(4, 4, 4) == Block::Stone); +} + +VF_TEST(cancellingAnOpenBatchRollsTheWorldBackAndRecordsNothing) { + World world = layeredWorld(3, 2); + uint64_t before = worldFingerprint(world); + EditHistory history; + + history.beginBatch("oops", 1u); + for (int x = 0; x < 3; ++x) history.recordSet(world, {x, 5, 0}, Block::Lava); + CHECK(worldFingerprint(world) != before); + + CHECK(history.cancelBatch(world)); + CHECK_EQ(worldFingerprint(world), before); + CHECK_EQ(history.undoDepth(), 0u); + CHECK(!history.batchOpen()); + CHECK(!history.cancelBatch(world)); +} + +VF_TEST(historyStatsReportBatchesOperationsBytesAndCounters) { + World world; + EditHistory history; + CHECK_EQ(history.stats().bytes, 0u); + + for (int batch = 0; batch < 3; ++batch) { + history.beginBatch("b", static_cast(batch)); + for (int i = 0; i < 4; ++i) + history.recordSet(world, {batch, 0, i}, Block::Stone); + history.endBatch(); + } + + HistoryStats summary = history.stats(); + CHECK_EQ(summary.batchCount, 3u); + CHECK_EQ(summary.opCount, 12u); + CHECK(summary.bytes >= 12u * sizeof(EditOp)); + CHECK_EQ(summary.undoCount, 0u); + CHECK_EQ(summary.redoBatchCount, 0u); + + history.undo(world); + history.undo(world); + summary = history.stats(); + CHECK_EQ(summary.batchCount, 1u); + CHECK_EQ(summary.redoBatchCount, 2u); + CHECK_EQ(summary.opCount, 4u); + CHECK_EQ(summary.undoCount, 2u); + + history.clear(); + summary = history.stats(); + CHECK_EQ(summary.batchCount, 0u); + CHECK_EQ(summary.redoBatchCount, 0u); + CHECK_EQ(summary.bytes, 0u); + CHECK(history.peekUndo() == nullptr); + CHECK(history.peekRedo() == nullptr); +}