use std::path::{Path, PathBuf};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GroundTruth {
pub level: String,
pub scenarios: Vec<ScenarioTruth>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ScenarioTruth {
pub id: String,
pub image: String,
pub expected: serde_json::Value,
}
pub fn create_solid_png(width: u32, height: u32, rgba: [u8; 4]) -> Vec<u8> {
use std::io::Write;
let mut buf = Vec::new();
buf.write_all(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
.unwrap();
let mut ihdr_data = Vec::new();
ihdr_data.extend_from_slice(&width.to_be_bytes());
ihdr_data.extend_from_slice(&height.to_be_bytes());
ihdr_data.push(8); ihdr_data.push(6); ihdr_data.push(0); ihdr_data.push(0); ihdr_data.push(0); write_png_chunk(&mut buf, b"IHDR", &ihdr_data);
let mut raw_data = Vec::new();
for _y in 0..height {
raw_data.push(0); for _x in 0..width {
raw_data.extend_from_slice(&rgba);
}
}
let compressed = deflate_compress(&raw_data);
write_png_chunk(&mut buf, b"IDAT", &compressed);
write_png_chunk(&mut buf, b"IEND", &[]);
buf
}
pub fn text_to_png(text: &str, char_width: u32, char_height: u32) -> Vec<u8> {
let lines: Vec<&str> = text.lines().collect();
let max_cols = lines.iter().map(|l| l.len()).max().unwrap_or(0) as u32;
let num_rows = lines.len() as u32;
let width = max_cols * char_width;
let height = num_rows * char_height;
if width == 0 || height == 0 {
return create_solid_png(1, 1, [0, 0, 0, 255]);
}
let mut pixels = vec![0u8; (width * height * 4) as usize];
for (row, line) in lines.iter().enumerate() {
for (col, ch) in line.chars().enumerate() {
if ch != ' ' {
let base_x = col as u32 * char_width;
let base_y = row as u32 * char_height;
for dy in 1..char_height.saturating_sub(1) {
for dx in 1..char_width.saturating_sub(1) {
let px = base_x + dx;
let py = base_y + dy;
if px < width && py < height {
let idx = ((py * width + px) * 4) as usize;
pixels[idx] = 255; pixels[idx + 1] = 255; pixels[idx + 2] = 255; pixels[idx + 3] = 255; }
}
}
}
}
}
encode_rgba_png(width, height, &pixels)
}
fn encode_rgba_png(width: u32, height: u32, pixels: &[u8]) -> Vec<u8> {
use std::io::Write;
let mut buf = Vec::new();
buf.write_all(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
.unwrap();
let mut ihdr_data = Vec::new();
ihdr_data.extend_from_slice(&width.to_be_bytes());
ihdr_data.extend_from_slice(&height.to_be_bytes());
ihdr_data.push(8);
ihdr_data.push(6);
ihdr_data.push(0);
ihdr_data.push(0);
ihdr_data.push(0);
write_png_chunk(&mut buf, b"IHDR", &ihdr_data);
let mut raw = Vec::new();
let row_bytes = (width * 4) as usize;
for y in 0..height as usize {
raw.push(0); let start = y * row_bytes;
let end = start + row_bytes;
if end <= pixels.len() {
raw.extend_from_slice(&pixels[start..end]);
} else {
raw.extend(std::iter::repeat_n(0, row_bytes));
}
}
let compressed = deflate_compress(&raw);
write_png_chunk(&mut buf, b"IDAT", &compressed);
write_png_chunk(&mut buf, b"IEND", &[]);
buf
}
fn write_png_chunk(buf: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
use std::io::Write;
buf.write_all(&(data.len() as u32).to_be_bytes()).unwrap();
buf.write_all(chunk_type).unwrap();
buf.write_all(data).unwrap();
let mut crc_data = Vec::with_capacity(4 + data.len());
crc_data.extend_from_slice(chunk_type);
crc_data.extend_from_slice(data);
let crc = crc32(&crc_data);
buf.write_all(&crc.to_be_bytes()).unwrap();
}
fn crc32(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in data {
crc ^= byte as u32;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xEDB8_8320;
} else {
crc >>= 1;
}
}
}
!crc
}
fn deflate_compress(data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.push(0x78);
out.push(0x01);
let chunks: Vec<&[u8]> = data.chunks(65535).collect();
for (i, chunk) in chunks.iter().enumerate() {
let is_last = i == chunks.len() - 1;
out.push(if is_last { 0x01 } else { 0x00 }); let len = chunk.len() as u16;
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(&(!len).to_le_bytes()); out.extend_from_slice(chunk);
}
let adler = adler32(data);
out.extend_from_slice(&adler.to_be_bytes());
out
}
fn adler32(data: &[u8]) -> u32 {
let mut a: u32 = 1;
let mut b: u32 = 0;
for &byte in data {
a = (a + byte as u32) % 65521;
b = (b + a) % 65521;
}
(b << 16) | a
}
pub fn ensure_fixture_dir(base: &Path, level: &str) -> std::io::Result<PathBuf> {
let dir = base.join(level);
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
pub fn load_ground_truth(path: &Path) -> anyhow::Result<GroundTruth> {
let content = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&content)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_solid_png_valid() {
let png = create_solid_png(4, 4, [255, 0, 0, 255]);
assert_eq!(
&png[0..8],
&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
);
assert!(png.len() > 50);
}
#[test]
fn test_create_solid_png_1x1() {
let png = create_solid_png(1, 1, [0, 0, 0, 255]);
assert_eq!(
&png[0..8],
&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
);
}
#[test]
fn test_text_to_png_nonempty() {
let png = text_to_png("Hello\nWorld", 6, 10);
assert_eq!(
&png[0..8],
&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
);
assert!(png.len() > 100);
}
#[test]
fn test_text_to_png_empty() {
let png = text_to_png("", 6, 10);
assert_eq!(
&png[0..8],
&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
);
}
#[test]
fn test_crc32_known() {
let crc = crc32(b"");
assert_eq!(crc, 0x0000_0000);
}
#[test]
fn test_adler32_known() {
let a = adler32(b"Wikipedia");
assert_eq!(a, 0x11E6_0398);
}
#[test]
fn test_ground_truth_serde() {
let gt = GroundTruth {
level: "l1_tui_state".into(),
scenarios: vec![ScenarioTruth {
id: "dashboard_normal".into(),
image: "dashboard_normal.png".into(),
expected: serde_json::json!({
"panel": "dashboard",
"status": "ok"
}),
}],
};
let json = serde_json::to_string(>).unwrap();
let parsed: GroundTruth = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.level, "l1_tui_state");
assert_eq!(parsed.scenarios.len(), 1);
}
#[test]
fn test_deflate_compress_decompresses_to_original() {
let data = b"Hello, World! This is test data for compression.";
let compressed = deflate_compress(data);
assert_eq!(compressed[0], 0x78); assert!(compressed.len() > data.len()); }
}