Skip to content

Test gaps: raw-mode/SIGWINCH lifecycle, resize, and CI-without-a-TTY #6

Description

@TheCloudlet

Context

A survey of how the top TUI libraries in Rust (ratatui, cursive, crossterm), C (ncurses, notcurses, termbox2), C++ (FTXUI, tvision, finalcut), and Haskell (brick, vty, reflex-vty) test raw-mode/signal/resize/CI-without-a-TTY behavior, cross-referenced against Patchwork's actual test suite. Full citations and per-library findings: see .scratch/tui-testing-landscape-research.md in a local checkout that ran this research (gitignored, not in this repo's history — ask if you need it regenerated).

Out of scope, not a gap: keyboard/mouse/paste/IME, focus/overlay/popup, and widget layout constraints don't exist anywhere in Patchwork today (see CONTEXT.md — Patchwork is a painting library: Buffer, Surface, Pane, Drawable, with no input layer and no constraint-based layout engine). Testing them would mean testing a hypothetical API. Not listed below.

Confirmed gap, not assumption: src/terminal.rs (SIGWINCH self-pipe, TIOCGWINSZ resize detection, raw-mode entry) and src/raw_mode.rs (RAII TTY restore) have zero tests today — verified by grep, no #[test]/mod tests in either file.

Each item below is independently actionable.


1. RawMode restores the terminal on early return / panic, not just clean exit

Why it matters: the whole point of the RAII wrapper is that a panic mid-frame doesn't leave the user's terminal in raw mode. This is untested.
Technique: cheapest first — a unit test that constructs RawMode, panics inside a catch_unwind, and asserts the termios state was restored (compare tcgetattr before/after). No pty needed, no mock.

2. Terminal::next_event reports a resize after a real SIGWINCH

Why it matters: this is the one piece of behavior that cannot be verified by a mock — a signal handler's async-signal-safety and actual OS-level interruption behavior require a real signal to be delivered.
Technique: termbox2's approach (raise(SIGWINCH) against the real running process, then assert the self-pipe woke poll() and the next event reports the new size) — see tests/test_resize/test.php in termbox2. This is the heaviest technique in the survey; justified specifically because nothing cheaper tests real signal delivery.

3. Resize during an in-flight frame doesn't corrupt output

Why it matters: tests/renderer_frame.rs::shrink_resize_clips_instead_of_corrupting already tests a resize between frames, but not a SIGWINCH arriving while a frame is being drawn.
Technique: ratatui's TestBackend::resize() pattern — resize the in-memory backend mid-draw and assert clipping still holds. Cheap: no real terminal needed.

4. Terminal::new and raw-mode entry behave correctly with no real TTY (CI environment)

Why it matters: this is the exact "CI has no real TTY" scenario the user asked about. Two real-world precedents diverge here: notcurses requires a real terminal and CI only works because the runner happens to provide one; brick's own render test silently no-ops when no terminal is available (a named anti-pattern — the test doesn't run in exactly the environment it most needs to).
Technique: assert Patchwork's own behavior is neither of those — it should either work headlessly or fail loudly with a clear error, not silently skip. Test by redirecting stdin/stdout to a pipe (not a pty) and asserting the documented behavior, whatever it currently is.

5. SIGWINCH self-pipe doesn't leak file descriptors across repeated Terminal construction/drop

Why it matters: raw_mode.rs/terminal.rs install signal handlers and pipes; termbox2 has a dedicated test_fd_leak scenario for exactly this class of bug in a TUI library.
Technique: cheap — construct and drop Terminal N times in a loop, assert the process's open-fd count doesn't grow (/proc/self/fd count on Linux, or lsof-equivalent).

6. Keyboard/stdin event reading works via a plain pipe, no real pty required

Why it matters: FTXUI's cheapest-found technique in the whole survey — pipe() + dup2() onto STDIN_FILENO, write raw bytes, no pty, no library. If Terminal's stdin-reading path can be tested this way, it's the cheapest possible input test and doesn't require a real terminal in CI.
Technique: exactly FTXUI's app_piped_input_test.cpp pattern, applied to whatever Patchwork's stdin-reading path is once/if one exists.

7. The renderer's ANSI output bytes are asserted directly, not just the logical buffer diff

Why it matters: tests/renderer_frame.rs compares logical buffer content; renderer.rs::diff_to_ansi's actual escape-sequence output (cursor moves, SGR codes) has thinner direct coverage than the buffer-diff logic itself.
Technique: vty's Graphics.Vty.Output.Mock pattern — assert against the raw byte stream, one level lower than a rendered-buffer comparison. Cheap: no real terminal, just string assertions on diff_to_ansi's output.

8. Alternate-screen enter/exit is symmetric even when combined with resize

Why it matters: README states Patchwork handles "the alternate screen" — untested in combination with a resize event (does re-entering after a resize require special handling?).
Technique: unit/integration test constructing Terminal, triggering a simulated resize, then verifying the alternate-screen escape sequences are still well-formed on drop.

9. Nested/repeated raw-mode enter calls behave predictably

Why it matters: no test currently establishes what happens if raw mode is entered twice (double-init) or restored twice (double-drop) — a real risk surface for a resource-lifecycle type.
Technique: cheapest first — direct unit test on RawMode, asserting idempotency or a clear panic/error, whichever is the intended contract (this ticket surfaces the gap; the contract itself may need a decision first).

10. Document (not necessarily test-automate) multiplexer behavior as a manual verification checklist

Why it matters: tmux/screen/zellij each interpret TERM, mouse-reporting, and alternate-screen escapes slightly differently; the existing compare_with_upstream.py (local, gitignored, nyancat demo) already does real-tmux-based comparison, proving this is testable, but automating multiplexer permutations in CI is high-cost for a solo/early-stage project (0.2.0, README says "early and experimental").
Technique: per ponytail's first rung ("does this need to exist at all?") — a documented manual checklist (which multiplexers were manually verified, at which Patchwork version) is the cheapest thing that actually reduces risk here, versus standing up tmux/screen/zellij automation in CI before there's evidence of real breakage under any of them.


Labels applied: ready-for-agent (this is a well-scoped list; individual items can be claimed and split into their own tickets/PRs as needed — this issue is the punch list, not a single monolithic PR).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions