fix-deviant(terminal): no panics on input-reachable terminal states
Intentional deviation from Ghostty 1.3.1 (DEVIANT_FIXES.md entry 10). Two terminal inputs reach states that Zig handles only by assertion (Debug/ReleaseSafe abort; ReleaseFast continues): - Toggling mode 2027 around a BOM before a wide character hits `assert(!b)` in print_grapheme. This repository's release profile keeps debug assertions, so terminal output could crash a release build. The assertion is removed and the path behaves like Zig ReleaseFast. - A grapheme (e.g. a Devanagari conjunct) at the right margin on the bottom row with scrolling margins returned InvalidCoordinate and left a grapheme flag without data, panicking later in the formatter. The grapheme now moves from the cell that actually holds it and no cell is flagged without data. tools/difftest gains a Rust-only panic hunt (`difftest_fuzz panics`, lenient driver): ~60k debug-build cases now run without any panic, and 3000 Zig differential cases show 0 divergences. Test Plan: - cargo test -p ghostty-terminal difftest_mode_2027_toggle_with_bom_does_not_panic difftest_grapheme_wrap_scrolling_margins_keeps_grapheme - cargo run -p difftest --bin difftest_fuzz -- panics
This commit is contained in:
1 parent
4fa55855e8
commit
11c8e057ff
5 files changed
+107
-10
No files matched your search
@@ -4,6 +4,7 @@
|
||||
//! difftest_fuzz run [--seed N] [--cases N] [--jobs N] [--tokens N]
|
||||
//! [--zig PATH] [--work DIR] [--size CxR] [--sb BYTES]
|
||||
//! difftest_fuzz minz|minr <cols> <rows> <sb> <input-file> # minimise an abort
|
||||
//! difftest_fuzz panics [--seed N] [--cases N] [--tokens N] # Rust only, no Zig
|
||||
//! difftest_fuzz min <cols> <rows> <sb> <input-file> # minimise a divergence
|
||||
//! difftest_fuzz check <cols> <rows> <sb> <input-file> # compare one input
|
||||
//! ```
|
||||
@@ -168,6 +169,10 @@ fn main() {
|
||||
let work = root.join("target/difftest-work");
|
||||
std::fs::create_dir_all(&work).expect("mkdir");
|
||||
let ctx = Arc::new(Ctx { zig, work });
|
||||
if a.first().map(String::as_str) == Some("panics") {
|
||||
panics(&a, &ctx);
|
||||
return;
|
||||
}
|
||||
std::panic::set_hook(Box::new(|_| {}));
|
||||
match a.first().map(String::as_str) {
|
||||
Some("check" | "min" | "minz" | "minr") => {
|
||||
@@ -305,3 +310,43 @@ fn run(ctx: &Arc<Ctx>, a: &[String]) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn lenient_panics(input: &[u8], c: u16, r: u16, sb: usize) -> bool {
|
||||
let input = input.to_vec();
|
||||
std::panic::catch_unwind(move || ghostty_difftest::drive_lenient(&input, c, r, sb)).is_err()
|
||||
}
|
||||
|
||||
/// Rust-only panic hunt (run it with a debug build so assertions are on):
|
||||
/// untrusted terminal input must never panic, even where Zig aborts.
|
||||
fn panics(a: &[String], ctx: &Ctx) {
|
||||
let seed: u64 = arg(a, "--seed", 1);
|
||||
let cases: usize = arg(a, "--cases", 1000);
|
||||
let tokens: usize = arg(a, "--tokens", 60);
|
||||
let mut found = 0;
|
||||
for i in 0..cases {
|
||||
let mut rng = Rng(seed.wrapping_mul(0x1_0000_0001).wrapping_add(i as u64));
|
||||
let (c, r, sb) = config(&mut rng);
|
||||
let n = 1 + rng.below(tokens);
|
||||
let input = generate(&mut rng, n);
|
||||
if lenient_panics(&input, c, r, sb) {
|
||||
found += 1;
|
||||
let mut min = input.clone();
|
||||
let mut i = 0;
|
||||
while i < min.len() {
|
||||
let mut cand = min.clone();
|
||||
cand.remove(i);
|
||||
if lenient_panics(&cand, c, r, sb) {
|
||||
min = cand;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
let _ = ctx;
|
||||
eprintln!("case {i}: {c}x{r} sb={sb} minimised={min:02x?}");
|
||||
}
|
||||
}
|
||||
println!("seed={seed} cases={cases} rust panics={found}");
|
||||
if found > 0 {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,16 @@ pub fn dump(input: &[u8], cols: u16, rows: u16, max_scrollback: usize) -> String
|
||||
dump_terminal(&t)
|
||||
}
|
||||
|
||||
/// Feed `input` and ignore stream errors (they are reported to the caller as
|
||||
/// `Err`, not a crash). Used by the Rust-only panic hunt.
|
||||
pub fn drive_lenient(input: &[u8], cols: u16, rows: u16, max_scrollback: usize) -> String {
|
||||
let mut opts = Options::new(cols, rows);
|
||||
opts.max_scrollback = max_scrollback;
|
||||
let mut t = Terminal::new(opts).expect("terminal");
|
||||
let _ = t.vt_stream().next_slice(input);
|
||||
dump_terminal(&t)
|
||||
}
|
||||
|
||||
/// Canonical dump of an already-driven terminal.
|
||||
pub fn dump_terminal(t: &Terminal) -> String {
|
||||
let mut w = String::new();
|
||||
|
||||
Reference in new issue
Block a user