Skip to content

Split ghostel-test.el into per-topic files#290

Merged
dakra merged 3 commits into
mainfrom
split-tests
May 18, 2026
Merged

Split ghostel-test.el into per-topic files#290
dakra merged 3 commits into
mainfrom
split-tests

Conversation

@dakra
Copy link
Copy Markdown
Owner

@dakra dakra commented May 18, 2026

Summary

  • The single 16,147-line test/ghostel-test.el is now 16 ghostel-*-test.el files plus a shared test/ghostel-test-helpers.el. Per-file sizes range from 93 lines (exec) to 2,110 lines (scrollback).
  • Tests that need the Zig native module carry :tags '(native). The runner functions select via (tag native) / (not (tag native)) instead of the hand-maintained ghostel-test--elisp-tests whitelist (gone).
  • The Makefile loads all test/ghostel-*-test.el via wildcard, so adding a new file requires no Makefile edit. checkdoc also picks them up via file-expand-wildcards.

Why

ghostel-test.el was the largest file in the repo. New tests landed in whichever section vaguely matched, helpers piled up at the top, and the whitelist had to be updated by hand every time a new elisp-only test arrived (and silently rotted when a name changed). The split organizes by topic so new tests have an obvious home, and the tag-based runner makes the elisp/native split a property of each test rather than a separately-maintained list.

What's in each file

File Tests Lines Topic
ghostel-scrollback-test.el 51 2110 Scrollback materialization, eviction, dirty-row reuse, redraw scroll/anchor preservation, hidden-buffer deferral
ghostel-shell-test.el 68 1905 bash/zsh/fish OSC 7, OSC 133 prompts, password detection, prompt navigation, imenu, command-finish hooks
ghostel-line-mode-test.el 67 1716 Line mode entry/exit, alt-screen pause/resume, input-region API, snapshot/restore, TAB completion
ghostel-compile-test.el 57 1510 ghostel-compile--finalize, recompile, global/toggle mode, interactive form, mode-line
ghostel-tramp-test.el 43 1331 TRAMP, login wrap, remote start-process, environment plumbing, SIGWINCH, real-process resize
ghostel-mouse-paste-test.el 53 1267 mouse-1/-2, xterm-paste, yank-pop, readonly-copy/RET, focus events, scroll-on-input, scroll-intercept
ghostel-render-test.el 31 1027 SGR, dim, face props, multibyte, wide-char, title, CRLF, ANSI palette, theme sync, hyperlinks, URL detection
ghostel-osc-test.el 35 854 OSC 4/8/9/10/11/51/52/777, color queries, progress/notification dispatch, spinners
ghostel-glyph-kitty-test.el 39 848 cell-pixel-scale, kitty graphics, bold-is-bright, glyph-adjust
ghostel-module-test.el 36 842 Native module download, install, sidecar versioning, platform tag
ghostel-modes-test.el 33 699 Input mode predicates, char/emacs/copy mode transitions, fake cursor, hl-line
ghostel-keys-test.el 43 668 Key encoding, send-event, kitty keyboard, control/meta/special keys, send-encoded, send-next-key, public API
ghostel-debug-test.el 10 370 ghostel-debug-keypress, ghostel-debug-info sections
ghostel-project-test.el 8 197 ghostel-project buffer naming, identity match, return-buffer semantics
ghostel-terminal-test.el 10 184 Core VT primitives: create, write-input, cursor mvmt, erase, resize
ghostel-exec-test.el 5 93 ghostel-exec and ghostel-eshell integration

Helpers used by >1 bucket (with-compile-buffer, row0, cursor, wait-for) live in ghostel-test-helpers.el. Section-local helpers (with-cat-process, kitty-fixture, with-input-fixture, with-spawn-capture, with-focus-stub, the glyph-mock family, etc.) move with their tests.

File-local (defvar X) declarations are preserved per-file where needed (mouse-paste-test.el for xterm-store-paste-on-kill-ring, scrollback-test.el for the preedit-overlay vars). Without them, cross-file let bindings would be lexical and the wrapped code wouldn't see the rebound value — caught by an initial test failure during this work.

