The row cache rebuilt a string for every line of the file after each edit and had one production caller left (filetype detection in the ImGui frontend, which needs only the first line). Removed with it: rows_, nrows_, the dirty flag set on every edit, ensure_rows_cache and a mutex guarding only that cache. Tests read lines with GetLineString, Nrows or, where they compare every line, a test-only LinesForTests(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
191 lines
5.7 KiB
C++
191 lines
5.7 KiB
C++
/*
|
|
* test_daily_workflows.cc - Integration tests for real-world editing scenarios
|
|
*
|
|
* This file demonstrates end-to-end testing of kte functionality by simulating
|
|
* complete user workflows without requiring a UI. Tests execute commands directly
|
|
* through the command system, validating that the entire stack (Editor, Buffer,
|
|
* PieceTable, UndoSystem, SwapManager) works together correctly.
|
|
*
|
|
* Key workflows tested:
|
|
* - Open file → Edit → Save: Basic editing lifecycle
|
|
* - Multi-buffer management: Opening, switching, and closing multiple files
|
|
* - Crash recovery: Swap file recording and replay after simulated crash
|
|
*
|
|
* These tests are valuable examples for developers because they show:
|
|
* 1. How to test complex interactions without a frontend
|
|
* 2. How commands compose to implement user workflows
|
|
* 3. How to verify end-to-end behavior including file I/O and crash recovery
|
|
*
|
|
* When adding new features, consider adding integration tests here to validate
|
|
* that they work correctly in realistic scenarios.
|
|
*/
|
|
#include "Test.h"
|
|
|
|
#include "Command.h"
|
|
#include "Editor.h"
|
|
|
|
#include "tests/TestHarness.h" // for ktet::InstallDefaultCommandsOnce
|
|
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
|
|
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.LinesForTests();
|
|
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;
|
|
}
|
|
|
|
|
|
TEST (DailyWorkflow_OpenEditSave_Transcript)
|
|
{
|
|
ktet::InstallDefaultCommandsOnce();
|
|
|
|
const std::string path = "./.kte_ut_daily_open_edit_save.txt";
|
|
std::remove(path.c_str());
|
|
write_file_bytes(path, "one\n");
|
|
const std::string npath = std::filesystem::canonical(path).string();
|
|
|
|
Editor ed;
|
|
ed.SetDimensions(24, 80);
|
|
// Seed an empty buffer so OpenFile can reuse it.
|
|
{
|
|
Buffer scratch;
|
|
ed.AddBuffer(std::move(scratch));
|
|
}
|
|
|
|
std::string err;
|
|
ASSERT_TRUE(ed.OpenFile(path, err));
|
|
ASSERT_TRUE(ed.CurrentBuffer() != nullptr);
|
|
ASSERT_EQ(ed.CurrentBuffer()->Filename(), npath);
|
|
|
|
// Append two new lines via commands (no UI).
|
|
ASSERT_TRUE(Execute(ed, CommandId::MoveFileEnd));
|
|
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "two"));
|
|
ASSERT_TRUE(Execute(ed, CommandId::Newline));
|
|
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "three"));
|
|
|
|
ASSERT_TRUE(Execute(ed, CommandId::Save));
|
|
ASSERT_TRUE(ed.CurrentBuffer() != nullptr);
|
|
ASSERT_EQ(read_file_bytes(npath), buffer_bytes_via_views(*ed.CurrentBuffer()));
|
|
|
|
std::remove(path.c_str());
|
|
std::remove(npath.c_str());
|
|
}
|
|
|
|
|
|
TEST (DailyWorkflow_MultiBufferSwitchClose_Transcript)
|
|
{
|
|
ktet::InstallDefaultCommandsOnce();
|
|
|
|
const std::string p1 = "./.kte_ut_daily_buf_1.txt";
|
|
const std::string p2 = "./.kte_ut_daily_buf_2.txt";
|
|
std::remove(p1.c_str());
|
|
std::remove(p2.c_str());
|
|
write_file_bytes(p1, "aaa\n");
|
|
write_file_bytes(p2, "bbb\n");
|
|
const std::string np1 = std::filesystem::canonical(p1).string();
|
|
const std::string np2 = std::filesystem::canonical(p2).string();
|
|
|
|
Editor ed;
|
|
ed.SetDimensions(24, 80);
|
|
{
|
|
Buffer scratch;
|
|
ed.AddBuffer(std::move(scratch));
|
|
}
|
|
|
|
std::string err;
|
|
ASSERT_TRUE(ed.OpenFile(p1, err));
|
|
ASSERT_TRUE(ed.OpenFile(p2, err));
|
|
ASSERT_EQ(ed.BufferCount(), (std::size_t) 2);
|
|
ASSERT_TRUE(ed.CurrentBuffer() != nullptr);
|
|
ASSERT_EQ(ed.CurrentBuffer()->Filename(), np2);
|
|
|
|
// Switch back and forth.
|
|
ASSERT_TRUE(Execute(ed, CommandId::BufferPrev));
|
|
ASSERT_EQ(ed.CurrentBuffer()->Filename(), np1);
|
|
ASSERT_TRUE(Execute(ed, CommandId::BufferNext));
|
|
ASSERT_EQ(ed.CurrentBuffer()->Filename(), np2);
|
|
|
|
// Close current buffer (p2); ensure we land on p1.
|
|
ASSERT_TRUE(Execute(ed, CommandId::BufferClose));
|
|
ASSERT_EQ(ed.BufferCount(), (std::size_t) 1);
|
|
ASSERT_TRUE(ed.CurrentBuffer() != nullptr);
|
|
ASSERT_EQ(ed.CurrentBuffer()->Filename(), np1);
|
|
|
|
std::remove(p1.c_str());
|
|
std::remove(p2.c_str());
|
|
std::remove(np1.c_str());
|
|
std::remove(np2.c_str());
|
|
}
|
|
|
|
|
|
TEST (DailyWorkflow_CrashRecovery_SwapReplay_Transcript)
|
|
{
|
|
ktet::InstallDefaultCommandsOnce();
|
|
|
|
const std::string path = "./.kte_ut_daily_swap_recover.txt";
|
|
std::remove(path.c_str());
|
|
write_file_bytes(path, "base\nline2\n");
|
|
|
|
Editor ed;
|
|
ed.SetDimensions(24, 80);
|
|
{
|
|
Buffer scratch;
|
|
ed.AddBuffer(std::move(scratch));
|
|
}
|
|
|
|
std::string err;
|
|
ASSERT_TRUE(ed.OpenFile(path, err));
|
|
Buffer *buf = ed.CurrentBuffer();
|
|
ASSERT_TRUE(buf != nullptr);
|
|
|
|
// Make unsaved edits through command execution.
|
|
ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart));
|
|
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "X"));
|
|
ASSERT_TRUE(Execute(ed, CommandId::MoveDown));
|
|
ASSERT_TRUE(Execute(ed, CommandId::MoveHome));
|
|
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "ZZ"));
|
|
ASSERT_TRUE(Execute(ed, CommandId::MoveFileEnd));
|
|
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "TAIL"));
|
|
|
|
// Ensure journal is durable and capture expected bytes.
|
|
ed.Swap()->Flush(buf);
|
|
const std::string swap_path = kte::SwapManager::ComputeSwapPathForTests(*buf);
|
|
const std::string expected = buffer_bytes_via_views(*buf);
|
|
|
|
// "Crash": reopen from disk (original file content) into a fresh Buffer and replay.
|
|
Buffer recovered;
|
|
ASSERT_TRUE(recovered.OpenFromFile(path, err));
|
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(recovered, swap_path, err));
|
|
ASSERT_EQ(buffer_bytes_via_views(recovered), expected);
|
|
|
|
// Cleanup.
|
|
ed.Swap()->Detach(buf);
|
|
std::remove(path.c_str());
|
|
std::remove(swap_path.c_str());
|
|
} |