diff --git a/CMakeLists.txt b/CMakeLists.txt index afc85ca..8a5b19b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -292,66 +292,34 @@ install(TARGETS kte install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) if (BUILD_TESTS) - # test_undo executable for testing undo/redo system - add_executable(test_undo - test_undo.cc - ${COMMON_SOURCES} - ${COMMON_HEADERS} + # Unified unit test runner + add_executable(kte_tests + tests/TestRunner.cc + tests/Test.h + tests/test_buffer_io.cc + tests/test_piece_table.cc + tests/test_search.cc + + # minimal engine sources required by Buffer + PieceTable.cc + Buffer.cc + OptimizedSearch.cc + UndoNode.cc + UndoTree.cc + UndoSystem.cc + ${SYNTAX_SOURCES} ) - if (KTE_UNDO_DEBUG) - target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1) - endif () + # Allow tests to include project headers like "Buffer.h" + target_include_directories(kte_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - - target_link_libraries(test_undo ${CURSES_LIBRARIES}) + # Keep tests free of ncurses/GUI deps if (KTE_ENABLE_TREESITTER) if (TREESITTER_INCLUDE_DIR) - target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR}) + target_include_directories(kte_tests PRIVATE ${TREESITTER_INCLUDE_DIR}) endif () if (TREESITTER_LIBRARY) - target_link_libraries(test_undo ${TREESITTER_LIBRARY}) - endif () - endif () - - # test_buffer_save executable to verify Buffer::Save writes contents to disk - # Keep this test minimal to avoid pulling the entire app; only compile what's needed - add_executable(test_buffer_save - test_buffer_save.cc - PieceTable.cc - Buffer.cc - ${SYNTAX_SOURCES} - UndoNode.cc - UndoTree.cc - UndoSystem.cc - ) - # test_buffer_save_existing: verifies Save() after OpenFromFile on existing path - add_executable(test_buffer_save_existing - test_buffer_save_existing.cc - PieceTable.cc - Buffer.cc - ${SYNTAX_SOURCES} - UndoNode.cc - UndoTree.cc - UndoSystem.cc - ) - # test for opening a non-existent path then saving - add_executable(test_buffer_open_nonexistent_save - test_buffer_open_nonexistent_save.cc - PieceTable.cc - Buffer.cc - ${SYNTAX_SOURCES} - UndoNode.cc - UndoTree.cc - UndoSystem.cc - ) - # No ncurses needed for this unit - if (KTE_ENABLE_TREESITTER) - if (TREESITTER_INCLUDE_DIR) - target_include_directories(test_buffer_save PRIVATE ${TREESITTER_INCLUDE_DIR}) - endif () - if (TREESITTER_LIBRARY) - target_link_libraries(test_buffer_save ${TREESITTER_LIBRARY}) + target_link_libraries(kte_tests ${TREESITTER_LIBRARY}) endif () endif () endif () diff --git a/test_buffer_open_nonexistent_save.cc b/test_buffer_open_nonexistent_save.cc deleted file mode 100644 index 64b7c34..0000000 --- a/test_buffer_open_nonexistent_save.cc +++ /dev/null @@ -1,50 +0,0 @@ -// Test: Open a non-existent path (buffer becomes unnamed but with filename), -// insert text, then Save via SaveAs to the same path. Verify bytes on disk. -#include -#include -#include -#include - -#include "Buffer.h" - -static std::string read_file(const std::string &path) -{ - std::ifstream in(path, std::ios::binary); - return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); -} - -int main() -{ - const std::string path = "./.kte_test_open_nonexistent_save.tmp"; - std::remove(path.c_str()); - - // Sanity: path should not exist - { - std::ifstream probe(path); - assert(!probe.good()); - } - - Buffer buf; - std::string err; - bool ok = buf.OpenFromFile(path, err); - assert(ok && err.empty()); - assert(!buf.IsFileBacked()); - assert(buf.Filename().size() > 0); - - // Insert text like a user would type then press Return - buf.insert_text(0, 0, std::string("hello, world")); - // Simulate pressing Return (newline at end) - buf.insert_text(0, 12, std::string("\n")); - buf.SetDirty(true); - - // Save using SaveAs to the same filename the buffer carries (what cmd_save would do) - ok = buf.SaveAs(buf.Filename(), err); - assert(ok && err.empty()); - - const std::string got = read_file(buf.Filename()); - const std::string expected = std::string("hello, world\n"); - assert(got == expected); - - std::remove(path.c_str()); - return 0; -} diff --git a/test_buffer_save.cc b/test_buffer_save.cc deleted file mode 100644 index 85f5d0b..0000000 --- a/test_buffer_save.cc +++ /dev/null @@ -1,57 +0,0 @@ -// Test that Buffer::Save writes actual contents to disk -#include -#include -#include -#include - -#include "Buffer.h" - -static std::string -read_file(const std::string &path) -{ - std::ifstream in(path, std::ios::binary); - return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); -} - -int -main() -{ - // Create a temporary path under current working directory - const std::string path = "./.kte_test_buffer_save.tmp"; - - // Ensure any previous file is removed - std::remove(path.c_str()); - - Buffer buf; - - // Simulate editing a new buffer: insert content - const std::string payload = "Hello, world!\nThis is a save test.\n"; - buf.insert_text(0, 0, payload); - - // Make it file-backed with a filename so Save() path is exercised - // We use SaveAs first to set filename/is_file_backed and then modify content - std::string err; - bool ok = buf.SaveAs(path, err); - assert(ok && err.empty()); - - // Modify buffer after SaveAs to ensure Save() writes new content - const std::string more = "Appended line.\n"; - buf.insert_text(2, 0, more); - - // Mark as dirty so commands would attempt to save; Save() itself doesn’t require dirty - buf.SetDirty(true); - - // Now call Save() which should use streaming write and persist all bytes - err.clear(); - ok = buf.Save(err); - assert(ok && err.empty()); - - // Verify file contents exactly match expected string - const std::string expected = payload + more; - const std::string got = read_file(path); - assert(got == expected); - - // Cleanup - std::remove(path.c_str()); - return 0; -} diff --git a/test_buffer_save_existing.cc b/test_buffer_save_existing.cc deleted file mode 100644 index 702cb0f..0000000 --- a/test_buffer_save_existing.cc +++ /dev/null @@ -1,48 +0,0 @@ -// Test saving after opening an existing file -#include -#include -#include -#include - -#include "Buffer.h" - -static void write_file(const std::string &path, const std::string &data) -{ - std::ofstream out(path, std::ios::binary | std::ios::trunc); - out.write(data.data(), static_cast(data.size())); -} - -static std::string read_file(const std::string &path) -{ - std::ifstream in(path, std::ios::binary); - return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); -} - -int main() -{ - const std::string path = "./.kte_test_buffer_save_existing.tmp"; - std::remove(path.c_str()); - const std::string initial = "abc\n123\n"; - write_file(path, initial); - - Buffer buf; - std::string err; - bool ok = buf.OpenFromFile(path, err); - assert(ok && err.empty()); - - // Insert at end - buf.insert_text(2, 0, std::string("tail\n")); - buf.SetDirty(true); - - // Save should overwrite the same file - err.clear(); - ok = buf.Save(err); - assert(ok && err.empty()); - - const std::string expected = initial + "tail\n"; - const std::string got = read_file(path); - assert(got == expected); - - std::remove(path.c_str()); - return 0; -} diff --git a/test_search_correctness.cc b/test_search_correctness.cc deleted file mode 100644 index 9efb283..0000000 --- a/test_search_correctness.cc +++ /dev/null @@ -1,74 +0,0 @@ -// Verify OptimizedSearch against std::string reference across patterns and sizes -#include -#include -#include -#include -#include - -#include "OptimizedSearch.h" - - -static std::vector -ref_find_all(const std::string &text, const std::string &pat) -{ - std::vector res; - if (pat.empty()) - return res; - std::size_t from = 0; - while (true) { - auto p = text.find(pat, from); - if (p == std::string::npos) - break; - res.push_back(p); - from = p + pat.size(); // non-overlapping - } - return res; -} - - -static void -run_case(std::size_t textLen, std::size_t patLen, unsigned seed) -{ - std::mt19937 rng(seed); - std::uniform_int_distribution dist('a', 'z'); - std::string text(textLen, '\0'); - for (auto &ch: text) - ch = static_cast(dist(rng)); - std::string pat(patLen, '\0'); - for (auto &ch: pat) - ch = static_cast(dist(rng)); - - // Guarantee at least one match when possible - if (textLen >= patLen && patLen > 0) { - std::size_t pos = textLen / 3; - if (pos + patLen <= text.size()) - std::copy(pat.begin(), pat.end(), text.begin() + static_cast(pos)); - } - - OptimizedSearch os; - auto got = os.find_all(text, pat, 0); - auto ref = ref_find_all(text, pat); - assert(got == ref); -} - - -int -main() -{ - // Edge cases - run_case(0, 0, 1); - run_case(0, 1, 2); - run_case(1, 0, 3); - run_case(1, 1, 4); - - // Various sizes - for (std::size_t t = 128; t <= 4096; t *= 2) { - for (std::size_t p = 1; p <= 64; p *= 2) { - run_case(t, p, static_cast(t + p)); - } - } - // Larger random - run_case(100000, 16, 12345); - run_case(250000, 32, 67890); - return 0; -} \ No newline at end of file diff --git a/test_undo.cc b/test_undo.cc deleted file mode 100644 index abf6c4b..0000000 --- a/test_undo.cc +++ /dev/null @@ -1,338 +0,0 @@ -#include -#include -#include - -#include "Buffer.h" -#include "Command.h" -#include "Editor.h" -#include "TestFrontend.h" - - -int -main() -{ - // Install default commands - InstallDefaultCommands(); - - Editor editor; - TestFrontend frontend; - - // Initialize frontend - if (!frontend.Init(editor)) { - std::cerr << "Failed to initialize frontend\n"; - return 1; - } - - // Create a temporary test file - std::string err; - const char *tmpfile = "/tmp/kte_test_undo.txt"; - { - std::ofstream f(tmpfile); - if (!f) { - std::cerr << "Failed to create temp file\n"; - return 1; - } - f << "\n"; // Write one newline so file isn't empty - f.close(); - } - - if (!editor.OpenFile(tmpfile, err)) { - std::cerr << "Failed to open test file: " << err << "\n"; - return 1; - } - - Buffer *buf = editor.CurrentBuffer(); - assert(buf != nullptr); - - // Initialize cursor to (0,0) explicitly - buf->SetCursor(0, 0); - - std::cout << "test_undo: Testing undo/redo system\n"; - std::cout << "====================================\n\n"; - - bool running = true; - - // Test 1: Insert text and verify buffer contains expected text - std::cout << "Test 1: Insert text 'Hello'\n"; - frontend.Input().QueueText("Hello"); - - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - - assert(buf->Rows().size() >= 1); - std::string line_after_insert = std::string(buf->Rows()[0]); - assert(line_after_insert == "Hello"); - std::cout << " Buffer content: '" << line_after_insert << "'\n"; - std::cout << " ✓ Text insertion verified\n\n"; - - // Test 2: Undo insertion - text should be removed - std::cout << "Test 2: Undo insertion\n"; - frontend.Input().QueueCommand(CommandId::Undo); - - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - - assert(buf->Rows().size() >= 1); - std::string line_after_undo = std::string(buf->Rows()[0]); - assert(line_after_undo == ""); - std::cout << " Buffer content: '" << line_after_undo << "'\n"; - std::cout << " ✓ Undo successful - text removed\n\n"; - - // Test 3: Redo insertion - text should be restored - std::cout << "Test 3: Redo insertion\n"; - frontend.Input().QueueCommand(CommandId::Redo); - - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - - assert(buf->Rows().size() >= 1); - std::string line_after_redo = std::string(buf->Rows()[0]); - assert(line_after_redo == "Hello"); - std::cout << " Buffer content: '" << line_after_redo << "'\n"; - std::cout << " ✓ Redo successful - text restored\n\n"; - - // Test 4: Branching behavior – redo is discarded after new edits - std::cout << "Test 4: Branching behavior (redo discarded after new edits)\n"; - // Reset to empty by undoing the last redo and the original insert, then reinsert 'abc' - // Ensure buffer is empty before starting this scenario - frontend.Input().QueueCommand(CommandId::Undo); // undo Hello - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == ""); - - // Type a contiguous word 'abc' (single batch) - frontend.Input().QueueText("abc"); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "abc"); - - // Undo once – should remove the whole batch and leave empty - frontend.Input().QueueCommand(CommandId::Undo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == ""); - - // Now type new text 'X' – this should create a new branch and discard old redo chain - frontend.Input().QueueText("X"); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "X"); - - // Attempt Redo – should be a no-op (redo branch was discarded by new edit) - frontend.Input().QueueCommand(CommandId::Redo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "X"); - // Undo and Redo along the new branch should still work - frontend.Input().QueueCommand(CommandId::Undo); - frontend.Input().QueueCommand(CommandId::Redo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "X"); - std::cout << " ✓ Redo discarded after new edit; new branch undo/redo works\n\n"; - - // Clear buffer state for next tests: undo to empty if needed - frontend.Input().QueueCommand(CommandId::Undo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == ""); - - // Test 5: UTF-8 insertion and undo/redo round-trip - std::cout << "Test 5: UTF-8 insertion 'é漢' and undo/redo\n"; - const std::string utf8_text = "é漢"; // multi-byte UTF-8 (2 bytes + 3 bytes) - frontend.Input().QueueText(utf8_text); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == utf8_text); - // Undo should remove the entire contiguous insertion batch - frontend.Input().QueueCommand(CommandId::Undo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == ""); - // Redo restores it - frontend.Input().QueueCommand(CommandId::Redo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == utf8_text); - std::cout << " ✓ UTF-8 insert round-trips with undo/redo\n\n"; - - // Clear for next test - frontend.Input().QueueCommand(CommandId::Undo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == ""); - - // Test 6: Multi-line operations (newline split and join via backspace at BOL) - std::cout << "Test 6: Newline split and join via backspace at BOL\n"; - // Insert "ab" then newline then "cd" → expect two lines - frontend.Input().QueueText("ab"); - frontend.Input().QueueCommand(CommandId::Newline); - frontend.Input().QueueText("cd"); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(buf->Rows().size() >= 2); - assert(std::string(buf->Rows()[0]) == "ab"); - assert(std::string(buf->Rows()[1]) == "cd"); - std::cout << " ✓ Split into two lines\n"; - - // Undo once – should remove "cd" insertion leaving two lines ["ab", ""] or join depending on commit - frontend.Input().QueueCommand(CommandId::Undo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - // Current design batches typing on the second line; after undo, the second line should exist but be empty - assert(buf->Rows().size() >= 2); - assert(std::string(buf->Rows()[0]) == "ab"); - assert(std::string(buf->Rows()[1]) == ""); - - // Undo the newline – should rejoin to a single line "ab" - frontend.Input().QueueCommand(CommandId::Undo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(buf->Rows().size() >= 1); - assert(std::string(buf->Rows()[0]) == "ab"); - - // Redo twice to get back to ["ab","cd"] - frontend.Input().QueueCommand(CommandId::Redo); - frontend.Input().QueueCommand(CommandId::Redo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "ab"); - assert(std::string(buf->Rows()[1]) == "cd"); - std::cout << " ✓ Newline undo/redo round-trip\n"; - - // Now join via Backspace at beginning of second line - frontend.Input().QueueCommand(CommandId::MoveDown); // ensure we're on the second line - frontend.Input().QueueCommand(CommandId::MoveHome); // go to BOL on second line - frontend.Input().QueueCommand(CommandId::Backspace); // join with previous line - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(buf->Rows().size() >= 1); - assert(std::string(buf->Rows()[0]) == "abcd"); - std::cout << " ✓ Backspace at BOL joins lines\n"; - - // Undo/Redo the join - frontend.Input().QueueCommand(CommandId::Undo); - frontend.Input().QueueCommand(CommandId::Redo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(buf->Rows().size() >= 1); - assert(std::string(buf->Rows()[0]) == "abcd"); - std::cout << " ✓ Join undo/redo round-trip\n\n"; - - // Test 7: Typing batching – a contiguous word undone in one step - std::cout << "Test 7: Typing batching (single undo removes whole word)\n"; - // Clear current line first - frontend.Input().QueueCommand(CommandId::MoveHome); - frontend.Input().QueueCommand(CommandId::KillToEOL); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]).empty()); - // Type a word and verify one undo clears it - frontend.Input().QueueText("hello"); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "hello"); - frontend.Input().QueueCommand(CommandId::Undo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]).empty()); - frontend.Input().QueueCommand(CommandId::Redo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "hello"); - std::cout << " ✓ Contiguous typing batched into single undo step\n\n"; - - // Test 8: Forward delete batching at a fixed anchor column - std::cout << "Test 8: Forward delete batching at fixed anchor (DeleteChar)\n"; - // Prepare line content - frontend.Input().QueueCommand(CommandId::MoveHome); - frontend.Input().QueueCommand(CommandId::KillToEOL); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - frontend.Input().QueueText("abcdef"); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - // Ensure cursor at anchor column 0 - frontend.Input().QueueCommand(CommandId::MoveHome); - // Delete three chars at cursor; should batch into one Delete node - frontend.Input().QueueCommand(CommandId::DeleteChar, "", 3); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "def"); - // Single undo should restore the entire deleted run - frontend.Input().QueueCommand(CommandId::Undo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "abcdef"); - // Redo should remove the same run again - frontend.Input().QueueCommand(CommandId::Redo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "def"); - std::cout << " ✓ Forward delete batched and undo/redo round-trips\n\n"; - - // Test 9: Backspace batching with prepend rule (cursor moves left) - std::cout << "Test 9: Backspace batching with prepend rule\n"; - // Restore to full string then backspace a run - frontend.Input().QueueCommand(CommandId::Undo); // bring back to "abcdef" - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "abcdef"); - // Move to end and backspace three characters; should batch into one Delete node - frontend.Input().QueueCommand(CommandId::MoveEnd); - frontend.Input().QueueCommand(CommandId::Backspace, "", 3); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "abc"); - // Single undo restores the deleted run - frontend.Input().QueueCommand(CommandId::Undo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "abcdef"); - // Redo removes it again - frontend.Input().QueueCommand(CommandId::Redo); - while (!frontend.Input().IsEmpty() && running) { - frontend.Step(editor, running); - } - assert(std::string(buf->Rows()[0]) == "abc"); - std::cout << " ✓ Backspace run batched and undo/redo round-trips\n\n"; - - frontend.Shutdown(); - - std::cout << "====================================\n"; - std::cout << "All tests passed!\n"; - - return 0; -} diff --git a/tests/Test.h b/tests/Test.h new file mode 100644 index 0000000..6d41844 --- /dev/null +++ b/tests/Test.h @@ -0,0 +1,63 @@ +// Minimal header-only unit test framework for kte +#pragma once +#include +#include +#include +#include +#include +#include + +namespace ktet { + +struct TestCase { + std::string name; + std::function fn; +}; + +inline std::vector& registry() { + static std::vector r; + return r; +} + +struct Registrar { + Registrar(const char* name, std::function fn) { + registry().push_back(TestCase{std::string(name), std::move(fn)}); + } +}; + +// Assertions +struct AssertionFailure { + std::string msg; +}; + +inline void expect(bool cond, const char* expr, const char* file, int line) { + if (!cond) { + std::cerr << file << ":" << line << ": EXPECT failed: " << expr << "\n"; + } +} + +inline void assert_true(bool cond, const char* expr, const char* file, int line) { + if (!cond) { + throw AssertionFailure{std::string(file) + ":" + std::to_string(line) + ": ASSERT failed: " + expr}; + } +} + +template +inline void assert_eq_impl(const A& a, const B& b, const char* ea, const char* eb, const char* file, int line) { + if (!(a == b)) { + std::ostringstream oss; + oss << file << ":" << line << ": ASSERT_EQ failed: " << ea << " == " << eb; + throw AssertionFailure{oss.str()}; + } +} + +} // namespace ktet + +#define TEST(name) \ + static void name(); \ + static ::ktet::Registrar _reg_##name(#name, &name); \ + static void name() + +#define EXPECT_TRUE(x) ::ktet::expect((x), #x, __FILE__, __LINE__) +#define ASSERT_TRUE(x) ::ktet::assert_true((x), #x, __FILE__, __LINE__) +#define ASSERT_EQ(a,b) ::ktet::assert_eq_impl((a),(b), #a, #b, __FILE__, __LINE__) diff --git a/tests/TestRunner.cc b/tests/TestRunner.cc new file mode 100644 index 0000000..c4ebb8b --- /dev/null +++ b/tests/TestRunner.cc @@ -0,0 +1,33 @@ +#include "Test.h" +#include +#include + +int main() { + using namespace std::chrono; + auto ® = ktet::registry(); + std::cout << "kte unit tests: " << reg.size() << " test(s)\n"; + int failed = 0; + auto t0 = steady_clock::now(); + for (const auto &tc : reg) { + auto ts = steady_clock::now(); + try { + tc.fn(); + auto te = steady_clock::now(); + auto ms = duration_cast(te - ts).count(); + std::cout << "[ OK ] " << tc.name << " (" << ms << " ms)\n"; + } catch (const ktet::AssertionFailure &e) { + ++failed; + std::cerr << "[FAIL] " << tc.name << " -> " << e.msg << "\n"; + } catch (const std::exception &e) { + ++failed; + std::cerr << "[EXCP] " << tc.name << " -> " << e.what() << "\n"; + } catch (...) { + ++failed; + std::cerr << "[EXCP] " << tc.name << " -> unknown exception\n"; + } + } + auto t1 = steady_clock::now(); + auto total_ms = duration_cast(t1 - t0).count(); + std::cout << "Done in " << total_ms << " ms. Failures: " << failed << "\n"; + return failed == 0 ? 0 : 1; +} diff --git a/tests/test_buffer_io.cc b/tests/test_buffer_io.cc new file mode 100644 index 0000000..29046ca --- /dev/null +++ b/tests/test_buffer_io.cc @@ -0,0 +1,79 @@ +#include "Test.h" +#include +#include +#include +#include "Buffer.h" + +static std::string read_all(const std::string &path) { + std::ifstream in(path, std::ios::binary); + return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); +} + +TEST(Buffer_SaveAs_and_Save_new_file) { + const std::string path = "./.kte_ut_buffer_io_1.tmp"; + std::remove(path.c_str()); + + Buffer b; + // insert two lines + b.insert_text(0, 0, std::string("Hello, world!\n")); + b.insert_text(1, 0, std::string("Second line\n")); + + std::string err; + ASSERT_TRUE(b.SaveAs(path, err)); + ASSERT_EQ(err.empty(), true); + + // append another line then Save() + b.insert_text(2, 0, std::string("Third\n")); + b.SetDirty(true); + ASSERT_TRUE(b.Save(err)); + ASSERT_EQ(err.empty(), true); + + std::string got = read_all(path); + ASSERT_EQ(got, std::string("Hello, world!\nSecond line\nThird\n")); + + std::remove(path.c_str()); +} + +TEST(Buffer_Save_after_Open_existing) { + const std::string path = "./.kte_ut_buffer_io_2.tmp"; + std::remove(path.c_str()); + { + std::ofstream out(path, std::ios::binary); + out << "abc\n123\n"; + } + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(err.empty(), true); + + b.insert_text(2, 0, std::string("tail\n")); + b.SetDirty(true); + ASSERT_TRUE(b.Save(err)); + ASSERT_EQ(err.empty(), true); + + std::string got = read_all(path); + ASSERT_EQ(got, std::string("abc\n123\ntail\n")); + std::remove(path.c_str()); +} + +TEST(Buffer_Open_nonexistent_then_SaveAs) { + const std::string path = "./.kte_ut_buffer_io_3.tmp"; + std::remove(path.c_str()); + + Buffer b; + std::string err; + ASSERT_TRUE(b.OpenFromFile(path, err)); + ASSERT_EQ(err.empty(), true); + ASSERT_EQ(b.IsFileBacked(), false); + + b.insert_text(0, 0, std::string("hello, world")); + b.insert_text(0, 12, std::string("\n")); + b.SetDirty(true); + ASSERT_TRUE(b.SaveAs(path, err)); + ASSERT_EQ(err.empty(), true); + + std::string got = read_all(path); + ASSERT_EQ(got, std::string("hello, world\n")); + std::remove(path.c_str()); +} diff --git a/tests/test_piece_table.cc b/tests/test_piece_table.cc new file mode 100644 index 0000000..edcd0c1 --- /dev/null +++ b/tests/test_piece_table.cc @@ -0,0 +1,49 @@ +#include "Test.h" +#include "PieceTable.h" +#include + +TEST(PieceTable_Insert_Delete_LineCount) { + PieceTable pt; + // start empty + ASSERT_EQ(pt.Size(), (std::size_t)0); + ASSERT_EQ(pt.LineCount(), (std::size_t)1); // empty buffer has 1 logical line + + // Insert some text with newlines + const char *t = "abc\n123\nxyz"; // last line without trailing NL + pt.Insert(0, t, 11); + ASSERT_EQ(pt.Size(), (std::size_t)11); + ASSERT_EQ(pt.LineCount(), (std::size_t)3); + + // Check get line + ASSERT_EQ(pt.GetLine(0), std::string("abc")); + ASSERT_EQ(pt.GetLine(1), std::string("123")); + ASSERT_EQ(pt.GetLine(2), std::string("xyz")); + + // Delete middle line entirely including its trailing NL + auto r = pt.GetLineRange(1); // [start,end) points to start of line 1 to start of line 2 + pt.Delete(r.first, r.second - r.first); + ASSERT_EQ(pt.LineCount(), (std::size_t)2); + ASSERT_EQ(pt.GetLine(0), std::string("abc")); + ASSERT_EQ(pt.GetLine(1), std::string("xyz")); +} + +TEST(PieceTable_LineCol_Conversions) { + PieceTable pt; + std::string s = "hello\nworld\n"; // two lines with trailing NL + pt.Insert(0, s.data(), s.size()); + + // Byte offsets of starts + auto off0 = pt.LineColToByteOffset(0, 0); + auto off1 = pt.LineColToByteOffset(1, 0); + auto off2 = pt.LineColToByteOffset(2, 0); // EOF + ASSERT_EQ(off0, (std::size_t)0); + ASSERT_EQ(off1, (std::size_t)6); // "hello\n" + ASSERT_EQ(off2, pt.Size()); + + auto lc0 = pt.ByteOffsetToLineCol(0); + auto lc1 = pt.ByteOffsetToLineCol(6); + ASSERT_EQ(lc0.first, (std::size_t)0); + ASSERT_EQ(lc0.second, (std::size_t)0); + ASSERT_EQ(lc1.first, (std::size_t)1); + ASSERT_EQ(lc1.second, (std::size_t)0); +} diff --git a/tests/test_search.cc b/tests/test_search.cc new file mode 100644 index 0000000..4067b90 --- /dev/null +++ b/tests/test_search.cc @@ -0,0 +1,36 @@ +#include "Test.h" +#include "OptimizedSearch.h" +#include +#include + +static std::vector ref_find_all(const std::string &text, const std::string &pat) { + std::vector res; + if (pat.empty()) return res; + std::size_t from = 0; + while (true) { + auto p = text.find(pat, from); + if (p == std::string::npos) break; + res.push_back(p); + from = p + pat.size(); + } + return res; +} + +TEST(OptimizedSearch_basic_cases) { + OptimizedSearch os; + struct Case { std::string text; std::string pat; } cases[] = { + {"", ""}, + {"", "a"}, + {"a", ""}, + {"a", "a"}, + {"aaaaa", "aa"}, + {"hello world", "world"}, + {"abcabcabc", "abc"}, + {"the quick brown fox", "fox"}, + }; + for (auto &c : cases) { + auto got = os.find_all(c.text, c.pat, 0); + auto ref = ref_find_all(c.text, c.pat); + ASSERT_EQ(got, ref); + } +}