Functional equivalence

Same 589 tests, same pass/fail outcomes. The original whitelist had 399 entries but one (ghostel-test-eshell) was stale — no matching deftest existed. Effective elisp count is 398 + 191 native = 589. ghostel-test-fish-backspace already carried :tags '(:fish) and now carries :tags '(:fish native).

Known pre-existing test quality issues (not addressed here)

A review pass over the split files surfaced some tests that pass but don't actually exercise their target. None of these are introduced by this PR — they already existed in the monolith. Filing separately as follow-up; highest-priority items:

  • test/ghostel-exec-test.el:57 and test/ghostel-tramp-test.el:940 both mock ghostel--set-size, but the real code path now calls ghostel--set-size-with-cell-dims. Mocks never fire; tests pass vacuously.
  • Several tautological assertions (modes-test.el:117, line-mode-test.el:272, mouse-paste-test.el:302/315, tramp-test.el:1117).
  • mouse-paste-test.el mocks ghostel-readonly-exit in 8 readonly tests, so the actual exit logic is never end-to-end tested.

Test plan

  • make -j4 all clean: 78 zig + 191 native + 415 elisp pass, 0 unexpected, 2 pre-existing skips (bash-completion).
  • make test-evil clean: 78/78.
  • Round-trip integrity verified: identical set of 589 ghostel-test-* deftests in old vs. new layout, no duplicates across files.
  • Spot-check tagged tests via ert: :tags '(native) recognized and docstrings preserved (docstring must come before :tags for ert to keep it).

The single 16,147-line ghostel-test.el is now 16 ghostel-*-test.el
files plus a shared ghostel-test-helpers.el. Tests that need the Zig
native module carry :tags '(native); the runner functions select via
(tag native) / (not (tag native)) instead of the hand-maintained
ghostel-test--elisp-tests whitelist.

Functionally equivalent to before: same 589 tests, same pass/fail
outcomes (whitelist had one stale entry, ghostel-test-eshell, with no
matching deftest — so effective elisp count is 398, native 191).

Per-file sizes range from 93 lines (exec) to 2110 lines (scrollback).
Section-local helpers (with-cat-process, kitty-fixture, with-input-fixture,
etc.) move with their tests; cross-bucket helpers (with-compile-buffer,
row0, cursor, wait-for) live in ghostel-test-helpers.el.

File-local dynamic-var declarations preserved per-file:
mouse-paste-test.el needs (defvar xterm-store-paste-on-kill-ring) and
scrollback-test.el needs the preedit-overlay defvars — without these
the cross-file let bindings would be lexical and the wrapped code
wouldn't see the rebound value.
dakra added 2 commits May 18, 2026 18:52
A review pass over the per-topic test files surfaced ~30 places where
tests were passing for the wrong reasons: tautological assertions,
mocks of the function under test, or — in two cases — references to
symbols that no longer exist. None of these are regressions from the
split; they were inherited from the monolithic file and became easier
to spot once tests were organised by topic.

Highlights:

- exec / tramp: the size-setter mock was `ghostel--set-size` but the
  real code path now calls `ghostel--set-size-with-cell-dims`.  The
  mock never fired.  Renamed the symbol; exec also now asserts all 5
  arguments to `ghostel--new` instead of the first 2.

- keys: `ghostel-test-special-key-modifier-bindings` was querying
  `ghostel-mode-map`, where no special-key bindings exist.  The
  `(when binding ...)` guard hid that the test covered zero keys.
  Fix points it at `ghostel-semi-char-mode-map` and handles the
  documented `S-<insert>` -> `ghostel-yank' exception.  Same file
  also tightens `(should binding)' to `(should (commandp binding))'
  so a stray prefix-arg or sub-keymap can't pass for "command."

- terminal: `ignore-cursor-change' now seeds `cursor-type' to a
  known-different value before each call, so the assertion proves
  the function mutated state instead of matching a coincident
  default.

