summaryrefslogtreecommitdiffstats
path: root/vendor/snapbox/src/assert.rs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/snapbox/src/assert.rs')
-rw-r--r--vendor/snapbox/src/assert.rs527
1 files changed, 527 insertions, 0 deletions
diff --git a/vendor/snapbox/src/assert.rs b/vendor/snapbox/src/assert.rs
new file mode 100644
index 000000000..ab87f1554
--- /dev/null
+++ b/vendor/snapbox/src/assert.rs
@@ -0,0 +1,527 @@
+use crate::data::{DataFormat, NormalizeMatches, NormalizeNewlines, NormalizePaths};
+use crate::Action;
+
+/// Snapshot assertion against a file's contents
+///
+/// Useful for one-off assertions with the snapshot stored in a file
+///
+/// # Examples
+///
+/// ```rust,no_run
+/// let actual = "...";
+/// snapbox::Assert::new()
+/// .action_env("SNAPSHOTS")
+/// .matches_path(actual, "tests/fixtures/help_output_is_clean.txt");
+/// ```
+#[derive(Clone, Debug)]
+pub struct Assert {
+ action: Action,
+ action_var: Option<String>,
+ normalize_paths: bool,
+ substitutions: crate::Substitutions,
+ pub(crate) palette: crate::report::Palette,
+ pub(crate) data_format: Option<DataFormat>,
+}
+
+/// # Assertions
+impl Assert {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Check if a value is the same as an expected value
+ ///
+ /// When the content is text, newlines are normalized.
+ ///
+ /// ```rust
+ /// let output = "something";
+ /// let expected = "something";
+ /// snapbox::Assert::new().eq(expected, output);
+ /// ```
+ #[track_caller]
+ pub fn eq(&self, expected: impl Into<crate::Data>, actual: impl Into<crate::Data>) {
+ let expected = expected.into();
+ let actual = actual.into();
+ self.eq_inner(expected, actual);
+ }
+
+ #[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,
+ );
+ }
+
+ /// Check if a value matches the pattern in a file
+ ///
+ /// 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`])
+ ///
+ /// Normalization:
+ /// - Newlines
+ /// - `\` to `/`
+ ///
+ /// ```rust,no_run
+ /// let output = "something";
+ /// let expected_path = "tests/snapshots/output.txt";
+ /// snapbox::Assert::new().matches_path(expected_path, output);
+ /// ```
+ #[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();
+ let actual = actual.into();
+ self.matches_path_inner(pattern_path, actual);
+ }
+
+ #[track_caller]
+ fn matches_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_match(expected, actual);
+
+ self.do_action(
+ expected,
+ actual,
+ Some(&crate::path::display_relpath(pattern_path)),
+ Some(&"In-memory"),
+ pattern_path,
+ );
+ }
+
+ pub(crate) fn normalize_eq(
+ &self,
+ expected: crate::Result<crate::Data>,
+ mut actual: crate::Data,
+ ) -> (crate::Result<crate::Data>, crate::Data) {
+ let expected = expected.map(|d| d.normalize(NormalizeNewlines));
+ // On `expected` being an error, make a best guess
+ let format = expected
+ .as_ref()
+ .map(|d| d.format())
+ .unwrap_or(DataFormat::Text);
+
+ actual = actual.try_coerce(format).normalize(NormalizeNewlines);
+
+ (expected, actual)
+ }
+
+ pub(crate) fn normalize_match(
+ &self,
+ expected: crate::Result<crate::Data>,
+ mut actual: crate::Data,
+ ) -> (crate::Result<crate::Data>, crate::Data) {
+ let expected = expected.map(|d| d.normalize(NormalizeNewlines));
+ // On `expected` being an error, make a best guess
+ let format = expected.as_ref().map(|e| e.format()).unwrap_or_default();
+ actual = actual.try_coerce(format);
+
+ if self.normalize_paths {
+ actual = actual.normalize(NormalizePaths);
+ }
+ // Always normalize new lines
+ 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));
+ }
+
+ (expected, actual)
+ }
+
+ #[track_caller]
+ pub(crate) fn do_action(
+ &self,
+ expected: crate::Result<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));
+ if let Err(err) = result {
+ match self.action {
+ Action::Skip => unreachable!("Bailed out earlier"),
+ Action::Ignore => {
+ use std::io::Write;
+
+ let _ = writeln!(
+ std::io::stderr(),
+ "{}: {}",
+ self.palette.warn("Ignoring failure"),
+ err
+ );
+ }
+ Action::Verify => {
+ use std::fmt::Write;
+ let mut buffer = String::new();
+ write!(&mut buffer, "{}", err).unwrap();
+ if let Some(action_var) = self.action_var.as_deref() {
+ writeln!(
+ &mut buffer,
+ "{}",
+ self.palette
+ .hint(format_args!("Update with {}=overwrite", action_var))
+ )
+ .unwrap();
+ }
+ panic!("{}", buffer);
+ }
+ Action::Overwrite => {
+ use std::io::Write;
+
+ let _ = writeln!(
+ std::io::stderr(),
+ "{}: {}",
+ self.palette.warn("Fixing"),
+ err
+ );
+ actual.write_to(expected_path).unwrap();
+ }
+ }
+ }
+ }
+
+ pub(crate) fn try_verify(
+ &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 {
+ let mut buf = String::new();
+ crate::report::write_diff(
+ &mut buf,
+ expected,
+ actual,
+ expected_name,
+ actual_name,
+ self.palette,
+ )
+ .map_err(|e| e.to_string())?;
+ Err(buf.into())
+ } else {
+ Ok(())
+ }
+ }
+}
+
+/// # Directory Assertions
+#[cfg(feature = "path")]
+impl Assert {
+ #[track_caller]
+ pub fn subset_eq(
+ &self,
+ expected_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+ ) {
+ let expected_root = expected_root.into();
+ let actual_root = actual_root.into();
+ self.subset_eq_inner(expected_root, actual_root)
+ }
+
+ #[track_caller]
+ fn subset_eq_inner(&self, expected_root: std::path::PathBuf, actual_root: std::path::PathBuf) {
+ match self.action {
+ Action::Skip => {
+ return;
+ }
+ Action::Ignore | Action::Verify | Action::Overwrite => {}
+ }
+
+ let checks: Vec<_> =
+ crate::path::PathDiff::subset_eq_iter_inner(expected_root, actual_root).collect();
+ self.verify(checks);
+ }
+
+ #[track_caller]
+ pub fn subset_matches(
+ &self,
+ pattern_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+ ) {
+ let pattern_root = pattern_root.into();
+ let actual_root = actual_root.into();
+ self.subset_matches_inner(pattern_root, actual_root)
+ }
+
+ #[track_caller]
+ fn subset_matches_inner(
+ &self,
+ expected_root: std::path::PathBuf,
+ actual_root: std::path::PathBuf,
+ ) {
+ match self.action {
+ Action::Skip => {
+ return;
+ }
+ Action::Ignore | Action::Verify | Action::Overwrite => {}
+ }
+
+ let checks: Vec<_> = crate::path::PathDiff::subset_matches_iter_inner(
+ expected_root,
+ actual_root,
+ &self.substitutions,
+ )
+ .collect();
+ self.verify(checks);
+ }
+
+ #[track_caller]
+ fn verify(
+ &self,
+ mut checks: Vec<Result<(std::path::PathBuf, std::path::PathBuf), crate::path::PathDiff>>,
+ ) {
+ if checks.iter().all(Result::is_ok) {
+ for check in checks {
+ let (_expected_path, _actual_path) = check.unwrap();
+ crate::debug!(
+ "{}: is {}",
+ _expected_path.display(),
+ self.palette.info("good")
+ );
+ }
+ } else {
+ checks.sort_by_key(|c| match c {
+ Ok((expected_path, _actual_path)) => Some(expected_path.clone()),
+ Err(diff) => diff.expected_path().map(|p| p.to_owned()),
+ });
+
+ let mut buffer = String::new();
+ let mut ok = true;
+ for check in checks {
+ use std::fmt::Write;
+ match check {
+ Ok((expected_path, _actual_path)) => {
+ let _ = writeln!(
+ &mut buffer,
+ "{}: is {}",
+ expected_path.display(),
+ self.palette.info("good"),
+ );
+ }
+ Err(diff) => {
+ let _ = diff.write(&mut buffer, self.palette);
+ match self.action {
+ Action::Skip => unreachable!("Bailed out earlier"),
+ Action::Ignore | Action::Verify => {
+ ok = false;
+ }
+ Action::Overwrite => {
+ if let Err(err) = diff.overwrite() {
+ ok = false;
+ let path = diff
+ .expected_path()
+ .expect("always present when overwrite can fail");
+ let _ = writeln!(
+ &mut buffer,
+ "{} to overwrite {}: {}",
+ self.palette.error("Failed"),
+ path.display(),
+ err
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+ if ok {
+ use std::io::Write;
+ let _ = write!(std::io::stderr(), "{}", buffer);
+ match self.action {
+ Action::Skip => unreachable!("Bailed out earlier"),
+ Action::Ignore => {
+ let _ = write!(
+ std::io::stderr(),
+ "{}",
+ self.palette.warn("Ignoring above failures")
+ );
+ }
+ Action::Verify => unreachable!("Something had to fail to get here"),
+ Action::Overwrite => {
+ let _ = write!(
+ std::io::stderr(),
+ "{}",
+ self.palette.warn("Overwrote above failures")
+ );
+ }
+ }
+ } else {
+ match self.action {
+ Action::Skip => unreachable!("Bailed out earlier"),
+ Action::Ignore => unreachable!("Shouldn't be able to fail"),
+ Action::Verify => {
+ use std::fmt::Write;
+ if let Some(action_var) = self.action_var.as_deref() {
+ writeln!(
+ &mut buffer,
+ "{}",
+ self.palette
+ .hint(format_args!("Update with {}=overwrite", action_var))
+ )
+ .unwrap();
+ }
+ }
+ Action::Overwrite => {}
+ }
+ panic!("{}", buffer);
+ }
+ }
+ }
+}
+
+/// # Customize Behavior
+impl Assert {
+ /// Override the color palette
+ pub fn palette(mut self, palette: crate::report::Palette) -> Self {
+ self.palette = palette;
+ self
+ }
+
+ /// Read the failure action from an environment variable
+ pub fn action_env(mut self, var_name: &str) -> Self {
+ let action = Action::with_env_var(var_name);
+ self.action = action.unwrap_or(self.action);
+ self.action_var = Some(var_name.to_owned());
+ self
+ }
+
+ /// Override the failure action
+ pub fn action(mut self, action: Action) -> Self {
+ self.action = action;
+ self.action_var = None;
+ self
+ }
+
+ /// Override the default [`Substitutions`][crate::Substitutions]
+ pub fn substitutions(mut self, substitutions: crate::Substitutions) -> Self {
+ self.substitutions = substitutions;
+ self
+ }
+
+ /// Specify whether text should have path separators normalized
+ ///
+ /// The default is normalized
+ pub fn normalize_paths(mut self, yes: bool) -> Self {
+ 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 {
+ fn default() -> Self {
+ Self {
+ action: Default::default(),
+ action_var: Default::default(),
+ normalize_paths: true,
+ substitutions: Default::default(),
+ palette: crate::report::Palette::auto(),
+ data_format: Default::default(),
+ }
+ .substitutions(crate::Substitutions::with_exe())
+ }
+}