test: differential fuzzing of the terminal core against Ghostty's Zig code

Adds tools/difftest: a Zig driver built against a copy of Ghostty
1.3.1's own src/terminal (Zig 0.15.2, set up under target/ by
tools/difftest/setup.sh) and a Rust twin on ghostty-terminal, both
printing a canonical dump of every scrollback/active row (cells,
graphemes, styles, hyperlinks, row flags), cursor, modes, regions,
tabstops and colors. A seeded generator produces escape-heavy streams
(UTF-8/wide/combining/emoji, C0, CSI with edge params, SGR, modes, ESC,
OSC, DCS, margins, insert/delete); `just difftest` runs it with Zig
assertions on (ReleaseSafe vs debug Rust) and off (ReleaseFast vs
production Rust), with minimizers and a triage script.

Divergences found and fixed in Rust (each with a regression test):
- ill-formed UTF-8 decoding follows the SIMD path a default Ghostty
  build uses (not Zig's scalar fallback)
- PageList::erase_rows keeps Zig's row-flag behavior on regrow
- grapheme move on wrap with left/right margins uses the original cell

Upstream bugs found and deliberately NOT replicated (per the project
rule): a stale cell pointer after page growth in Terminal.zig:482/539
that drops a grapheme, plus several Zig assertion failures/segfaults;
documented with minimized repros in docs/rewrite/notes/difftest-DF.md.

Final run: 390k cases across many geometries, 0 divergences (Zig-abort
cases, ~0.1-0.5%, are not compared).

Test Plan:
- just difftest 20000 "1 2 3"
- cargo test -p ghostty-terminal (regression tests)
This commit is contained in:
ddidderr committed 2026-10-12 13:24:12 +02:00
1 parent 6def1f3b5d
commit 4eb03f637d
19 files changed
+2033 -165

No files matched your search

+20
View File
@@ -47,6 +47,26 @@ test:
clean:
cargo clean
# Differential fuzzing of the terminal core, Zig (reference) vs Rust. Sets up
# Zig 0.15.2 + a copy of the Zig source + the Zig driver under target/ on the
# first run (network needed), then fuzzes SEEDS x CASES random escape-heavy
# streams twice: assertions on (debug Rust vs ReleaseSafe Zig) and assertions
# off (production Rust vs ReleaseFast Zig). Divergences are saved under
# target/difftest-work/fails-<seed>/ (minimise with `difftest_fuzz min`). See
# docs/rewrite/notes/difftest-DF.md.
difftest CASES="20000" SEEDS="1 2 3":
#!/usr/bin/env bash
set -euo pipefail
tools/difftest/setup.sh
cargo build -p ghostty-difftest
cargo build --profile production -p ghostty-difftest
jobs="$(nproc)"
for s in {{ SEEDS }}; do
timeout 3000 target/debug/difftest_fuzz run --seed "$s" --cases {{ CASES }} --jobs "$jobs" --tokens 120
timeout 3000 target/production/difftest_fuzz run --seed "$s" --cases {{ CASES }} --jobs "$jobs" --tokens 120 \
--zig target/difftest-zig-fast/bin/difftest_zig
done
# Build the build-data generator (Zig: `ghostty-build-data`,
# src/main_build_data.zig) in release mode.
build-data: