281 lines
8.8 KiB
C++
281 lines
8.8 KiB
C++
#include "Test.h"
|
|
|
|
#include "Buffer.h"
|
|
#include "Command.h"
|
|
#include "Editor.h"
|
|
#include "Swap.h"
|
|
|
|
#include "tests/TestHarness.h" // for ktet::InstallDefaultCommandsOnce
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <unistd.h>
|
|
|
|
|
|
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<char>(in)), std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
|
|
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);
|
|
}
|