Files
kte/tests/test_daily_workflows.cc
T
Claude 29d0ee8244 Remove Buffer::Rows() and the Line wrapper
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
2026-09-24 12:19:59 -07:00

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());
}