diff --git a/version0/CMakeLists.txt b/version0/CMakeLists.txt index 29404ad..53f143d 100644 --- a/version0/CMakeLists.txt +++ b/version0/CMakeLists.txt @@ -5,8 +5,20 @@ project(crdt_version0 CXX C) ######################################## add_executable(test1 + ./list.hpp + ./text_document.hpp ./test1.cpp ) target_compile_features(test1 PUBLIC cxx_std_17) +######################################## + +add_executable(test2 + ./list.hpp + ./text_document.hpp + ./test2.cpp +) + +target_compile_features(test2 PUBLIC cxx_std_17) + diff --git a/version0/test2.cpp b/version0/test2.cpp new file mode 100644 index 0000000..e58888b --- /dev/null +++ b/version0/test2.cpp @@ -0,0 +1,222 @@ +#include "./text_document.hpp" + +#include +#include +#include +#include +#include + +// single letter agent, for testing only +using Agent = char; +using Doc = GreenCRDT::TextDocument; +using Op = GreenCRDT::TextDocument::Op; +using ListType = Doc::ListType; + +// maybe switch it up? +using Rng = std::minstd_rand; + +// 10*7 -> 70 permutations , ggwp +// | 1add | 1del | 1rep | 2add | 2del | 2rep | random add | random del | random rep | random +// empty doc | | 0 | 0 | | 0 | 0 | x | 0 | 0 | +// before 1 char | | | | | | | | | | +// after 1 char | | | | | | | | | | +// before 2 char | | | | | | | | | | +// in 2 char | | | | | | | | | | +// after 2 char | | | | | | | | | | +// random | | | | | | | | | | + +static const std::vector random_chars { + 'a', 'b', 'c', 'd', 'e', + 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', + 'z', + + 'A', 'B', 'C', 'D', 'E', + 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', + 'Z', +}; + +std::ostream& operator<<(std::ostream& out, const std::optional& id) { + if (id.has_value()) { + out << id.value().id << "-" << id.value().seq; + } else { + out << "null"; + } + return out; +} + +std::ostream& operator<<(std::ostream& out, const ListType::OpAdd& op) { + out + << "{ id:" << op.id.id + << "-" << op.id.seq + << ", v:" << op.value + << ", l:" << op.parent_left + << ", r:" << op.parent_right + << " }" + ; + return out; +} + +// genX() changes doc, uses local agent + +Op genAdd(Rng& rng, Doc& doc) { + ListType::OpAdd op { + {doc.local_agent, 0u}, + std::nullopt, + std::nullopt, + random_chars[rng()%random_chars.size()] + }; + + // first id is 0 + if (doc.state.last_seen_seq.count(doc.local_agent)) { + op.id.seq = doc.state.last_seen_seq[doc.local_agent] + 1; + } + + if (!doc.state.list.empty()) { + // gen parents + size_t li = rng()%(1+doc.state.list.size()); + if (li != doc.state.list.size()) { // nullopt + op.parent_left = doc.state.list[li].id; + } + + //size_t r_range = 1+doc.state.list.size(); + //if (li != doc.state.list.size()) { + //r_range -= li+1; + //} + //size_t ri = rng()%r_range; + //if (li != doc.state.list.size()) { + //ri += li+1; + //} + //if (ri != doc.state.list.size()) { // nullopt + //op.parent_right = doc.state.list[li].id; + //} + + if (op.parent_left.has_value()) { + if (doc.state.list.size() != li + 1) { // left is not last + op.parent_right = doc.state.list[li+1].id; + } + } else { + // left is before first, so right is first + op.parent_right = doc.state.list.front().id; + } + } // else first char, both nullopt + + //std::cout << "op: " << op << "\n"; + + { + bool r = doc.state.add(op.id, op.value, op.parent_left, op.parent_right); + if (!r) { + std::cout << "op: " << op << "\n"; + } + assert(r); + } + + return op; +} + +//genDel() +//genRep() +//genAddContRange() +//genDelContRange() +//genRepContRange() + +//genRand() +//genRandRanges() +std::vector genRandAll(Rng& rng, Doc& doc) { + switch (rng() % 1) { + case 0: + return {genAdd(rng, doc)}; + } + + return {}; +} + +void testEmptyDocAdds(size_t seed) { + Rng rng(seed); + + Doc doc; // empty + doc.local_agent = 'A'; + + std::string changed_text; + { + // for modifying + Doc doctmp = doc; + + const size_t loop_count = (rng() % 55)+1; + for (size_t i = 0; i < loop_count; i++) { + genAdd(rng, doctmp); + } + + changed_text = doctmp.getText(); + } + + assert(doc.getText() != changed_text); + + std::cout << "changed_text: " << changed_text << "\n"; + + doc.merge(changed_text); + + assert(doc.getText() == changed_text); +} + +void test1CharDocAdds(size_t seed) { + Rng rng(seed); + + Doc doc; + doc.local_agent = 'A'; + + doc.addText(std::nullopt, std::nullopt, "0"); + + assert(doc.getText() == "0"); + + std::string changed_text; + { + // for modifying + Doc doctmp = doc; + + const size_t loop_count = (rng() % 13)+1; + for (size_t i = 0; i < loop_count; i++) { + genAdd(rng, doctmp); + } + + changed_text = doctmp.getText(); + } + + assert(doc.getText() != changed_text); + + std::cout << "changed_text: " << changed_text << "\n"; + + doc.merge(changed_text); + + assert(doc.getText() == changed_text); +} + +int main(void) { + { + std::cout << "testEmptyDocAdds:\n"; + for (size_t i = 0; i < 1'000; i++) { + std::cout << "i " << i << "\n"; + testEmptyDocAdds(1337+i); + std::cout << std::string(40, '-') << "\n"; + } + std::cout << std::string(40, '=') << "\n"; + } + + { + std::cout << "test1CharDocAdds:\n"; + for (size_t i = 0; i < 1'000; i++) { + std::cout << "i " << i << "\n"; + test1CharDocAdds(1337+i); + std::cout << std::string(40, '-') << "\n"; + } + std::cout << std::string(40, '=') << "\n"; + } + + return 0; +} + diff --git a/version0/text_document.hpp b/version0/text_document.hpp index 072ae43..1121be0 100644 --- a/version0/text_document.hpp +++ b/version0/text_document.hpp @@ -6,6 +6,8 @@ #include #include +#include // debug + namespace GreenCRDT { template @@ -37,14 +39,14 @@ struct TextDocument { } static std::vector text2adds( - const AgentType& agent, uint64_t& last_seq, + const AgentType& agent, uint64_t seq, // seq is the first seq std::optional parent_left, std::optional parent_right, std::string_view text ) { std::vector ops; for (size_t i = 0; i < text.size(); i++) { - typename ListType::ListID new_id {agent, ++last_seq}; + typename ListType::ListID new_id {agent, seq++}; ops.emplace_back(typename ListType::OpAdd{ new_id, @@ -68,7 +70,7 @@ struct TextDocument { ) { // TODO: look up typesystem and fix (move? decltype?) std::vector ops = text2adds( - local_agent, state.last_seen_seq[local_agent], + local_agent, state.last_seen_seq.count(local_agent) ? state.last_seen_seq[local_agent] : 0u, parent_left, parent_right, text @@ -77,10 +79,12 @@ struct TextDocument { // TODO: make this better // and apply for (const auto& op : ops) { - if constexpr (std::holds_alternative(op)) { + if(std::holds_alternative(op)) { const auto& add_op = std::get(op); - state.add(add_op.id, add_op.value, add_op.parent_left, add_op.parent_right); - } else if constexpr (std::holds_alternative(op)) { + //std::cout << "a:" << add_op.id.id << " s:" << add_op.id.seq << " v:" << add_op.value << "\n"; + bool r = state.add(add_op.id, add_op.value, add_op.parent_left, add_op.parent_right); + assert(r); + } else if (std::holds_alternative(op)) { const auto& del_op = std::get(op); state.del(del_op.id); } else { @@ -134,25 +138,95 @@ struct TextDocument { // generates ops from the difference // note: rn it only creates 1 diff patch - std::vector merge(std::string_view other_text) { - if (other_text.empty()) { - return {}; + std::vector merge(std::string_view text) { + // cases: + // - [ ] text is empty + // - [x] doc is empty (deep) + // - [ ] doc is empty (shallow) // interesting? + // + // - [x] no changes -> change_start will go through to end + // + // not at start or end: + // - [ ] single char added -> doc.start > doc.end && text.start == text.end -> emit add + // - [ ] single char deleted -> doc.start == doc.end && text.start > text.end -> emit del + // - [ ] single char replaced -> doc.start == doc.end && text.start == text.end -> emit del, emit add + // + // - [ ] 2 chars added(together) -> doc.start > doc.end && text.start < text.end -> emit 2add + // - [ ] 2 chars deleted(together) -> doc.start < doc.end && text.start > text.end -> emit 2del + // - [ ] 2 chars replaced(together) -> doc.start == doc.end && text.start == text.end -> emit 2del, 2add + + + if (text.empty()) { + if (state.list.empty()) { + return {}; + } else { + assert(false && "impl me"); + return {}; + } } + // text not empty if (state.list.empty()) { return addText( std::nullopt, std::nullopt, - other_text + text ); } + // neither empty + // find start and end of changes // start - size_t list_idx_start = 0; - size_t other_idx_start = 0; - //for (; idx_start < state.list.size(); idx_start++) {} + size_t list_start = 0; + size_t text_start = 0; + bool differ = false; + for (; list_start < state.list.size() && text_start < text.size();) { + // jump over tombstones + if (!state.list[list_start].value.has_value()) { + list_start++; + continue; + } + if (state.list[list_start].value != text[text_start]) { + differ = true; + break; + } + + list_start++; + text_start++; + } + + // doc and text dont differ + if (!differ) { + return {}; + } + + std::cout << "list.size: " << state.list.size() << "(" << getText().size() << ")" << " text.size: " << text.size() << "\n"; + std::cout << "list_start: " << list_start << " text_start: " << text_start << "\n"; + + // +1 so i can have unsigned + size_t list_end = state.list.size(); + size_t text_end = text.size(); + for (; list_end > 0 && text_end > 0 && list_end >= list_start && text_end >= text_start;) { + // jump over tombstones + if (!state.list[list_end-1].value.has_value()) { + list_end--; + continue; + } + + if (state.list[list_end-1].value.value() != text[text_end-1]) { + break; + } + + list_end--; + text_end--; + } + + std::cout << "list_end: " << list_end << " text_end: " << text_end << "\n"; + + assert(false && "implement me"); + return {}; } };