Files
buzz/desktop/src-tauri/crates/buzz-terminal/tests/slicing_adversarial.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

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);
}