Skip to content

test: replace the shell with the emit fixture at 72 of 99 sites - #276

Merged
vyncint merged 21 commits into
mainfrom
emit-fixture
Sep 8, 2026
Merged

test: replace the shell with the emit fixture at 72 of 99 sites#276
vyncint merged 21 commits into
mainfrom
emit-fixture

Conversation

@vyncint

@vyncint vyncint commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Closes #249. Step 1 of #149.

What this is

fixtures/emit — a std-only program that writes exactly the bytes its arguments describe, then waits, sleeps or exits as told. One argument per step; the steps are the crate doc of fixtures/emit/src/main.rs:

TEXT  NL  CR  --text WORD        literal bytes
--esc BYTES  --csi BYTES         ESC …  /  ESC [ …
--raw SPEC                       \e \n \r \t \a \\ \xNN
--sleep DUR  --wait  --exit N    the only timing, the instant-exit guard, the status
--echo-line  --echo              read a line and write it back / copy stdin until EOF
--seq N  --cwd  --pid            the flood, the directory, the process id
--env NAME  --environ            one variable or all of them, sorted
--loop                           run what precedes once, what follows forever

tests/common::spawn_emit(builder, steps) runs it from any builder. Then the migration, one test file per commit so each diff is one file's scripts against their emit form: tabs, styles, charset, utf8, state, frames, process, builder_validation, observe, timeouts, stable, basic, concurrency, scrollback, input, queries, fixtures. Where a test used to be one printf, it is one --raw, with \033 read as \e — a reviewer diffs the escape sequences directly.

The count

grep -c 'spawn("/bin/sh")\|spawn("sh")' crates/termlens/tests/*.rs: 99 → 27, and every one of the 27 has a comment or file header saying why it is a shell:

file left why
queries.rs 13 read a reply byte for byte under stty -icanon -echo
input.rs 7 read the wire (mouse, paste, cursor keys) under stty
drain.rs 3 flood + raw mode; the deadlock the harness must never cause
backpressure.rs 2 dd an exact byte count of replies under stty
basic.rs 1 kill -TERM $$ — a process signalling itself
process.rs 1 trap … TERM — a signal handler

The 25 raw-mode sites share one blocker: reading what termlens typed back needs -icanon -echo, and std has no tcsetattr. That is the fixture's stated rule (std only, no timing but --sleep) doing exactly what it was written to do — the shell is the honest tool for a test that is about the line discipline. If those 25 should go too, the step is --raw-mode on the fixture via libc (one tcsetattr) or crossterm, and that is a decision about the rule, not about the migration. Flagging rather than deciding.

Two tests changed shape on purpose

  • basic.rs send_str_and_enter_round_trip… waited for got: hello, which the line discipline's echo of the typed hello could satisfy on its own. It now waits for hello back — the fixture's read plus a suffix the echo cannot produce.
  • builder_validation.rs a_bare_program_name_under_env_clear… used sh as the bare name and /usr/bin:/bin as the PATH remedy. It now uses the fixture as the bare name and the fixture's own directory as the remedy: the same refusal, no Unix path baked in.

Everything else asserts what it asserted before.

Verified

Every gate in CONTRIBUTING §1 locally, including the cross-target clippy; full suite three feature configurations. The windows and stress workflows are dispatched against this branch — results in a comment below, since #249's done-when names the stress workflow and #149's step 4 is what the Windows count is for.

`fixtures/emit` writes exactly the bytes its arguments describe — one
argument per step, `--esc`/`--csi`/`--raw` for the sequences, `--wait`
to hold the terminal open, `--sleep`, `--exit` — and nothing else: std
only, no timing but the explicit sleep, so it cannot become a second
thing the suite tests. `tests/common::spawn_emit` runs it from any
builder. tabs.rs is the first file through it: eleven scripts that were
`printf '\033[4G\033H…'; read _` read as the sequences they send.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Seven scripts, one --raw per former printf so a reviewer diffs the
escape sequences directly: \033 becomes \e and nothing else changes.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Twelve scripts of designations and locking shifts, one --raw per line
the test draws: \016/\017 read as \x0e/\x0f, \0337/\0338 as \e7/\e8.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
The bytes that are not UTF-8 go through --raw as \xe9 and a split
\xe6 … \xb1\x89, with --sleep where the script slept.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Nine programs walking the out-of-band state: one --raw per former
printf, --wait for each pause the test releases with Enter.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Sixteen sites. A burst that must arrive as one read is one --raw, and
the three-frame burst two tests share is a named constant.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
--pid stands in for the shell's $$. --loop used to repeat every step,
which reprints the text before the loop each time round; the bell loop
in stable.rs needs the text once and the bell forever.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Seven of eight. The SIGTERM trap stays on /bin/sh with a comment: the
signal handler is what that test is about, and std has none.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
The bare-name test now uses the fixture as the bare name, with its own
directory as the PATH remedy — the same refusal, on every platform, and
no /usr/bin:/bin baked into it.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Eight programs; the blocked kitty query no longer needs stty, since the
fixture reads nothing the harness might echo.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
The bell loop is `--loop --raw '\a'`, one write per iteration and no
process spawned per iteration; the flood is --seq.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
What `echo "$NAME"` and `/usr/bin/env` did for the environment tests:
one variable, or all of them sorted, with no shell to add PWD and
friends of its own.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Ten of eleven. The self-kill stays on /bin/sh with a comment — a
process signalling itself is what that test is about. The round-trip
test now waits for a suffix the line discipline's echo cannot produce,
so it proves the read rather than the echo.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
The numbered lines are built in the test and handed over as one
argument; the shell loop was the only reason they were ever a loop.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Every program in both files puts the terminal in raw mode with stty to
read replies byte for byte, which the std-only fixture cannot do.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Five sites. The seven that read the wire under stty -icanon -echo stay
on /bin/sh, with the reason in the file header.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Eight sites whose point is that no answer comes: --wait blocks on a
line exactly as head did in canonical mode. The fourteen that read a
reply stay on /bin/sh, with the reason in the file header.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
common/mod.rs is compiled into every test binary that declares it, and
readme_example uses neither helper.

Refs #249

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
@vyncint vyncint added this to the v0.10 milestone Sep 8, 2026
@vyncint

vyncint commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

windows.yml against this branch — run 34179522277

345 passed, 92 failed — against 274 / 163 on main after #275. Of the 92, 30 name a shell (the 27 sites kept on purpose, plus the two wait_frame doctests and the tiocgwinsz python one); the other 60 are tests that used to die on /bin/sh before they could say anything and now run far enough to hit ConPTY. They line up with the step-3 table on #149 — every frames.rs, graphics.rs, and queries.rs reply test; mouse modes, focus, OSC 8 ids, DECSTR, DEC Special Graphics in charset.rs, the raw bytes in utf8.rs — with one new finding worth adding to that table:

  • Child exit is not EOF on ConPTY. a_departed_child_is_refused_identically_on_every_platform, typed_input_to_a_departed_child_is_a_typed_error, a_mouse_click_at_a_departed_child_blames_the_child, resize_after_the_child_exits_is_refused_like_send and waits_fail_fast_on_eof… all fail the same way: the child has exited, and the harness has not seen EOF, because the output pipe stays open until the pseudoconsole is closed. On Unix the liveness check reads EOF off the master; on Windows it would have to read the process handle. That is a harness change for step 4, not a test change.

Nothing here is a regression on Unix — the suite, all three feature configurations, is green on the PR — and nothing here is the fixture misbehaving on Windows either: emit built and ran there, --wait blocked on cooked-mode input as read did, --environ reported the environment, and the two bin!-style absolute-path spawns work. The stress run is dispatched too; numbers when it lands.

@vyncint

vyncint commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

stress.yml against this branch — run 34179524611

All ten shards green: ubuntu and macOS at 1, 2, 4, 8 and 16 threads, the suite repeated per shard as the workflow weights them. That is #249's last done-when — the stress workflow passes on the migrated suite — and it is the one that mattered most, since the shell's process model was what leaked into the wait-semantics tests before (#243).

@vyncint
vyncint merged commit c2b5ad2 into main Sep 8, 2026
25 checks passed
@vyncint
vyncint deleted the emit-fixture branch September 8, 2026 03:04
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.

The suite drives /bin/sh at 98 sites, so every test depends on the host shell and none can run where there is none

1 participant