Files
ghostty-rs/crates/ghostty-config/tests/zig_parity.rs
T
ddidderr 79a7fd4f76 fix(config): match Zig's numeric grammar, NUL handling and diagnostics
Resolves the 6 differential gap groups in docs/rewrite/notes/review-
rv9-codex.md (details in final-Z11.md):
- ghostty_os::zig_fmt ports std.fmt.parseInt/parseUnsigned/parseFloat
  (underscores, 0x/0o/0b prefixes, hex floats, inf/nan) with the Zig std
  tests and replaces every int/float parse in config, keybind
  parameters, ssh-cache and desktop detection.
- Interior NUL no longer panics in font names and ends C strings like
  Zig's backing bytes (fontconfig, C config getters, title actions,
  clipboard callback, relative paths, config-file, shaders, background
  image, GTK CSS).
- GTK new-window arguments are trimmed of ASCII whitespace only.
- Command tokenization uses a port of std.process.ArgIteratorGeneral
  (incl. CR/LF separators) with its std tests.
- UTF-8 validation happens where Zig's field parsers do it, so error
  kinds match (metric modifiers, quick-terminal size, link, ...).

A scripted differential run of 202 fields x ~220 hostile/valid values
against /usr/bin/ghostty (+validate-config and +show-config: exit code,
stdout, stderr) now has zero mismatches besides build-specific defaults
(theme resources path, auto-update-channel, gtk-opengl-debug); it also
surfaced and fixed background-blur failure state, NaN formatting and
NaN/-0 finalize clamps. Regression tests: ghostty-config/tests/
zig_parity.rs.

Test Plan:
- cargo test --workspace --no-fail-fast (3456 passed, exit 0)
- just check-macos (both), just check-windows, just check-wasm
2026-10-12 12:35:37 +02:00

148 lines
5.9 KiB
Rust

//! Parity regressions found by differential runs against Ghostty 1.3.1:
//! numeric grammars, field-specific error kinds for non-UTF-8 input and
//! float formatting.
#![allow(clippy::unwrap_used)]
use ghostty_config::{cli_args::ArgError, config::Config};
fn parse(key: &str, value: &[u8]) -> Result<Config, ArgError> {
let mut config = Config::default();
config.parse_field_bytes(key, Some(value))?;
Ok(config)
}
fn kind(key: &str, value: &[u8]) -> String {
parse(key, value).map_or_else(|e| e.to_string(), |_| "ok".to_string())
}
#[test]
fn numeric_grammars_accept_zig_spellings() {
assert_eq!(parse("window-height", b"0X10").unwrap().window_height, 16);
assert_eq!(parse("window-height", b"1__0").unwrap().window_height, 10);
assert_eq!(parse("window-height", b"0b1_0").unwrap().window_height, 2);
assert_eq!(parse("font-size", b"1_4").unwrap().font_size, 14.0);
assert_eq!(parse("font-size", b"0x1.cp+3").unwrap().font_size, 14.0);
assert_eq!(
parse("window-padding-x", b"1_0")
.unwrap()
.window_padding_x
.top_left,
10
);
assert_eq!(kind("window-padding-x", b"0x10"), "InvalidValue");
assert_eq!(kind("window-height", b"1_"), "InvalidValue");
assert_eq!(kind("font-size", b"1__0"), "InvalidValue");
// Decimal-only places keep rejecting prefixes.
assert_eq!(kind("adjust-cell-width", b"0x10"), "InvalidFormat");
assert_eq!(kind("adjust-cell-width", b"1_0"), "ok");
assert_eq!(kind("adjust-cell-width", b"0x1p3%"), "ok");
assert_eq!(kind("quick-terminal-size", b"1_0px"), "ok");
assert_eq!(kind("quick-terminal-size", b"0x1p3%,1_0px"), "ok");
assert_eq!(kind("quick-terminal-size", b"0x10px"), "InvalidValue");
assert_eq!(kind("font-variation", b"wght=1_0.5"), "ok");
assert_eq!(kind("font-variation", b"wght=0x1p4"), "ok");
assert_eq!(kind("mouse-scroll-multiplier", b"1_0"), "ok");
assert_eq!(kind("palette", b"0x10=#fff"), "ok");
assert_eq!(kind("palette", b"256=#fff"), "Overflow");
assert_eq!(kind("palette", b"-1=#fff"), "Overflow");
assert_eq!(kind("palette", b"-0=#fff"), "ok");
assert_eq!(kind("palette", b"1x=#fff"), "InvalidCharacter");
assert_eq!(kind("background-blur", b"0b11"), "ok");
assert_eq!(kind("keybind", b"a=scroll_page_lines:1_0"), "ok");
assert_eq!(
kind("keybind", b"a=scroll_page_lines:0x10"),
"InvalidFormat"
);
assert_eq!(kind("keybind", b"a=set_font_size:1_4"), "ok");
assert_eq!(kind("keybind", b"a=set_font_size:0x1.cp+3"), "ok");
assert_eq!(kind("keybind", b"a=resize_split:up,1_0"), "ok");
assert_eq!(kind("keybind", b"a=goto_tab:1__0"), "ok");
assert_eq!(kind("keybind", b"a=goto_tab:_1"), "InvalidFormat");
}
#[test]
fn invalid_bytes_keep_field_specific_error_kinds() {
for key in [
"adjust-cell-width",
"adjust-cursor-height",
"adjust-icon-height",
] {
assert_eq!(kind(key, b"\xff"), "InvalidFormat", "{key}");
assert_eq!(kind(key, b"10px\xff"), "InvalidFormat", "{key}");
assert_eq!(kind(key, b"\xff%"), "InvalidFormat", "{key}");
}
assert_eq!(kind("quick-terminal-size", b"\xff"), "MissingUnit");
assert_eq!(kind("quick-terminal-size", b"a\xffb"), "MissingUnit");
assert_eq!(kind("quick-terminal-size", b"\xff%"), "InvalidValue");
assert_eq!(kind("quick-terminal-size", b"1px,\xff"), "MissingUnit");
assert_eq!(kind("link", b"\xff"), "NotImplemented");
assert_eq!(kind("link", b"x"), "NotImplemented");
for key in [
"background",
"foreground",
"cursor-color",
"bold-color",
"split-divider-color",
] {
assert_eq!(kind(key, b"a\xffb"), "InvalidCharacter", "{key}");
assert_eq!(kind(key, b"\xff"), "InvalidValue", "{key}");
}
assert_eq!(kind("palette", b"1=a\xffb"), "InvalidCharacter");
assert_eq!(kind("palette", b"1=\xff"), "InvalidValue");
assert_eq!(kind("keybind", b"\xff=a"), "InvalidAction");
assert_eq!(kind("keybind", b"\xff=ignore"), "InvalidFormat");
assert_eq!(kind("keybind", b"ctrl+a=goto_tab:\xff"), "InvalidFormat");
assert_eq!(kind("keybind", b"ctrl+a=nope:\xff"), "InvalidAction");
assert_eq!(kind("keybind", b"ctrl+a=text:\xff"), "ok");
}
#[test]
fn failed_background_blur_parse_leaves_a_zero_radius_like_zig() {
use ghostty_config::types_b::BackgroundBlur;
let mut config = Config::default();
assert!(
config
.parse_field_bytes("background-blur", Some(b"bad"))
.is_err()
);
assert_eq!(config.background_blur, BackgroundBlur::Radius(0));
config
.parse_field_bytes("background-blur", Some(b"7"))
.unwrap();
assert!(
config
.parse_field_bytes("background-blur", Some(b"\xff"))
.is_err()
);
assert_eq!(config.background_blur, BackgroundBlur::Radius(7));
}
#[test]
fn float_nan_formats_like_zig_and_finalize_clamps_like_zig() {
let mut config = parse("font-size", b"nan").unwrap();
let mut out = String::new();
ghostty_config::formatter::format_entry("font-size", &config.font_size, &mut out).unwrap();
assert_eq!(out, "font-size = nan\n");
config
.parse_field_bytes("faint-opacity", Some(b"nan"))
.unwrap();
config
.parse_field_bytes("unfocused-split-opacity", Some(b"nan"))
.unwrap();
config
.parse_field_bytes("minimum-contrast", Some(b"nan"))
.unwrap();
config
.parse_field_bytes("mouse-scroll-multiplier", Some(b"nan"))
.unwrap();
config.finalize().unwrap();
assert_eq!(config.faint_opacity, 1.0);
assert_eq!(config.unfocused_split_opacity, 0.15);
assert_eq!(config.minimum_contrast, 1.0);
assert_eq!(config.mouse_scroll_multiplier.precision, 0.01);
let mut config = parse("faint-opacity", b"-0").unwrap();
config.finalize().unwrap();
assert!(config.faint_opacity.is_sign_positive());
}