9 Commits

Author SHA1 Message Date
cc0c187481 Improve macOS app build process and bundle handling.
- Updated `make-app-release` script to use `macdeployqt` with proper verbosity and bundle fixup.
- Introduced post-build fixup using CMake's `BundleUtilities` to internalize non-Qt dylibs.
- Enhanced macOS bundle RPATH settings for accurate Framework resolution.
- Added optional `kge_fixup_bundle` CMake target for post-build handling.
- Refined `default.nix` to load Nixpkgs in a default argument.
2025-12-09 18:49:16 -08:00
a8dcfbec58 Fix C-k c handling. 2025-12-08 15:28:45 -08:00
65705e3354 bump version 2025-12-07 15:25:50 -08:00
e1f9a9eb6a Preserve cursor position on buffer reload.
- Remember and restore the cursor's position after reloading a buffer, clamping if necessary.
- Improve user experience by maintaining editing context.
2025-12-07 15:25:40 -08:00
c9f34003f2 Add unit testing plan documentation.
- Introduced comprehensive test plan to guide development and ensure coverage.
- Documented test principles, execution harness, build steps, and test catalog.
- Categorized test cases by functionality (e.g., filesystem I/O, PieceTable semantics, buffer editing, undo system, etc.).
- Outlined regression tests and performance/stress scenarios.
- Provided a phased roadmap for implementing planned test cases.
2025-12-07 12:34:47 -08:00
f450ef825c 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.
2025-12-07 00:37:16 -08:00
f6f0c11be4 Add PieceTable-based buffer tests and improvements for file I/O and editing.
- Introduced comprehensive tests:
  - `test_buffer_open_nonexistent_save.cc`: Save after opening a non-existent file.
  - `test_buffer_save.cc`: Save buffer contents to disk.
  - `test_buffer_save_existing.cc`: Save after opening existing files.
- Implemented `PieceTable::WriteToStream()` to directly stream content without full materialization.
- Updated `Buffer::Save` and `Buffer::SaveAs` to use efficient streaming via `PieceTable`.
- Enhanced editing commands (`Insert`, `Delete`, `Replace`, etc.) to use PieceTable APIs, ensuring proper undo and save functionality.
2025-12-07 00:30:11 -08:00
657c9bbc19 bump version 2025-12-06 11:40:27 -08:00
3493695165 Add support for creating a new empty buffer (C-k i).
- Introduced `BufferNew` command to create and switch to a new unnamed buffer.
- Registered `BufferNew` in the command registry and updated keymap and help text.
- Implemented `cmd_buffer_new()` to handle buffer creation and switching logic.
2025-12-06 11:40:00 -08:00
22 changed files with 1043 additions and 972 deletions

View File

@@ -1,5 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="sccl" />
</state>
</component>

View File

@@ -292,28 +292,29 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
bool
Buffer::Save(std::string &err) const
{
if (!is_file_backed_ || filename_.empty()) {
err = "Buffer is not file-backed; use SaveAs()";
return false;
}
std::ofstream out(filename_, std::ios::out | std::ios::binary | std::ios::trunc);
if (!out) {
err = "Failed to open for write: " + filename_ + ". Error: " + std::string(std::strerror(errno));
return false;
}
// Write the entire buffer in a single block to minimize I/O calls.
const char *data = content_.Data();
const auto size = static_cast<std::streamsize>(content_.Size());
if (data != nullptr && size > 0) {
out.write(data, size);
}
if (!out.good()) {
err = "Write error: " + filename_ + ". Error: " + std::string(std::strerror(errno));
return false;
}
// Note: const method cannot change dirty_. Intentionally const to allow UI code
// to decide when to flip dirty flag after successful save.
return true;
if (!is_file_backed_ || filename_.empty()) {
err = "Buffer is not file-backed; use SaveAs()";
return false;
}
std::ofstream out(filename_, std::ios::out | std::ios::binary | std::ios::trunc);
if (!out) {
err = "Failed to open for write: " + filename_ + ". Error: " + std::string(std::strerror(errno));
return false;
}
// Stream the content directly from the piece table to avoid relying on
// full materialization, which may yield an empty pointer when size > 0.
if (content_.Size() > 0) {
content_.WriteToStream(out);
}
// Ensure data hits the OS buffers
out.flush();
if (!out.good()) {
err = "Write error: " + filename_ + ". Error: " + std::string(std::strerror(errno));
return false;
}
// Note: const method cannot change dirty_. Intentionally const to allow UI code
// to decide when to flip dirty flag after successful save.
return true;
}
@@ -345,16 +346,16 @@ Buffer::SaveAs(const std::string &path, std::string &err)
err = "Failed to open for write: " + out_path + ". Error: " + std::string(std::strerror(errno));
return false;
}
// Write whole content in a single I/O operation
const char *data = content_.Data();
const auto size = static_cast<std::streamsize>(content_.Size());
if (data != nullptr && size > 0) {
out.write(data, size);
}
if (!out.good()) {
err = "Write error: " + out_path + ". Error: " + std::string(std::strerror(errno));
return false;
}
// Stream content without forcing full materialization
if (content_.Size() > 0) {
content_.WriteToStream(out);
}
// Ensure data hits the OS buffers
out.flush();
if (!out.good()) {
err = "Write error: " + out_path + ". Error: " + std::string(std::strerror(errno));
return false;
}
filename_ = out_path;
is_file_backed_ = true;

