From a3580207b2a824f962440075202ee47937393cf9 Mon Sep 17 00:00:00 2001 From: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta Date: Sun, 2 Aug 2026 02:08:34 -0400 Subject: [PATCH] test(terminal): keep slicing fixtures within size budget Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta --- .../crates/buzz-terminal/tests/slicing.rs | 384 ----------------- .../tests/slicing_adversarial.rs | 390 ++++++++++++++++++ 2 files changed, 390 insertions(+), 384 deletions(-) create mode 100644 desktop/src-tauri/crates/buzz-terminal/tests/slicing_adversarial.rs diff --git a/desktop/src-tauri/crates/buzz-terminal/tests/slicing.rs b/desktop/src-tauri/crates/buzz-terminal/tests/slicing.rs index 26097430a..3a3834404 100644 --- a/desktop/src-tauri/crates/buzz-terminal/tests/slicing.rs +++ b/desktop/src-tauri/crates/buzz-terminal/tests/slicing.rs @@ -871,387 +871,3 @@ fn atoms_larger_than_the_budget_still_make_progress() { ); } } - -/// A scrollback change reprices RIS *and* the slicing derived from it. -/// -/// Kills: updating the feeder's columns and lines on resize but not its -/// scrollback -- and, separately, a repair that reprices the charge while -/// leaving slice width stale. Those are different failures and neither -/// observable sees the other: fix only the charge and the drain count stays -/// wrong; fix only the derivation and the charge stays wrong. -/// -/// Two properties, because one is not enough: -/// -/// * The exact RIS charge at the new depth. Direct, and it is what a -/// pricing-only repair passes. -/// * Equality with a terminal *constructed* at the new depth, across work -/// and drain count. A resized feeder that is genuinely repaired is -/// indistinguishable from one that was born there. This is stronger than a -/// hand-picked threshold and immune to `WORK_BUDGET`/`MIN_SLICE` moving, -/// since both arms move together -- and the sanity arm proves the -/// comparison is deterministic before it is used to judge anything. -/// -/// `completed_units` is deliberately *not* the discriminator here: it reads -/// 200 in both arms, because the same callbacks run either way and only their -/// cost and slicing differ. It is asserted anyway as the invariant that must -/// hold -- no unit lost or duplicated across a resize -- while carrying none -/// of the discrimination. -#[test] -fn a_scrollback_change_reprices_the_densest_atom_and_the_slicing() { - let shallow = Size { - columns: 200, - screen_lines: 50, - scrollback: 100, - }; - let deep = Size { - scrollback: 10_000, - ..shallow - }; - let cells = (deep.columns * deep.screen_lines) as u64; - - // Preconditions, asserted rather than assumed, because both are easy to - // break by "generalising" this fixture later: - // - // * The geometry must let the *scheduling* fields separate. They only do - // when the two depths land on different slice widths, and the deep side - // is always floored -- so the shallow side must not be. At 1600x50 the - // visible grid alone floors every depth from 0 upward, and three of the - // four observables below go silently inert. - // * The payload must be RIS. It is the only escape reaching the only - // weight carrying a scrollback term (`units::reset_state`); DECALN and - // every other atom are priced on cells or columns and are blind to - // depth, so a conforming repair would show work identical to the - // control and the assertions here would invert into false failures. - assert!( - slice_bytes_remaining( - shallow.columns, - shallow.screen_lines, - shallow.scrollback, - 0, - 0 - ) > 1, - "geometry cannot discriminate: the shallow arm is already floored", - ); - assert_eq!( - slice_bytes_remaining(deep.columns, deep.screen_lines, deep.scrollback, 0, 0), - 1, - ); - - // How a terminal at `size` retires 200 RIS: work, and how many - // acquisitions it took. Both are feeder behaviour, not helper output. - let run = |size: Size, resize_from: Option| { - let (mut term, _a) = Terminal::new(resize_from.unwrap_or(size), Fences::ALL); - if resize_from.is_some() { - term.resize(size); - } - term.reset_stats(); - let mut drains = 1; - let mut more = term.feed(&b"c".repeat(200)); - while more { - more = term.drain(); - drains += 1; - } - ( - term.stats().completed_units, - term.stats().completed_work, - drains, - ) - }; - - let control = run(deep, None); - let sanity = run(deep, None); - assert_eq!( - control, sanity, - "two terminals built the same way must agree before this comparison can judge anything", - ); - - let resized = run(deep, Some(shallow)); - assert_eq!(resized.0, 200, "no unit may be lost or duplicated"); - assert_eq!( - resized, control, - "a feeder resized to a depth must be indistinguishable from one constructed at it -- in charge and in how many acquisitions it took", - ); - - // The exact charge, stated rather than inferred from the equality: a - // repair that made both arms equally *wrong* would pass the comparison. - let (mut term, _a) = Terminal::new(shallow, Fences::ALL); - term.resize(deep); - term.reset_stats(); - term.feed_fully(b"c"); - assert_eq!( - term.stats().completed_work, - 2 * cells + (deep.scrollback * deep.columns) as u64, - ); - - // Shrinking retains the debt, and the fixture proves retention rather - // than merely permitting it. - // - // `>= fresh` alone is the predicate three of us proposed and all three - // withdrew: a feeder that dropped the debt reads *exactly* equal to a - // fresh shallow one, so `>=` passes on the unrepaired state. Strictness - // on the pricing field is what rejects it. The scheduling fields are - // asserted directionally with per-field signs -- `first_units` inverts, - // because a narrower slice retires fewer atoms per un-preemptable drain, - // which is the fence working -- but none of them is the discriminator: - // they separate only when the two depths straddle the slice floor, and - // `completed_work` separates at every positive depth gap. - // - // Every comparison is against the fresh control's own field, never a - // literal: a constant or geometry change must move both sides together, - // or the fixture starts asserting the arithmetic of the day it was - // written. - let measure = |term: &mut Terminal| { - term.reset_stats(); - let mut drains = 1; - let mut more = term.feed(&b"\x1bc".repeat(200)); - let first_units = term.stats().completed_units; - let first_pending = term.pending_bytes(); - while more { - more = term.drain(); - drains += 1; - } - ( - first_units, - first_pending, - drains, - term.stats().completed_units, - term.stats().completed_work, - ) - }; - - // The terminal under test stays alive past its measurement, so the - // geometry arm below runs on the feeder that actually shrank rather than - // on a lookalike that only ever grew. - let (mut shrunk_term, _a) = Terminal::new(shallow, Fences::ALL); - shrunk_term.resize(deep); - shrunk_term.resize(shallow); - let shrunk = measure(&mut shrunk_term); - - let (mut fresh_term, _a) = Terminal::new(shallow, Fences::ALL); - let fresh = measure(&mut fresh_term); - - assert_eq!( - shrunk.3, fresh.3, - "no unit may be lost on the way down either" - ); - assert!( - shrunk.4 > fresh.4, - "a feeder that has been deep must still price deep after shrinking: \ - {} against a fresh shallow {}. Equality here is the signature of a \ - feeder that dropped the debt, which is indistinguishable from one \ - that never had it", - shrunk.4, - fresh.4, - ); - assert!( - shrunk.0 <= fresh.0, - "narrower slices retire fewer atoms per drain: {} against {}", - shrunk.0, - fresh.0, - ); - assert!( - shrunk.1 >= fresh.1, - "and leave more pending after the first call: {} against {}", - shrunk.1, - fresh.1, - ); - assert!( - shrunk.2 >= fresh.2, - "and take more drains to finish: {} against {}", - shrunk.2, - fresh.2, - ); - - // The debt survives a later resize on a different axis. Two things make - // this arm bite, and it was inert without either: - // - // * It runs on the terminal that actually went shallow -> deep -> - // shallow. A lookalike that only ever grew passes it while an - // implementation that retains on shrink and drops on the next geometry - // change fails. - // * The resize carries the *shallow* depth. Passing the debt's own value - // back in means `max(debt, new)` and a plain assignment agree, so the - // arm cannot tell them apart -- which is how it survived a mutant that - // retained only when columns and lines were unchanged. - shrunk_term.resize(Size { - columns: shallow.columns * 2, - screen_lines: shallow.screen_lines, - scrollback: shallow.scrollback, - }); - shrunk_term.reset_stats(); - shrunk_term.feed_fully(b"\x1bc"); - assert_eq!( - shrunk_term.stats().completed_work, - 2 * (shallow.columns * 2 * shallow.screen_lines) as u64 - + (deep.scrollback * shallow.columns * 2) as u64, - "a columns resize must keep the deep scrollback debt, not fall back \ - to the current shallow depth", - ); -} - -/// One oversized atom per drain -- no callback runs after the one that -/// crosses the budget. -/// -/// Kills: sizing slices from the *whole* budget rather than what remains of -/// it. RIS at any real scrollback depth is worth more than an entire budget, -/// so a slice wide enough for several callbacks runs several: measured -/// `completed_units == 3` for `ESC c` followed by `Xmore`, where the law -/// permits exactly one. The fix makes slice width a function of `remaining`, -/// which is a single byte once an atom this size is in play. -/// -/// Also asserts the tail survives it: yielding after the crossing atom is -/// only correct if what follows is still parsed, exactly once. -#[test] -fn an_oversized_atom_yields_before_the_next_callback() { - let size = Size { - columns: 400, - screen_lines: 100, - scrollback: 10_000, - }; - let (mut term, _a) = Terminal::new(size, Fences::ALL); - let ris_work = - 2 * (size.columns * size.screen_lines) as u64 + (size.scrollback * size.columns) as u64; - assert!( - ris_work > WORK_BUDGET, - "this arm needs an atom bigger than the whole budget", - ); - - let more = term.feed(b"\x1bcXmore"); - - assert!(more, "the drain must yield with a tail"); - assert_eq!( - term.stats().completed_units, - 1, - "exactly the crossing atom ran: a callback after it is post-atom \ - overrun, which is the thing the budget cannot preempt and therefore \ - must not start", - ); - assert_eq!(term.stats().completed_work, ris_work); - - while term.drain() {} - assert_eq!( - term.stats().completed_units, - 1 + 5, - "the five characters after it must still be parsed, exactly once", - ); - assert_eq!(term.pending_bytes(), 0); -} - -/// Extreme dimensions saturate rather than wrapping or panicking. -/// -/// Kills: `columns * lines` in `usize` before the cast. `Size` is unclamped -/// and reaches the weight path from a caller, so this product is a reachable -/// overflow -- a debug panic inside the accounting path, or a release wrap -/// that reports the most expensive callback in the emulator as one of the -/// cheapest. Saturating is the only one of the three that fails safe. -#[test] -fn extreme_dimensions_saturate_instead_of_wrapping() { - let huge = usize::MAX / 2; - assert_eq!(max_atom_work(huge, huge, huge), u64::MAX); - assert_eq!(max_drain_work(huge, huge, huge), u64::MAX); - - // The *direction* is the assertion, not merely the absence of a panic. - // A wrapping build does not produce a slightly-wrong bound, it produces a - // tiny one -- and `slice_bytes_remaining` divides the budget by it, so an - // undercharged atom yields an *oversized* slice exactly when the atom is - // most expensive. Wrapping inverts the fence. So: the widest possible - // atom must give the narrowest possible slice. - assert_eq!( - slice_bytes_remaining(huge, huge, huge, 0, 0), - 1, - "an overflowing grid must clamp to the smallest slice; a wrapped \ - `max_atom_work` would hand back a generous one", - ); - assert_eq!( - slice_bytes_remaining(huge, huge, huge, 0, 0), - 1, - "and the escape at the front of such a grid gets a single byte", - ); - - // The property behind those endpoints, and the stronger statement: a - // grid that costs more may never buy a wider slice. Endpoints pin the - // ends; only a sweep catches a non-monotone middle, and a wrap *is* a - // non-monotone middle -- it makes the worst grid look cheap and hands it - // the widest slice of all. - // Every axis independently: a wrap on any one of the three products is a - // non-monotone middle on that axis alone, and sweeping only scrollback - // would miss a truncating `columns * lines`. - for (axis, at) in [ - ( - "scrollback", - (|n| slice_bytes_remaining(200, 50, n, 0, 0)) as fn(usize) -> usize, - ), - ("columns", |n| slice_bytes_remaining(n.max(1), 50, 0, 0, 0)), - ("lines", |n| slice_bytes_remaining(200, n.max(1), 0, 0, 0)), - ] { - let mut previous = usize::MAX; - for exponent in 0..60 { - let width = at(1usize << exponent); - assert!( - width <= previous, - "slice widened from {previous} to {width} at {axis} \ - 2^{exponent}: more expensive grid, more generous slice", - ); - assert!(width >= 1); - previous = width; - } - } - - // Just past 32 bits on one axis: large enough that a narrowing cast - // shows (`1 << 32` truncates to 0 in `u32`, pricing an enormous grid at - // nothing), small enough that the honest answer is exact rather than - // saturated. Neither the extreme endpoints above nor the ordinary grids - // below can see this -- the endpoints saturate either way and the - // ordinary ones fit in 32 bits. - assert_eq!(max_atom_work(1 << 32, 1, 0), 2 * (1u64 << 32)); - assert_eq!(max_atom_work(1, 1 << 32, 0), 2 * (1u64 << 32)); - assert_eq!(max_atom_work(1, 1, 1 << 32), 2 + (1u64 << 32)); - - // Ordinary grids are untouched by the saturation: exact, not clamped. - assert_eq!(max_atom_work(80, 24, 0), 2 * 80 * 24); - assert_eq!(max_atom_work(80, 24, 100), 2 * 80 * 24 + 100 * 80); -} - -/// An escape split across slices keeps its escape metering. -/// -/// Kills: deciding "plain run or escape?" by looking only at the bytes ahead. -/// After a slice ending on a lone `ESC`, the next byte is `c` -- which looks -/// like ordinary text and is in fact a full grid reset. Meter it as text and -/// the oversized atom rides into a wide slice with whatever follows, which is -/// the post-atom overrun arriving through a different door. Found by the -/// oversized-atom fixture failing after I "optimised" the plain path, which -/// is the argument for keeping both. -#[test] -fn an_escape_split_across_slices_keeps_its_metering() { - let size = Size { - columns: 400, - screen_lines: 100, - scrollback: 10_000, - }; - let ris_work = - 2 * (size.columns * size.screen_lines) as u64 + (size.scrollback * size.columns) as u64; - - // Deliver the escape one byte at a time, so the parser is left mid- - // sequence with a tail that begins on the continuation byte. - let (mut term, _a) = Terminal::new(size, Fences::ALL); - term.feed(b"\x1b"); - assert_eq!( - term.stats().completed_units, - 0, - "ESC alone dispatches nothing" - ); - - let more = term.feed(b"cXmore"); - - assert!(more, "the completed RIS must still yield with a tail"); - assert_eq!( - term.stats().completed_units, - 1, - "the continuation byte completed a grid reset; nothing may run after it", - ); - assert_eq!(term.stats().completed_work, ris_work); - - while term.drain() {} - assert_eq!(term.stats().completed_units, 1 + 5); - assert_eq!(term.pending_bytes(), 0); -} diff --git a/desktop/src-tauri/crates/buzz-terminal/tests/slicing_adversarial.rs b/desktop/src-tauri/crates/buzz-terminal/tests/slicing_adversarial.rs new file mode 100644 index 000000000..6bf7c1d96 --- /dev/null +++ b/desktop/src-tauri/crates/buzz-terminal/tests/slicing_adversarial.rs @@ -0,0 +1,390 @@ +//! Adversarial slicing cases for resize debt, oversized atoms, and arithmetic extremes. + +use buzz_terminal::fences::{ + max_atom_work, max_drain_work, slice_bytes_remaining, Fences, WORK_BUDGET, +}; +use buzz_terminal::{Size, Terminal}; + +/// A scrollback change reprices RIS *and* the slicing derived from it. +/// +/// Kills: updating the feeder's columns and lines on resize but not its +/// scrollback -- and, separately, a repair that reprices the charge while +/// leaving slice width stale. Those are different failures and neither +/// observable sees the other: fix only the charge and the drain count stays +/// wrong; fix only the derivation and the charge stays wrong. +/// +/// Two properties, because one is not enough: +/// +/// * The exact RIS charge at the new depth. Direct, and it is what a +/// pricing-only repair passes. +/// * Equality with a terminal *constructed* at the new depth, across work +/// and drain count. A resized feeder that is genuinely repaired is +/// indistinguishable from one that was born there. This is stronger than a +/// hand-picked threshold and immune to `WORK_BUDGET`/`MIN_SLICE` moving, +/// since both arms move together -- and the sanity arm proves the +/// comparison is deterministic before it is used to judge anything. +/// +/// `completed_units` is deliberately *not* the discriminator here: it reads +/// 200 in both arms, because the same callbacks run either way and only their +/// cost and slicing differ. It is asserted anyway as the invariant that must +/// hold -- no unit lost or duplicated across a resize -- while carrying none +/// of the discrimination. +#[test] +fn a_scrollback_change_reprices_the_densest_atom_and_the_slicing() { + let shallow = Size { + columns: 200, + screen_lines: 50, + scrollback: 100, + }; + let deep = Size { + scrollback: 10_000, + ..shallow + }; + let cells = (deep.columns * deep.screen_lines) as u64; + + // Preconditions, asserted rather than assumed, because both are easy to + // break by "generalising" this fixture later: + // + // * The geometry must let the *scheduling* fields separate. They only do + // when the two depths land on different slice widths, and the deep side + // is always floored -- so the shallow side must not be. At 1600x50 the + // visible grid alone floors every depth from 0 upward, and three of the + // four observables below go silently inert. + // * The payload must be RIS. It is the only escape reaching the only + // weight carrying a scrollback term (`units::reset_state`); DECALN and + // every other atom are priced on cells or columns and are blind to + // depth, so a conforming repair would show work identical to the + // control and the assertions here would invert into false failures. + assert!( + slice_bytes_remaining( + shallow.columns, + shallow.screen_lines, + shallow.scrollback, + 0, + 0 + ) > 1, + "geometry cannot discriminate: the shallow arm is already floored", + ); + assert_eq!( + slice_bytes_remaining(deep.columns, deep.screen_lines, deep.scrollback, 0, 0), + 1, + ); + + // How a terminal at `size` retires 200 RIS: work, and how many + // acquisitions it took. Both are feeder behaviour, not helper output. + let run = |size: Size, resize_from: Option| { + let (mut term, _a) = Terminal::new(resize_from.unwrap_or(size), Fences::ALL); + if resize_from.is_some() { + term.resize(size); + } + term.reset_stats(); + let mut drains = 1; + let mut more = term.feed(&b"c".repeat(200)); + while more { + more = term.drain(); + drains += 1; + } + ( + term.stats().completed_units, + term.stats().completed_work, + drains, + ) + }; + + let control = run(deep, None); + let sanity = run(deep, None); + assert_eq!( + control, sanity, + "two terminals built the same way must agree before this comparison can judge anything", + ); + + let resized = run(deep, Some(shallow)); + assert_eq!(resized.0, 200, "no unit may be lost or duplicated"); + assert_eq!( + resized, control, + "a feeder resized to a depth must be indistinguishable from one constructed at it -- in charge and in how many acquisitions it took", + ); + + // The exact charge, stated rather than inferred from the equality: a + // repair that made both arms equally *wrong* would pass the comparison. + let (mut term, _a) = Terminal::new(shallow, Fences::ALL); + term.resize(deep); + term.reset_stats(); + term.feed_fully(b"c"); + assert_eq!( + term.stats().completed_work, + 2 * cells + (deep.scrollback * deep.columns) as u64, + ); + + // Shrinking retains the debt, and the fixture proves retention rather + // than merely permitting it. + // + // `>= fresh` alone is the predicate three of us proposed and all three + // withdrew: a feeder that dropped the debt reads *exactly* equal to a + // fresh shallow one, so `>=` passes on the unrepaired state. Strictness + // on the pricing field is what rejects it. The scheduling fields are + // asserted directionally with per-field signs -- `first_units` inverts, + // because a narrower slice retires fewer atoms per un-preemptable drain, + // which is the fence working -- but none of them is the discriminator: + // they separate only when the two depths straddle the slice floor, and + // `completed_work` separates at every positive depth gap. + // + // Every comparison is against the fresh control's own field, never a + // literal: a constant or geometry change must move both sides together, + // or the fixture starts asserting the arithmetic of the day it was + // written. + let measure = |term: &mut Terminal| { + term.reset_stats(); + let mut drains = 1; + let mut more = term.feed(&b"\x1bc".repeat(200)); + let first_units = term.stats().completed_units; + let first_pending = term.pending_bytes(); + while more { + more = term.drain(); + drains += 1; + } + ( + first_units, + first_pending, + drains, + term.stats().completed_units, + term.stats().completed_work, + ) + }; + + // The terminal under test stays alive past its measurement, so the + // geometry arm below runs on the feeder that actually shrank rather than + // on a lookalike that only ever grew. + let (mut shrunk_term, _a) = Terminal::new(shallow, Fences::ALL); + shrunk_term.resize(deep); + shrunk_term.resize(shallow); + let shrunk = measure(&mut shrunk_term); + + let (mut fresh_term, _a) = Terminal::new(shallow, Fences::ALL); + let fresh = measure(&mut fresh_term); + + assert_eq!( + shrunk.3, fresh.3, + "no unit may be lost on the way down either" + ); + assert!( + shrunk.4 > fresh.4, + "a feeder that has been deep must still price deep after shrinking: \ + {} against a fresh shallow {}. Equality here is the signature of a \ + feeder that dropped the debt, which is indistinguishable from one \ + that never had it", + shrunk.4, + fresh.4, + ); + assert!( + shrunk.0 <= fresh.0, + "narrower slices retire fewer atoms per drain: {} against {}", + shrunk.0, + fresh.0, + ); + assert!( + shrunk.1 >= fresh.1, + "and leave more pending after the first call: {} against {}", + shrunk.1, + fresh.1, + ); + assert!( + shrunk.2 >= fresh.2, + "and take more drains to finish: {} against {}", + shrunk.2, + fresh.2, + ); + + // The debt survives a later resize on a different axis. Two things make + // this arm bite, and it was inert without either: + // + // * It runs on the terminal that actually went shallow -> deep -> + // shallow. A lookalike that only ever grew passes it while an + // implementation that retains on shrink and drops on the next geometry + // change fails. + // * The resize carries the *shallow* depth. Passing the debt's own value + // back in means `max(debt, new)` and a plain assignment agree, so the + // arm cannot tell them apart -- which is how it survived a mutant that + // retained only when columns and lines were unchanged. + shrunk_term.resize(Size { + columns: shallow.columns * 2, + screen_lines: shallow.screen_lines, + scrollback: shallow.scrollback, + }); + shrunk_term.reset_stats(); + shrunk_term.feed_fully(b"\x1bc"); + assert_eq!( + shrunk_term.stats().completed_work, + 2 * (shallow.columns * 2 * shallow.screen_lines) as u64 + + (deep.scrollback * shallow.columns * 2) as u64, + "a columns resize must keep the deep scrollback debt, not fall back \ + to the current shallow depth", + ); +} + +/// One oversized atom per drain -- no callback runs after the one that +/// crosses the budget. +/// +/// Kills: sizing slices from the *whole* budget rather than what remains of +/// it. RIS at any real scrollback depth is worth more than an entire budget, +/// so a slice wide enough for several callbacks runs several: measured +/// `completed_units == 3` for `ESC c` followed by `Xmore`, where the law +/// permits exactly one. The fix makes slice width a function of `remaining`, +/// which is a single byte once an atom this size is in play. +/// +/// Also asserts the tail survives it: yielding after the crossing atom is +/// only correct if what follows is still parsed, exactly once. +#[test] +fn an_oversized_atom_yields_before_the_next_callback() { + let size = Size { + columns: 400, + screen_lines: 100, + scrollback: 10_000, + }; + let (mut term, _a) = Terminal::new(size, Fences::ALL); + let ris_work = + 2 * (size.columns * size.screen_lines) as u64 + (size.scrollback * size.columns) as u64; + assert!( + ris_work > WORK_BUDGET, + "this arm needs an atom bigger than the whole budget", + ); + + let more = term.feed(b"\x1bcXmore"); + + assert!(more, "the drain must yield with a tail"); + assert_eq!( + term.stats().completed_units, + 1, + "exactly the crossing atom ran: a callback after it is post-atom \ + overrun, which is the thing the budget cannot preempt and therefore \ + must not start", + ); + assert_eq!(term.stats().completed_work, ris_work); + + while term.drain() {} + assert_eq!( + term.stats().completed_units, + 1 + 5, + "the five characters after it must still be parsed, exactly once", + ); + assert_eq!(term.pending_bytes(), 0); +} + +/// Extreme dimensions saturate rather than wrapping or panicking. +/// +/// Kills: `columns * lines` in `usize` before the cast. `Size` is unclamped +/// and reaches the weight path from a caller, so this product is a reachable +/// overflow -- a debug panic inside the accounting path, or a release wrap +/// that reports the most expensive callback in the emulator as one of the +/// cheapest. Saturating is the only one of the three that fails safe. +#[test] +fn extreme_dimensions_saturate_instead_of_wrapping() { + let huge = usize::MAX / 2; + assert_eq!(max_atom_work(huge, huge, huge), u64::MAX); + assert_eq!(max_drain_work(huge, huge, huge), u64::MAX); + + // The *direction* is the assertion, not merely the absence of a panic. + // A wrapping build does not produce a slightly-wrong bound, it produces a + // tiny one -- and `slice_bytes_remaining` divides the budget by it, so an + // undercharged atom yields an *oversized* slice exactly when the atom is + // most expensive. Wrapping inverts the fence. So: the widest possible + // atom must give the narrowest possible slice. + assert_eq!( + slice_bytes_remaining(huge, huge, huge, 0, 0), + 1, + "an overflowing grid must clamp to the smallest slice; a wrapped \ + `max_atom_work` would hand back a generous one", + ); + assert_eq!( + slice_bytes_remaining(huge, huge, huge, 0, 0), + 1, + "and the escape at the front of such a grid gets a single byte", + ); + + // The property behind those endpoints, and the stronger statement: a + // grid that costs more may never buy a wider slice. Endpoints pin the + // ends; only a sweep catches a non-monotone middle, and a wrap *is* a + // non-monotone middle -- it makes the worst grid look cheap and hands it + // the widest slice of all. + // Every axis independently: a wrap on any one of the three products is a + // non-monotone middle on that axis alone, and sweeping only scrollback + // would miss a truncating `columns * lines`. + for (axis, at) in [ + ( + "scrollback", + (|n| slice_bytes_remaining(200, 50, n, 0, 0)) as fn(usize) -> usize, + ), + ("columns", |n| slice_bytes_remaining(n.max(1), 50, 0, 0, 0)), + ("lines", |n| slice_bytes_remaining(200, n.max(1), 0, 0, 0)), + ] { + let mut previous = usize::MAX; + for exponent in 0..60 { + let width = at(1usize << exponent); + assert!( + width <= previous, + "slice widened from {previous} to {width} at {axis} \ + 2^{exponent}: more expensive grid, more generous slice", + ); + assert!(width >= 1); + previous = width; + } + } + + // Just past 32 bits on one axis: large enough that a narrowing cast + // shows (`1 << 32` truncates to 0 in `u32`, pricing an enormous grid at + // nothing), small enough that the honest answer is exact rather than + // saturated. Neither the extreme endpoints above nor the ordinary grids + // below can see this -- the endpoints saturate either way and the + // ordinary ones fit in 32 bits. + assert_eq!(max_atom_work(1 << 32, 1, 0), 2 * (1u64 << 32)); + assert_eq!(max_atom_work(1, 1 << 32, 0), 2 * (1u64 << 32)); + assert_eq!(max_atom_work(1, 1, 1 << 32), 2 + (1u64 << 32)); + + // Ordinary grids are untouched by the saturation: exact, not clamped. + assert_eq!(max_atom_work(80, 24, 0), 2 * 80 * 24); + assert_eq!(max_atom_work(80, 24, 100), 2 * 80 * 24 + 100 * 80); +} + +/// An escape split across slices keeps its escape metering. +/// +/// Kills: deciding "plain run or escape?" by looking only at the bytes ahead. +/// After a slice ending on a lone `ESC`, the next byte is `c` -- which looks +/// like ordinary text and is in fact a full grid reset. Meter it as text and +/// the oversized atom rides into a wide slice with whatever follows, which is +/// the post-atom overrun arriving through a different door. Found by the +/// oversized-atom fixture failing after I "optimised" the plain path, which +/// is the argument for keeping both. +#[test] +fn an_escape_split_across_slices_keeps_its_metering() { + let size = Size { + columns: 400, + screen_lines: 100, + scrollback: 10_000, + }; + let ris_work = + 2 * (size.columns * size.screen_lines) as u64 + (size.scrollback * size.columns) as u64; + + // Deliver the escape one byte at a time, so the parser is left mid- + // sequence with a tail that begins on the continuation byte. + let (mut term, _a) = Terminal::new(size, Fences::ALL); + term.feed(b"\x1b"); + assert_eq!( + term.stats().completed_units, + 0, + "ESC alone dispatches nothing" + ); + + let more = term.feed(b"cXmore"); + + assert!(more, "the completed RIS must still yield with a tail"); + assert_eq!( + term.stats().completed_units, + 1, + "the continuation byte completed a grid reset; nothing may run after it", + ); + assert_eq!(term.stats().completed_work, ris_work); + + while term.drain() {} + assert_eq!(term.stats().completed_units, 1 + 5); + assert_eq!(term.pending_bytes(), 0); +}