diff options
Diffstat (limited to 'src/ghostty/png_forward_tests.rs')
| -rw-r--r-- | src/ghostty/png_forward_tests.rs | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/src/ghostty/png_forward_tests.rs b/src/ghostty/png_forward_tests.rs new file mode 100644 index 00000000..6d3904b2 --- /dev/null +++ b/src/ghostty/png_forward_tests.rs @@ -0,0 +1,232 @@ +use super::*; +use base64::Engine as _; + +fn fixture() -> Vec<u8> { + fixture_size(32, 16) +} + +fn fixture_size(width: u32, height: u32) -> Vec<u8> { + let mut bytes = Vec::new(); + { + let mut encoder = png::Encoder::new(&mut bytes, width, height); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + encoder + .write_header() + .unwrap() + .write_image_data(&vec![42; (width * height * 4) as usize]) + .unwrap(); + } + bytes +} + +fn terminal() -> Terminal { + let mut terminal = Terminal::new(20, 10, 0).unwrap(); + terminal.enable_kitty_graphics().unwrap(); + terminal.set_kitty_png_forwarding(true).unwrap(); + terminal.resize(20, 10, 8, 16).unwrap(); + terminal +} + +#[test] +#[ignore = "manual fixed-geometry native PNG ingestion/extraction scaling profile"] +fn native_png_render_scale_profile() { + let png = fixture_size(800, 480); + let upload = format!( + "\x1b[H\x1b_Ga=T,f=100,i=7,c=60,r=20,C=1,q=2;{}\x1b\\", + base64::engine::general_purpose::STANDARD.encode(&png) + ); + for count in [1, 15] { + for forwarding in [false, true] { + let mut panes = (0..count) + .map(|_| { + let mut pane = Terminal::new(120, 40, 0).unwrap(); + pane.enable_kitty_graphics().unwrap(); + pane.set_kitty_png_forwarding(forwarding).unwrap(); + pane.resize(120, 40, 8, 16).unwrap(); + pane + }) + .collect::<Vec<_>>(); + let mut samples = Vec::new(); + for iteration in 0..65 { + let begin = std::time::Instant::now(); + for pane in &mut panes { + pane.write(upload.as_bytes()); + let placements = pane.kitty_image_placements().unwrap(); + assert_eq!(placements.len(), 1); + assert_eq!( + placements[0].data.len(), + if forwarding { png.len() } else { 800 * 480 * 4 } + ); + std::hint::black_box(placements); + } + if iteration >= 5 { + samples.push(begin.elapsed().as_micros()); + } + } + samples.sort_unstable(); + eprintln!("native PNG panes={count} forwarding={forwarding} median_us={} p95_us={} png_bytes={}", samples[30], samples[57], png.len()); + } + } +} + +#[test] +fn normal_kitty_graphics_fully_decodes_quiet_png_uploads() { + let mut terminal = Terminal::new(20, 10, 0).unwrap(); + terminal.enable_kitty_graphics().unwrap(); + terminal.resize(20, 10, 8, 16).unwrap(); + let before = PNG_DECODE_CALLS.get(); + terminal.write( + format!( + "\x1b_Ga=T,f=100,i=7,c=4,r=1,q=2;{}\x1b\\", + base64::engine::general_purpose::STANDARD.encode(fixture()) + ) + .as_bytes(), + ); + let placements = terminal.kitty_image_placements().unwrap(); + assert_eq!(placements.len(), 1); + assert_eq!(placements[0].format, KittyImageFormat::Rgba); + assert_eq!(placements[0].data, vec![42; 32 * 16 * 4]); + assert_eq!(PNG_DECODE_CALLS.get(), before + 1); +} + +#[test] +fn quiet_png_forwarding_retains_exact_payload_without_pixel_decode() { + let bytes = fixture(); + let mut terminal = terminal(); + let before = PNG_DECODE_CALLS.get(); + terminal.write( + format!( + "\x1b_Ga=T,f=100,i=7,c=4,r=1,q=2;{}\x1b\\", + base64::engine::general_purpose::STANDARD.encode(&bytes) + ) + .as_bytes(), + ); + let placements = terminal.kitty_image_placements().unwrap(); + assert_eq!(placements.len(), 1); + assert_eq!(placements[0].format, KittyImageFormat::Png); + assert_eq!(placements[0].data, bytes); + assert_eq!( + (placements[0].image_width, placements[0].image_height), + (32, 16) + ); + assert_eq!(PNG_DECODE_CALLS.get(), before); +} + +#[test] +fn placeholder_png_forwarding_and_lazy_animation_decode() { + let bytes = fixture(); + let mut terminal = terminal(); + let before = PNG_DECODE_CALLS.get(); + terminal.write( + format!( + "\x1b_Ga=T,f=100,U=1,i=7,c=4,r=1,q=2;{}\x1b\\", + base64::engine::general_purpose::STANDARD.encode(&bytes) + ) + .as_bytes(), + ); + terminal.write("\x1b[H\x1b[38;2;0;0;7m\u{10eeee}\u{0305}\u{0305}\x1b[0m".as_bytes()); + let placements = terminal.kitty_image_placements().unwrap(); + assert_eq!(placements.len(), 1); + assert_eq!(placements[0].format, KittyImageFormat::Png); + assert_eq!(placements[0].data, bytes); + assert_eq!(PNG_DECODE_CALLS.get(), before); + let fingerprint = placements[0].data_fingerprint; + terminal.write(b"\x1b_Ga=a,i=7,q=2;\x1b\\"); + let materialized = terminal.kitty_image_placements().unwrap(); + assert_eq!(PNG_DECODE_CALLS.get(), before + 1); + assert_eq!(materialized[0].format, KittyImageFormat::Rgba); + assert_eq!(materialized[0].data, [42; 32 * 16 * 4]); + assert_ne!(materialized[0].data_fingerprint, fingerprint); +} + +#[test] +fn experimental_quiet_png_defers_crc_valid_compressed_data_errors() { + let mut bytes = fixture(); + let mut offset = 8; + loop { + let len = u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize; + if &bytes[offset + 4..offset + 8] == b"IDAT" { + bytes[offset + 8] = 0; // Invalid zlib header, but structurally valid PNG chunks. + let mut crc = !0u32; + for &byte in &bytes[offset + 4..offset + 8 + len] { + crc ^= u32::from(byte); + for _ in 0..8 { + crc = (crc >> 1) ^ (0xedb88320u32 & 0u32.wrapping_sub(crc & 1)); + } + } + bytes[offset + 8 + len..offset + 12 + len].copy_from_slice(&(!crc).to_be_bytes()); + break; + } + offset += len + 12; + } + for quiet in [0, 2] { + let mut terminal = terminal(); + let before = PNG_DECODE_CALLS.get(); + terminal.write( + format!( + "\x1b_Ga=T,f=100,i=7,c=4,r=1,q={quiet};{}\x1b\\", + base64::engine::general_purpose::STANDARD.encode(&bytes) + ) + .as_bytes(), + ); + let placements = terminal.kitty_image_placements().unwrap(); + if quiet == 0 { + assert!(placements.is_empty()); + assert_eq!(PNG_DECODE_CALLS.get(), before + 1); + } else { + assert_eq!(placements[0].data, bytes); + assert_eq!(PNG_DECODE_CALLS.get(), before); + terminal.write(b"\x1b_Ga=a,i=7,q=2;\x1b\\"); + assert_eq!(PNG_DECODE_CALLS.get(), before + 1); + assert_eq!(terminal.kitty_image_placements().unwrap()[0].data, bytes); + } + } +} + +#[test] +fn response_bearing_png_and_queries_keep_full_validation() { + let bytes = fixture(); + for (action, quiet) in [('T', 0), ('T', 1), ('q', 2)] { + let mut terminal = terminal(); + let before = PNG_DECODE_CALLS.get(); + terminal.write( + format!( + "\x1b_Ga={action},f=100,i=7,c=4,r=1,q={quiet};{}\x1b\\", + base64::engine::general_purpose::STANDARD.encode(&bytes) + ) + .as_bytes(), + ); + assert_eq!( + PNG_DECODE_CALLS.get(), + before + 1, + "action {action}, quiet {quiet}" + ); + let placements = terminal.kitty_image_placements().unwrap(); + if action == 'q' { + assert!(placements.is_empty()); + } else { + assert_eq!(placements[0].format, KittyImageFormat::Rgba); + } + } +} + +#[test] +fn chunked_png_forwarding_inherits_quiet_mode() { + let bytes = fixture(); + let mut terminal = terminal(); + let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes); + let split = (encoded.len() / 8) * 4; + let before = PNG_DECODE_CALLS.get(); + terminal.write( + format!( + "\x1b_Ga=T,f=100,i=7,c=4,r=1,q=2,m=1;{}\x1b\\", + &encoded[..split] + ) + .as_bytes(), + ); + assert!(terminal.kitty_image_placements().unwrap().is_empty()); + terminal.write(format!("\x1b_Gm=0;{}\x1b\\", &encoded[split..]).as_bytes()); + assert_eq!(terminal.kitty_image_placements().unwrap()[0].data, bytes); + assert_eq!(PNG_DECODE_CALLS.get(), before); +} |
