#include "Test.h" #include "Buffer.h" #include "Command.h" #include "Editor.h" #include "Swap.h" #include "tests/TestHarness.h" // for ktet::InstallDefaultCommandsOnce #include #include #include #include #include #include namespace { 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; } struct ScopedXdgStateHome { std::string old; bool had{false}; explicit ScopedXdgStateHome(const std::string &p) { const char *old_p = std::getenv("XDG_STATE_HOME"); had = (old_p && *old_p); old = old_p ? std::string(old_p) : std::string(); setenv("XDG_STATE_HOME", p.c_str(), 1); } ~ScopedXdgStateHome() { if (had && !old.empty()) { setenv("XDG_STATE_HOME", old.c_str(), 1); } else { unsetenv("XDG_STATE_HOME"); } } }; } // namespace TEST (SwapRecoveryPrompt_Recover_ReplaysSwap) { ktet::InstallDefaultCommandsOnce(); const std::filesystem::path xdg_root = std::filesystem::temp_directory_path() / (std::string("kte_ut_xdg_state_recover_") + std::to_string((int) ::getpid())); std::filesystem::remove_all(xdg_root); std::filesystem::create_directories(xdg_root); const ScopedXdgStateHome scoped(xdg_root.string()); const std::filesystem::path work = xdg_root / "work"; std::filesystem::create_directories(work); const std::string file_path = (work / "recover.txt").string(); write_file_bytes(file_path, "base\nline2\n"); // Create a swap journal with unsaved edits. Buffer b; std::string err; ASSERT_TRUE(b.OpenFromFile(file_path, err)); kte::SwapManager sm; sm.Attach(&b); b.SetSwapRecorder(sm.RecorderFor(&b)); b.insert_text(0, 0, std::string("X")); b.insert_text(1, 0, std::string("ZZ")); 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); // Now attempt to open via Editor deferred-open; this should trigger a recovery prompt. Editor ed; ed.SetDimensions(24, 80); ed.AddBuffer(Buffer()); ed.RequestOpenFile(b.Filename()); ASSERT_EQ(ed.ProcessPendingOpens(), false); ASSERT_EQ(ed.PendingRecoveryPrompt(), Editor::RecoveryPromptKind::RecoverOrDiscard); ASSERT_EQ(ed.PromptActive(), true); // Answer 'y' to recover. ASSERT_TRUE(Execute(ed, CommandId::InsertText, "y")); ASSERT_TRUE(Execute(ed, CommandId::Newline)); ASSERT_EQ(ed.PendingRecoveryPrompt(), Editor::RecoveryPromptKind::None); ASSERT_EQ(ed.PromptActive(), false); ASSERT_TRUE(ed.CurrentBuffer() != nullptr); ASSERT_EQ(buffer_bytes_via_views(*ed.CurrentBuffer()), expected); ASSERT_EQ(ed.CurrentBuffer()->Dirty(), true); ASSERT_TRUE(std::filesystem::exists(swap_path)); std::remove(file_path.c_str()); std::remove(swap_path.c_str()); std::filesystem::remove_all(xdg_root); } TEST (SwapRecoveryPrompt_Discard_DeletesSwapAndOpensClean) { ktet::InstallDefaultCommandsOnce(); const std::filesystem::path xdg_root = std::filesystem::temp_directory_path() / (std::string("kte_ut_xdg_state_discard_") + std::to_string((int) ::getpid())); std::filesystem::remove_all(xdg_root); std::filesystem::create_directories(xdg_root); const ScopedXdgStateHome scoped(xdg_root.string()); const std::filesystem::path work = xdg_root / "work"; std::filesystem::create_directories(work); const std::string file_path = (work / "discard.txt").string(); write_file_bytes(file_path, "base\n"); Buffer b; std::string err; ASSERT_TRUE(b.OpenFromFile(file_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); ASSERT_TRUE(std::filesystem::exists(swap_path)); Editor ed; ed.SetDimensions(24, 80); ed.AddBuffer(Buffer()); ed.RequestOpenFile(b.Filename()); ASSERT_EQ(ed.ProcessPendingOpens(), false); ASSERT_EQ(ed.PendingRecoveryPrompt(), Editor::RecoveryPromptKind::RecoverOrDiscard); ASSERT_EQ(ed.PromptActive(), true); // Default answer (empty) is 'no' => discard. ASSERT_TRUE(Execute(ed, CommandId::Newline)); ASSERT_EQ(ed.PendingRecoveryPrompt(), Editor::RecoveryPromptKind::None); ASSERT_EQ(ed.PromptActive(), false); ASSERT_TRUE(ed.CurrentBuffer() != nullptr); ASSERT_EQ(buffer_bytes_via_views(*ed.CurrentBuffer()), read_file_bytes(b.Filename())); ASSERT_EQ(ed.CurrentBuffer()->Dirty(), false); ASSERT_EQ(std::filesystem::exists(swap_path), false); std::remove(file_path.c_str()); std::filesystem::remove_all(xdg_root); } TEST (SwapRecoveryPrompt_Cancel_AbortsOpen) { ktet::InstallDefaultCommandsOnce(); const std::filesystem::path xdg_root = std::filesystem::temp_directory_path() / (std::string("kte_ut_xdg_state_cancel_") + std::to_string((int) ::getpid())); std::filesystem::remove_all(xdg_root); std::filesystem::create_directories(xdg_root); const ScopedXdgStateHome scoped(xdg_root.string()); const std::filesystem::path work = xdg_root / "work"; std::filesystem::create_directories(work); const std::string file_path = (work / "cancel.txt").string(); write_file_bytes(file_path, "base\n"); Buffer b; std::string err; ASSERT_TRUE(b.OpenFromFile(file_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); Editor ed; ed.SetDimensions(24, 80); ed.AddBuffer(Buffer()); ed.RequestOpenFile(b.Filename()); ASSERT_EQ(ed.ProcessPendingOpens(), false); ASSERT_EQ(ed.PendingRecoveryPrompt(), Editor::RecoveryPromptKind::RecoverOrDiscard); ASSERT_EQ(ed.PromptActive(), true); // Cancel the prompt (C-g / Refresh). ASSERT_TRUE(Execute(ed, CommandId::Refresh)); ASSERT_EQ(ed.PendingRecoveryPrompt(), Editor::RecoveryPromptKind::None); ASSERT_EQ(ed.PromptActive(), false); ASSERT_TRUE(ed.CurrentBuffer() != nullptr); ASSERT_EQ(ed.CurrentBuffer()->Filename().empty(), true); ASSERT_TRUE(std::filesystem::exists(swap_path)); std::remove(file_path.c_str()); std::remove(swap_path.c_str()); std::filesystem::remove_all(xdg_root); } TEST (SwapRecoveryPrompt_CorruptSwap_OffersDelete) { ktet::InstallDefaultCommandsOnce(); const std::filesystem::path xdg_root = std::filesystem::temp_directory_path() / (std::string("kte_ut_xdg_state_corrupt_") + std::to_string((int) ::getpid())); std::filesystem::remove_all(xdg_root); std::filesystem::create_directories(xdg_root); const ScopedXdgStateHome scoped(xdg_root.string()); const std::filesystem::path work = xdg_root / "work"; std::filesystem::create_directories(work); const std::string file_path = (work / "corrupt.txt").string(); write_file_bytes(file_path, "base\n"); Buffer b; std::string err; ASSERT_TRUE(b.OpenFromFile(file_path, err)); const std::string swap_path = kte::SwapManager::ComputeSwapPathForTests(b); // Write a corrupt swap file at the expected location. try { std::filesystem::create_directories(std::filesystem::path(swap_path).parent_path()); } catch (...) { // ignore } write_file_bytes(swap_path, "x"); ASSERT_TRUE(std::filesystem::exists(swap_path)); Editor ed; ed.SetDimensions(24, 80); ed.AddBuffer(Buffer()); ed.RequestOpenFile(b.Filename()); ASSERT_EQ(ed.ProcessPendingOpens(), false); ASSERT_EQ(ed.PendingRecoveryPrompt(), Editor::RecoveryPromptKind::DeleteCorruptSwap); ASSERT_EQ(ed.PromptActive(), true); // Answer 'y' to delete the corrupt swap and proceed. ASSERT_TRUE(Execute(ed, CommandId::InsertText, "y")); ASSERT_TRUE(Execute(ed, CommandId::Newline)); ASSERT_EQ(ed.PendingRecoveryPrompt(), Editor::RecoveryPromptKind::None); ASSERT_EQ(ed.PromptActive(), false); ASSERT_TRUE(ed.CurrentBuffer() != nullptr); ASSERT_EQ(buffer_bytes_via_views(*ed.CurrentBuffer()), read_file_bytes(b.Filename())); ASSERT_EQ(std::filesystem::exists(swap_path), false); std::remove(file_path.c_str()); std::filesystem::remove_all(xdg_root); }