use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
use crate::Plot;
use crate::graphics::GraphicsArea;
fn fixture_path(path: impl AsRef<Path>) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join(path)
}
fn first_diff_char_index(left: &str, right: &str) -> usize {
left.chars()
.zip(right.chars())
.position(|(a, b)| a != b)
.unwrap_or_else(|| left.chars().count().min(right.chars().count()))
}
fn split_preserving_newlines(text: &str) -> Vec<&str> {
if text.is_empty() {
return Vec::new();
}
text.split_inclusive('\n').collect()
}
#[track_caller]
pub(crate) fn assert_fixture_eq(actual: &str, relative_path: impl AsRef<Path>) {
let resolved_path = fixture_path(relative_path);
let expected = fs::read_to_string(&resolved_path).unwrap_or_else(|error| {
panic!(
"failed to read fixture {}: {error}",
resolved_path.display()
)
});
if expected == actual {
return;
}
let expected_lines = split_preserving_newlines(&expected);
let actual_lines = split_preserving_newlines(actual);
let max_lines = expected_lines.len().max(actual_lines.len());
let mut details = String::new();
for index in 0..max_lines {
let expected_line = expected_lines.get(index).copied();
let actual_line = actual_lines.get(index).copied();
if expected_line == actual_line {
continue;
}
let char_index = match (expected_line, actual_line) {
(Some(left), Some(right)) => first_diff_char_index(left, right),
_ => 0,
};
let expected_display = expected_line.unwrap_or("<missing>");
let actual_display = actual_line.unwrap_or("<missing>");
let _ = writeln!(
details,
"line {:>4}, char {:>4}: expected {:?}, actual {:?}",
index + 1,
char_index,
expected_display,
actual_display
);
}
panic!(
"fixture mismatch for {}\n{}",
resolved_path.display(),
details
);
}
#[track_caller]
pub(crate) fn render_plot_text<G: GraphicsArea>(plot: &Plot<G>, color: bool) -> String {
let mut bytes = Vec::new();
plot.render(&mut bytes, color)
.expect("rendering fixture text should succeed");
String::from_utf8(bytes).expect("plot output should be valid UTF-8")
}
#[cfg(test)]
mod tests {
use std::any::Any;
use std::fs;
use std::path::Path;
use super::assert_fixture_eq;
use super::first_diff_char_index;
fn panic_message(payload: &(dyn Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<String>() {
return message.clone();
}
if let Some(message) = payload.downcast_ref::<&'static str>() {
return (*message).to_owned();
}
"<non-string panic payload>".to_owned()
}
fn write_temp_fixture(relative_path: &str, content: &str) {
let absolute_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(relative_path);
if let Some(parent) = absolute_path.parent() {
fs::create_dir_all(parent).expect("parent dir should be creatable");
}
fs::write(&absolute_path, content).expect("fixture file should be writable");
}
#[test]
fn first_diff_char_index_reports_prefix_extension() {
assert_eq!(3, first_diff_char_index("abc", "abcd"));
}
#[test]
fn first_diff_char_index_reports_same_length_difference() {
assert_eq!(2, first_diff_char_index("abz", "abx"));
}
#[test]
fn assert_fixture_eq_accepts_exact_match() {
let path = "tests/fixtures/data/randn.txt";
let contents = fs::read_to_string(path).expect("fixture should load");
assert_fixture_eq(&contents, path);
}
#[test]
fn assert_fixture_eq_supports_ansi_escaped_text() {
let path = "target/test-fixtures/ansi.txt";
let content = "\x1b[31mred\x1b[0m\n";
write_temp_fixture(path, content);
assert_fixture_eq(content, path);
}
#[test]
fn assert_fixture_eq_reports_line_and_char_for_mismatch() {
let path = "target/test-fixtures/mismatch.txt";
write_temp_fixture(path, "abc\ndef\n");
let panic = std::panic::catch_unwind(|| assert_fixture_eq("abc\ndxf\n", path))
.expect_err("mismatch should panic");
let message = panic_message(panic.as_ref());
assert!(message.contains(path));
assert!(message.contains("line 2, char 1"));
assert!(message.contains("expected \"def\\n\""));
assert!(message.contains("actual \"dxf\\n\""));
}
#[test]
fn assert_fixture_eq_reports_trailing_newline_mismatch() {
let path = "target/test-fixtures/newline-shape.txt";
write_temp_fixture(path, "abc\n");
let panic = std::panic::catch_unwind(|| assert_fixture_eq("abc", path))
.expect_err("newline mismatch should panic");
let message = panic_message(panic.as_ref());
assert!(message.contains(path));
assert!(message.contains("line 1, char 3"));
assert!(message.contains("expected \"abc\\n\""));
assert!(message.contains("actual \"abc\""));
}
}