View File

@@ -4,7 +4,7 @@ project(kte)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 20)
set(KTE_VERSION "1.5.1")
set(KTE_VERSION "1.5.3")
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
@@ -292,25 +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})
target_link_libraries(kte_tests ${TREESITTER_LIBRARY})
endif ()
endif ()
endif ()
@@ -370,12 +379,18 @@ if (${BUILD_GUI})
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
@ONLY)
# Ensure proper macOS bundle properties and RPATH so our bundled
# frameworks are preferred over system/Homebrew ones.
set_target_properties(kge PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
MACOSX_BUNDLE_BUNDLE_NAME "kge"
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist"
# Prefer the app's bundled frameworks at runtime
INSTALL_RPATH "@executable_path/../Frameworks"
BUILD_WITH_INSTALL_RPATH TRUE
)
add_dependencies(kge kte)
add_custom_command(TARGET kge POST_BUILD
@@ -399,4 +414,19 @@ if (${BUILD_GUI})
# Install kge man page only when GUI is built
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
# Optional post-build bundle fixup (can also be run from scripts).
# This provides a CMake target to run BundleUtilities' fixup_bundle on the
# built app, useful after macdeployqt to ensure non-Qt dylibs are internalized.
if (APPLE)
include(CMakeParseArguments)
add_custom_target(kge_fixup_bundle ALL
COMMAND ${CMAKE_COMMAND}
-DAPP_BUNDLE=$<TARGET_BUNDLE_DIR:kge>
-P ${CMAKE_CURRENT_LIST_DIR}/cmake/fix_bundle.cmake
BYPRODUCTS $<TARGET_BUNDLE_DIR:kge>/Contents/Frameworks
COMMENT "Running fixup_bundle on kge.app to internalize non-Qt dylibs"
VERBATIM)
add_dependencies(kge_fixup_bundle kge)
endif ()
endif ()

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,7 @@ enum class CommandId {
VisualFontPickerToggle,
// Buffers
BufferSwitchStart, // begin buffer switch prompt
BufferNew, // create a new empty, unnamed buffer (C-k i)
BufferClose,
BufferNext,
BufferPrev,

View File

@@ -197,9 +197,11 @@ Editor::OpenFile(const std::string &path, std::string &err)
eng->InvalidateFrom(0);
}
}
return true;
}
}
// Defensive: ensure any active prompt is closed after a successful open
CancelPrompt();
return true;
}
}
Buffer b;
if (!b.OpenFromFile(path, err)) {
@@ -237,8 +239,10 @@ Editor::OpenFile(const std::string &path, std::string &err)
}
// Add as a new buffer and switch to it
std::size_t idx = AddBuffer(std::move(b));
SwitchTo(idx);
return true;
SwitchTo(idx);
// Defensive: ensure any active prompt is closed after a successful open
CancelPrompt();
return true;
}

View File

@@ -31,6 +31,7 @@ HelpText::Text()
" C-k c Close current buffer\n"
" C-k d Kill to end of line\n"
" C-k e Open file (prompt)\n"
" C-k i New empty buffer\n"
" C-k f Flush kill ring\n"
" C-k g Jump to line\n"
" C-k h Show this help\n"

View File

