diff --git a/docs/plans/test-plan.md b/docs/plans/test-plan.md new file mode 100644 index 0000000..8ae5781 --- /dev/null +++ b/docs/plans/test-plan.md @@ -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 A–H described later. “Implemented” reflects current coverage in `kte_tests`. + +| Suite | ID | Name | Description (1‑line) | 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 | 1–4 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 | UTF‑8 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 doesn’t 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 ~1–4 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.1–A.3), PieceTable basics (B.1–B.2), Search parity (E.1). +- Phase 2 (add next): + - Buffer I/O edge cases (A.4–A.7), deeper PieceTable ops (B.3–B.6), Buffer helpers and cache (C.1–C.4), Undo semantics (D.1–D.2), Regression set (G.1–G.3). +- Phase 3: + - Editor flows (F.1–F.3), performance/stress (H.1–H.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.