Skip to content

test(pty): Add a terminal harness for screen tests - #1100

Open
JeanMertz wants to merge 3 commits into
visible-mcp-logsfrom
ticket-0994dfa
Open

test(pty): Add a terminal harness for screen tests#1100
JeanMertz wants to merge 3 commits into
visible-mcp-logsfrom
ticket-0994dfa

Conversation

@JeanMertz

Copy link
Copy Markdown
Collaborator

A test can now assert on what a terminal renders rather than on the
bytes JP emitted. jp_pty::Terminal hands out a Writer for the code
under test to draw into and answers through Screen: what row N
holds, where the cursor is, whether a row wrapped. Scrolling, deferred
wrap at the right margin, and resize only exist on the far side of that
boundary, so none of them was reachable from a byte assertion.

Two backends sit behind one screen API. Terminal::pty opens a real pty
through portable-pty, so output crosses the kernel's line discipline,
the writer is a tty, and a child can be spawned into it, typed at, and
resized under it. Terminal::modelled feeds the vt100 screen model
directly, imitating a tty's ONLCR. Terminal::open takes the first
where the platform allows and falls back to the second, which is what
Windows gets: ConPTY is reachable only by a child process, so a
pty-only harness would have left every in-process case unix-only.
expectrl was the alternative considered and rejected: it matches a
spawned child's output stream and cannot hand a tty to in-process code.

wait_for blocks on a condvar woken by arriving bytes and returns the
screen that satisfied its predicate, so the assertions after it are made
against that snapshot rather than a later one; nothing sleeps for a
fixed interval. A timeout renders the screen with the cursor marked, and
Error's Debug defers to Display, so an unwrap in a test prints
it.

A test can now assert on what a terminal renders rather than on the
bytes JP emitted. `jp_pty::Terminal` hands out a `Writer` for the code
under test to draw into and answers through `Screen`: what row `N`
holds, where the cursor is, whether a row wrapped. Scrolling, deferred
wrap at the right margin, and resize only exist on the far side of that
boundary, so none of them was reachable from a byte assertion.

Two backends sit behind one screen API. `Terminal::pty` opens a real pty
through `portable-pty`, so output crosses the kernel's line discipline,
the writer is a tty, and a child can be spawned into it, typed at, and
resized under it. `Terminal::modelled` feeds the `vt100` screen model
directly, imitating a tty's `ONLCR`. `Terminal::open` takes the first
where the platform allows and falls back to the second, which is what
Windows gets: ConPTY is reachable only by a child process, so a
pty-only harness would have left every in-process case unix-only.
`expectrl` was the alternative considered and rejected: it matches a
spawned child's output stream and cannot hand a tty to in-process code.

`wait_for` blocks on a condvar woken by arriving bytes and returns the
screen that satisfied its predicate, so the assertions after it are made
against that snapshot rather than a later one; nothing sleeps for a
fixed interval. A timeout renders the screen with the cursor marked, and
`Error`'s `Debug` defers to `Display`, so an `unwrap` in a test prints
it.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
The status region's multi-row draw and erase cases run against a
terminal from `jp_pty`, a real pty where the platform has one, and
assert on the rows and the cursor it renders. Three byte snapshots of
the same behaviour go with them. A byte assertion cannot say whether a
terminal scrolled, wrapped, or clamped the cursor, and that is where
multi-row erasure goes wrong.

`shrinking_below_the_block_spares_the_content_above_it` was asserting
less than its name claimed: `cursor().0 < 4` holds whether or not the
erase caps its walk, because a cursor-up clamps at row 0 either way.
Which rows survive a shrink turns out to be the terminal's reflow
policy, which the screen model does not imitate. The case is renamed to
`an_erase_after_a_shrink_stops_at_the_top_of_the_viewport` and pins the
answerable part: every reachable row is cleared and the walk stops at
the top. It fails when the erase is removed.

`examples/region_spike.rs` is deleted. It measured the cursor position
around each draw against a live terminal for a human to read off, and
the four cases it settled are assertions now.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
The status region's multi-row draw and erase now run against a real
console. `region_probe` prints past the bottom of the screen, claims a
region with a two-row window, and either holds the block or releases it
and writes over where it was; `tests/region_pty.rs` spawns it and asserts
on the rows and the cursor that come back.

The in-process cases reach a pty on unix and the screen model everywhere
else, because a pty's subsidiary end cannot be written to from this
process on Windows. Running them there is regression coverage — a change
to JP's own sequence still fails — but it is the same
platform-independent Rust that runs on Linux, so it cannot answer what a
console does with that sequence. That was the question RFD 091 phase 4's
spike left open, and a spawned child is the only way into a ConPTY.
`Terminal::pty` is used directly rather than `Terminal::open`, so a
platform without one fails the case instead of quietly measuring the
model.

A pty echoes what is typed into it, and that moves the screen. The probe
first took its step as a command on stdin, and the echoed line landed at
the cursor — which sits inside the block — scrolling the screen by a row
the region never accounted for, stranding its top row and putting every
later erase one row low. It reproduced identically on every run and read
exactly like a multi-row erase bug.
`a_block_released_at_the_bottom_leaves_no_row_behind` covers the same
sequence in process and passes, which is what placed the fault in the
measurement rather than the code. The step is an argument now and nothing
is typed at the probe.

Signed-off-by: Jean Mertz <git@jeanmertz.com>
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