Skip to content

Commit

Permalink
Merge pull request #247 from epage/rust-snap
Browse files Browse the repository at this point in the history
feat(snap)!: `expect_file!` macro instead of `_path`-variants of asserts
  • Loading branch information
epage authored Feb 14, 2024
2 parents 1b3ebc0 + f892781 commit 40d7985
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 398 deletions.
196 changes: 58 additions & 138 deletions crates/snapbox/src/assert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use anstream::stderr;
#[cfg(not(feature = "color"))]
use std::io::stderr;

use crate::data::{DataFormat, NormalizeMatches, NormalizeNewlines, NormalizePaths};
use crate::data::{NormalizeMatches, NormalizeNewlines, NormalizePaths};
use crate::Action;

/// Snapshot assertion against a file's contents
Expand All @@ -15,10 +15,10 @@ use crate::Action;
/// # Examples
///
/// ```rust,no_run
/// let actual = "...";
/// snapbox::Assert::new()
/// .action_env("SNAPSHOTS")
/// .matches_path(actual, "tests/fixtures/help_output_is_clean.txt");
/// # use snapbox::Assert;
/// # use snapbox::expect_file;
/// let actual = "something";
/// Assert::new().matches(expect_file!["output.txt"], actual);
/// ```
#[derive(Clone, Debug)]
pub struct Assert {
Expand All @@ -27,7 +27,6 @@ pub struct Assert {
normalize_paths: bool,
substitutions: crate::Substitutions,
pub(crate) palette: crate::report::Palette,
pub(crate) data_format: Option<DataFormat>,
}

/// # Assertions
Expand All @@ -41,9 +40,18 @@ impl Assert {
/// When the content is text, newlines are normalized.
///
/// ```rust
/// let output = "something";
/// # use snapbox::Assert;
/// let actual = "something";
/// let expected = "something";
/// snapbox::Assert::new().eq(expected, output);
/// Assert::new().eq(expected, actual);
/// ```
///
/// Can combine this with [`expect_file!`][crate::expect_file]
/// ```rust,no_run
/// # use snapbox::Assert;
/// # use snapbox::expect_file;
/// let actual = "something";
/// Assert::new().eq(expect_file!["output.txt"], actual);
/// ```
#[track_caller]
pub fn eq(&self, expected: impl Into<crate::Data>, actual: impl Into<crate::Data>) {
Expand All @@ -54,143 +62,72 @@ impl Assert {

#[track_caller]
fn eq_inner(&self, expected: crate::Data, actual: crate::Data) {
let (pattern, actual) = self.normalize_eq(Ok(expected), actual);
if let Err(desc) = pattern.and_then(|p| self.try_verify(&p, &actual, None, None)) {
panic!("{}: {}", self.palette.error("Eq failed"), desc);
}
}

/// Check if a value matches a pattern
///
/// Pattern syntax:
/// - `...` is a line-wildcard when on a line by itself
/// - `[..]` is a character-wildcard when inside a line
/// - `[EXE]` matches `.exe` on Windows
///
/// Normalization:
/// - Newlines
/// - `\` to `/`
///
/// ```rust
/// let output = "something";
/// let expected = "so[..]g";
/// snapbox::Assert::new().matches(expected, output);
/// ```
#[track_caller]
pub fn matches(&self, pattern: impl Into<crate::Data>, actual: impl Into<crate::Data>) {
let pattern = pattern.into();
let actual = actual.into();
self.matches_inner(pattern, actual);
}

#[track_caller]
fn matches_inner(&self, pattern: crate::Data, actual: crate::Data) {
let (pattern, actual) = self.normalize_match(Ok(pattern), actual);
if let Err(desc) = pattern.and_then(|p| self.try_verify(&p, &actual, None, None)) {
panic!("{}: {}", self.palette.error("Match failed"), desc);
}
}

/// Check if a value matches the content of a file
///
/// When the content is text, newlines are normalized.
///
/// ```rust,no_run
/// let output = "something";
/// let expected_path = "tests/snapshots/output.txt";
/// snapbox::Assert::new().eq_path(output, expected_path);
/// ```
#[track_caller]
pub fn eq_path(
&self,
expected_path: impl AsRef<std::path::Path>,
actual: impl Into<crate::Data>,
) {
let expected_path = expected_path.as_ref();
let actual = actual.into();
self.eq_path_inner(expected_path, actual);
}

#[track_caller]
fn eq_path_inner(&self, pattern_path: &std::path::Path, actual: crate::Data) {
match self.action {
Action::Skip => {
return;
}
Action::Ignore | Action::Verify | Action::Overwrite => {}
}

let expected = crate::Data::read_from(pattern_path, self.data_format());
let (expected, actual) = self.normalize_eq(expected, actual);

self.do_action(
expected,
actual,
Some(&crate::path::display_relpath(pattern_path)),
Some(&"In-memory"),
pattern_path,
);
self.do_action(expected, actual, Some(&"In-memory"));
}

/// Check if a value matches the pattern in a file
/// Check if a value matches a pattern
///
/// Pattern syntax:
/// - `...` is a line-wildcard when on a line by itself
/// - `[..]` is a character-wildcard when inside a line
/// - `[EXE]` matches `.exe` on Windows (override with [`Assert::substitutions`])
/// - `[EXE]` matches `.exe` on Windows
///
/// Normalization:
/// - Newlines
/// - `\` to `/`
///
/// ```rust
/// # use snapbox::Assert;
/// let actual = "something";
/// let expected = "so[..]g";
/// Assert::new().matches(expected, actual);
/// ```
///
/// Can combine this with [`expect_file!`][crate::expect_file]
/// ```rust,no_run
/// let output = "something";
/// let expected_path = "tests/snapshots/output.txt";
/// snapbox::Assert::new().matches_path(expected_path, output);
/// # use snapbox::Assert;
/// # use snapbox::expect_file;
/// let actual = "something";
/// Assert::new().matches(expect_file!["output.txt"], actual);
/// ```
#[track_caller]
pub fn matches_path(
&self,
pattern_path: impl AsRef<std::path::Path>,
actual: impl Into<crate::Data>,
) {
let pattern_path = pattern_path.as_ref();
pub fn matches(&self, pattern: impl Into<crate::Data>, actual: impl Into<crate::Data>) {
let pattern = pattern.into();
let actual = actual.into();
self.matches_path_inner(pattern_path, actual);
self.matches_inner(pattern, actual);
}

#[track_caller]
fn matches_path_inner(&self, pattern_path: &std::path::Path, actual: crate::Data) {
fn matches_inner(&self, pattern: crate::Data, actual: crate::Data) {
match self.action {
Action::Skip => {
return;
}
Action::Ignore | Action::Verify | Action::Overwrite => {}
}

let expected = crate::Data::read_from(pattern_path, self.data_format());
let (expected, actual) = self.normalize_match(expected, actual);
let (expected, actual) = self.normalize_match(pattern, actual);

self.do_action(
expected,
actual,
Some(&crate::path::display_relpath(pattern_path)),
Some(&"In-memory"),
pattern_path,
);
self.do_action(expected, actual, Some(&"In-memory"));
}

pub(crate) fn normalize_eq(
&self,
expected: crate::Result<crate::Data>,
expected: crate::Data,
mut actual: crate::Data,
) -> (crate::Result<crate::Data>, crate::Data) {
let expected = expected.map(|d| d.normalize(NormalizeNewlines));
) -> (crate::Data, crate::Data) {
let expected = expected.normalize(NormalizeNewlines);
// On `expected` being an error, make a best guess
let format = expected
.as_ref()
.map(|d| d.format())
.unwrap_or(DataFormat::Text);
let format = expected.format();

actual = actual.try_coerce(format).normalize(NormalizeNewlines);

Expand All @@ -199,12 +136,12 @@ impl Assert {

pub(crate) fn normalize_match(
&self,
expected: crate::Result<crate::Data>,
expected: crate::Data,
mut actual: crate::Data,
) -> (crate::Result<crate::Data>, crate::Data) {
let expected = expected.map(|d| d.normalize(NormalizeNewlines));
) -> (crate::Data, crate::Data) {
let expected = expected.normalize(NormalizeNewlines);
// On `expected` being an error, make a best guess
let format = expected.as_ref().map(|e| e.format()).unwrap_or_default();
let format = expected.format();
actual = actual.try_coerce(format);

if self.normalize_paths {
Expand All @@ -214,24 +151,19 @@ impl Assert {
actual = actual.normalize(NormalizeNewlines);

// If expected is not an error normalize matches
if let Ok(expected) = expected.as_ref() {
actual = actual.normalize(NormalizeMatches::new(&self.substitutions, expected));
}
actual = actual.normalize(NormalizeMatches::new(&self.substitutions, &expected));

(expected, actual)
}

#[track_caller]
pub(crate) fn do_action(
&self,
expected: crate::Result<crate::Data>,
expected: crate::Data,
actual: crate::Data,
expected_name: Option<&dyn std::fmt::Display>,
actual_name: Option<&dyn std::fmt::Display>,
expected_path: &std::path::Path,
) {
let result =
expected.and_then(|e| self.try_verify(&e, &actual, expected_name, actual_name));
let result = self.try_verify(&expected, &actual, actual_name);
if let Err(err) = result {
match self.action {
Action::Skip => unreachable!("Bailed out earlier"),
Expand All @@ -246,7 +178,9 @@ impl Assert {
);
}
Action::Verify => {
let message = if let Some(action_var) = self.action_var.as_deref() {
let message = if expected.source().is_none() {
crate::report::Styled::new(String::new(), Default::default())
} else if let Some(action_var) = self.action_var.as_deref() {
self.palette
.hint(format!("Update with {}=overwrite", action_var))
} else {
Expand All @@ -257,8 +191,12 @@ impl Assert {
Action::Overwrite => {
use std::io::Write;

let _ = writeln!(stderr(), "{}: {}", self.palette.warn("Fixing"), err);
actual.write_to(expected_path).unwrap();
if let Some(source) = expected.source() {
let _ = writeln!(stderr(), "{}: {}", self.palette.warn("Fixing"), err);
actual.write_to(source).unwrap();
} else {
panic!("{err}");
}
}
}
}
Expand All @@ -268,7 +206,6 @@ impl Assert {
&self,
expected: &crate::Data,
actual: &crate::Data,
expected_name: Option<&dyn std::fmt::Display>,
actual_name: Option<&dyn std::fmt::Display>,
) -> crate::Result<()> {
if expected != actual {
Expand All @@ -277,7 +214,7 @@ impl Assert {
&mut buf,
expected,
actual,
expected_name,
expected.source().map(|s| s as &dyn std::fmt::Display),
actual_name,
self.palette,
)
Expand Down Expand Up @@ -487,22 +424,6 @@ impl Assert {
self.normalize_paths = yes;
self
}

/// Specify whether the content should be treated as binary or not
///
/// The default is to auto-detect
pub fn binary(mut self, yes: bool) -> Self {
self.data_format = if yes {
Some(DataFormat::Binary)
} else {
Some(DataFormat::Text)
};
self
}

pub(crate) fn data_format(&self) -> Option<DataFormat> {
self.data_format
}
}

impl Default for Assert {
Expand All @@ -513,7 +434,6 @@ impl Default for Assert {
normalize_paths: true,
substitutions: Default::default(),
palette: crate::report::Palette::color(),
data_format: Default::default(),
}
.substitutions(crate::Substitutions::with_exe())
}
Expand Down
Loading

0 comments on commit 40d7985

Please sign in to comment.