Replace individual test binaries with unified test runner.

- Removed standalone test executables (`test_undo`, `test_buffer_save`, `test_buffer_open_nonexistent_save`, etc.).
- Introduced `kte_tests` as a unified test runner.
- Migrated existing tests to a new minimal, reusable framework in `tests/Test.h`.
- Updated `CMakeLists.txt` to build a single `kte_tests` executable.
- Simplified dependencies, reducing the need for ncurses/GUI in test builds.
This commit is contained in:
2025-12-07 00:37:16 -08:00
parent f6f0c11be4
commit f450ef825c
11 changed files with 281 additions and 620 deletions

View File

@@ -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 ()

View File

@@ -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 <cassert>
#include <cstdio>
#include <fstream>
#include <string>
#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<char>(in)), std::istreambuf_iterator<char>());
}
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;
}

View File

@@ -1,57 +0,0 @@
// Test that Buffer::Save writes actual contents to disk
#include <cassert>
#include <cstdio>
#include <fstream>
#include <string>
#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<char>(in)), std::istreambuf_iterator<char>());
}
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 doesnt 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;
}

View File

@@ -1,48 +0,0 @@
// Test saving after opening an existing file
#include <cassert>
#include <cstdio>
#include <fstream>
#include <string>
#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<std::streamsize>(data.size()));
}
static std::string read_file(const std::string &path)
{
std::ifstream in(path, std::ios::binary);
return std::string((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
}
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;
}

View File

@@ -1,74 +0,0 @@
// Verify OptimizedSearch against std::string reference across patterns and sizes
#include <cassert>
#include <cstddef>
#include <random>
#include <string>
#include <vector>
#include "OptimizedSearch.h"
static std::vector<std::size_t>
ref_find_all(const std::string &text, const std::string &pat)
{
std::vector<std::size_t> 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<int> dist('a', 'z');
std::string text(textLen, '\0');
for (auto &ch: text)
ch = static_cast<char>(dist(rng));
std::string pat(patLen, '\0');
for (auto &ch: pat)
ch = static_cast<char>(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<long>(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<unsigned>(t + p));
}
}
// Larger random
run_case(100000, 16, 12345);
run_case(250000, 32, 67890);
return 0;
}

View File

@@ -1,338 +0,0 @@
#include <cassert>
#include <fstream>
#include <iostream>
#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;
}

63
tests/Test.h Normal file
View File

@@ -0,0 +1,63 @@
// Minimal header-only unit test framework for kte
#pragma once
#include <functional>
#include <iostream>
#include <string>
#include <vector>
#include <chrono>
#include <sstream>
namespace ktet {
struct TestCase {
std::string name;
std::function<void()> fn;
};
inline std::vector<TestCase>& registry() {
static std::vector<TestCase> r;
return r;
}
struct Registrar {
Registrar(const char* name, std::function<void()> 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<typename A, typename B>
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__)

33
tests/TestRunner.cc Normal file
View File

@@ -0,0 +1,33 @@
#include "Test.h"
#include <iostream>
#include <chrono>
int main() {
using namespace std::chrono;
auto &reg = 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<milliseconds>(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<milliseconds>(t1 - t0).count();
std::cout << "Done in " << total_ms << " ms. Failures: " << failed << "\n";
return failed == 0 ? 0 : 1;
}

79
tests/test_buffer_io.cc Normal file
View File

@@ -0,0 +1,79 @@
#include "Test.h"
#include <fstream>
#include <cstdio>
#include <string>
#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<char>(in)), std::istreambuf_iterator<char>());
}
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());
}

49
tests/test_piece_table.cc Normal file
View File

@@ -0,0 +1,49 @@
#include "Test.h"
#include "PieceTable.h"
#include <string>
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);
}

36
tests/test_search.cc Normal file
View File

@@ -0,0 +1,36 @@
#include "Test.h"
#include "OptimizedSearch.h"
#include <string>
#include <vector>
static std::vector<std::size_t> ref_find_all(const std::string &text, const std::string &pat) {
std::vector<std::size_t> 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);
}
}