#![allow(dead_code)]
use std::fs;
use std::path::PathBuf;
use super::renderer::TestRenderer;
use crate::core::Element;
const GOLDEN_DIR: &str = "tests/golden";
#[derive(Debug)]
pub enum GoldenResult {
Match,
Created,
Missing { path: PathBuf, actual: String },
Mismatch {
expected: String,
actual: String,
diff: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GoldenFormat {
#[default]
Plain,
Ansi,
}
pub struct GoldenTest {
name: String,
width: u16,
height: u16,
format: GoldenFormat,
}
impl GoldenTest {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
width: 80,
height: 24,
format: GoldenFormat::Plain,
}
}
pub fn with_size(mut self, width: u16, height: u16) -> Self {
self.width = width;
self.height = height;
self
}
pub fn with_format(mut self, format: GoldenFormat) -> Self {
self.format = format;
self
}
pub fn plain(self) -> Self {
self.with_format(GoldenFormat::Plain)
}
pub fn ansi(self) -> Self {
self.with_format(GoldenFormat::Ansi)
}
fn golden_path(&self) -> PathBuf {
let suffix = match self.format {
GoldenFormat::Plain => "txt",
GoldenFormat::Ansi => "ansi.txt",
};
PathBuf::from(GOLDEN_DIR).join(format!("{}.{}", self.name, suffix))
}
pub fn assert_match(&self, element: &Element) {
let result = self.compare(element);
match result {
GoldenResult::Match => {}
GoldenResult::Created => {
println!("Golden file created: {}", self.golden_path().display());
}
GoldenResult::Missing { path, actual } => {
panic!(
"\n\nGolden file missing for '{}': {}\n\nActual:\n{}\n\nRun with UPDATE_GOLDEN=1 to create it.",
self.name,
path.display(),
actual
);
}
GoldenResult::Mismatch {
expected,
actual,
diff,
} => {
panic!(
"\n\nGolden file mismatch for '{}':\n\n{}\n\nExpected:\n{}\n\nActual:\n{}\n",
self.name, diff, expected, actual
);
}
}
}
pub fn compare(&self, element: &Element) -> GoldenResult {
let actual = self.render(element);
let golden_path = self.golden_path();
if !golden_path.exists() {
if should_update_golden() {
if let Some(parent) = golden_path.parent() {
if fs::create_dir_all(parent).is_err() {
return GoldenResult::Missing {
path: golden_path,
actual,
};
}
}
return if fs::write(&golden_path, &actual).is_ok() {
GoldenResult::Created
} else {
GoldenResult::Missing {
path: golden_path,
actual,
}
};
}
return GoldenResult::Missing {
path: golden_path,
actual,
};
}
let expected = normalize_golden_output(
&fs::read_to_string(&golden_path).unwrap_or_else(|_| String::new()),
);
if actual == expected {
GoldenResult::Match
} else {
GoldenResult::Mismatch {
expected: expected.clone(),
actual: actual.clone(),
diff: simple_diff(&expected, &actual),
}
}
}
pub fn update(&self, element: &Element) -> std::io::Result<()> {
let output = self.render(element);
let golden_path = self.golden_path();
if let Some(parent) = golden_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(golden_path, output)
}
fn render(&self, element: &Element) -> String {
let renderer = TestRenderer::new(self.width, self.height);
let output = match self.format {
GoldenFormat::Plain => renderer.render_to_plain(element),
GoldenFormat::Ansi => renderer.render_to_ansi(element),
};
normalize_golden_output(&output)
}
}
fn should_update_golden() -> bool {
std::env::var("UPDATE_GOLDEN").is_ok()
}
fn normalize_golden_output(output: &str) -> String {
let output = output.replace("\r\n", "\n").replace('\r', "\n");
output
.lines()
.map(str::trim_end)
.collect::<Vec<_>>()
.join("\n")
}
fn simple_diff(expected: &str, actual: &str) -> String {
let expected_lines: Vec<&str> = expected.lines().collect();
let actual_lines: Vec<&str> = actual.lines().collect();
let mut diff = String::new();
let max_lines = expected_lines.len().max(actual_lines.len());
for i in 0..max_lines {
let exp_line = expected_lines.get(i).copied().unwrap_or("");
let act_line = actual_lines.get(i).copied().unwrap_or("");
if exp_line != act_line {
diff.push_str(&format!("Line {}:\n", i + 1));
diff.push_str(&format!(" - {}\n", exp_line));
diff.push_str(&format!(" + {}\n", act_line));
}
}
if diff.is_empty() && expected_lines.len() != actual_lines.len() {
diff = format!(
"Line count differs: expected {}, got {}",
expected_lines.len(),
actual_lines.len()
);
}
diff
}
#[macro_export]
macro_rules! golden_test {
($name:ident, $element:expr) => {
#[test]
fn $name() {
let golden = $crate::testing::GoldenTest::new(stringify!($name));
golden.assert_match(&$element);
}
};
($name:ident, $width:expr, $height:expr, $element:expr) => {
#[test]
fn $name() {
let golden =
$crate::testing::GoldenTest::new(stringify!($name)).with_size($width, $height);
golden.assert_match(&$element);
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_diff() {
let diff = simple_diff("hello\nworld", "hello\nearth");
assert!(diff.contains("Line 2"));
assert!(diff.contains("- world"));
assert!(diff.contains("+ earth"));
}
#[test]
fn test_golden_path() {
let golden = GoldenTest::new("my_test");
assert!(golden.golden_path().ends_with("my_test.txt"));
}
#[test]
fn test_golden_ansi_path() {
let golden = GoldenTest::new("my_test").ansi();
assert!(golden.golden_path().ends_with("my_test.ansi.txt"));
}
#[test]
fn test_normalize_golden_output_removes_terminal_padding() {
assert_eq!(
normalize_golden_output("alpha \r\nbeta\t \n"),
"alpha\nbeta"
);
}
}