#include "Test.h" #include "Buffer.h" #include "Swap.h" #include #include #include #include #include static void write_file_bytes(const std::string &path, const std::string &bytes) { std::ofstream out(path, std::ios::binary | std::ios::trunc); out.write(bytes.data(), (std::streamsize) bytes.size()); } static std::string read_file_bytes(const std::string &path) { std::ifstream in(path, std::ios::binary); return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); } static std::string buffer_bytes_via_views(const Buffer &b) { const auto &rows = b.Rows(); std::string out; for (std::size_t i = 0; i < rows.size(); i++) { auto v = b.GetLineView(i); out.append(v.data(), v.size()); } return out; } static std::vector record_types_from_bytes(const std::string &bytes) { std::vector types; if (bytes.size() < 64) return types; std::size_t off = 64; while (off < bytes.size()) { if (bytes.size() - off < 8) break; const std::uint8_t type = static_cast(bytes[off + 0]); const std::uint32_t len = (std::uint32_t) static_cast(bytes[off + 1]) | ((std::uint32_t) static_cast(bytes[off + 2]) << 8) | ((std::uint32_t) static_cast(bytes[off + 3]) << 16); const std::size_t crc_off = off + 4 + (std::size_t) len; if (crc_off + 4 > bytes.size()) break; types.push_back(type); off = crc_off + 4; } return types; } TEST (SwapReplay_RecordFlushReopenReplay_ExactBytesMatch) { const std::string path = "./.kte_ut_swap_replay_1.txt"; std::remove(path.c_str()); write_file_bytes(path, "base\nline2\n"); Buffer b; std::string err; ASSERT_TRUE(b.OpenFromFile(path, err)); kte::SwapManager sm; sm.Attach(&b); b.SetSwapRecorder(sm.RecorderFor(&b)); // Edits (no save): swap should capture these. b.insert_text(0, 0, std::string("X")); // Xbase\nline2\n b.delete_text(1, 1, 2); // delete "in" from "line2" b.split_line(0, 3); // Xba\nse... b.join_lines(0); // join back b.insert_text(1, 0, std::string("ZZ")); // insert at start of line2 b.delete_text(0, 0, 1); // delete leading X sm.Flush(&b); const std::string swap_path = kte::SwapManager::ComputeSwapPathForTests(b); const std::string expected = buffer_bytes_via_views(b); // Close journal before replaying (for determinism) b.SetSwapRecorder(nullptr); sm.Detach(&b); Buffer b2; ASSERT_TRUE(b2.OpenFromFile(path, err)); ASSERT_TRUE(kte::SwapManager::ReplayFile(b2, swap_path, err)); ASSERT_EQ(buffer_bytes_via_views(b2), expected); std::remove(path.c_str()); std::remove(swap_path.c_str()); } TEST (SwapReplay_TruncatedLog_FailsSafely) { const std::string path = "./.kte_ut_swap_replay_2.txt"; std::remove(path.c_str()); write_file_bytes(path, "hello\n"); Buffer b; std::string err; ASSERT_TRUE(b.OpenFromFile(path, err)); kte::SwapManager sm; sm.Attach(&b); b.SetSwapRecorder(sm.RecorderFor(&b)); b.insert_text(0, 0, std::string("X")); sm.Flush(&b); const std::string swap_path = kte::SwapManager::ComputeSwapPathForTests(b); b.SetSwapRecorder(nullptr); sm.Detach(&b); const std::string bytes = read_file_bytes(swap_path); ASSERT_TRUE(bytes.size() > 70); // header + at least one record const std::string trunc_path = swap_path + ".trunc"; write_file_bytes(trunc_path, bytes.substr(0, bytes.size() - 1)); Buffer b2; ASSERT_TRUE(b2.OpenFromFile(path, err)); std::string rerr; ASSERT_EQ(kte::SwapManager::ReplayFile(b2, trunc_path, rerr), false); ASSERT_EQ(rerr.empty(), false); std::remove(path.c_str()); std::remove(swap_path.c_str()); std::remove(trunc_path.c_str()); } TEST (SwapReplay_Checkpoint_Midstream_ExactBytesMatch) { const std::string path = "./.kte_ut_swap_replay_chkpt_1.txt"; std::remove(path.c_str()); write_file_bytes(path, "base\nline2\n"); Buffer b; std::string err; ASSERT_TRUE(b.OpenFromFile(path, err)); kte::SwapManager sm; sm.Attach(&b); b.SetSwapRecorder(sm.RecorderFor(&b)); // Some edits, then an explicit checkpoint, then more edits. b.insert_text(0, 0, std::string("X")); sm.Checkpoint(&b); b.insert_text(1, 0, std::string("ZZ")); b.delete_text(0, 0, 1); sm.Flush(&b); const std::string swap_path = kte::SwapManager::ComputeSwapPathForTests(b); const std::string expected = buffer_bytes_via_views(b); b.SetSwapRecorder(nullptr); sm.Detach(&b); Buffer b2; ASSERT_TRUE(b2.OpenFromFile(path, err)); ASSERT_TRUE(kte::SwapManager::ReplayFile(b2, swap_path, err)); ASSERT_EQ(buffer_bytes_via_views(b2), expected); std::remove(path.c_str()); std::remove(swap_path.c_str()); } TEST (SwapCompaction_RewritesToSingleCheckpoint) { const std::string path = "./.kte_ut_swap_compact_1.txt"; std::remove(path.c_str()); write_file_bytes(path, "base\n"); Buffer b; std::string err; ASSERT_TRUE(b.OpenFromFile(path, err)); kte::SwapManager sm; kte::SwapConfig cfg; cfg.checkpoint_bytes = 0; cfg.checkpoint_interval_ms = 0; cfg.compact_bytes = 1; // force compaction on any checkpoint sm.SetConfig(cfg); sm.Attach(&b); b.SetSwapRecorder(sm.RecorderFor(&b)); // Ensure there is at least one non-checkpoint record on disk first. b.insert_text(0, 0, std::string("abc")); sm.Flush(&b); // Now emit a checkpoint; compaction should rewrite the file to just that checkpoint. sm.Checkpoint(&b); sm.Flush(&b); const std::string swap_path = kte::SwapManager::ComputeSwapPathForTests(b); const std::string expected = buffer_bytes_via_views(b); // Close journal. b.SetSwapRecorder(nullptr); sm.Detach(&b); const std::string bytes = read_file_bytes(swap_path); const std::vector types = record_types_from_bytes(bytes); ASSERT_EQ(types.size(), (std::size_t) 1); ASSERT_EQ(types[0], (std::uint8_t) kte::SwapRecType::CHKPT); Buffer b2; ASSERT_TRUE(b2.OpenFromFile(path, err)); ASSERT_TRUE(kte::SwapManager::ReplayFile(b2, swap_path, err)); ASSERT_EQ(buffer_bytes_via_views(b2), expected); std::remove(path.c_str()); std::remove(swap_path.c_str()); }