use crate::diff::Diff;
use alloc::string::String;
use core::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Snapshot {
text: String,
}
impl Snapshot {
pub fn new(text: impl AsRef<str>) -> Self {
Snapshot {
text: normalize(text.as_ref()),
}
}
pub fn display(value: &impl fmt::Display) -> Self {
Snapshot::new(alloc::format!("{value}"))
}
pub fn debug(value: &impl fmt::Debug) -> Self {
Snapshot::new(alloc::format!("{value:#?}"))
}
pub fn per_line<I>(items: I) -> Self
where
I: IntoIterator,
I::Item: fmt::Display,
{
let mut text = String::new();
for (i, item) in items.into_iter().enumerate() {
if i > 0 {
text.push('\n');
}
let _ = fmt::write(&mut text, format_args!("{item}"));
}
Snapshot::new(text)
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.text
}
pub fn check(&self, expected: impl AsRef<str>) -> Result<(), Mismatch> {
let expected = normalize(expected.as_ref());
let diff = Diff::lines(&expected, &self.text);
if diff.is_empty() {
Ok(())
} else {
Err(Mismatch { diff })
}
}
}
impl fmt::Display for Snapshot {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.text)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub struct Mismatch {
diff: Diff,
}
impl Mismatch {
#[inline]
pub fn diff(&self) -> &Diff {
&self.diff
}
}
impl fmt::Display for Mismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("snapshot mismatch (-expected +actual):\n")?;
fmt::Display::fmt(&self.diff, f)
}
}
impl core::error::Error for Mismatch {}
fn normalize(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let unified = text.replace("\r\n", "\n");
let unified = unified.replace('\r', "\n");
for line in unified.split('\n') {
out.push_str(line.trim_end_matches([' ', '\t']));
out.push('\n');
}
let trimmed = out.trim_end_matches('\n');
out.truncate(trimmed.len());
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn test_new_normalizes_crlf_to_lf() {
assert_eq!(Snapshot::new("a\r\nb").as_str(), "a\nb");
}
#[test]
fn test_new_normalizes_lone_cr() {
assert_eq!(Snapshot::new("a\rb").as_str(), "a\nb");
}
#[test]
fn test_new_strips_trailing_whitespace_per_line() {
assert_eq!(Snapshot::new("a \t\nb ").as_str(), "a\nb");
}
#[test]
fn test_new_strips_trailing_newlines() {
assert_eq!(Snapshot::new("a\nb\n\n\n").as_str(), "a\nb");
}
#[test]
fn test_new_preserves_interior_blank_lines() {
assert_eq!(Snapshot::new("a\n\nb").as_str(), "a\n\nb");
}
#[test]
fn test_new_preserves_leading_indentation() {
assert_eq!(Snapshot::new(" indented").as_str(), " indented");
}
#[test]
fn test_new_empty_input_is_empty() {
assert_eq!(Snapshot::new("").as_str(), "");
assert_eq!(Snapshot::new("\n\n").as_str(), "");
}
#[test]
fn test_display_renders_value() {
assert_eq!(Snapshot::display(&123).as_str(), "123");
}
#[test]
fn test_debug_uses_pretty_form() {
#[derive(Debug)]
#[allow(dead_code)]
struct Node {
kind: u8,
}
let snap = Snapshot::debug(&Node { kind: 7 });
assert!(snap.as_str().contains("kind: 7"));
}
#[test]
fn test_per_line_joins_with_newlines() {
assert_eq!(Snapshot::per_line(["a", "b", "c"]).as_str(), "a\nb\nc");
}
#[test]
fn test_per_line_empty_is_empty() {
let empty: [&str; 0] = [];
assert_eq!(Snapshot::per_line(empty).as_str(), "");
}
#[test]
fn test_check_matching_returns_ok() {
assert!(Snapshot::new("a\nb").check("a\nb").is_ok());
}
#[test]
fn test_check_matching_ignores_trailing_newline_difference() {
assert!(Snapshot::new("a\nb").check("a\nb\n").is_ok());
}
#[test]
fn test_check_matching_ignores_line_ending_difference() {
assert!(Snapshot::new("a\nb").check("a\r\nb").is_ok());
}
#[test]
fn test_check_mismatch_returns_err() {
let err = Snapshot::new("a\nb").check("a\nc").unwrap_err();
assert!(err.to_string().contains("-c"));
assert!(err.to_string().contains("+b"));
}
#[test]
fn test_check_mismatch_header_present() {
let err = Snapshot::new("x").check("y").unwrap_err();
assert!(err.to_string().starts_with("snapshot mismatch"));
}
#[test]
fn test_mismatch_diff_accessor() {
let err = Snapshot::new("a").check("b").unwrap_err();
assert!(!err.diff().is_empty());
}
#[test]
fn test_display_impl_matches_as_str() {
let snap = Snapshot::new("a\nb");
assert_eq!(snap.to_string(), snap.as_str());
}
}