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