|
| 1 | +use yofi::config::Config; |
| 2 | +use yofi::mode::Mode; |
| 3 | +use yofi::state::State; |
| 4 | +use yofi::window::Params; |
| 5 | + |
| 6 | +pub enum Action { |
| 7 | + Type(&'static str), |
| 8 | + NextItem, |
| 9 | +} |
| 10 | + |
| 11 | +pub fn test_entries() -> Vec<String> { |
| 12 | + [ |
| 13 | + "Firefox", |
| 14 | + "Chromium", |
| 15 | + "Terminal", |
| 16 | + "Files", |
| 17 | + "Settings", |
| 18 | + "Calculator", |
| 19 | + "Text Editor", |
| 20 | + "Music Player", |
| 21 | + ] |
| 22 | + .iter() |
| 23 | + .map(|s| s.to_string()) |
| 24 | + .collect() |
| 25 | +} |
| 26 | + |
| 27 | +fn unpremultiply_to_rgba(buffer: &[u32]) -> Vec<u8> { |
| 28 | + let mut rgba = Vec::with_capacity(buffer.len() * 4); |
| 29 | + for &px in buffer { |
| 30 | + let a = (px >> 24) & 0xff; |
| 31 | + let r = (px >> 16) & 0xff; |
| 32 | + let g = (px >> 8) & 0xff; |
| 33 | + let b = px & 0xff; |
| 34 | + if a == 0 { |
| 35 | + rgba.extend_from_slice(&[0, 0, 0, 0]); |
| 36 | + } else { |
| 37 | + rgba.push((r * 255 / a) as u8); |
| 38 | + rgba.push((g * 255 / a) as u8); |
| 39 | + rgba.push((b * 255 / a) as u8); |
| 40 | + rgba.push(a as u8); |
| 41 | + } |
| 42 | + } |
| 43 | + rgba |
| 44 | +} |
| 45 | + |
| 46 | +fn save_png(path: &str, width: u32, height: u32, rgba: &[u8]) { |
| 47 | + let file = std::fs::File::create(path).expect("failed to create PNG file"); |
| 48 | + let mut encoder = png::Encoder::new(file, width, height); |
| 49 | + encoder.set_color(png::ColorType::Rgba); |
| 50 | + encoder.set_depth(png::BitDepth::Eight); |
| 51 | + let mut writer = encoder.write_header().expect("failed to write PNG header"); |
| 52 | + writer |
| 53 | + .write_image_data(rgba) |
| 54 | + .expect("failed to write PNG data"); |
| 55 | +} |
| 56 | + |
| 57 | +fn make_diff_image(expected: &[u8], actual: &[u8], width: u32, height: u32) -> Vec<u8> { |
| 58 | + let mut diff = Vec::with_capacity(expected.len()); |
| 59 | + for (exp, act) in expected.chunks_exact(4).zip(actual.chunks_exact(4)) { |
| 60 | + if exp == act { |
| 61 | + // Matching pixel: dim grayscale |
| 62 | + let gray = ((exp[0] as u16 + exp[1] as u16 + exp[2] as u16) / 3 / 3) as u8; |
| 63 | + diff.extend_from_slice(&[gray, gray, gray, 255]); |
| 64 | + } else { |
| 65 | + // Differing pixel: bright red |
| 66 | + diff.extend_from_slice(&[255, 0, 0, 255]); |
| 67 | + } |
| 68 | + } |
| 69 | + // Pad if sizes differ (dimension mismatch) |
| 70 | + let total = (width as usize) * (height as usize) * 4; |
| 71 | + diff.resize(total, 0); |
| 72 | + diff |
| 73 | +} |
| 74 | + |
| 75 | +fn load_png_rgba(path: &str) -> (u32, u32, Vec<u8>) { |
| 76 | + let file = std::fs::File::open(path).unwrap_or_else(|_| { |
| 77 | + panic!("fixture not found: {path}\nRun with YOFI_BLESS=1 to generate reference snapshots") |
| 78 | + }); |
| 79 | + let decoder = png::Decoder::new(file); |
| 80 | + let mut reader = decoder.read_info().unwrap(); |
| 81 | + let mut rgba = vec![0u8; reader.output_buffer_size()]; |
| 82 | + let info = reader.next_frame(&mut rgba).unwrap(); |
| 83 | + rgba.truncate(info.buffer_size()); |
| 84 | + (info.width, info.height, rgba) |
| 85 | +} |
| 86 | + |
| 87 | +pub fn run_regression(name: &str, entries: Vec<String>, actions: &[Action]) { |
| 88 | + let config = Config::default(); |
| 89 | + |
| 90 | + let mode = Mode::dialog_from_lines(entries); |
| 91 | + let mut state = State::new(mode); |
| 92 | + |
| 93 | + for action in actions { |
| 94 | + match action { |
| 95 | + Action::Type(s) => state.append_to_input(s), |
| 96 | + Action::NextItem => state.next_item(), |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + let params: Params = config.param(); |
| 101 | + let scale = params.scale.unwrap_or(1); |
| 102 | + let w = (params.width * u32::from(scale)) as i32; |
| 103 | + let h = (params.height * u32::from(scale)) as i32; |
| 104 | + let mut buffer = vec![0u32; (w * h) as usize]; |
| 105 | + yofi::render_to_buffer(&config, &mut state, scale, w, h, &mut buffer); |
| 106 | + |
| 107 | + let actual_rgba = unpremultiply_to_rgba(&buffer); |
| 108 | + let w = w as u32; |
| 109 | + let h = h as u32; |
| 110 | + |
| 111 | + let fixture = format!("tests/fixtures/{name}.png"); |
| 112 | + let new_file = format!("tests/fixtures/{name}.new.png"); |
| 113 | + let diff_file = format!("tests/fixtures/{name}.diff.png"); |
| 114 | + |
| 115 | + if std::env::var("YOFI_BLESS").is_ok() { |
| 116 | + save_png(&fixture, w, h, &actual_rgba); |
| 117 | + eprintln!("blessed: {fixture}"); |
| 118 | + } else { |
| 119 | + let (rw, rh, reference_rgba) = load_png_rgba(&fixture); |
| 120 | + assert!( |
| 121 | + rw == w && rh == h, |
| 122 | + "dimension mismatch for '{name}': expected {rw}x{rh}, got {w}x{h}" |
| 123 | + ); |
| 124 | + if actual_rgba != reference_rgba { |
| 125 | + save_png(&new_file, w, h, &actual_rgba); |
| 126 | + let diff = make_diff_image(&reference_rgba, &actual_rgba, w, h); |
| 127 | + save_png(&diff_file, w, h, &diff); |
| 128 | + panic!( |
| 129 | + "pixel mismatch for '{name}'\n \ |
| 130 | + new: {new_file}\n \ |
| 131 | + diff: {diff_file}\n \ |
| 132 | + Run with YOFI_BLESS=1 to update" |
| 133 | + ); |
| 134 | + } |
| 135 | + } |
| 136 | +} |
0 commit comments