diff options
Diffstat (limited to 'test/unit/testCellBuffer.cxx')
-rw-r--r-- | test/unit/testCellBuffer.cxx | 593 |
1 files changed, 587 insertions, 6 deletions
diff --git a/test/unit/testCellBuffer.cxx b/test/unit/testCellBuffer.cxx index ef7db099b..b6735339a 100644 --- a/test/unit/testCellBuffer.cxx +++ b/test/unit/testCellBuffer.cxx @@ -3,6 +3,7 @@ **/ #include <cstddef> +#include <cassert> #include <cstring> #include <stdexcept> #include <string_view> @@ -19,6 +20,8 @@ #include "SplitVector.h" #include "Partitioning.h" #include "RunStyles.h" +#include "SparseVector.h" +#include "ChangeHistory.h" #include "CellBuffer.h" #include "catch.hpp" @@ -37,7 +40,7 @@ TEST_CASE("CellBuffer") { SECTION("InsertOneLine") { bool startSequence = false; - const char *cpChange = cb.InsertString(0, sText, static_cast<int>(sLength), startSequence); + const char *cpChange = cb.InsertString(0, sText, sLength, startSequence); REQUIRE(startSequence); REQUIRE(sLength == cb.Length()); REQUIRE(memcmp(cpChange, sText, sLength) == 0); @@ -54,7 +57,7 @@ TEST_CASE("CellBuffer") { const char sText2[] = "Two\nLines"; const Sci::Position sLength2 = static_cast<Sci::Position>(strlen(sText2)); bool startSequence = false; - const char *cpChange = cb.InsertString(0, sText2, static_cast<int>(sLength), startSequence); + const char *cpChange = cb.InsertString(0, sText2, sLength2, startSequence); REQUIRE(startSequence); REQUIRE(sLength2 == cb.Length()); REQUIRE(memcmp(cpChange, sText2, sLength2) == 0); @@ -64,7 +67,7 @@ TEST_CASE("CellBuffer") { REQUIRE(4 == cb.LineStart(1)); REQUIRE(1 == cb.LineFromPosition(5)); REQUIRE(sLength2 == cb.LineStart(2)); - REQUIRE(1 == cb.LineFromPosition(static_cast<int>(sLength))); + REQUIRE(1 == cb.LineFromPosition(sLength2)); REQUIRE(cb.CanUndo()); REQUIRE(!cb.CanRedo()); } @@ -74,7 +77,7 @@ TEST_CASE("CellBuffer") { cb.SetUndoCollection(false); REQUIRE(!cb.IsCollectingUndo()); bool startSequence = false; - const char *cpChange = cb.InsertString(0, sText, static_cast<int>(sLength), startSequence); + const char *cpChange = cb.InsertString(0, sText, sLength, startSequence); REQUIRE(!startSequence); REQUIRE(sLength == cb.Length()); REQUIRE(memcmp(cpChange, sText, sLength) == 0); @@ -86,7 +89,7 @@ TEST_CASE("CellBuffer") { const char sTextDeleted[] = "ci"; const char sTextAfterDeletion[] = "Sntilla"; bool startSequence = false; - const char *cpChange = cb.InsertString(0, sText, static_cast<int>(sLength), startSequence); + const char *cpChange = cb.InsertString(0, sText, sLength, startSequence); REQUIRE(startSequence); REQUIRE(sLength == cb.Length()); REQUIRE(memcmp(cpChange, sText, sLength) == 0); @@ -146,7 +149,7 @@ TEST_CASE("CellBuffer") { cb.SetReadOnly(true); REQUIRE(cb.IsReadOnly()); bool startSequence = false; - cb.InsertString(0, sText, static_cast<int>(sLength), startSequence); + cb.InsertString(0, sText, sLength, startSequence); REQUIRE(cb.Length() == 0); } @@ -438,3 +441,581 @@ TEST_CASE("CharacterIndex") { REQUIRE(cb.IndexLineStart(3, LineCharacterIndexType::Utf16) == 5); } } + +TEST_CASE("ChangeHistory") { + + ChangeHistory il; + const EditionSet empty; + struct Spanner { + Sci::Position position = 0; + Sci::Position length = 0; + }; + + SECTION("Start") { + REQUIRE(il.Length() == 0); + REQUIRE(il.DeletionCount(0,0) == 0); + REQUIRE(il.EditionAt(0) == 0); + REQUIRE(il.EditionEndRun(0) == 0); + REQUIRE(il.EditionDeletesAt(0) == 0); + REQUIRE(il.EditionNextDelete(0) == 1); + } + + SECTION("Some Space") { + il.Insert(0, 10, false, true); + REQUIRE(il.Length() == 10); + REQUIRE(il.DeletionCount(0,10) == 0); + REQUIRE(il.EditionAt(0) == 0); + REQUIRE(il.EditionEndRun(0) == 10); + REQUIRE(il.EditionDeletesAt(0) == 0); + REQUIRE(il.EditionNextDelete(0) == 10); + REQUIRE(il.EditionDeletesAt(10) == 0); + REQUIRE(il.EditionNextDelete(10) == 11); + } + + SECTION("An insert") { + il.Insert(0, 7, false, true); + il.SetSavePoint(); + il.Insert(2, 3, true, true); + REQUIRE(il.Length() == 10); + REQUIRE(il.DeletionCount(0,10) == 0); + + REQUIRE(il.EditionAt(0) == 0); + REQUIRE(il.EditionEndRun(0) == 2); + REQUIRE(il.EditionAt(2) == 2); + REQUIRE(il.EditionEndRun(2) == 5); + REQUIRE(il.EditionAt(5) == 0); + REQUIRE(il.EditionEndRun(5) == 10); + REQUIRE(il.EditionAt(10) == 0); + + REQUIRE(il.EditionDeletesAt(0) == 0); + REQUIRE(il.EditionNextDelete(0) == 10); + REQUIRE(il.EditionDeletesAt(10) == 0); + } + + SECTION("A delete") { + il.Insert(0, 10, false, true); + il.SetSavePoint(); + il.DeleteRangeSavingHistory(2, 3, true, false); + REQUIRE(il.Length() == 7); + REQUIRE(il.DeletionCount(0,7) == 1); + + REQUIRE(il.EditionAt(0) == 0); + REQUIRE(il.EditionEndRun(0) == 7); + REQUIRE(il.EditionAt(7) == 0); + + REQUIRE(il.EditionDeletesAt(0) == 0); + const EditionSet one{ 2 }; + REQUIRE(il.EditionNextDelete(0) == 2); + REQUIRE(il.EditionDeletesAt(2) == 2); + REQUIRE(il.EditionNextDelete(2) == 7); + REQUIRE(il.EditionDeletesAt(7) == 0); + } + + SECTION("Insert, delete, and undo") { + il.Insert(0, 9, false, true); + il.SetSavePoint(); + il.Insert(3, 1, true, true); + REQUIRE(il.EditionEndRun(0) == 3); + REQUIRE(il.EditionAt(3) == 2); + REQUIRE(il.EditionEndRun(3) == 4); + REQUIRE(il.EditionAt(4) == 0); + + il.DeleteRangeSavingHistory(2, 3, true, false); + REQUIRE(il.Length() == 7); + REQUIRE(il.DeletionCount(0,7) == 1); + + REQUIRE(il.EditionAt(0) == 0); + REQUIRE(il.EditionEndRun(0) == 7); + REQUIRE(il.EditionAt(7) == 0); + + REQUIRE(il.EditionDeletesAt(0) == 0); + const EditionSet one{ 2 }; + REQUIRE(il.EditionNextDelete(0) == 2); + REQUIRE(il.EditionDeletesAt(2) == 2); + REQUIRE(il.EditionNextDelete(2) == 7); + REQUIRE(il.EditionDeletesAt(7) == 0); + + // Undo in detail (normally inside CellBuffer::PerformUndoStep) + il.UndoDeleteStep(2, 3, false); + REQUIRE(il.Length() == 10); + REQUIRE(il.DeletionCount(0, 10) == 0); + // The insertion has reappeared + REQUIRE(il.EditionEndRun(0) == 3); + REQUIRE(il.EditionAt(3) == 2); + REQUIRE(il.EditionEndRun(3) == 4); + REQUIRE(il.EditionAt(4) == 0); + } + + SECTION("Deletes") { + il.Insert(0, 10, false, true); + il.SetSavePoint(); + il.DeleteRangeSavingHistory(2, 3, true, false); + REQUIRE(il.Length() == 7); + REQUIRE(il.DeletionCount(0,7) == 1); + + REQUIRE(il.EditionDeletesAt(0) == 0); + REQUIRE(il.EditionNextDelete(0) == 2); + REQUIRE(il.EditionDeletesAt(2) == 2); + REQUIRE(il.EditionNextDelete(2) == 7); + REQUIRE(il.EditionDeletesAt(7) == 0); + + il.DeleteRangeSavingHistory(2, 1, true, false); + REQUIRE(il.Length() == 6); + REQUIRE(il.DeletionCount(0,6) == 2); + + REQUIRE(il.EditionDeletesAt(0) == 0); + REQUIRE(il.EditionNextDelete(0) == 2); + REQUIRE(il.EditionDeletesAt(2) == 2); + REQUIRE(il.EditionNextDelete(2) == 6); + REQUIRE(il.EditionDeletesAt(6) == 0); + + // Undo in detail (normally inside CellBuffer::PerformUndoStep) + il.UndoDeleteStep(2, 1, false); + REQUIRE(il.Length() == 7); + REQUIRE(il.DeletionCount(0, 7) == 1); + + // Undo in detail (normally inside CellBuffer::PerformUndoStep) + il.UndoDeleteStep(2, 3, false); + REQUIRE(il.Length() == 10); + REQUIRE(il.DeletionCount(0, 10) == 0); + } + + SECTION("Deletes 101") { + // Deletes that hit the start and end permanent positions + il.Insert(0, 3, false, true); + il.SetSavePoint(); + REQUIRE(il.DeletionCount(0, 2) == 0); + il.DeleteRangeSavingHistory(1, 1, true, false); + REQUIRE(il.DeletionCount(0,2) == 1); + const EditionSet at1 = {2}; + REQUIRE(il.DeletionsAt(1) == at1); + il.DeleteRangeSavingHistory(1, 1, false, false); + REQUIRE(il.DeletionCount(0,1) == 2); + const EditionSet at2 = { 2, 3 }; + REQUIRE(il.DeletionsAt(1) == at2); + il.DeleteRangeSavingHistory(0, 1, false, false); + const EditionSet at3 = { 2, 3, 3 }; + REQUIRE(il.DeletionsAt(0) == at3); + REQUIRE(il.DeletionCount(0,0) == 3); + + // Undo them + il.UndoDeleteStep(0, 1, false); + REQUIRE(il.DeletionCount(0, 1) == 2); + REQUIRE(il.DeletionsAt(1) == at2); + il.UndoDeleteStep(1, 1, false); + REQUIRE(il.DeletionCount(0, 2) == 1); + REQUIRE(il.DeletionsAt(1) == at1); + il.UndoDeleteStep(1, 1, false); + REQUIRE(il.DeletionCount(0, 3) == 0); + } + + SECTION("Deletes Stack") { + std::vector<Spanner> spans = { + {5, 1}, + {4, 3}, + {1, 1}, + {1, 1}, + {0, 1}, + {0, 3}, + }; + + // Deletes that hit the start and end permanent positions + il.Insert(0, 10, false, true); + REQUIRE(il.Length() == 10); + il.SetSavePoint(); + REQUIRE(il.DeletionCount(0, 10) == 0); + for (size_t i = 0; i < std::size(spans); i++) { + il.DeleteRangeSavingHistory(spans[i].position, spans[i].length, false, false); + } + REQUIRE(il.Length() == 0); + for (size_t j = 0; j < std::size(spans); j++) { + const size_t i = std::size(spans) - j - 1; + il.UndoDeleteStep(spans[i].position, spans[i].length, false); + } + REQUIRE(il.DeletionCount(0, 10) == 0); + REQUIRE(il.Length() == 10); + } +} + +struct InsertionResult { + Sci::Position position; + Sci::Position length; + int state; + bool operator==(const InsertionResult &other) const noexcept { + return position == other.position && + length == other.length && + state == other.state; + } +}; + +std::ostream &operator << (std::ostream &os, InsertionResult const &value) { + os << value.position << " " << value.length << " " << value.state; + return os; +} + +using Insertions = std::vector<InsertionResult>; + +std::ostream &operator << (std::ostream &os, Insertions const &value) { + os << "("; + for (const InsertionResult &el : value) { + os << "(" << el << ") "; + } + os << ")"; + return os; +} + +Insertions HistoryInsertions(const CellBuffer &cb) { + Insertions result; + Sci::Position startPos = 0; + while (startPos < cb.Length()) { + const Sci::Position endPos = cb.EditionEndRun(startPos); + const int ed = cb.EditionAt(startPos); + if (ed) { + result.push_back({ startPos, endPos - startPos, ed }); + } + startPos = endPos; + } + return result; +} + +struct DeletionResult { + Sci::Position position; + int state; + bool operator==(const DeletionResult &other) const noexcept { + return position == other.position && + state == other.state; + } +}; + +std::ostream &operator << (std::ostream &os, DeletionResult const &value) { + os << value.position << " " << value.state; + return os; +} + +using Deletions = std::vector<DeletionResult>; + +std::ostream &operator << (std::ostream &os, Deletions const &value) { + os << "("; + for (const DeletionResult &el : value) { + os << "(" << el << ") "; + } + os << ")"; + return os; +} + +Deletions HistoryDeletions(const CellBuffer &cb) { + Deletions result; + Sci::Position positionDeletion = 0; + while (positionDeletion <= cb.Length()) { + const unsigned int editions = cb.EditionDeletesAt(positionDeletion); + if (editions & 1) { + result.push_back({ positionDeletion, 1 }); + } + if (editions & 2) { + result.push_back({ positionDeletion, 2 }); + } + if (editions & 4) { + result.push_back({ positionDeletion, 3 }); + } + if (editions & 8) { + result.push_back({ positionDeletion, 4 }); + } + positionDeletion = cb.EditionNextDelete(positionDeletion); + } + return result; +} + +struct History { + Insertions insertions; + Deletions deletions; + bool operator==(const History &other) const { + return insertions == other.insertions && + deletions == other.deletions; + } +}; + +std::ostream &operator << (std::ostream &os, History const &value) { + os << value.insertions << " " << value.deletions; + return os; +} + +History HistoryOf(const CellBuffer &cb) { + return { HistoryInsertions(cb), HistoryDeletions(cb) }; +} + +void UndoBlock(CellBuffer &cb) { + const int steps = cb.StartUndo(); + for (int step = 0; step < steps; step++) { + cb.PerformUndoStep(); + } +} + +void RedoBlock(CellBuffer &cb) { + const int steps = cb.StartRedo(); + for (int step = 0; step < steps; step++) { + cb.PerformRedoStep(); + } +} + +TEST_CASE("CellBufferWithChangeHistory") { + + SECTION("StraightUndoRedoSaveRevertRedo") { + CellBuffer cb(true, false); + cb.SetUndoCollection(false); + std::string sInsert = "abcdefghijklmnopqrstuvwxyz"; + bool startSequence = false; + cb.InsertString(0, sInsert.c_str(), sInsert.length(), startSequence); + cb.SetUndoCollection(true); + cb.SetSavePoint(); + cb.ChangeHistorySet(true); + + const History history0 { {}, {} }; + REQUIRE(HistoryOf(cb) == history0); + + // 1 + cb.InsertString(4, "_", 1, startSequence); + const History history1{ {{4, 1, 3}}, {} }; + REQUIRE(HistoryOf(cb) == history1); + + // 2 + cb.DeleteChars(2, 1, startSequence); + const History history2{ {{3, 1, 3}}, + {{2, 3}} }; + REQUIRE(HistoryOf(cb) == history2); + + // 3 + cb.InsertString(1, "[!]", 3, startSequence); + const History history3{ { {1, 3, 3}, {6, 1, 3} }, + { {5, 3} } }; + REQUIRE(HistoryOf(cb) == history3); + + // 4 + cb.DeleteChars(2, 1, startSequence); // Inside an insertion + const History history4{ { {1, 2, 3}, {5, 1, 3} }, + { {2, 3}, {4, 3} }}; + REQUIRE(HistoryOf(cb) == history4); + + // 5 Delete all the insertions and deletions + cb.DeleteChars(1, 6, startSequence); // Inside an insertion + const History history5{ { }, + { {1, 3} } }; + REQUIRE(HistoryOf(cb) == history5); + + // Undo all + UndoBlock(cb); + REQUIRE(HistoryOf(cb) == history4); + + UndoBlock(cb); + REQUIRE(HistoryOf(cb) == history3); + + UndoBlock(cb); + REQUIRE(HistoryOf(cb) == history2); + + UndoBlock(cb); + REQUIRE(HistoryOf(cb) == history1); + + UndoBlock(cb); + REQUIRE(HistoryOf(cb) == history0); + + // Redo all + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == history1); + + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == history2); + + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == history3); + + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == history4); + + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == history5); + + cb.SetSavePoint(); + const History history5s{ { }, + { {1, 2} } }; + REQUIRE(HistoryOf(cb) == history5s); + + // Change past save point + cb.InsertString(4, "123", 3, startSequence); + const History history6{ { {4, 3, 3} }, + { {1, 2} } }; + REQUIRE(HistoryOf(cb) == history6); + + // Undo to save point: same as 5 but with save state instead of unsaved + UndoBlock(cb); + REQUIRE(HistoryOf(cb) == history5s); + + // Reverting past save point, similar to 4 but with most saved and + // reverted delete at 1 + UndoBlock(cb); // Reinsert most of original changes + const History history4s{ { {1, 2, 4}, {3, 2, 1}, {5, 1, 4}, {6, 1, 1} }, + { {2, 2}, {4, 2} } }; + REQUIRE(HistoryOf(cb) == history4s); + + UndoBlock(cb); // Reinsert "!", + const History history3s{ { {1, 3, 4}, {4, 2, 1}, {6, 1, 4}, {7, 1, 1} }, + { {5, 2} } }; + REQUIRE(HistoryOf(cb) == history3s); + + UndoBlock(cb); // Revert insertion of [!] + const History history2s{ { {1, 2, 1}, {3, 1, 4}, {4, 1, 1} }, + { {1, 1}, {2, 2} } }; + REQUIRE(HistoryOf(cb) == history2s); + + UndoBlock(cb); // Revert deletion, inserts at 2 + const History history1s{ { {1, 3, 1}, {4, 1, 4}, {5, 1, 1} }, + { {1, 1} } }; + REQUIRE(HistoryOf(cb) == history1s); + + UndoBlock(cb); // Revert insertion of _ at 4, drops middle insertion run + // So merges down to 1 insertion + const History history0s{ { {1, 4, 1} }, + { {1, 1}, {4, 1} } }; + REQUIRE(HistoryOf(cb) == history0s); + + // At origin but with changes from disk + // Now redo the steps + + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == history1s); + + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == history2s); + + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == history3s); + + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == history4s); + + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == history5s); + + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == history6); + } + + SECTION("Detached") { + CellBuffer cb(true, false); + cb.SetUndoCollection(false); + std::string sInsert = "abcdefghijklmnopqrstuvwxyz"; + bool startSequence = false; + cb.InsertString(0, sInsert.c_str(), sInsert.length(), startSequence); + cb.SetUndoCollection(true); + cb.SetSavePoint(); + cb.ChangeHistorySet(true); + + const History history0{ {}, {} }; + REQUIRE(HistoryOf(cb) == history0); + + // 1 + cb.InsertString(4, "_", 1, startSequence); + const History history1{ {{4, 1, 3}}, {} }; + REQUIRE(HistoryOf(cb) == history1); + + // 2 + cb.DeleteChars(2, 1, startSequence); + const History history2{ {{3, 1, 3}}, + {{2, 3}} }; + REQUIRE(HistoryOf(cb) == history2); + + cb.SetSavePoint(); + + UndoBlock(cb); + const History history1s{ {{2, 1, 1}, {4, 1, 2}}, {} }; + REQUIRE(HistoryOf(cb) == history1s); + + cb.InsertString(6, "()", 2, startSequence); + const History detached2{ {{2, 1, 1}, {4, 1, 2}, {6, 2, 3}}, {} }; + REQUIRE(HistoryOf(cb) == detached2); + + cb.DeleteChars(9, 3, startSequence); + const History detached3{ {{2, 1, 1}, {4, 1, 2}, {6, 2, 3}}, {{9,3}} }; + REQUIRE(HistoryOf(cb) == detached3); + + UndoBlock(cb); + REQUIRE(HistoryOf(cb) == detached2); + UndoBlock(cb); + const History detached1{ {{2, 1, 1}, {4, 1, 2}}, {} }; + REQUIRE(HistoryOf(cb) == detached1); + UndoBlock(cb); + const History detached0{ {{2, 1, 1}}, {{4,1}} }; + REQUIRE(HistoryOf(cb) == detached0); + REQUIRE(!cb.CanUndo()); + + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == detached1); + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == detached2); + RedoBlock(cb); + REQUIRE(HistoryOf(cb) == detached3); + } +} + +namespace { + +// Implement low quality reproducible pseudo-random numbers. +// Pseudo-random algorithm based on R. G. Dromey "How to Solve it by Computer" page 122. + +class RandomSequence { + static constexpr int mult = 109; + static constexpr int incr = 853; + static constexpr int modulus = 4096; + int randomValue = 127; +public: + int Next() noexcept { + randomValue = (mult * randomValue + incr) % modulus; + return randomValue; + } +}; + +} + +#if 1 +TEST_CASE("CellBufferLong") { + + // Call methods on CellBuffer pseudo-randomly trying to trigger assertion failures + + CellBuffer cb(true, false); + + SECTION("Random") { + RandomSequence rseq; + for (size_t i = 0l; i < 20000; i++) { + const int r = rseq.Next() % 10; + if (r <= 2) { // 30% + // Insert text + const int pos = rseq.Next() % (cb.Length() + 1); + const int len = rseq.Next() % 10 + 1; + std::string sInsert; + for (int j = 0; j < len; j++) { + sInsert.push_back(static_cast<char>('a' + j)); + } + bool startSequence = false; + cb.InsertString(pos, sInsert.c_str(), len, startSequence); + } else if (r <= 5) { // 30% + // Delete Text + const Sci::Position pos = rseq.Next() % (cb.Length() + 1); + const int len = rseq.Next() % 10 + 1; + if (pos + len <= cb.Length()) { + bool startSequence = false; + cb.DeleteChars(pos, len, startSequence); + } + } else if (r <= 8) { // 30% + // Undo or redo + const bool undo = rseq.Next() % 2 == 1; + if (undo) { + UndoBlock(cb); + } else { + RedoBlock(cb); + } + } else { // 10% + // Save + cb.SetSavePoint(); + } + } + } +} +#endif
\ No newline at end of file |