- line-mode: `cursor-point-tracks-cursor-char-pos' compared the
  variable to a trivial accessor returning it.  Replaced with a
  point-moved check (cursor follows char-pos, not point) and the
  documented nil-when-no-cursor branch.

- modes: dropped tautological `(should global-hl-line-mode)' that
  re-asserted a `let' binding; added missing `terminal-frozen-p'
  predicate in the char-mode block (semi-char and copy already had
  it); rewrote `copy-mode-recenter' to verify the window actually
  recenters instead of mocking out `recenter'.

- project: `project-universal-arg' now captures `ghostel-buffer-name'
  at `ghostel' call time so the test proves the project-prefixed
  binding (`*myproj-ghostel*') actually took effect, instead of only
  observing the prefix-arg passthrough.

- osc: switched mocks from the dispatchers (`ghostel--osc-progress',
  `ghostel--handle-notification') to the sinks
  (`ghostel-progress-function', `ghostel-notification-function') so
  the real dispatch + string->symbol conversion runs; expected values
  updated to match.  `default-notify' now also asserts the wiring
  via `ghostel--handle-notification'.

- render: `apply-palette-ghostel-default-face' now captures the
  arguments to `ghostel--set-default-colors' to prove the function
  consumed the mocked colour value rather than a hardcoded one;
  `coalesces-plain-link-detection' moves its assertion out of the
  mock body.

- glyph-kitty: `glyph-adjust-covered-by-main-font' had a vacuous
  "font-at would be unbound" guard (font-at is always bound).
  Replaced with a real `cl-letf' trip-wire over `font-at' that fails
  if called; added a `;; FIXME:' on `kitty-graphics-emit-end-to-end'
  whose mock obliterates the function under test.  Added a
  null-check before `(cadr (assq ...))' so a missing `min-width'
  entry fails clearly instead of via the confusing
  `(equal nil '(2))' form.

- scrollback / mouse-paste: minimised stubs and added `;; FIXME:'
  comments on the few tests whose end-to-end rewrite needs a real
  terminal/window fixture.

Test results unchanged: 78 zig + 191 native + 415 elisp pass; only
the two pre-existing `bash-completion' skips.
Each ghostel-*-test.el now gets its own .build/tests/<kind>-<name>.ok
stamp.  `test' and `test-native' depend on the full set, so
`make -j$(nproc)' fan-outs across cores — the slowest single file
sets the wall-clock floor, not the sum.

Numbers on this machine (warm caches):
- make -j8 test:      12.5s -> 6.6s
- make -j8 test-native: 11.2s -> 7.8s
- make -j8 all:       10.4s -> 8.4s
- make -j8 all (re-run, no changes): ~0.9s

The slowest two files are tramp-test (~5.5s, SIGWINCH subprocesses)
and shell-test (~5.2s, fish/zsh subprocess spawning).  Further wins
would require splitting those, which is out of scope here.

Stamps invalidate on the right inputs:
- elisp stamps depend on the test file, ghostel-test-helpers.el, and
  $(ELC).  Editing one test file re-runs only that one.
- native stamps additionally depend on $(MODULE), and $(MODULE) is a
  real-file target depending on $(ZIG_SOURCES).  Zig is invoked only
  when sources actually change, not on every `make' invocation.

Recommended invocation now documented in the Makefile:
  make -j$(nproc) all        # Linux
  make -j$(sysctl -n hw.ncpu) all  # macOS
  make -j$(getconf _NPROCESSORS_ONLN) all  # portable

New EMACSFLAGS variable lets callers inject load paths into every
emacs invocation (CI uses it for `-L /tmp/compat').

CI updated to use the new targets:
- byte-compile, test, test-native steps now call `make -jN' with
  EMACSFLAGS=-L /tmp/compat.  test-evil left as a direct emacs
  invocation since its runner is the older whitelist-based file.
- test-native touches the downloaded module artifact so its mtime is
  newer than the Zig sources (otherwise make would try to re-invoke
  `zig build', which isn't installed in the test-native job).
- `-O target' is not passed because macOS ships GNU make 3.81.

`.build/' added to .gitignore; `clean' removes it.
@dakra dakra merged commit 46fd5fd into main May 18, 2026
41 of 42 checks passed
@dakra dakra deleted the split-tests branch May 18, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant