test(core): add headless end-to-end tests and fix bugs they found
Adds crates/ghostty-core/tests/e2e_*.rs: 81 tests that drive the real core Surface, termio, pty and child processes through a fake runtime (recording apprt actions, in-memory clipboard) with a headless renderer (ghostty-renderer/src/headless.rs). They cover keys (legacy, DECCKM, kitty, modifyOtherKeys), keybinding actions and sequences, mouse selection/reporting/scrolling/links, clipboard incl. OSC 52 and paste safety, resize, focus, titles/pwd/bell/notifications/progress, child exit, and IME preedit. Fixes for issues found by the tests and by the Codex audit (docs/rewrite/notes/review-core-codex.md, all 9 items): - replacing a selection with copy-on-select panicked (stale tracked handles); global new_window no longer inherits a parent; the child environment uses the runtime's env wholesale so GTK's removals stick; Flatpak host resources path; locale recovery (ensureLocale) in apprt-gtk before GTK init and for CLI actions; failed screen-file opens propagate errors; OSC 52 base64 rules, CSI/ESC binding length limits and GHOSTTY_LOG parsing match Zig. - Also includes V2's termio cleanups (tmux viewer wiring in the stream handler, Pty slave semantics docs, shared cgroup wait loop). apprt-gtk gains a debug input replay hook (GHOSTTY_DEBUG_INPUT_SCRIPT) exercising the real GTK key path. With it, the real app was verified on Wayland: typing and output, new tab, splits, select-all/copy/paste and font size all work (docs/rewrite/notes/verification-V1.md). Not yet exercised at GTK level: mouse, real IM-context IME, clipboard dialogs. Test Plan: - cargo test --workspace (2983 passed) - cargo test -p ghostty-core --tests (81 e2e tests)
This commit is contained in:
1 parent
d650e5171b
commit
be6d706ba5
33 files changed
+3016
-222
No files matched your search
@@ -1334,7 +1334,14 @@ impl Surface {
|
||||
t: &mut ghostty_terminal::terminal::Terminal,
|
||||
sel: Option<Selection>,
|
||||
) -> Result<(), ghostty_terminal::page::Error> {
|
||||
let prev = t.screen().selection;
|
||||
// Compare against the previous selection BEFORE replacing it:
|
||||
// `select` releases the previous selection's tracked pins, after
|
||||
// which they can no longer be resolved (Zig compares through raw
|
||||
// pointers that happen to stay readable).
|
||||
let unchanged = match (sel, t.screen().selection) {
|
||||
(Some(sel), Some(prev)) => sel.eql(&t.screen().pages, &prev),
|
||||
_ => false,
|
||||
};
|
||||
t.screen_mut().select(sel)?;
|
||||
|
||||
let cfg = self.cfg();
|
||||
@@ -1352,9 +1359,7 @@ impl Surface {
|
||||
let Some(sel) = sel else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(prev) = prev
|
||||
&& sel.eql(&t.screen().pages, &prev)
|
||||
{
|
||||
if unchanged {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1708,28 +1713,40 @@ impl Surface {
|
||||
if self.debug_shot_done.get() || self.created.elapsed().as_millis() < u128::from(delay) {
|
||||
return;
|
||||
}
|
||||
let Some((w, h, mut px)) = self.renderer.last_frame_pixels() else {
|
||||
return;
|
||||
if self
|
||||
.write_screenshot_png(std::path::Path::new(&path))
|
||||
.is_ok()
|
||||
{
|
||||
self.debug_shot_done.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the last presented frame as a PNG. Requires the GL context to be
|
||||
/// current.
|
||||
pub fn write_screenshot_png(&self, path: &std::path::Path) -> Result<(), CoreError> {
|
||||
let Some((w, h, px)) = self.renderer.last_frame_pixels() else {
|
||||
return Err("no frame has been presented yet".into());
|
||||
};
|
||||
self.debug_shot_done.set(true);
|
||||
// Bottom-up RGBA -> top-down.
|
||||
let stride = w * 4;
|
||||
let mut flipped = Vec::with_capacity(px.len());
|
||||
for row in px.chunks_exact_mut(stride).rev() {
|
||||
for row in px.chunks_exact(stride).rev() {
|
||||
flipped.extend_from_slice(row);
|
||||
}
|
||||
let write = || -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file = std::io::BufWriter::new(std::fs::File::create(&path)?);
|
||||
let mut enc = png::Encoder::new(file, w as u32, h as u32);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
enc.write_header()?.write_image_data(&flipped)?;
|
||||
Ok(())
|
||||
};
|
||||
match write() {
|
||||
Ok(()) => log::info!("wrote debug screenshot to {}", path.to_string_lossy()),
|
||||
Err(err) => log::warn!("debug screenshot failed: {err}"),
|
||||
}
|
||||
let file = std::io::BufWriter::new(std::fs::File::create(path)?);
|
||||
let mut enc = png::Encoder::new(file, w as u32, h as u32);
|
||||
enc.set_color(png::ColorType::Rgba);
|
||||
enc.set_depth(png::BitDepth::Eight);
|
||||
enc.write_header()?.write_image_data(&flipped)?;
|
||||
log::info!("wrote screenshot to {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The pixels of the last presented frame (`width`, `height`, RGBA8,
|
||||
/// bottom-up). Test/debug hook for headless integration tests; requires
|
||||
/// the GL context to be current on the calling thread.
|
||||
pub fn frame_pixels(&self) -> Option<(usize, usize, Vec<u8>)> {
|
||||
self.renderer.last_frame_pixels()
|
||||
}
|
||||
|
||||
/// The GLArea was (re)realized; the GL context is current.
|
||||
@@ -1777,15 +1794,16 @@ impl Drop for Surface {
|
||||
}
|
||||
}
|
||||
|
||||
/// The environment for the child: whatever the runtime wants plus the
|
||||
/// process environment (Zig: `rt_surface.defaultTermioEnv()` falling back to
|
||||
/// `getEnvMap`).
|
||||
/// The environment for the child: exactly what the runtime provides
|
||||
/// (`rt_surface.defaultTermioEnv()`, which already removed the variables it
|
||||
/// does not want to leak); only if it provides nothing (the Zig error path)
|
||||
/// do we fall back to the process environment (`getEnvMap`).
|
||||
fn rt_surface_env(rt: &dyn RtSurface) -> std::collections::HashMap<String, String> {
|
||||
let mut env = ghostty_os::env::get_env_map();
|
||||
for (k, v) in rt.default_termio_env() {
|
||||
env.insert(k, v);
|
||||
let rt_env = rt.default_termio_env();
|
||||
if rt_env.is_empty() {
|
||||
return ghostty_os::env::get_env_map();
|
||||
}
|
||||
env
|
||||
rt_env.into_iter().collect()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
@@ -1917,6 +1935,10 @@ impl CoreSurface for Surface {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_screenshot(&self, path: &str) -> Result<(), CoreError> {
|
||||
self.write_screenshot_png(std::path::Path::new(path))
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<SurfaceId> {
|
||||
Some(self.id)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user