@@ -158,16 +158,17 @@ map_key(const SDL_Keycode key,
ascii_key = static_cast<int>(key);
}
bool ctrl2 = (mod & KMOD_CTRL) != 0;
// If user typed a literal 'C' (or '^') as a control qualifier, keep k-prefix active
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
k_ctrl_pending = true;
// Keep waiting for the next suffix; show status and suppress ensuing TEXTINPUT
if (ed)
ed->SetStatus("C-k C _");
suppress_textinput_once = true;
out.hasCommand = false;
return true;
}
// If user typed a literal 'C' (uppercase) or '^' as a control qualifier, keep k-prefix active
// Do NOT treat lowercase 'c' as a qualifier; 'c' is a valid k-command (BufferClose).
if (ascii_key == 'C' || ascii_key == '^') {
k_ctrl_pending = true;
// Keep waiting for the next suffix; show status and suppress ensuing TEXTINPUT
if (ed)
ed->SetStatus("C-k C _");
suppress_textinput_once = true;
out.hasCommand = false;
return true;
}
// Otherwise, consume the k-prefix now for the actual suffix
k_prefix = false;
if (ascii_key != 0) {
@@ -472,16 +473,16 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
ascii_key = static_cast<int>(c0);
}
if (ascii_key != 0) {
// Qualifier via TEXTINPUT: 'C' or '^'
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
k_ctrl_pending_ = true;
if (ed_)
ed_->SetStatus("C-k C _");
// Keep k-prefix active; do not emit a command
k_prefix_ = true;
produced = true;
break;
}
// Qualifier via TEXTINPUT: uppercase 'C' or '^' only
if (ascii_key == 'C' || ascii_key == '^') {
k_ctrl_pending_ = true;
if (ed_)
ed_->SetStatus("C-k C _");
// Keep k-prefix active; do not emit a command
k_prefix_ = true;
produced = true;
break;
}
// Map via k-prefix table; do not pass Ctrl for TEXTINPUT case
CommandId id;
bool pass_ctrl = k_ctrl_pending_;

View File

@@ -42,6 +42,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
case 'a':
out = CommandId::MarkAllAndJumpEnd;
return true;
case 'i':
out = CommandId::BufferNew; // C-k i new empty buffer
return true;
case 'k':
out = CommandId::CenterOnCursor; // C-k k center current line
return true;

View File

@@ -1,6 +1,7 @@
#include <algorithm>
#include <utility>
#include <limits>
#include <ostream>
#include "PieceTable.h"
@@ -757,3 +758,17 @@ PieceTable::Find(const std::string &needle, std::size_t start) const
find_cache_.result = pos;
return pos;
}
void
PieceTable::WriteToStream(std::ostream &out) const
{
// Stream the content piece-by-piece without forcing full materialization
for (const auto &p : pieces_) {
if (p.len == 0)
continue;
const std::string &src = (p.src == Source::Original) ? original_ : add_;
const char *base = src.data() + static_cast<std::ptrdiff_t>(p.start);
out.write(base, static_cast<std::streamsize>(p.len));
}
}

View File

@@ -5,6 +5,7 @@
#include <cstddef>
#include <cstdint>
#include <string>
#include <ostream>
#include <vector>
#include <limits>
@@ -100,6 +101,9 @@ public:
// Simple search utility; returns byte offset or npos
[[nodiscard]] std::size_t Find(const std::string &needle, std::size_t start = 0) const;
// Stream out content without materializing the entire buffer
void WriteToStream(std::ostream &out) const;
// Heuristic configuration
void SetConsolidationParams(std::size_t piece_limit,
std::size_t small_piece_threshold,

View File

@@ -178,14 +178,15 @@ map_key_to_command(const int ch,
ctrl = true;
ascii_key = 'a' + (ch - 1);
}
// If user typed literal 'C'/'c' or '^' as a qualifier, keep k-prefix and set pending
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
k_ctrl_pending = true;
if (ed)
ed->SetStatus("C-k C _");
out.hasCommand = false;
return true;
}
// If user typed literal 'C' or '^' as a qualifier, keep k-prefix and set pending
// Note: Do NOT treat lowercase 'c' as a qualifier, since 'c' is a valid C-k command (BufferClose).
if (ascii_key == 'C' || ascii_key == '^') {
k_ctrl_pending = true;
if (ed)
ed->SetStatus("C-k C _");
out.hasCommand = false;
return true;
}
// For actual suffix, consume the k-prefix
k_prefix = false;
// Do NOT lowercase here; KLookupKCommand handles case-sensitive bindings

View File

