mirror of
https://github.com/block/buzz.git
synced 2026-11-01 23:11:58 +00:00
## Summary - ship **Buzz Term** end to end: the terminal engine/runtime, mounted desktop substrate, and user-visible naming - add Quinn's tape-deck-inspired banner: a beveled chassis filled by the `buzz term` wordmark, surrounded by a complete-hex field - derive the wordmark's three-stop sweep from each theme's terminal palette so primary, secondary, and accent roles remain visibly distinct across all 62 shipped themes, including light themes - paint the banner once on its own pointer-transparent canvas; PTY rendering beneath it remains unchanged ## Banner behavior - uses the renderer's shared `8.4 × 17` cell metrics and production aspect ratio `2.0238` - regenerates only for viewport/theme changes; palette switches repaint correctly while the banner is visible - dismisses on non-empty output from the active terminal session; empty output and inactive sessions do not dismiss it - fails closed below **70 columns** rather than squeezing or clipping the wordmark - adds **8 lines** to `terminalRenderer.ts` for shared cell metrics and **zero lines inside `paint()`** ## Screenshots | Buzz (light) | Buzz Dark | |---|---| |  |  | | Kanagawa Lotus (light) | Red | |---|---| |  |  | Additional production-aspect finals: [Vesper](https://buzz.block.builderlab.xyz/media/9ca6514b63f8cfb2107a85ca46f16a940c0883848e6fbc718e411af94aa13100.png), [Min Dark](https://buzz.block.builderlab.xyz/media/f67bd2970e5d64ffb07b1ae78ab58c847e6ebc23e7e7a48e067eb024dba64ec8.png), and [Dark Plus](https://buzz.block.builderlab.xyz/media/290fee08924f37d064abc687ecf3e9526ab05b87e8e56d610f23048949793dbe.png). The screenshot harness was checked against the shipped painter at this exact head: all **2,541 draw calls** matched on color, glyph, x, and y; four deliberate divergence controls fired. ## Verification at `98ebc8f9048bd5f0ceb7e843b67874d642f0b7fd` - desktop tests: **3,946 / 3,946** - TypeScript: clean - checks: pass (two pre-existing informational `useTemplate` notices only) - integration/e2e: PASS (independent exact-SHA lane; artifacts recorded in the originating Buzz thread) - artifact/dead-path sweep: clean - redteam G1–G7: PASS - all six named banner emitter-deletion mutants die - independent handwritten five-row full-wordmark fixture kills Quinn's seven-mutant battery, including a one-pixel glyph change - real `112 × 46` canvas-rect dismissal tests separately cover active non-empty, active empty, and inactive non-empty output - layer-drop and zero-draw painter mutants die; z-order and pointer-events verified - CI's `tsc && vite build` includes all three banner modules - performance at DPR 2 (worst-case measured envelope): - one-time content paint: **~0.7–0.8 ms**, paid only when the banner is built or its palette changes - busy compositor, CSS `1277 × 697`, backing `2554 × 1394`: **470–497 µs/frame** for the full banner (**2.82–2.98%** of a 60 Hz frame) - busy compositor, CSS `1920 × 1080`, backing `3840 × 2160`: **1,139–1,212 µs/frame** (**6.83–7.27%**) - empty, one-glyph, and full-banner controls converge: compositor cost follows backing-layer area and DPR rather than painted-cell count - in the actual idle welcome state, cost is below both vsync-clamped rigs' resolution; it is not claimed as zero - **Pane cross-rig spread: resolved at matched loop rate.** Two independent rigs initially differed 2.3× (58–68 vs 136 µs/Mpx of backing store; pane, CSS 1277×697 / backing 2554×1394, DPR 2). The cause of *that* spread is rAF loop rate: the higher figure came from a free-running loop at ~1600fps. Throttled to ~200–236fps, both rigs read 58–68 µs/Mpx (1.25–1.44% of a 60Hz frame). The busy-composite figures quoted above remain the **unthrottled worst case** and are conservative by ~2.3× at the pane. Not established: the mechanism and sign of free-running distortion (one rig under-charges ~15%, the other over-charges 2.3×), and the 1080p figure has not been re-measured throttled. - the layer paints only on generation/theme/resize and dismisses on first non-empty active-session output, so the measurable busy cost is a short-lived worst case rather than a persistent PTY paint-path tax ## Follow-ups in this PR These are intentionally subsequent commits after the certified static-banner head, not claims about `98ebc8f90`: 1. close the compositor metrology: remeasure the 1080p point throttled and characterize the opposite-sign free-running rAF distortion, with each measurement regime stated 2. add Tyler's animated honeycomb color waves, gated by `prefers-reduced-motion`, a full 62-theme phase-sweep contrast check, and DPR-2 per-tick performance certification 3. land the already-proven mounted theme-switch regression probe from `RESEARCH/BUZZ_TERM_G3A_PROBE/` 4. bound the slow/hang-shaped G1-c mutant `waitFor` 5. optionally trim the generator to its ink bounding box, reducing the minimum viewport from 70 to 62 columns --------- Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
400 lines
16 KiB
Rust
400 lines
16 KiB
Rust
//! Adversarial slicing cases for resize debt, oversized atoms, and arithmetic extremes.
|
||
//!
|
||
//! **This is half a suite.** The remaining work-bound cases live in
|
||
//! `slicing.rs`; the two files are one set of contracts split only for the
|
||
//! file-size ratchet. A mutation check scoped with `--test slicing_adversarial`
|
||
//! covers 4 of 76 package tests and can report a confident pass while the
|
||
//! killing fixture sits in the sibling file. Sizing from the whole budget does
|
||
//! exactly that, then dies under the package.
|
||
//!
|
||
//! Mutation checks run the package, never a file: `cargo test -p buzz-terminal`.
|
||
|
||
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<Size>| {
|
||
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);
|
||
}
|