Files
buzz/desktop/src-tauri/crates/buzz-terminal/tests/slicing.rs
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

883 lines
34 KiB
Rust

//! The work-denominated slicing seam: what bounds one lock hold, what bounds
//! the queue behind it, and what proves the work was actually done.
//!
//! **This is half a suite.** The adversarial resize and overflow cases live
//! in `slicing_adversarial.rs`, split out for the file-size ratchet; the two
//! files are one set of contracts. A mutation check scoped with
//! `--test slicing` covers 20 of 76 package tests and can report a confident
//! pass while the killing fixture sits in the sibling file. Dropping the
//! scrollback debt does exactly that, then dies under the package.
//!
//! Mutation checks run the package, never a file: `cargo test -p buzz-terminal`.
//!
//! Every assertion here is an **exact** expected value, never a `> 0`. A fix
//! that bounds the lock by *dropping* work instead of deferring it reports a
//! beautiful latency and a perfect screen-content receipt -- DECALN fills the
//! grid with `E`, and the second DECALN overwrites the first, so grid content
//! saturates after one of ten thousand. `completed_units == expected` is the
//! only predicate that separates "deferred the work" from "skipped it", and
//! `> 0` is satisfied by a seam that executed exactly one unit.
use buzz_terminal::fences::{
max_atom_work, max_drain_work, slice_bytes_remaining, Fences, MAX_SLICE, SYNC_CAP, TAIL_CAP,
WORK_BUDGET,
};
use buzz_terminal::{Size, Terminal};
const COLUMNS: usize = 200;
const LINES: usize = 50;
const CELLS: u64 = (COLUMNS * LINES) as u64;
fn terminal() -> Terminal {
Terminal::new(
Size {
columns: COLUMNS,
screen_lines: LINES,
scrollback: 100,
},
Fences::ALL,
)
.0
}
/// A deliberately tiny grid, for the arms that must fill the 4 MiB tail.
///
/// Filling the cap is cheap; *draining* it is not, and on a 200x50 grid a
/// full tail of DECALN is ~1e10 work units of real parsing. The cap is a
/// property of the byte depth, not of the grid, so a small grid exercises the
/// same thresholds in seconds instead of minutes -- but it does change what
/// is being tested, so it is named rather than reused silently: these arms
/// test the *depth* predicates, and the arms above test the work bound.
fn tiny() -> Terminal {
Terminal::new(
Size {
columns: 10,
screen_lines: 2,
scrollback: 10,
},
Fences::ALL,
)
.0
}
/// Feed until the tail reaches its cap, or give up.
///
/// Bounded on purpose. A test that loops until a predicate goes true hangs
/// forever when the predicate is what broke, which turns a killed mutant into
/// a wedged CI job -- and a suite that hangs instead of failing is a suite
/// nobody can bisect.
fn fill_tail(term: &mut Terminal, payload: &[u8]) -> bool {
for _ in 0..10_000 {
if term.tail_full() {
return true;
}
term.feed(payload);
}
false
}
/// Pump to completion, counting acquisitions. A drain that needed no second
/// call returns 1.
fn pump(term: &mut Terminal, bytes: &[u8]) -> usize {
let mut calls = 1;
let mut more = term.feed(bytes);
while more {
more = term.drain();
calls += 1;
}
calls
}
/// One `feed` may not spend an unbounded amount of work, however much the
/// stream asks for.
///
/// Kills: deleting the `spent >= WORK_BUDGET` break, which restores the
/// unbounded hold this whole seam exists to prevent. Deliberately asserts on
/// *work* rather than wall time -- a time assertion is a flake on a loaded
/// machine, and the work bound is the thing the code actually promises.
#[test]
fn one_feed_spends_at_most_one_budget_plus_a_slice() {
let mut term = terminal();
let decalns = 10_000;
term.feed(&b"\x1b#8".repeat(decalns));
let spent = term.stats().completed_work;
// Two terms, both irreducible: the budget is checked between slices, and
// a slice is sized so it holds at most one budget of the densest payload;
// and the callback that crosses the line cannot be preempted.
let ceiling = max_drain_work(COLUMNS, LINES, 100);
assert!(
spent <= ceiling,
"one feed spent {spent} work, over budget+overshoot ({ceiling})",
);
assert!(
term.pending_bytes() > 0,
"10000 DECALNs is {} work and the budget is {WORK_BUDGET}; if nothing \
is pending the seam ran the whole payload in one hold",
decalns as u64 * CELLS,
);
}
/// Every deferred byte is eventually executed -- exactly once, and all of it.
///
/// Kills: bounding the hold by dropping the remainder instead of keeping it
/// (`self.pending.clear()` in place of the tail), which passes any latency
/// gate and any grid-content check. The unit count is the only witness.
#[test]
fn a_deferred_tail_executes_every_unit_exactly_once() {
let mut term = terminal();
let decalns = 10_000;
let calls = pump(&mut term, &b"\x1b#8".repeat(decalns));
assert!(
calls > 1,
"a payload this dense must have needed a second call"
);
assert_eq!(
term.stats().completed_units,
decalns as u64,
"every DECALN must execute exactly once: no drops, no double-parse",
);
assert_eq!(term.stats().completed_work, decalns as u64 * CELLS);
assert_eq!(term.pending_bytes(), 0, "nothing may be left behind");
}
/// The tail drains without another `feed` -- a reader with nothing new to
/// read must still be able to retire what it already accepted.
///
/// Kills: draining only from `feed`, which strands the tail whenever the
/// child goes quiet (`cat bigfile` then no more output: the last screenful
/// never appears).
#[test]
fn a_tail_drains_without_a_second_feed() {
let mut term = terminal();
let decalns = 2_000;
assert!(term.feed(&b"\x1b#8".repeat(decalns)), "expected a tail");
// Never feed again. Only drain.
while term.drain() {}
assert_eq!(term.stats().completed_units, decalns as u64);
assert_eq!(term.pending_bytes(), 0);
}
/// A slice is cut only at a byte boundary the parser has already passed, so
/// an escape sequence split across two slices still executes once.
///
/// Kills: cutting mid-sequence and restarting the parser, or double-feeding
/// the straddling bytes. `\x1b#8` is 3 bytes and slices are a multiple of
/// neither, so at this length hundreds of sequences straddle a cut.
#[test]
fn a_sequence_split_across_slices_executes_exactly_once() {
let mut term = terminal();
let decalns = 3_000;
pump(&mut term, &b"\x1b#8".repeat(decalns));
assert_eq!(
term.stats().completed_units,
decalns as u64,
"a straddling sequence was dropped or executed twice",
);
// Same payload, delivered one byte per feed: every sequence straddles.
let mut byte_at_a_time = terminal();
for chunk in b"\x1b#8".repeat(decalns).chunks(1) {
byte_at_a_time.feed(chunk);
}
while byte_at_a_time.drain() {}
assert_eq!(byte_at_a_time.stats().completed_units, decalns as u64);
}
/// The tail is a bound on the queue, and the breach counter is loud.
///
/// Kills: a silent cap -- a tail that grows past `TAIL_CAP` without saying
/// so is indistinguishable from a reader that is obeying backpressure, which
/// is exactly the confusion that hides an unbounded queue.
#[test]
fn an_overrun_tail_is_capped_and_counted() {
let mut term = tiny();
assert!(!term.tail_full(), "a fresh terminal is not full");
assert!(term.tail_drained(), "a fresh terminal is drained");
assert_eq!(term.stats().tail_breaches, 0);
// A reader that ignores `tail_full` and keeps shovelling.
assert!(
fill_tail(&mut term, &b"\x1b#8".repeat(20_000)),
"the tail never reached its cap: the queue is not bounded",
);
assert!(term.pending_bytes() >= TAIL_CAP);
assert!(
term.stats().tail_breaches > 0,
"reaching the cap must be counted, not absorbed silently",
);
assert!(!term.tail_drained(), "a full tail is not a drained tail");
}
/// Resume is hysteretic: `tail_drained` does not go true the instant the tail
/// falls one byte below the cap.
///
/// Kills: `tail_drained() == !tail_full()`, which makes a reader flap between
/// paused and reading once per slice at exactly the moment it is most loaded.
#[test]
fn resume_waits_for_a_low_water_mark_not_merely_a_non_full_tail() {
let mut term = tiny();
assert!(
fill_tail(&mut term, &b"\x1b#8".repeat(20_000)),
"expected a full tail"
);
// Drain until the reader is allowed to resume, watching for a window in
// which it is neither full nor drained -- that gap *is* the hysteresis.
let mut saw_gap = false;
for _ in 0..1_000_000 {
if term.tail_drained() {
break;
}
assert!(term.drain() || term.tail_drained());
if !term.tail_full() && !term.tail_drained() {
saw_gap = true;
}
}
assert!(
term.tail_drained(),
"the tail never drained to the resume mark"
);
assert!(
saw_gap,
"no depth was both non-full and non-drained: the two thresholds are \
the same value and the reader will flap",
);
}
/// Close must not be held behind parser work.
///
/// Kills: draining the tail on close instead of discarding it. Measured
/// elsewhere in this project: teardown that finishes parsing before killing
/// the child costs ~600 ms on macOS, and no byte of that work reaches a
/// renderer -- publication is detached before shutdown drains.
#[test]
fn close_may_abandon_the_tail_and_says_how_much_it_dropped() {
let mut term = terminal();
term.feed(&b"\x1b#8".repeat(10_000));
let stranded = term.pending_bytes();
assert!(stranded > 0);
let abandoned = term.abandon_tail();
assert_eq!(abandoned, stranded);
assert_eq!(term.pending_bytes(), 0);
assert_eq!(
term.stats().abandoned_bytes,
stranded as u64,
"dropped bytes must be counted: this is lossy by design and silent \
loss is how it stops being by design",
);
assert!(
term.tail_drained(),
"an abandoned tail cannot strand a reader"
);
}
/// The grid the weights are priced against tracks resizes.
///
/// Kills: dropping `Feeder::resize`. A stale grid misprices every O(cells)
/// charge for as long as it is wrong -- and it is wrong in the *unsafe*
/// direction whenever the window grows, which is the common case.
#[test]
fn a_resize_reprices_the_same_escape() {
let mut small = terminal();
small.feed_fully(b"\x1b#8");
let before = small.stats().completed_work;
assert_eq!(before, CELLS);
small.resize(Size {
columns: COLUMNS * 2,
screen_lines: LINES,
scrollback: 100,
});
small.reset_stats();
small.feed_fully(b"\x1b#8");
assert_eq!(
small.stats().completed_work,
CELLS * 2,
"the same escape on a grid twice as wide must cost twice as much",
);
assert_eq!(small.stats().completed_units, 1, "still one callback");
}
/// A resize *between* slices of one payload reprices the remainder.
///
/// Kills: caching the slice size or the grid across a drain. The tail
/// outlives the call that accepted it, so a resize can land in the middle of
/// it -- the untouched remainder must be charged at the new grid, not the one
/// that was current when the bytes arrived.
#[test]
fn a_resize_mid_tail_reprices_the_remainder() {
let mut term = terminal();
let decalns = 4_000;
assert!(term.feed(&b"\x1b#8".repeat(decalns)), "expected a tail");
let done_before = term.stats().completed_units;
let work_before = term.stats().completed_work;
assert_eq!(work_before, done_before * CELLS);
term.resize(Size {
columns: COLUMNS * 2,
screen_lines: LINES,
scrollback: 100,
});
while term.drain() {}
let after = term.stats();
assert_eq!(after.completed_units, decalns as u64, "no unit may be lost");
assert_eq!(
after.completed_work,
work_before + (decalns as u64 - done_before) * CELLS * 2,
"the remainder must be priced at the resized grid",
);
}
/// Slice size is derived from the worst atom the grid admits, because a fixed
/// byte count cannot bound a lock hold: `ESC c` is two bytes and resets both
/// grids plus scrollback.
///
/// Kills: replacing `slice_bytes_remaining` with a constant, or deriving it from
/// `cells` while the worst atom is larger than `cells`. Measured: 256 bytes
/// of DECALN is 1.6 ms at 200x50 and ~14 ms at 1600x50, so no one constant
/// serves both.
#[test]
fn slice_size_shrinks_as_the_worst_atom_grows() {
let small = slice_bytes_remaining(80, 24, 0, 0, 0);
let large = slice_bytes_remaining(1600, 50, 0, 0, 0);
assert!(
small > large,
"a bigger grid makes each byte more expensive, so slices must shrink: \
80x24 -> {small}, 1600x50 -> {large}",
);
assert!(
slice_bytes_remaining(200, 50, 10_000, 0, 0) <= slice_bytes_remaining(200, 50, 0, 0, 0),
"scrollback makes RIS more expensive, so it may only shrink slices",
);
for (columns, lines, scrollback) in [(80, 24, 0), (200, 50, 0), (400, 100, 0), (1600, 50, 0)] {
assert!((1..=MAX_SLICE).contains(&slice_bytes_remaining(columns, lines, scrollback, 0, 0)));
// One slice holds at most N/2 of the densest atom. Either that fits a
// budget, or the floor binds -- and then the overshoot is stated by
// `max_drain_work` rather than being an accident.
let width = slice_bytes_remaining(columns, lines, scrollback, 0, 0);
let worst = (width as u64 / 2) * max_atom_work(columns, lines, scrollback);
assert!(
worst <= WORK_BUDGET || width == 1,
"{columns}x{lines}: a slice buys {worst} work against a \
{WORK_BUDGET} budget without the MIN clamp to excuse it",
);
}
}
/// Work released by an F1 abort is counted.
///
/// Kills: leaving the `stop_sync` flush out of the accounting. F1 aborts a
/// runaway synchronized update by flushing its buffer through the handler --
/// those callbacks run, cost time, and hold the lock, so a scheduler that
/// does not see them is blind on exactly the path the fence created. The
/// escapes here are `ESC#8` so the flushed work is unmistakable against the
/// buffered bytes.
#[test]
fn work_flushed_by_a_sync_abort_is_counted() {
let (mut term, _a) = Terminal::new(
Size {
columns: 80,
screen_lines: 24,
scrollback: 0,
},
Fences::SYNC_ONLY,
);
let cells = 80 * 24;
// Open a synchronized update and never close it: F1 must abort it once
// the buffer passes SYNC_CAP, flushing everything buffered so far.
term.feed_fully(b"\x1b[?2026h");
let decalns = SYNC_CAP / 3 + 1000;
term.feed_fully(&b"\x1b#8".repeat(decalns));
let stats = term.stats();
assert!(stats.sync_aborts > 0, "the fence must have fired");
// Every DECALN fed must be accounted for. The comparison is against the
// *input*, not against the counters' own internal consistency: an
// uncounted flush leaves both counters small together, so checking them
// against each other would pass over the mutant.
// Two bookkeeping callbacks besides the DECALNs: the `ESC[?2026h` that
// opened the update, and the `unset_private_mode` that `stop_sync` emits
// per abort to report the mode off (`vte-0.15.0/src/ansi.rs:353`).
let bookkeeping = 1 + stats.sync_aborts;
assert_eq!(
stats.completed_units,
decalns as u64 + bookkeeping,
"every DECALN must be counted, including the ones released by the \
abort, plus {bookkeeping} mode callbacks",
);
assert_eq!(
stats.completed_work,
decalns as u64 * cells + bookkeeping,
"and their work: {decalns} DECALNs at {cells} cells each",
);
}
/// Cheap traffic is not taxed by slicing: an ordinary screenful retires in
/// one call.
///
/// Kills: a budget so small, or a slice so small, that normal output pays the
/// deferral machinery. This is the companion to the DECALN arm -- a seam that
/// bounds the hold by making everything slow has not fixed anything.
#[test]
fn ordinary_output_needs_no_second_call() {
let mut term = terminal();
let line = b"\x1b[1;32mbuzz\x1b[0m substrate line of output 0123456789\r\n";
let screenful = line.repeat(LINES);
assert!(
!term.feed(&screenful),
"a screenful of ordinary output must retire in one call, not defer",
);
assert_eq!(term.pending_bytes(), 0);
assert_eq!(term.stats().tail_breaches, 0);
}
/// The work bound holds on the **first drain of a fresh feeder**, for the
/// densest payload upstream offers.
///
/// Kills: sizing slices from observed density. A learned bound is not a bound
/// on the first slice -- a cold feeder has seen nothing, so it hands the
/// parser a wide slice, and a wide slice of `ESC c` spends many budgets
/// before anything checks. This is the arm that a warm-up-based scheduler
/// passes on the second call and fails on the first, so it asserts on a
/// terminal that has never parsed a byte.
#[test]
fn a_cold_feeder_bounds_its_very_first_slice() {
for (columns, lines) in [(80, 24), (200, 50), (400, 100), (1600, 50)] {
for (label, atom) in [("RIS", &b"\x1bc"[..]), ("DECALN", &b"\x1b#8"[..])] {
let (mut term, _a) = Terminal::new(
Size {
columns,
screen_lines: lines,
scrollback: 100,
},
Fences::ALL,
);
// Never fed before: `density`-style state, if any existed, is at
// its initial value.
term.feed(&atom.repeat(5_000));
let spent = term.stats().completed_work;
let ceiling = max_drain_work(columns, lines, 100);
assert!(
spent <= ceiling,
"{label} at {columns}x{lines}: first drain of a cold feeder \
spent {spent} work, over budget+overshoot ({ceiling})",
);
assert!(term.pending_bytes() > 0, "{label}: expected a tail");
}
}
}
/// Exact price of every escape whose cost the grid can amplify.
///
/// One table, exact `completed_work` per escape, at two widths so a weight
/// that dropped its `columns` factor cannot hide. Kills, one row each:
///
/// * `delete_chars`/`insert_blank` charged by `N` -- their cost *falls* as N
/// rises (the swap loop runs `columns - end` times), so N=1 is the worst
/// case and pricing by N is backwards.
/// * `erase_chars` charged raw `N` -- upstream clamps to the row, so
/// `ESC[65535X` on an 80-column grid touches 80 cells, not 65535.
/// * `scroll_up`/`delete_lines` losing their `columns` factor -- the rows are
/// reset, and a row reset is O(columns).
/// * `clear_line`, `decaln`, `clear_screen` mispriced by an axis.
///
/// Exact equality, never a bound: a `<=` assertion passes for every weight
/// smaller than the truth, which is the direction that hurts.
#[test]
fn every_amplifiable_escape_is_priced_exactly() {
for (columns, lines) in [(80usize, 24usize), (400, 50)] {
let cells = (columns * lines) as u64;
let c = columns as u64;
let cases: &[(&str, String, u64)] = &[
("decaln", "\u{1b}#8".into(), cells),
("clear_screen", "\u{1b}[2J".into(), cells),
("clear_line", "\u{1b}[2K".into(), c),
("erase_chars N=1", "\u{1b}[1X".into(), 1),
("erase_chars N=20", "\u{1b}[20X".into(), 20),
("erase_chars N=huge", "\u{1b}[65535X".into(), c),
("delete_chars N=1", "\u{1b}[1P".into(), c),
("delete_chars N=huge", "\u{1b}[65535P".into(), c),
("insert_blank N=1", "\u{1b}[1@".into(), c),
("scroll_up N=1", "\u{1b}[1S".into(), c),
("scroll_up N=5", "\u{1b}[5S".into(), 5 * c),
("scroll_up N=huge", "\u{1b}[65535S".into(), lines as u64 * c),
("scroll_down N=1", "\u{1b}[1T".into(), c),
("scroll_down N=4", "\u{1b}[4T".into(), 4 * c),
(
"scroll_down N=huge",
"\u{1b}[65535T".into(),
lines as u64 * c,
),
("delete_lines N=3", "\u{1b}[3M".into(), 3 * c),
(
"delete_lines N=huge",
"\u{1b}[65535M".into(),
lines as u64 * c,
),
("insert_lines N=1", "\u{1b}[1L".into(), c),
("insert_lines N=6", "\u{1b}[6L".into(), 6 * c),
(
"insert_lines N=huge",
"\u{1b}[65535L".into(),
lines as u64 * c,
),
("put_tab N=1", "\t".into(), c),
("fwd_tabs N=1", "\u{1b}[1I".into(), c),
("fwd_tabs N=huge", "\u{1b}[65535I".into(), c),
("insert_blank N=huge", "\u{1b}[65535@".into(), c),
("clear_line ESC[0K", "\u{1b}[0K".into(), c),
("clear_line ESC[1K", "\u{1b}[1K".into(), c),
("clear_screen ESC[0J", "\u{1b}[0J".into(), cells),
("clear_screen ESC[1J", "\u{1b}[1J".into(), cells),
("sgr", "\u{1b}[m".into(), 1),
("goto", "\u{1b}[1;1H".into(), 1),
];
for (label, seq, expected) in cases {
let (mut term, _a) = Terminal::new(
Size {
columns,
screen_lines: lines,
scrollback: 0,
},
Fences::ALL,
);
// Home first so nothing scrolls, then measure only the escape.
term.feed_fully(b"\x1b[1;1H");
term.reset_stats();
term.feed_fully(seq.as_bytes());
assert_eq!(term.stats().completed_units, 1, "{label}: one callback");
assert_eq!(
term.stats().completed_work,
*expected,
"{label} at {columns}x{lines} priced wrong",
);
}
}
}
/// RIS is priced with its history axis, not just its cells.
///
/// Kills: charging `cells`, or dropping the history term.
///
/// On the alt-screen arm, honestly labelled: the active-`history_size()`
/// mispricing it was written against is **unrepresentable in this design**,
/// not merely untested. `Counting` holds `scrollback` as a scalar copied at
/// construction and has no path to a live grid, so there is no way to write
/// the mutant. The arm is kept as a regression witness -- if a `Term`
/// reference is ever wired into the wrapper it becomes load-bearing the same
/// day -- and both arms are evaluated before either can report, so the
/// primary cannot short-circuit the alt.
#[test]
fn ris_is_priced_for_both_grids_and_the_scrollback_it_walks() {
let (columns, lines) = (80usize, 24usize);
let cells = (columns * lines) as u64;
let mut observed = vec![];
for scrollback in [0usize, 100, 10_000] {
for (label, prefix) in [("primary", ""), ("alt screen", "\u{1b}[?1049h")] {
let (mut term, _a) = Terminal::new(
Size {
columns,
screen_lines: lines,
scrollback,
},
Fences::ALL,
);
term.feed_fully(prefix.as_bytes());
term.reset_stats();
term.feed_fully(b"\x1bc");
observed.push((label, scrollback, term.stats().completed_work));
}
}
// One comparison over the whole vector, not a loop of comparisons.
// Collecting first stops an arm from being *skipped*; asserting the
// vectors is what stops a failure from being *truncated* to the first
// mismatch. Otherwise the alt-screen receipt still never prints, which
// was the point of collecting.
let expected: Vec<_> = observed
.iter()
.map(|&(label, scrollback, _)| {
(label, scrollback, 2 * cells + (scrollback * columns) as u64)
})
.collect();
assert_eq!(
observed, expected,
"RIS must be priced on configured depth, identically on both grids",
);
}
/// CBT is charged for exactly the cells it scans -- an equality, in both
/// directions.
///
/// Kills: delegating `move_backward_tabs` verbatim, and deleting the
/// fixed-point break. With tabstops cleared and the cursor at the right
/// margin, upstream never advances the cursor, so its `col == 0` exit is
/// unreachable and all N iterations rescan the row -- `ESC[3g ESC[65535Z` is
/// 8 bytes for 82 ms at 1600 columns.
///
/// Two traps this had to be written around, both of which I walked into
/// first:
///
/// * **The cursor is not the witness.** Deleting the break lands on the same
/// column; only the cost differs. A fixture checking where the cursor ended
/// up passes over the mutant.
/// * **An upper bound is not the witness either.** Deleting the break makes
/// the loop run without charging -- measured `work == 1` for 29 ms of real
/// scanning -- so `spent <= bound` *passes*. Under-charging is exactly the
/// direction that hurts, and only an equality sees it.
///
/// The expected value is the scan the source performs: with no stop below the
/// cursor, one pass over `cursor_column` cells, then a permanent fixed point.
/// Both arms come to `columns` -- the telescoping sum of a walk, or one
/// failed pass -- which is the bound this whole change buys.
#[test]
fn the_worst_atom_is_charged_for_exactly_what_it_scans() {
for columns in [80usize, 400, 1600] {
let (mut term, _a) = Terminal::new(
Size {
columns,
screen_lines: 50,
scrollback: 0,
},
Fences::ALL,
);
// Adversarial for cost: every tabstop gone, cursor at the right
// margin, count far past the width.
term.feed_fully(format!("\u{1b}[3g\u{1b}[1;{columns}H").as_bytes());
term.reset_stats();
term.feed_fully(b"\x1b[65535Z");
assert_eq!(term.stats().completed_units, 1, "one escape, one callback");
assert_eq!(
term.stats().completed_work,
1 + (columns as u64 - 1),
"one failed scan over the whole prefix, then a permanent fixed \
point: the charge is the escape plus that one scan. A loop that \
kept going would charge this much per iteration, 65535 times",
);
// The real guard on the loop: with a stop reachable, the charge must
// equal the distance actually travelled. A break-less loop scans the
// row 65535 times and charges for one crossing.
let (mut walk, _a) = Terminal::new(
Size {
columns,
screen_lines: 50,
scrollback: 0,
},
Fences::ALL,
);
// Default tabstops every 8: from the right margin a huge count walks
// to column 0, crossing every column on the way.
walk.feed_fully(format!("\u{1b}[1;{columns}H").as_bytes());
walk.reset_stats();
walk.feed_fully(b"\x1b[65535Z");
assert_eq!(walk.term().grid().cursor.point.column.0, 0);
assert_eq!(
walk.stats().completed_work,
1 + (columns as u64 - 1),
"the charge must be the distance travelled: one unit for the \
escape plus one per column crossed",
);
}
}
/// CBT at column 0 is free, and stays free.
///
/// Kills: removing the `before == 0` guard. Upstream has its own `col == 0`
/// break, so deleting the wrapper's copy is invisible to the cursor and
/// invisible to timing -- it only shows up as work charged for a scan over
/// zero cells that the wrapper attributed to itself. The left margin is also
/// the position both earlier sweeps of this op homed to, which is why it is
/// the position where a defect hides best.
#[test]
fn the_worst_atom_costs_nothing_at_the_left_margin() {
for columns in [80usize, 400] {
let (mut term, _a) = Terminal::new(
Size {
columns,
screen_lines: 50,
scrollback: 0,
},
Fences::ALL,
);
term.feed_fully(b"\x1b[3g\x1b[1;1H");
term.reset_stats();
term.feed_fully(b"\x1b[65535Z");
assert_eq!(
term.stats().completed_work,
1,
"at column 0 there is nothing to the left to scan, so the escape \
costs one unit and no cells",
);
assert_eq!(term.term().grid().cursor.point.column.0, 0);
}
}
/// The other adversary: every tabstop *set*, which maximises the number of
/// delegated single steps rather than the length of one scan.
///
/// Kills: pricing CBT per-step-times-width. Cleared tabstops attack the
/// clamp; all-set attacks the break, forcing `columns - 1` steps of one
/// column each. The two layouts peak in different terms and neither may
/// exceed the bound, so both are here -- a suite that tested only the famous
/// one would miss the shape it chose against.
#[test]
fn the_worst_atom_is_bounded_under_the_layout_that_maximises_steps() {
for columns in [80usize, 400] {
let (mut term, _a) = Terminal::new(
Size {
columns,
screen_lines: 50,
scrollback: 100,
},
Fences::ALL,
);
// A tabstop in every column, then start from the right margin.
term.feed_fully(b"\x1b[3g");
for c in 1..=columns {
term.feed_fully(format!("\u{1b}[1;{c}H\u{1b}H").as_bytes());
}
term.feed_fully(format!("\u{1b}[1;{columns}H").as_bytes());
term.reset_stats();
term.feed_fully(b"\x1b[65535Z");
assert_eq!(term.stats().completed_units, 1);
assert_eq!(
term.stats().completed_work,
1 + (columns as u64 - 1),
"with a stop in every column the walk crosses each of them once, \
so the charge is exact: one unit for the escape plus one per \
column crossed. An inequality here would not catch a 2x \
overcharge -- which lands on 159, not 160, because the escape's \
own unit is charged separately and is not doubled",
);
assert_eq!(
term.term().grid().cursor.point.column.0,
0,
"with a stop in every column the cursor must walk all the way",
);
}
}
/// Stopping CBT early does not change where the cursor lands.
///
/// The companion to the two cost tests above: they assert the work fell,
/// this asserts the behaviour did not move. Kills: stopping at something that
/// is *not* a fixed point -- `min(N, 1)`, or breaking whenever a scan fails
/// even though an earlier step still had stops to find. Cases are the ones
/// the exhaustive probe found interesting: no stops, one stop mid-row, and
/// default stops, each from the right margin with a count past the width.
#[test]
fn stopping_the_worst_atom_early_preserves_its_semantics() {
let columns = 40usize;
let cursor_column = |setup: &str| -> usize {
let (mut term, _a) = Terminal::new(
Size {
columns,
screen_lines: 3,
scrollback: 0,
},
Fences::ALL,
);
term.feed_fully(setup.as_bytes());
term.term().grid().cursor.point.column.0
};
// No stops: the cursor cannot move, whatever the count.
assert_eq!(cursor_column("\u{1b}[3g\u{1b}[1;40H\u{1b}[65535Z"), 39);
assert_eq!(cursor_column("\u{1b}[3g\u{1b}[1;40H\u{1b}[40Z"), 39);
// One stop at column 20 (1-based 21): reachable once, then stuck.
let one_stop = "\u{1b}[3g\u{1b}[1;21H\u{1b}H\u{1b}[1;40H";
assert_eq!(
cursor_column(&format!("{one_stop}\u{1b}[65535Z")),
cursor_column(&format!("{one_stop}\u{1b}[40Z")),
);
// Default stops every 8: a large count walks all the way to column 0.
assert_eq!(cursor_column("\u{1b}[1;40H\u{1b}[65535Z"), 0);
}
/// A stream of atoms each worth more than the whole budget still drains, and
/// every drain makes progress.
///
/// The liveness half of the bound. `max_drain_work` says how much one drain
/// may cost; it says nothing about whether the loop terminates, and an
/// oversized atom is exactly where a work-denominated scheduler could refuse
/// to start one -- spending its budget checking, never advancing, and hanging
/// the terminal with a full tail. RIS on a 10k-scrollback grid is ~16x the
/// budget, so this is not hypothetical.
///
/// Kills: any yield that can decline to start work -- a `width` that reaches
/// 0, a `remaining`-scaled slice that underflows to nothing, a guard that
/// skips a slice deemed too expensive for what is left of the budget. Each of
/// those is a plausible thing to reach for when an atom costs more than the
/// whole budget, and each hangs a terminal on legitimate input.
///
/// Note on a mutant it does *not* kill: moving the budget check from after
/// the slice to before it is **equivalent**, not a defect -- `spent` is zero
/// at entry, so the first slice runs either way. Recorded because I wrote
/// this test believing it caught that, ran the mutant, and it lived.
#[test]
fn atoms_larger_than_the_budget_still_make_progress() {
for (columns, lines, scrollback) in [(80usize, 24usize, 10_000usize), (200, 50, 10_000)] {
let (mut term, _a) = Terminal::new(
Size {
columns,
screen_lines: lines,
scrollback,
},
Fences::ALL,
);
let atoms = 200usize;
let bound = max_drain_work(columns, lines, scrollback);
assert!(
bound > WORK_BUDGET * 4,
"this arm is only meaningful where one atom dwarfs the budget",
);
let mut more = term.feed(&b"\x1bc".repeat(atoms));
// `feed` already drained once; seed the baseline with its work or the
// first delta measured below silently doubles.
let mut previous = term.stats().completed_work;
let mut worst = previous;
let mut calls = 1;
while more {
let before = term.pending_bytes();
more = term.drain();
assert!(
term.pending_bytes() < before,
"no progress: the tail stuck at {before} bytes",
);
let now = term.stats().completed_work;
worst = worst.max(now - previous);
previous = now;
calls += 1;
assert!(calls < 10_000, "drain did not terminate");
}
assert_eq!(term.stats().completed_units, atoms as u64, "lost units");
assert_eq!(term.pending_bytes(), 0);
assert!(
worst <= bound,
"{columns}x{lines}: worst drain spent {worst}, over the stated \
bound {bound}",
);
}
}