@@ -1,5 +1,6 @@
{
lib,
pkgs ? import <nixpkgs> {},
lib ? pkgs.lib,
stdenv,
cmake,
ncurses,

163
docs/plans/test-plan.md Normal file
View File

@@ -0,0 +1,163 @@
### Unit testing plan (headless, no interactive frontend)
#### Principles
- Headless-only: exercise core components directly (`PieceTable`, `Buffer`, `UndoSystem`, `OptimizedSearch`, and minimal `Editor` flows) without starting `kte` or `kge`.
- Deterministic and fast: avoid timers, GUI, environment-specific behavior; prefer in-memory operations and temporary files.
- Regression-focused: encode prior failures (save/newline mismatch, legacy `rows_` writes) as explicit tests to prevent recurrences.
#### Harness and execution
- Single binary: use target `kte_tests` (already present) to compile and run all tests under `tests/` with the minimal in-tree framework (`tests/Test.h`, `tests/TestRunner.cc`).
- No GUI/ncurses deps: link only engine sources (PieceTable/Buffer/Undo/Search/Undo* and syntax minimal set), not frontends.
- How to build/run:
- Debug profile:
```
cmake -S /Users/kyle/src/kte -B /Users/kyle/src/kte/cmake-build-debug -DBUILD_TESTS=ON && \
cmake --build /Users/kyle/src/kte/cmake-build-debug --target kte_tests && \
/Users/kyle/src/kte/cmake-build-debug/kte_tests
```
- Release profile:
```
cmake -S /Users/kyle/src/kte -B /Users/kyle/src/kte/cmake-build-release -DBUILD_TESTS=ON && \
cmake --build /Users/kyle/src/kte/cmake-build-release --target kte_tests && \
/Users/kyle/src/kte/cmake-build-release/kte_tests
```
---
### Test catalog (summary table)
The table below catalogs all unit tests defined in this plan. It is headless-only and maps directly to the suites AH described later. “Implemented” reflects current coverage in `kte_tests`.
| Suite | ID | Name | Description (1line) | Headless | Implemented |
|:-----:|:---:|:------------------------------------------|:-------------------------------------------------------------------------------------|:--------:|:-----------:|
| A | 1 | SaveAs then Save (append) | New buffer → write two lines → `SaveAs` → append → `Save`; verify exact bytes. | Yes | ✓ |
| A | 2 | Open existing then Save | Open seeded file, append, `Save`; verify overwrite bytes. | Yes | ✓ |
| A | 3 | Open non-existent then SaveAs | Start from non-existent path, insert `hello, world\n`, `SaveAs`; verify bytes. | Yes | ✓ |
| A | 4 | Trailing newline preservation | Verify saving preserves presence/absence of final `\n`. | Yes | Planned |
| A | 5 | Empty buffer saves | Empty → `SaveAs` → 0 bytes; then insert `\n` → `Save` → 1 byte. | Yes | Planned |
| A | 6 | Large file streaming | 14 MiB with periodic newlines; size and content integrity. | Yes | Planned |
| A | 7 | Tilde expansion | `SaveAs` with `~/...`; re-open to confirm path/content. | Yes | Planned |
| A | 8 | Error propagation | Save to unwritable path → expect failure and error message. | Yes | Planned |
| B | 1 | Insert/Delete LineCount | Basic inserts/deletes and line counting sanity. | Yes | ✓ |
| B | 2 | Line/Col conversions | `LineColToByteOffset` and reverse around boundaries. | Yes | ✓ |
| B | 3 | Delete spanning newlines | Delete ranges that cross line breaks; verify bytes/lines. | Yes | Planned |
| B | 4 | Split/Join equivalence | `split_line` followed by `join_lines` yields original bytes. | Yes | Planned |
| B | 5 | Stream vs Data equivalence | `WriteToStream` matches `GetRange`/`Data()` after edits. | Yes | Planned |
| B | 6 | UTF8 bytes stability | Multibyte sequences behave correctly (byte-based ops). | Yes | Planned |
| C | 1 | insert_text/delete_text | Edits at start/middle/end; `Rows()` mirrors PieceTable. | Yes | Planned |
| C | 2 | split_line/join_lines | Effects and snapshots across multiple positions. | Yes | Planned |
| C | 3 | insert_row/delete_row | Replace paragraph by row ops; verify bytes/linecount. | Yes | Planned |
| C | 4 | Cache invalidation | After each mutation, `Rows()` matches `LineCount()`. | Yes | Planned |
| D | 1 | Grouped insert undo | Contiguous typing undone/redone as a group. | Yes | Planned |
| D | 2 | Delete/Newline undo/redo | Backspace/Delete and Newline transitions across undo/redo. | Yes | Planned |
| D | 3 | Mark saved & dirty | Dirty/save markers interact correctly with undo/redo. | Yes | Planned |
| E | 1 | Search parity basic | `OptimizedSearch::find_all` vs `std::string` reference. | Yes | ✓ |
| E | 2 | Large text search | ~1 MiB random text/patterns parity. | Yes | Planned |
| F | 1 | Editor open & reload | Open via `Editor`, modify, reload, verify on-disk bytes. | Yes | Planned |
| F | 2 | Read-only toggle | Toggle and verify enforcement/behavior of saves. | Yes | Planned |
| F | 3 | Prompt lifecycle | Start/Accept/Cancel prompt doesnt corrupt state. | Yes | Planned |
| G | 1 | Saved only newline regression | Insert text + newline; `Save` includes both bytes. | Yes | Planned |
| G | 2 | Backspace crash regression | PieceTable-backed delete/join path remains stable. | Yes | Planned |
| G | 3 | Overwrite-confirm path | Saving over existing path succeeds and is correct. | Yes | Planned |
| H | 1 | Many small edits | 10k small edits; final bytes correct within time bounds. | Yes | Planned |
| H | 2 | Consolidation equivalence | After many edits, stream vs data produce identical bytes. | Yes | Planned |
Legend: Implemented = ✓, Planned = to be added per Coverage roadmap.
### Test suites and cases
#### A) Filesystem I/O via Buffer
1) SaveAs then Save (append)
- New buffer → `insert_text` two lines (explicit `\n`) → `SaveAs(tmp)` → insert a third line → `Save()`.
- Assert file bytes equal exact expected string.
2) Open existing then Save
- Seed a file on disk; `OpenFromFile(path)` → append line → `Save()`.
- Assert file bytes updated exactly.
3) Open non-existent then SaveAs
- `OpenFromFile(nonexistent)` → assert `IsFileBacked()==false` → insert `"hello, world\n"` → `SaveAs(path)`.
- Read back exact bytes.
4) Trailing newline preservation
- Case (a) last line without `\n`; (b) last line with `\n` → save and verify bytes unchanged.
5) Empty buffer saves
- `SaveAs(tmp)` on empty buffer → 0-byte file. Then insert `"\n"` and `Save()` → 1-byte file.
6) Large file streaming
- Insert ~14 MiB of data with periodic newlines. `SaveAs` then `Save`; verify size matches `content_.Size()` and bytes integrity.
7) Path normalization and tilde expansion
- `SaveAs("~/.../file.txt")` → verify path expands to `$HOME` and file content round-trips with `OpenFromFile`.
8) Error propagation (guarded)
- Attempt save into a non-writable path; expect `Save/SaveAs` returns false with non-empty error. Mark as skipped in environments lacking such path.
#### B) PieceTable semantics
1) Line counting and deletion across lines
- Insert `"abc\n123\nxyz"` → 3 lines; delete middle line range → 2 lines; validate `GetLine` contents.
2) Position conversions
- Validate `LineColToByteOffset` and `ByteOffsetToLineCol` at start/end of lines and EOF, especially around `\n`.
3) Delete spanning newlines
- Remove a range that crosses line boundaries; verify resulting bytes, `LineCount` and line contents.
4) Split/join equivalence
- Split at various columns; then join adjacent lines; verify bytes equal original.
5) WriteToStream vs materialized `Data()`
- After multiple inserts/deletes (without forcing `Data()`), stream to `std::ostringstream`; compare with `GetRange(0, Size())`, then call `Data()` and re-compare.
6) UTF-8 bytes stability
- Insert multibyte sequences (e.g., `"héllo"`, `"中文"`, emoji) as raw bytes; ensure line counting and conversions behave (byte-based API; no crashes/corruption).
#### C) Buffer editing helpers and rows cache correctness
1) `insert_text`/`delete_text`
- Apply at start/middle/end of lines; immediately call `Rows()` and validate contents/lengths mirror PieceTable.
2) `split_line` and `join_lines`
- Verify content effects and `Rows()` snapshots for multiple positions and consecutive operations.
3) `insert_row`/`delete_row`
- Replace a paragraph by deleting N rows then inserting N rows; verify bytes and `LineCount`.
4) Cache invalidation
- After each mutation, fetch `Rows()`; assert `Nrows() == content.LineCount()` and no stale data remains.
#### D) UndoSystem semantics
1) Grouped contiguous insert undo
- Emulate typing at a single location via repeated `insert_text`; one `undo()` should remove the whole run; `redo()` restores it.
2) Delete/newline undo/redo
- Simulate backspace/delete (`delete_text` and `join_lines`) and newline (`split_line`); verify content transitions across `undo()`/`redo()`.
3) Mark saved and dirty flag
- After successful save, call `UndoSystem::mark_saved()` (via existing pathways) and ensure dirty state pairing behaves as intended (at least: `SetDirty(false)` plus save does not break undo/redo).
#### E) Search algorithms
1) Parity with `std::string::find`
- Use `OptimizedSearch::find_all` across edge cases (empty needle/text, overlaps like `"aaaaa"` vs `"aa"`, Unicode byte sequences). Compare to reference implementation.
2) Large text
- Random ASCII text ~1 MiB; random patterns; results match reference.
#### F) Editor non-interactive flows (no frontend)
1) Open and reload
- Through `Editor`, open file; modify the underlying `Buffer` directly; invoke reload (`Buffer::OpenFromFile` or `cmd_reload_buffer` if you bring `Command.cc` into the test target). Verify bytes match the on-disk file after reload.
2) Read-only toggle
- Toggle `Buffer::ToggleReadOnly()`; confirm flag value changes and that subsequent saves still execute when not read-only (or, if enforcement exists, that mutations are appropriately restricted).
3) Prompt lifecycle (headless)
- Exercise `StartPrompt` → `AcceptPrompt` → `CancelPrompt`; ensure state resets and does not corrupt buffer/editor state.
#### G) Regression tests for reported bugs
1) “Saved only newline”
- Build buffer content via `insert_text` followed by `split_line` for newline; `Save` then validate bytes include both the text and newline.
2) Backspace crash path
- Mimic backspace behavior using PieceTable-backed helpers (`delete_text`/`join_lines`); ensure no dependency on legacy `rows_` mutation and no memory issues.
3) Overwrite-confirm path behavior
- Start with non-file-backed buffer named to collide with an existing file; perform `SaveAs(existing_path)` and assert success and correctness on disk (unit test bypasses interactive confirm, validating underlying write path).
#### H) Performance/stress sanity
1) Many small edits
- 10k single-char inserts and interleaved deletes; assert final bytes; keep within conservative runtime bounds.
2) Consolidation heuristics
- After many edits, call both `WriteToStream` and `Data()` and verify identical bytes.
---
### Coverage roadmap
- Phase 1 (already implemented and passing):
- Buffer I/O basics (A.1A.3), PieceTable basics (B.1B.2), Search parity (E.1).
- Phase 2 (add next):
- Buffer I/O edge cases (A.4A.7), deeper PieceTable ops (B.3B.6), Buffer helpers and cache (C.1C.4), Undo semantics (D.1D.2), Regression set (G.1G.3).
- Phase 3:
- Editor flows (F.1F.3), performance/stress (H.1H.2), and optional integration of `Command.cc` into the test target to exercise non-interactive command execution paths directly.
### Notes
- Use per-test temp files under the repo root or a unique temp directory; ensure cleanup after assertions.
- For HOME-dependent tests (tilde expansion), set `HOME` in the test process if not present or skip with a clear message.
- On macOS Debug, a benign allocator warning may appear; rely on process exit code for pass/fail.

View File

@@ -16,14 +16,18 @@ open .
cd ..
mkdir -p cmake-build-release-qt
cmake -S . -B cmake-build-release -DBUILD_GUI=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF
cmake -S . -B cmake-build-release-qt -DBUILD_GUI=ON -DKTE_USE_QT=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF
cd cmake-build-release-qt
make clean
rm -fr kge.app* kge-qt.app*
make
mv kge.app kge-qt.app
macdeployqt kge-qt.app -always-overwrite
mv -f kge.app kge-qt.app
# Use the same Qt's macdeployqt as used for building; ensure it overwrites in-bundle paths
macdeployqt kge-qt.app -always-overwrite -verbose=3
# Run CMake BundleUtilities fixup to internalize non-Qt dylibs and rewrite install names
cmake -DAPP_BUNDLE="$(pwd)/kge-qt.app" -P "${PWD%/*}/cmake/fix_bundle.cmake"
zip -r kge-qt.app.zip kge-qt.app
sha256sum kge-qt.app.zip
open .

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