Skip to content

feat(snap)!: expect_file! macro instead of _path-variants of asserts #247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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