summaryrefslogtreecommitdiffstats
path: root/vendor/snapbox/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:35 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:35 +0000
commit7e5d7eea9c580ef4b41a765bde624af431942b96 (patch)
tree2c0d9ca12878fc4525650aa4e54d77a81a07cc09 /vendor/snapbox/src
parentAdding debian version 1.70.0+dfsg1-9. (diff)
downloadrustc-7e5d7eea9c580ef4b41a765bde624af431942b96.tar.xz
rustc-7e5d7eea9c580ef4b41a765bde624af431942b96.zip
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/snapbox/src')
-rw-r--r--vendor/snapbox/src/action.rs39
-rw-r--r--vendor/snapbox/src/assert.rs527
-rw-r--r--vendor/snapbox/src/bin/snap-fixture.rs60
-rw-r--r--vendor/snapbox/src/cmd.rs1030
-rw-r--r--vendor/snapbox/src/data.rs712
-rw-r--r--vendor/snapbox/src/error.rs95
-rw-r--r--vendor/snapbox/src/harness.rs212
-rw-r--r--vendor/snapbox/src/lib.rs246
-rw-r--r--vendor/snapbox/src/path.rs686
-rw-r--r--vendor/snapbox/src/report/color.rs127
-rw-r--r--vendor/snapbox/src/report/diff.rs384
-rw-r--r--vendor/snapbox/src/report/mod.rs9
-rw-r--r--vendor/snapbox/src/substitutions.rs420
-rw-r--r--vendor/snapbox/src/utils/lines.rs31
-rw-r--r--vendor/snapbox/src/utils/mod.rs30
15 files changed, 4608 insertions, 0 deletions
diff --git a/vendor/snapbox/src/action.rs b/vendor/snapbox/src/action.rs
new file mode 100644
index 000000000..a4b849919
--- /dev/null
+++ b/vendor/snapbox/src/action.rs
@@ -0,0 +1,39 @@
+pub const DEFAULT_ACTION_ENV: &str = "SNAPSHOTS";
+
+/// Test action, see [`Assert`][crate::Assert]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum Action {
+ /// Do not run the test
+ Skip,
+ /// Ignore test failures
+ Ignore,
+ /// Fail on mismatch
+ Verify,
+ /// Overwrite on mismatch
+ Overwrite,
+}
+
+impl Action {
+ pub fn with_env_var(var: impl AsRef<std::ffi::OsStr>) -> Option<Self> {
+ let var = var.as_ref();
+ let value = std::env::var_os(var)?;
+ Self::with_env_value(value)
+ }
+
+ pub fn with_env_value(value: impl AsRef<std::ffi::OsStr>) -> Option<Self> {
+ let value = value.as_ref();
+ match value.to_str()? {
+ "skip" => Some(Action::Skip),
+ "ignore" => Some(Action::Ignore),
+ "verify" => Some(Action::Verify),
+ "overwrite" => Some(Action::Overwrite),
+ _ => None,
+ }
+ }
+}
+
+impl Default for Action {
+ fn default() -> Self {
+ Self::Verify
+ }
+}
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())
+ }
+}
diff --git a/vendor/snapbox/src/bin/snap-fixture.rs b/vendor/snapbox/src/bin/snap-fixture.rs
new file mode 100644
index 000000000..6e13448a7
--- /dev/null
+++ b/vendor/snapbox/src/bin/snap-fixture.rs
@@ -0,0 +1,60 @@
+//! For `snapbox`s tests only
+
+use std::env;
+use std::error::Error;
+use std::io;
+use std::io::Write;
+use std::process;
+
+fn run() -> Result<(), Box<dyn Error>> {
+ if let Ok(text) = env::var("stdout") {
+ println!("{}", text);
+ }
+ if let Ok(text) = env::var("stderr") {
+ eprintln!("{}", text);
+ }
+
+ if env::var("echo_large").as_deref() == Ok("1") {
+ for i in 0..(128 * 1024) {
+ println!("{}", i);
+ }
+ }
+
+ if env::var("echo_cwd").as_deref() == Ok("1") {
+ if let Ok(cwd) = std::env::current_dir() {
+ eprintln!("{}", cwd.display());
+ }
+ }
+
+ if let Ok(raw) = env::var("write") {
+ let (path, text) = raw.split_once('=').unwrap_or((raw.as_str(), ""));
+ std::fs::write(path.trim(), text.trim()).unwrap();
+ }
+
+ if let Ok(path) = env::var("cat") {
+ let text = std::fs::read_to_string(path).unwrap();
+ eprintln!("{}", text);
+ }
+
+ if let Some(timeout) = env::var("sleep").ok().and_then(|s| s.parse().ok()) {
+ std::thread::sleep(std::time::Duration::from_secs(timeout));
+ }
+
+ let code = env::var("exit")
+ .ok()
+ .map(|v| v.parse::<i32>())
+ .map_or(Ok(None), |r| r.map(Some))?
+ .unwrap_or(0);
+ process::exit(code);
+}
+
+fn main() {
+ let code = match run() {
+ Ok(_) => 0,
+ Err(ref e) => {
+ write!(&mut io::stderr(), "{}", e).expect("writing to stderr won't fail");
+ 1
+ }
+ };
+ process::exit(code);
+}
diff --git a/vendor/snapbox/src/cmd.rs b/vendor/snapbox/src/cmd.rs
new file mode 100644
index 000000000..72de3563c
--- /dev/null
+++ b/vendor/snapbox/src/cmd.rs
@@ -0,0 +1,1030 @@
+//! Run commands and assert on their behavior
+
+/// Process spawning for testing of non-interactive commands
+#[derive(Debug)]
+pub struct Command {
+ cmd: std::process::Command,
+ stdin: Option<crate::Data>,
+ timeout: Option<std::time::Duration>,
+ _stderr_to_stdout: bool,
+ config: crate::Assert,
+}
+
+/// # Builder API
+impl Command {
+ pub fn new(program: impl AsRef<std::ffi::OsStr>) -> Self {
+ Self {
+ cmd: std::process::Command::new(program),
+ stdin: None,
+ timeout: None,
+ _stderr_to_stdout: false,
+ config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV),
+ }
+ }
+
+ /// Constructs a new `Command` from a `std` `Command`.
+ pub fn from_std(cmd: std::process::Command) -> Self {
+ Self {
+ cmd,
+ stdin: None,
+ timeout: None,
+ _stderr_to_stdout: false,
+ config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV),
+ }
+ }
+
+ /// Customize the assertion behavior
+ pub fn with_assert(mut self, config: crate::Assert) -> Self {
+ self.config = config;
+ self
+ }
+
+ /// Adds an argument to pass to the program.
+ ///
+ /// Only one argument can be passed per use. So instead of:
+ ///
+ /// ```no_run
+ /// # snapbox::cmd::Command::new("sh")
+ /// .arg("-C /path/to/repo")
+ /// # ;
+ /// ```
+ ///
+ /// usage would be:
+ ///
+ /// ```no_run
+ /// # snapbox::cmd::Command::new("sh")
+ /// .arg("-C")
+ /// .arg("/path/to/repo")
+ /// # ;
+ /// ```
+ ///
+ /// To pass multiple arguments see [`args`].
+ ///
+ /// [`args`]: Command::args()
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .arg("-l")
+ /// .arg("-a")
+ /// .assert()
+ /// .success();
+ /// ```
+ pub fn arg(mut self, arg: impl AsRef<std::ffi::OsStr>) -> Self {
+ self.cmd.arg(arg);
+ self
+ }
+
+ /// Adds multiple arguments to pass to the program.
+ ///
+ /// To pass a single argument see [`arg`].
+ ///
+ /// [`arg`]: Command::arg()
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .args(&["-l", "-a"])
+ /// .assert()
+ /// .success();
+ /// ```
+ pub fn args(mut self, args: impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>>) -> Self {
+ self.cmd.args(args);
+ self
+ }
+
+ /// Inserts or updates an environment variable mapping.
+ ///
+ /// Note that environment variable names are case-insensitive (but case-preserving) on Windows,
+ /// and case-sensitive on all other platforms.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .env("PATH", "/bin")
+ /// .assert()
+ /// .failure();
+ /// ```
+ pub fn env(
+ mut self,
+ key: impl AsRef<std::ffi::OsStr>,
+ value: impl AsRef<std::ffi::OsStr>,
+ ) -> Self {
+ self.cmd.env(key, value);
+ self
+ }
+
+ /// Adds or updates multiple environment variable mappings.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ /// use std::process::Stdio;
+ /// use std::env;
+ /// use std::collections::HashMap;
+ ///
+ /// let filtered_env : HashMap<String, String> =
+ /// env::vars().filter(|&(ref k, _)|
+ /// k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH"
+ /// ).collect();
+ ///
+ /// Command::new("printenv")
+ /// .env_clear()
+ /// .envs(&filtered_env)
+ /// .assert()
+ /// .success();
+ /// ```
+ pub fn envs(
+ mut self,
+ vars: impl IntoIterator<Item = (impl AsRef<std::ffi::OsStr>, impl AsRef<std::ffi::OsStr>)>,
+ ) -> Self {
+ self.cmd.envs(vars);
+ self
+ }
+
+ /// Removes an environment variable mapping.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .env_remove("PATH")
+ /// .assert()
+ /// .failure();
+ /// ```
+ pub fn env_remove(mut self, key: impl AsRef<std::ffi::OsStr>) -> Self {
+ self.cmd.env_remove(key);
+ self
+ }
+
+ /// Clears the entire environment map for the child process.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .env_clear()
+ /// .assert()
+ /// .failure();
+ /// ```
+ pub fn env_clear(mut self) -> Self {
+ self.cmd.env_clear();
+ self
+ }
+
+ /// Sets the working directory for the child process.
+ ///
+ /// # Platform-specific behavior
+ ///
+ /// If the program path is relative (e.g., `"./script.sh"`), it's ambiguous
+ /// whether it should be interpreted relative to the parent's working
+ /// directory or relative to `current_dir`. The behavior in this case is
+ /// platform specific and unstable, and it's recommended to use
+ /// [`canonicalize`] to get an absolute program path instead.
+ ///
+ /// # Examples
+ ///
+ /// Basic usage:
+ ///
+ /// ```no_run
+ /// use snapbox::cmd::Command;
+ ///
+ /// Command::new("ls")
+ /// .current_dir("/bin")
+ /// .assert()
+ /// .success();
+ /// ```
+ ///
+ /// [`canonicalize`]: std::fs::canonicalize()
+ pub fn current_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
+ self.cmd.current_dir(dir);
+ self
+ }
+
+ /// Write `buffer` to `stdin` when the `Command` is run.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use snapbox::cmd::Command;
+ ///
+ /// let mut cmd = Command::new("cat")
+ /// .arg("-et")
+ /// .stdin("42")
+ /// .assert()
+ /// .stdout_eq("42");
+ /// ```
+ pub fn stdin(mut self, stream: impl Into<crate::Data>) -> Self {
+ self.stdin = Some(stream.into());
+ self
+ }
+
+ /// Error out if a timeout is reached
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .timeout(std::time::Duration::from_secs(1))
+ /// .env("sleep", "100")
+ /// .assert()
+ /// .failure();
+ /// ```
+ #[cfg(feature = "cmd")]
+ pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
+ self.timeout = Some(timeout);
+ self
+ }
+
+ /// Merge `stderr` into `stdout`
+ #[cfg(feature = "cmd")]
+ pub fn stderr_to_stdout(mut self) -> Self {
+ self._stderr_to_stdout = true;
+ self
+ }
+}
+
+/// # Run Command
+impl Command {
+ /// Run the command and assert on the results
+ ///
+ /// ```rust
+ /// use snapbox::cmd::Command;
+ ///
+ /// let mut cmd = Command::new("cat")
+ /// .arg("-et")
+ /// .stdin("42")
+ /// .assert()
+ /// .stdout_eq("42");
+ /// ```
+ #[track_caller]
+ pub fn assert(self) -> OutputAssert {
+ let config = self.config.clone();
+ match self.output() {
+ Ok(output) => OutputAssert::new(output).with_assert(config),
+ Err(err) => {
+ panic!("Failed to spawn: {}", err)
+ }
+ }
+ }
+
+ /// Run the command and capture the `Output`
+ #[cfg(feature = "cmd")]
+ pub fn output(self) -> Result<std::process::Output, std::io::Error> {
+ if self._stderr_to_stdout {
+ self.single_output()
+ } else {
+ self.split_output()
+ }
+ }
+
+ #[cfg(not(feature = "cmd"))]
+ pub fn output(self) -> Result<std::process::Output, std::io::Error> {
+ self.split_output()
+ }
+
+ #[cfg(feature = "cmd")]
+ fn single_output(mut self) -> Result<std::process::Output, std::io::Error> {
+ self.cmd.stdin(std::process::Stdio::piped());
+ let (reader, writer) = os_pipe::pipe()?;
+ let writer_clone = writer.try_clone()?;
+ self.cmd.stdout(writer);
+ self.cmd.stderr(writer_clone);
+ let mut child = self.cmd.spawn()?;
+ // Avoid a deadlock! This parent process is still holding open pipe
+ // writers (inside the Command object), and we have to close those
+ // before we read. Here we do this by dropping the Command object.
+ drop(self.cmd);
+
+ let stdout = process_single_io(
+ &mut child,
+ reader,
+ self.stdin.as_ref().map(|d| d.to_bytes()),
+ )?;
+
+ let status = wait(child, self.timeout)?;
+ let stdout = stdout.join().unwrap().ok().unwrap_or_default();
+
+ Ok(std::process::Output {
+ status,
+ stdout,
+ stderr: Default::default(),
+ })
+ }
+
+ fn split_output(mut self) -> Result<std::process::Output, std::io::Error> {
+ self.cmd.stdin(std::process::Stdio::piped());
+ self.cmd.stdout(std::process::Stdio::piped());
+ self.cmd.stderr(std::process::Stdio::piped());
+ let mut child = self.cmd.spawn()?;
+
+ let (stdout, stderr) =
+ process_split_io(&mut child, self.stdin.as_ref().map(|d| d.to_bytes()))?;
+
+ let status = wait(child, self.timeout)?;
+ let stdout = stdout
+ .and_then(|t| t.join().unwrap().ok())
+ .unwrap_or_default();
+ let stderr = stderr
+ .and_then(|t| t.join().unwrap().ok())
+ .unwrap_or_default();
+
+ Ok(std::process::Output {
+ status,
+ stdout,
+ stderr,
+ })
+ }
+}
+
+fn process_split_io(
+ child: &mut std::process::Child,
+ input: Option<Vec<u8>>,
+) -> std::io::Result<(Option<Stream>, Option<Stream>)> {
+ use std::io::Write;
+
+ let stdin = input.and_then(|i| {
+ child
+ .stdin
+ .take()
+ .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
+ });
+ let stdout = child.stdout.take().map(threaded_read);
+ let stderr = child.stderr.take().map(threaded_read);
+
+ // Finish writing stdin before waiting, because waiting drops stdin.
+ stdin.and_then(|t| t.join().unwrap().ok());
+
+ Ok((stdout, stderr))
+}
+
+#[cfg(feature = "cmd")]
+fn process_single_io(
+ child: &mut std::process::Child,
+ stdout: os_pipe::PipeReader,
+ input: Option<Vec<u8>>,
+) -> std::io::Result<Stream> {
+ use std::io::Write;
+
+ let stdin = input.and_then(|i| {
+ child
+ .stdin
+ .take()
+ .map(|mut stdin| std::thread::spawn(move || stdin.write_all(&i)))
+ });
+ let stdout = threaded_read(stdout);
+ debug_assert!(child.stdout.is_none());
+ debug_assert!(child.stderr.is_none());
+
+ // Finish writing stdin before waiting, because waiting drops stdin.
+ stdin.and_then(|t| t.join().unwrap().ok());
+
+ Ok(stdout)
+}
+
+type Stream = std::thread::JoinHandle<Result<Vec<u8>, std::io::Error>>;
+
+fn threaded_read<R>(mut input: R) -> Stream
+where
+ R: std::io::Read + Send + 'static,
+{
+ std::thread::spawn(move || {
+ let mut ret = Vec::new();
+ input.read_to_end(&mut ret).map(|_| ret)
+ })
+}
+
+impl From<std::process::Command> for Command {
+ fn from(cmd: std::process::Command) -> Self {
+ Self::from_std(cmd)
+ }
+}
+
+/// Assert the state of a [`Command`]'s [`Output`].
+///
+/// Create an `OutputAssert` through the [`Command::assert`].
+///
+/// [`Output`]: std::process::Output
+pub struct OutputAssert {
+ output: std::process::Output,
+ config: crate::Assert,
+}
+
+impl OutputAssert {
+ /// Create an `Assert` for a given [`Output`].
+ ///
+ /// [`Output`]: std::process::Output
+ pub fn new(output: std::process::Output) -> Self {
+ Self {
+ output,
+ config: crate::Assert::new().action_env(crate::DEFAULT_ACTION_ENV),
+ }
+ }
+
+ /// Customize the assertion behavior
+ pub fn with_assert(mut self, config: crate::Assert) -> Self {
+ self.config = config;
+ self
+ }
+
+ /// Access the contained [`Output`].
+ ///
+ /// [`Output`]: std::process::Output
+ pub fn get_output(&self) -> &std::process::Output {
+ &self.output
+ }
+
+ /// Ensure the command succeeded.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .assert()
+ /// .success();
+ /// ```
+ #[track_caller]
+ pub fn success(self) -> Self {
+ if !self.output.status.success() {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info("success"),
+ self.config
+ .palette
+ .error(display_exit_status(self.output.status))
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command failed.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("exit", "1")
+ /// .assert()
+ /// .failure();
+ /// ```
+ #[track_caller]
+ pub fn failure(self) -> Self {
+ if self.output.status.success() {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info("failure"),
+ self.config.palette.error("success")
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command aborted before returning a code.
+ #[track_caller]
+ pub fn interrupted(self) -> Self {
+ if self.output.status.code().is_some() {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info("interrupted"),
+ self.config
+ .palette
+ .error(display_exit_status(self.output.status))
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command returned the expected code.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("exit", "42")
+ /// .assert()
+ /// .code(42);
+ /// ```
+ #[track_caller]
+ pub fn code(self, expected: i32) -> Self {
+ if self.output.status.code() != Some(expected) {
+ let desc = format!(
+ "Expected {}, was {}",
+ self.config.palette.info(expected),
+ self.config
+ .palette
+ .error(display_exit_status(self.output.status))
+ );
+
+ use std::fmt::Write;
+ let mut buf = String::new();
+ writeln!(&mut buf, "{}", desc).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_eq("hello");
+ /// ```
+ #[track_caller]
+ pub fn stdout_eq(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stdout_eq_inner(expected)
+ }
+
+ #[track_caller]
+ fn stdout_eq_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let (pattern, actual) = self.config.normalize_eq(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stdout")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_eq_path("tests/snapshots/output.txt");
+ /// ```
+ #[track_caller]
+ pub fn stdout_eq_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stdout_eq_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stdout_eq_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_eq(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&crate::path::display_relpath(expected_path)),
+ Some(&"stdout"),
+ expected_path,
+ );
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_matches("he[..]o");
+ /// ```
+ #[track_caller]
+ pub fn stdout_matches(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stdout_matches_inner(expected)
+ }
+
+ #[track_caller]
+ fn stdout_matches_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let (pattern, actual) = self.config.normalize_match(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stdout")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stderr(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stdout`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stdout_matches_path("tests/snapshots/output.txt");
+ /// ```
+ #[track_caller]
+ pub fn stdout_matches_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stdout_matches_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stdout_matches_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stdout.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_match(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&expected_path.display()),
+ Some(&"stdout"),
+ expected_path,
+ );
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_eq("world");
+ /// ```
+ #[track_caller]
+ pub fn stderr_eq(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stderr_eq_inner(expected)
+ }
+
+ #[track_caller]
+ fn stderr_eq_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let (pattern, actual) = self.config.normalize_eq(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stderr")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_eq_path("tests/snapshots/err.txt");
+ /// ```
+ #[track_caller]
+ pub fn stderr_eq_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stderr_eq_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stderr_eq_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_eq(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&expected_path.display()),
+ Some(&"stderr"),
+ expected_path,
+ );
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_matches("wo[..]d");
+ /// ```
+ #[track_caller]
+ pub fn stderr_matches(self, expected: impl Into<crate::Data>) -> Self {
+ let expected = expected.into();
+ self.stderr_matches_inner(expected)
+ }
+
+ #[track_caller]
+ fn stderr_matches_inner(self, expected: crate::Data) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let (pattern, actual) = self.config.normalize_match(Ok(expected), actual);
+ if let Err(desc) =
+ pattern.and_then(|p| self.config.try_verify(&p, &actual, None, Some(&"stderr")))
+ {
+ use std::fmt::Write;
+ let mut buf = String::new();
+ write!(&mut buf, "{}", desc).unwrap();
+ self.write_status(&mut buf).unwrap();
+ self.write_stdout(&mut buf).unwrap();
+ panic!("{}", buf);
+ }
+
+ self
+ }
+
+ /// Ensure the command wrote the expected data to `stderr`.
+ ///
+ /// ```rust,no_run
+ /// use snapbox::cmd::Command;
+ /// use snapbox::cmd::cargo_bin;
+ ///
+ /// let assert = Command::new(cargo_bin("snap-fixture"))
+ /// .env("stdout", "hello")
+ /// .env("stderr", "world")
+ /// .assert()
+ /// .stderr_matches_path("tests/snapshots/err.txt");
+ /// ```
+ #[track_caller]
+ pub fn stderr_matches_path(self, expected_path: impl AsRef<std::path::Path>) -> Self {
+ let expected_path = expected_path.as_ref();
+ self.stderr_matches_path_inner(expected_path)
+ }
+
+ #[track_caller]
+ fn stderr_matches_path_inner(self, expected_path: &std::path::Path) -> Self {
+ let actual = crate::Data::from(self.output.stderr.as_slice());
+ let expected = crate::Data::read_from(expected_path, self.config.data_format());
+ let (pattern, actual) = self.config.normalize_match(expected, actual);
+ self.config.do_action(
+ pattern,
+ actual,
+ Some(&crate::path::display_relpath(expected_path)),
+ Some(&"stderr"),
+ expected_path,
+ );
+
+ self
+ }
+
+ fn write_status(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
+ writeln!(
+ writer,
+ "Exit status: {}",
+ display_exit_status(self.output.status)
+ )?;
+ Ok(())
+ }
+
+ fn write_stdout(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
+ if !self.output.stdout.is_empty() {
+ writeln!(writer, "stdout:")?;
+ writeln!(writer, "```")?;
+ writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stdout))?;
+ writeln!(writer, "```")?;
+ }
+ Ok(())
+ }
+
+ fn write_stderr(&self, writer: &mut dyn std::fmt::Write) -> Result<(), std::fmt::Error> {
+ if !self.output.stderr.is_empty() {
+ writeln!(writer, "stderr:")?;
+ writeln!(writer, "```")?;
+ writeln!(writer, "{}", String::from_utf8_lossy(&self.output.stderr))?;
+ writeln!(writer, "```")?;
+ }
+ Ok(())
+ }
+}
+
+/// Converts an [`std::process::ExitStatus`] to a human-readable value
+#[cfg(not(feature = "cmd"))]
+pub fn display_exit_status(status: std::process::ExitStatus) -> String {
+ basic_exit_status(status)
+}
+
+/// Converts an [`std::process::ExitStatus`] to a human-readable value
+#[cfg(feature = "cmd")]
+pub fn display_exit_status(status: std::process::ExitStatus) -> String {
+ #[cfg(unix)]
+ fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
+ use std::os::unix::process::*;
+
+ let signal = status.signal()?;
+ let name = match signal as libc::c_int {
+ libc::SIGABRT => ", SIGABRT: process abort signal",
+ libc::SIGALRM => ", SIGALRM: alarm clock",
+ libc::SIGFPE => ", SIGFPE: erroneous arithmetic operation",
+ libc::SIGHUP => ", SIGHUP: hangup",
+ libc::SIGILL => ", SIGILL: illegal instruction",
+ libc::SIGINT => ", SIGINT: terminal interrupt signal",
+ libc::SIGKILL => ", SIGKILL: kill",
+ libc::SIGPIPE => ", SIGPIPE: write on a pipe with no one to read",
+ libc::SIGQUIT => ", SIGQUIT: terminal quit signal",
+ libc::SIGSEGV => ", SIGSEGV: invalid memory reference",
+ libc::SIGTERM => ", SIGTERM: termination signal",
+ libc::SIGBUS => ", SIGBUS: access to undefined memory",
+ #[cfg(not(target_os = "haiku"))]
+ libc::SIGSYS => ", SIGSYS: bad system call",
+ libc::SIGTRAP => ", SIGTRAP: trace/breakpoint trap",
+ _ => "",
+ };
+ Some(format!("signal: {}{}", signal, name))
+ }
+
+ #[cfg(windows)]
+ fn detailed_exit_status(status: std::process::ExitStatus) -> Option<String> {
+ use winapi::shared::minwindef::DWORD;
+ use winapi::um::winnt::*;
+
+ let extra = match status.code().unwrap() as DWORD {
+ STATUS_ACCESS_VIOLATION => "STATUS_ACCESS_VIOLATION",
+ STATUS_IN_PAGE_ERROR => "STATUS_IN_PAGE_ERROR",
+ STATUS_INVALID_HANDLE => "STATUS_INVALID_HANDLE",
+ STATUS_INVALID_PARAMETER => "STATUS_INVALID_PARAMETER",
+ STATUS_NO_MEMORY => "STATUS_NO_MEMORY",
+ STATUS_ILLEGAL_INSTRUCTION => "STATUS_ILLEGAL_INSTRUCTION",
+ STATUS_NONCONTINUABLE_EXCEPTION => "STATUS_NONCONTINUABLE_EXCEPTION",
+ STATUS_INVALID_DISPOSITION => "STATUS_INVALID_DISPOSITION",
+ STATUS_ARRAY_BOUNDS_EXCEEDED => "STATUS_ARRAY_BOUNDS_EXCEEDED",
+ STATUS_FLOAT_DENORMAL_OPERAND => "STATUS_FLOAT_DENORMAL_OPERAND",
+ STATUS_FLOAT_DIVIDE_BY_ZERO => "STATUS_FLOAT_DIVIDE_BY_ZERO",
+ STATUS_FLOAT_INEXACT_RESULT => "STATUS_FLOAT_INEXACT_RESULT",
+ STATUS_FLOAT_INVALID_OPERATION => "STATUS_FLOAT_INVALID_OPERATION",
+ STATUS_FLOAT_OVERFLOW => "STATUS_FLOAT_OVERFLOW",
+ STATUS_FLOAT_STACK_CHECK => "STATUS_FLOAT_STACK_CHECK",
+ STATUS_FLOAT_UNDERFLOW => "STATUS_FLOAT_UNDERFLOW",
+ STATUS_INTEGER_DIVIDE_BY_ZERO => "STATUS_INTEGER_DIVIDE_BY_ZERO",
+ STATUS_INTEGER_OVERFLOW => "STATUS_INTEGER_OVERFLOW",
+ STATUS_PRIVILEGED_INSTRUCTION => "STATUS_PRIVILEGED_INSTRUCTION",
+ STATUS_STACK_OVERFLOW => "STATUS_STACK_OVERFLOW",
+ STATUS_DLL_NOT_FOUND => "STATUS_DLL_NOT_FOUND",
+ STATUS_ORDINAL_NOT_FOUND => "STATUS_ORDINAL_NOT_FOUND",
+ STATUS_ENTRYPOINT_NOT_FOUND => "STATUS_ENTRYPOINT_NOT_FOUND",
+ STATUS_CONTROL_C_EXIT => "STATUS_CONTROL_C_EXIT",
+ STATUS_DLL_INIT_FAILED => "STATUS_DLL_INIT_FAILED",
+ STATUS_FLOAT_MULTIPLE_FAULTS => "STATUS_FLOAT_MULTIPLE_FAULTS",
+ STATUS_FLOAT_MULTIPLE_TRAPS => "STATUS_FLOAT_MULTIPLE_TRAPS",
+ STATUS_REG_NAT_CONSUMPTION => "STATUS_REG_NAT_CONSUMPTION",
+ STATUS_HEAP_CORRUPTION => "STATUS_HEAP_CORRUPTION",
+ STATUS_STACK_BUFFER_OVERRUN => "STATUS_STACK_BUFFER_OVERRUN",
+ STATUS_ASSERTION_FAILURE => "STATUS_ASSERTION_FAILURE",
+ _ => return None,
+ };
+ Some(extra.to_owned())
+ }
+
+ if let Some(extra) = detailed_exit_status(status) {
+ format!("{} ({})", basic_exit_status(status), extra)
+ } else {
+ basic_exit_status(status)
+ }
+}
+
+fn basic_exit_status(status: std::process::ExitStatus) -> String {
+ if let Some(code) = status.code() {
+ code.to_string()
+ } else {
+ "interrupted".to_owned()
+ }
+}
+
+#[cfg(feature = "cmd")]
+fn wait(
+ mut child: std::process::Child,
+ timeout: Option<std::time::Duration>,
+) -> std::io::Result<std::process::ExitStatus> {
+ if let Some(timeout) = timeout {
+ wait_timeout::ChildExt::wait_timeout(&mut child, timeout)
+ .transpose()
+ .unwrap_or_else(|| {
+ let _ = child.kill();
+ child.wait()
+ })
+ } else {
+ child.wait()
+ }
+}
+
+#[cfg(not(feature = "cmd"))]
+fn wait(
+ mut child: std::process::Child,
+ _timeout: Option<std::time::Duration>,
+) -> std::io::Result<std::process::ExitStatus> {
+ child.wait()
+}
+
+pub use snapbox_macros::cargo_bin;
+
+/// Look up the path to a cargo-built binary within an integration test.
+///
+/// **NOTE:** Prefer [`cargo_bin!`] as this makes assumptions about cargo
+pub fn cargo_bin(name: &str) -> std::path::PathBuf {
+ let file_name = format!("{}{}", name, std::env::consts::EXE_SUFFIX);
+ let target_dir = target_dir();
+ target_dir.join(&file_name)
+}
+
+// Adapted from
+// https://github.com/rust-lang/cargo/blob/485670b3983b52289a2f353d589c57fae2f60f82/tests/testsuite/support/mod.rs#L507
+fn target_dir() -> std::path::PathBuf {
+ std::env::current_exe()
+ .ok()
+ .map(|mut path| {
+ path.pop();
+ if path.ends_with("deps") {
+ path.pop();
+ }
+ path
+ })
+ .unwrap()
+}
diff --git a/vendor/snapbox/src/data.rs b/vendor/snapbox/src/data.rs
new file mode 100644
index 000000000..aa5f9b1ed
--- /dev/null
+++ b/vendor/snapbox/src/data.rs
@@ -0,0 +1,712 @@
+/// Test fixture, actual output, or expected result
+///
+/// This provides conveniences for tracking the intended format (binary vs text).
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Data {
+ inner: DataInner,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+enum DataInner {
+ Binary(Vec<u8>),
+ Text(String),
+ #[cfg(feature = "structured-data")]
+ Json(serde_json::Value),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Copy, Hash)]
+pub enum DataFormat {
+ Binary,
+ Text,
+ #[cfg(feature = "json")]
+ Json,
+}
+
+impl Default for DataFormat {
+ fn default() -> Self {
+ DataFormat::Text
+ }
+}
+
+impl Data {
+ /// Mark the data as binary (no post-processing)
+ pub fn binary(raw: impl Into<Vec<u8>>) -> Self {
+ Self {
+ inner: DataInner::Binary(raw.into()),
+ }
+ }
+
+ /// Mark the data as text (post-processing)
+ pub fn text(raw: impl Into<String>) -> Self {
+ Self {
+ inner: DataInner::Text(raw.into()),
+ }
+ }
+
+ #[cfg(feature = "json")]
+ pub fn json(raw: impl Into<serde_json::Value>) -> Self {
+ Self {
+ inner: DataInner::Json(raw.into()),
+ }
+ }
+
+ /// Empty test data
+ pub fn new() -> Self {
+ Self::text("")
+ }
+
+ /// Load test data from a file
+ pub fn read_from(
+ path: &std::path::Path,
+ data_format: Option<DataFormat>,
+ ) -> Result<Self, crate::Error> {
+ let data = match data_format {
+ Some(df) => match df {
+ DataFormat::Binary => {
+ let data = std::fs::read(&path)
+ .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+ Self::binary(data)
+ }
+ DataFormat::Text => {
+ let data = std::fs::read_to_string(&path)
+ .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+ Self::text(data)
+ }
+ #[cfg(feature = "json")]
+ DataFormat::Json => {
+ let data = std::fs::read_to_string(&path)
+ .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+ Self::json(serde_json::from_str::<serde_json::Value>(&data).unwrap())
+ }
+ },
+ None => {
+ let data = std::fs::read(&path)
+ .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+ let data = Self::binary(data);
+ match path
+ .extension()
+ .and_then(|e| e.to_str())
+ .unwrap_or_default()
+ {
+ #[cfg(feature = "json")]
+ "json" => data.try_coerce(DataFormat::Json),
+ _ => data.try_coerce(DataFormat::Text),
+ }
+ }
+ };
+ Ok(data)
+ }
+
+ /// Overwrite a snapshot
+ pub fn write_to(&self, path: &std::path::Path) -> Result<(), crate::Error> {
+ if let Some(parent) = path.parent() {
+ std::fs::create_dir_all(parent).map_err(|e| {
+ format!("Failed to create parent dir for {}: {}", path.display(), e)
+ })?;
+ }
+ std::fs::write(path, self.to_bytes())
+ .map_err(|e| format!("Failed to write {}: {}", path.display(), e).into())
+ }
+
+ /// Post-process text
+ ///
+ /// See [utils][crate::utils]
+ pub fn normalize(self, op: impl Normalize) -> Self {
+ op.normalize(self)
+ }
+
+ /// Return the underlying `String`
+ ///
+ /// Note: this will not inspect binary data for being a valid `String`.
+ pub fn render(&self) -> Option<String> {
+ match &self.inner {
+ DataInner::Binary(_) => None,
+ DataInner::Text(data) => Some(data.to_owned()),
+ #[cfg(feature = "json")]
+ DataInner::Json(value) => Some(serde_json::to_string_pretty(value).unwrap()),
+ }
+ }
+
+ pub fn to_bytes(&self) -> Vec<u8> {
+ match &self.inner {
+ DataInner::Binary(data) => data.clone(),
+ DataInner::Text(data) => data.clone().into_bytes(),
+ #[cfg(feature = "json")]
+ DataInner::Json(value) => serde_json::to_vec_pretty(value).unwrap(),
+ }
+ }
+
+ pub fn try_coerce(self, format: DataFormat) -> Self {
+ match (self.inner, format) {
+ (DataInner::Binary(inner), DataFormat::Binary) => Self::binary(inner),
+ (DataInner::Text(inner), DataFormat::Text) => Self::text(inner),
+ #[cfg(feature = "json")]
+ (DataInner::Json(inner), DataFormat::Json) => Self::json(inner),
+ (DataInner::Binary(inner), _) => {
+ if is_binary(&inner) {
+ Self::binary(inner)
+ } else {
+ match String::from_utf8(inner) {
+ Ok(str) => {
+ let coerced = Self::text(str).try_coerce(format);
+ // if the Text cannot be coerced into the correct format
+ // reset it back to Binary
+ if coerced.format() != format {
+ coerced.try_coerce(DataFormat::Binary)
+ } else {
+ coerced
+ }
+ }
+ Err(err) => {
+ let bin = err.into_bytes();
+ Self::binary(bin)
+ }
+ }
+ }
+ }
+ #[cfg(feature = "json")]
+ (DataInner::Text(inner), DataFormat::Json) => {
+ match serde_json::from_str::<serde_json::Value>(&inner) {
+ Ok(json) => Self::json(json),
+ Err(_) => Self::text(inner),
+ }
+ }
+ (inner, DataFormat::Binary) => Self::binary(Self { inner }.to_bytes()),
+ // This variant is already covered unless structured data is enabled
+ #[cfg(feature = "structured-data")]
+ (inner, DataFormat::Text) => {
+ let remake = Self { inner };
+ if let Some(str) = remake.render() {
+ Self::text(str)
+ } else {
+ remake
+ }
+ }
+ }
+ }
+
+ /// Outputs the current `DataFormat` of the underlying data
+ pub fn format(&self) -> DataFormat {
+ match &self.inner {
+ DataInner::Binary(_) => DataFormat::Binary,
+ DataInner::Text(_) => DataFormat::Text,
+ #[cfg(feature = "json")]
+ DataInner::Json(_) => DataFormat::Json,
+ }
+ }
+}
+
+impl std::fmt::Display for Data {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match &self.inner {
+ DataInner::Binary(data) => String::from_utf8_lossy(data).fmt(f),
+ DataInner::Text(data) => data.fmt(f),
+ #[cfg(feature = "json")]
+ DataInner::Json(data) => serde_json::to_string_pretty(data).unwrap().fmt(f),
+ }
+ }
+}
+
+impl Default for Data {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<'d> From<&'d Data> for Data {
+ fn from(other: &'d Data) -> Self {
+ other.clone()
+ }
+}
+
+impl From<Vec<u8>> for Data {
+ fn from(other: Vec<u8>) -> Self {
+ Self::binary(other)
+ }
+}
+
+impl<'b> From<&'b [u8]> for Data {
+ fn from(other: &'b [u8]) -> Self {
+ other.to_owned().into()
+ }
+}
+
+impl From<String> for Data {
+ fn from(other: String) -> Self {
+ Self::text(other)
+ }
+}
+
+impl<'s> From<&'s String> for Data {
+ fn from(other: &'s String) -> Self {
+ other.clone().into()
+ }
+}
+
+impl<'s> From<&'s str> for Data {
+ fn from(other: &'s str) -> Self {
+ other.to_owned().into()
+ }
+}
+
+pub trait Normalize {
+ fn normalize(&self, data: Data) -> Data;
+}
+
+pub struct NormalizeNewlines;
+impl Normalize for NormalizeNewlines {
+ fn normalize(&self, data: Data) -> Data {
+ match data.inner {
+ DataInner::Binary(bin) => Data::binary(bin),
+ DataInner::Text(text) => {
+ let lines = crate::utils::normalize_lines(&text);
+ Data::text(lines)
+ }
+ #[cfg(feature = "json")]
+ DataInner::Json(value) => {
+ let mut value = value;
+ normalize_value(&mut value, crate::utils::normalize_lines);
+ Data::json(value)
+ }
+ }
+ }
+}
+
+pub struct NormalizePaths;
+impl Normalize for NormalizePaths {
+ fn normalize(&self, data: Data) -> Data {
+ match data.inner {
+ DataInner::Binary(bin) => Data::binary(bin),
+ DataInner::Text(text) => {
+ let lines = crate::utils::normalize_paths(&text);
+ Data::text(lines)
+ }
+ #[cfg(feature = "json")]
+ DataInner::Json(value) => {
+ let mut value = value;
+ normalize_value(&mut value, crate::utils::normalize_paths);
+ Data::json(value)
+ }
+ }
+ }
+}
+
+pub struct NormalizeMatches<'a> {
+ substitutions: &'a crate::Substitutions,
+ pattern: &'a Data,
+}
+
+impl<'a> NormalizeMatches<'a> {
+ pub fn new(substitutions: &'a crate::Substitutions, pattern: &'a Data) -> Self {
+ NormalizeMatches {
+ substitutions,
+ pattern,
+ }
+ }
+}
+
+impl Normalize for NormalizeMatches<'_> {
+ fn normalize(&self, data: Data) -> Data {
+ match data.inner {
+ DataInner::Binary(bin) => Data::binary(bin),
+ DataInner::Text(text) => {
+ let lines = self
+ .substitutions
+ .normalize(&text, &self.pattern.render().unwrap());
+ Data::text(lines)
+ }
+ #[cfg(feature = "json")]
+ DataInner::Json(value) => {
+ let mut value = value;
+ if let DataInner::Json(exp) = &self.pattern.inner {
+ normalize_value_matches(&mut value, exp, self.substitutions);
+ }
+ Data::json(value)
+ }
+ }
+ }
+}
+
+#[cfg(feature = "structured-data")]
+fn normalize_value(value: &mut serde_json::Value, op: fn(&str) -> String) {
+ match value {
+ serde_json::Value::String(str) => {
+ *str = op(str);
+ }
+ serde_json::Value::Array(arr) => {
+ arr.iter_mut().for_each(|value| normalize_value(value, op));
+ }
+ serde_json::Value::Object(obj) => {
+ obj.iter_mut()
+ .for_each(|(_, value)| normalize_value(value, op));
+ }
+ _ => {}
+ }
+}
+
+#[cfg(feature = "structured-data")]
+fn normalize_value_matches(
+ actual: &mut serde_json::Value,
+ expected: &serde_json::Value,
+ substitutions: &crate::Substitutions,
+) {
+ use serde_json::Value::*;
+ match (actual, expected) {
+ // "{...}" is a wildcard
+ (act, String(exp)) if exp == "{...}" => {
+ *act = serde_json::json!("{...}");
+ }
+ (String(act), String(exp)) => {
+ *act = substitutions.normalize(act, exp);
+ }
+ (Array(act), Array(exp)) => {
+ act.iter_mut()
+ .zip(exp)
+ .for_each(|(a, e)| normalize_value_matches(a, e, substitutions));
+ }
+ (Object(act), Object(exp)) => {
+ act.iter_mut()
+ .zip(exp)
+ .filter(|(a, e)| a.0 == e.0)
+ .for_each(|(a, e)| normalize_value_matches(a.1, e.1, substitutions));
+ }
+ (_, _) => {}
+ }
+}
+
+#[cfg(feature = "detect-encoding")]
+fn is_binary(data: &[u8]) -> bool {
+ match content_inspector::inspect(data) {
+ content_inspector::ContentType::BINARY |
+ // We don't support these
+ content_inspector::ContentType::UTF_16LE |
+ content_inspector::ContentType::UTF_16BE |
+ content_inspector::ContentType::UTF_32LE |
+ content_inspector::ContentType::UTF_32BE => {
+ true
+ },
+ content_inspector::ContentType::UTF_8 |
+ content_inspector::ContentType::UTF_8_BOM => {
+ false
+ },
+ }
+}
+
+#[cfg(not(feature = "detect-encoding"))]
+fn is_binary(_data: &[u8]) -> bool {
+ false
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[cfg(feature = "json")]
+ use serde_json::json;
+
+ // Tests for checking to_bytes and render produce the same results
+ #[test]
+ fn text_to_bytes_render() {
+ let d = Data::text(String::from("test"));
+ let bytes = d.to_bytes();
+ let bytes = String::from_utf8(bytes).unwrap();
+ let rendered = d.render().unwrap();
+ assert_eq!(bytes, rendered);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_to_bytes_render() {
+ let d = Data::json(json!({"name": "John\\Doe\r\n"}));
+ let bytes = d.to_bytes();
+ let bytes = String::from_utf8(bytes).unwrap();
+ let rendered = d.render().unwrap();
+ assert_eq!(bytes, rendered);
+ }
+
+ // Tests for checking all types are coercible to each other and
+ // for when the coercion should fail
+ #[test]
+ fn binary_to_text() {
+ let binary = String::from("test").into_bytes();
+ let d = Data::binary(binary);
+ let text = d.try_coerce(DataFormat::Text);
+ assert_eq!(DataFormat::Text, text.format())
+ }
+
+ #[test]
+ fn binary_to_text_not_utf8() {
+ let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec();
+ let d = Data::binary(binary);
+ let d = d.try_coerce(DataFormat::Text);
+ assert_ne!(DataFormat::Text, d.format());
+ assert_eq!(DataFormat::Binary, d.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn binary_to_json() {
+ let value = json!({"name": "John\\Doe\r\n"});
+ let binary = serde_json::to_vec_pretty(&value).unwrap();
+ let d = Data::binary(binary);
+ let json = d.try_coerce(DataFormat::Json);
+ assert_eq!(DataFormat::Json, json.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn binary_to_json_not_utf8() {
+ let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec();
+ let d = Data::binary(binary);
+ let d = d.try_coerce(DataFormat::Json);
+ assert_ne!(DataFormat::Json, d.format());
+ assert_eq!(DataFormat::Binary, d.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn binary_to_json_not_json() {
+ let binary = String::from("test").into_bytes();
+ let d = Data::binary(binary);
+ let d = d.try_coerce(DataFormat::Json);
+ assert_ne!(DataFormat::Json, d.format());
+ assert_eq!(DataFormat::Binary, d.format());
+ }
+
+ #[test]
+ fn text_to_binary() {
+ let text = String::from("test");
+ let d = Data::text(text);
+ let binary = d.try_coerce(DataFormat::Binary);
+ assert_eq!(DataFormat::Binary, binary.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn text_to_json() {
+ let value = json!({"name": "John\\Doe\r\n"});
+ let text = serde_json::to_string_pretty(&value).unwrap();
+ let d = Data::text(text);
+ let json = d.try_coerce(DataFormat::Json);
+ assert_eq!(DataFormat::Json, json.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn text_to_json_not_json() {
+ let text = String::from("test");
+ let d = Data::text(text);
+ let json = d.try_coerce(DataFormat::Json);
+ assert_eq!(DataFormat::Text, json.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_to_binary() {
+ let value = json!({"name": "John\\Doe\r\n"});
+ let d = Data::json(value);
+ let binary = d.try_coerce(DataFormat::Binary);
+ assert_eq!(DataFormat::Binary, binary.format());
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_to_text() {
+ let value = json!({"name": "John\\Doe\r\n"});
+ let d = Data::json(value);
+ let text = d.try_coerce(DataFormat::Text);
+ assert_eq!(DataFormat::Text, text.format());
+ }
+
+ // Tests for coercible conversions create the same output as to_bytes/render
+ //
+ // render does not need to be checked against bin -> text since render
+ // outputs None for binary
+ #[test]
+ fn text_to_bin_coerce_equals_to_bytes() {
+ let text = String::from("test");
+ let d = Data::text(text);
+ let binary = d.clone().try_coerce(DataFormat::Binary);
+ assert_eq!(Data::binary(d.to_bytes()), binary);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_to_bin_coerce_equals_to_bytes() {
+ let json = json!({"name": "John\\Doe\r\n"});
+ let d = Data::json(json);
+ let binary = d.clone().try_coerce(DataFormat::Binary);
+ assert_eq!(Data::binary(d.to_bytes()), binary);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_to_text_coerce_equals_render() {
+ let json = json!({"name": "John\\Doe\r\n"});
+ let d = Data::json(json);
+ let text = d.clone().try_coerce(DataFormat::Text);
+ assert_eq!(Data::text(d.render().unwrap()), text);
+ }
+
+ // Tests for normalization on json
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_paths_and_lines() {
+ let json = json!({"name": "John\\Doe\r\n"});
+ let data = Data::json(json);
+ let data = data.normalize(NormalizePaths);
+ assert_eq!(Data::json(json!({"name": "John/Doe\r\n"})), data);
+ let data = data.normalize(NormalizeNewlines);
+ assert_eq!(Data::json(json!({"name": "John/Doe\n"})), data);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_obj_paths_and_lines() {
+ let json = json!({
+ "person": {
+ "name": "John\\Doe\r\n",
+ "nickname": "Jo\\hn\r\n",
+ }
+ });
+ let data = Data::json(json);
+ let data = data.normalize(NormalizePaths);
+ let assert = json!({
+ "person": {
+ "name": "John/Doe\r\n",
+ "nickname": "Jo/hn\r\n",
+ }
+ });
+ assert_eq!(Data::json(assert), data);
+ let data = data.normalize(NormalizeNewlines);
+ let assert = json!({
+ "person": {
+ "name": "John/Doe\n",
+ "nickname": "Jo/hn\n",
+ }
+ });
+ assert_eq!(Data::json(assert), data);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_array_paths_and_lines() {
+ let json = json!({"people": ["John\\Doe\r\n", "Jo\\hn\r\n"]});
+ let data = Data::json(json);
+ let data = data.normalize(NormalizePaths);
+ let paths = json!({"people": ["John/Doe\r\n", "Jo/hn\r\n"]});
+ assert_eq!(Data::json(paths), data);
+ let data = data.normalize(NormalizeNewlines);
+ let new_lines = json!({"people": ["John/Doe\n", "Jo/hn\n"]});
+ assert_eq!(Data::json(new_lines), data);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_array_obj_paths_and_lines() {
+ let json = json!({
+ "people": [
+ {
+ "name": "John\\Doe\r\n",
+ "nickname": "Jo\\hn\r\n",
+ }
+ ]
+ });
+ let data = Data::json(json);
+ let data = data.normalize(NormalizePaths);
+ let paths = json!({
+ "people": [
+ {
+ "name": "John/Doe\r\n",
+ "nickname": "Jo/hn\r\n",
+ }
+ ]
+ });
+ assert_eq!(Data::json(paths), data);
+ let data = data.normalize(NormalizeNewlines);
+ let new_lines = json!({
+ "people": [
+ {
+ "name": "John/Doe\n",
+ "nickname": "Jo/hn\n",
+ }
+ ]
+ });
+ assert_eq!(Data::json(new_lines), data);
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_matches_string() {
+ let exp = json!({"name": "{...}"});
+ let expected = Data::json(exp);
+ let actual = json!({"name": "JohnDoe"});
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_eq!(exp, act);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_matches_array() {
+ let exp = json!({"people": "{...}"});
+ let expected = Data::json(exp);
+ let actual = json!({
+ "people": [
+ {
+ "name": "JohnDoe",
+ "nickname": "John",
+ }
+ ]
+ });
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_eq!(exp, act);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_matches_obj() {
+ let exp = json!({"people": "{...}"});
+ let expected = Data::json(exp);
+ let actual = json!({
+ "people": {
+ "name": "JohnDoe",
+ "nickname": "John",
+ }
+ });
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_eq!(exp, act);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "json")]
+ fn json_normalize_matches_diff_order_array() {
+ let exp = json!({
+ "people": ["John", "Jane"]
+ });
+ let expected = Data::json(exp);
+ let actual = json!({
+ "people": ["Jane", "John"]
+ });
+ let actual = Data::json(actual).normalize(NormalizeMatches {
+ substitutions: &Default::default(),
+ pattern: &expected,
+ });
+ if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+ assert_ne!(exp, act);
+ }
+ }
+}
diff --git a/vendor/snapbox/src/error.rs b/vendor/snapbox/src/error.rs
new file mode 100644
index 000000000..55e901883
--- /dev/null
+++ b/vendor/snapbox/src/error.rs
@@ -0,0 +1,95 @@
+#[derive(Clone, Debug)]
+pub struct Error {
+ inner: String,
+ backtrace: Option<Backtrace>,
+}
+
+impl Error {
+ pub fn new(inner: impl std::fmt::Display) -> Self {
+ Self::with_string(inner.to_string())
+ }
+
+ fn with_string(inner: String) -> Self {
+ Self {
+ inner,
+ backtrace: Backtrace::new(),
+ }
+ }
+}
+
+impl PartialEq for Error {
+ fn eq(&self, other: &Self) -> bool {
+ self.inner == other.inner
+ }
+}
+
+impl Eq for Error {}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ writeln!(f, "{}", self.inner)?;
+ if let Some(backtrace) = self.backtrace.as_ref() {
+ writeln!(f)?;
+ writeln!(f, "Backtrace:")?;
+ writeln!(f, "{}", backtrace)?;
+ }
+ Ok(())
+ }
+}
+
+impl std::error::Error for Error {}
+
+impl<'s> From<&'s str> for Error {
+ fn from(other: &'s str) -> Self {
+ Self::with_string(other.to_owned())
+ }
+}
+
+impl<'s> From<&'s String> for Error {
+ fn from(other: &'s String) -> Self {
+ Self::with_string(other.clone())
+ }
+}
+
+impl From<String> for Error {
+ fn from(other: String) -> Self {
+ Self::with_string(other)
+ }
+}
+
+#[cfg(feature = "debug")]
+#[derive(Debug, Clone)]
+struct Backtrace(backtrace::Backtrace);
+
+#[cfg(feature = "debug")]
+impl Backtrace {
+ fn new() -> Option<Self> {
+ Some(Self(backtrace::Backtrace::new()))
+ }
+}
+
+#[cfg(feature = "debug")]
+impl std::fmt::Display for Backtrace {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ // `backtrace::Backtrace` uses `Debug` instead of `Display`
+ write!(f, "{:?}", self.0)
+ }
+}
+
+#[cfg(not(feature = "debug"))]
+#[derive(Debug, Copy, Clone)]
+struct Backtrace;
+
+#[cfg(not(feature = "debug"))]
+impl Backtrace {
+ fn new() -> Option<Self> {
+ None
+ }
+}
+
+#[cfg(not(feature = "debug"))]
+impl std::fmt::Display for Backtrace {
+ fn fmt(&self, _: &mut std::fmt::Formatter) -> std::fmt::Result {
+ Ok(())
+ }
+}
diff --git a/vendor/snapbox/src/harness.rs b/vendor/snapbox/src/harness.rs
new file mode 100644
index 000000000..ee1035aaa
--- /dev/null
+++ b/vendor/snapbox/src/harness.rs
@@ -0,0 +1,212 @@
+//! [`Harness`] for discovering test inputs and asserting against snapshot files
+//!
+//! # Examples
+//!
+//! ```rust,no_run
+//! snapbox::harness::Harness::new(
+//! "tests/fixtures/invalid",
+//! setup,
+//! test,
+//! )
+//! .select(["tests/cases/*.in"])
+//! .action_env("SNAPSHOTS")
+//! .test();
+//!
+//! fn setup(input_path: std::path::PathBuf) -> snapbox::harness::Case {
+//! let name = input_path.file_name().unwrap().to_str().unwrap().to_owned();
+//! let expected = input_path.with_extension("out");
+//! snapbox::harness::Case {
+//! name,
+//! fixture: input_path,
+//! expected,
+//! }
+//! }
+//!
+//! fn test(input_path: &std::path::Path) -> Result<usize, Box<dyn std::error::Error>> {
+//! let raw = std::fs::read_to_string(input_path)?;
+//! let num = raw.parse::<usize>()?;
+//!
+//! let actual = num + 10;
+//!
+//! Ok(actual)
+//! }
+//! ```
+
+use crate::data::{DataFormat, NormalizeNewlines};
+use crate::Action;
+
+use libtest_mimic::Trial;
+
+pub struct Harness<S, T> {
+ root: std::path::PathBuf,
+ overrides: Option<ignore::overrides::Override>,
+ setup: S,
+ test: T,
+ action: Action,
+}
+
+impl<S, T, I, E> Harness<S, T>
+where
+ I: std::fmt::Display,
+ E: std::fmt::Display,
+ S: Fn(std::path::PathBuf) -> Case + Send + Sync + 'static,
+ T: Fn(&std::path::Path) -> Result<I, E> + Send + Sync + 'static + Clone,
+{
+ pub fn new(root: impl Into<std::path::PathBuf>, setup: S, test: T) -> Self {
+ Self {
+ root: root.into(),
+ overrides: None,
+ setup,
+ test,
+ action: Action::Verify,
+ }
+ }
+
+ /// Path patterns for selecting input files
+ ///
+ /// This used gitignore syntax
+ pub fn select<'p>(mut self, patterns: impl IntoIterator<Item = &'p str>) -> Self {
+ let mut overrides = ignore::overrides::OverrideBuilder::new(&self.root);
+ for line in patterns {
+ overrides.add(line).unwrap();
+ }
+ self.overrides = Some(overrides.build().unwrap());
+ 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
+ }
+
+ /// Override the failure action
+ pub fn action(mut self, action: Action) -> Self {
+ self.action = action;
+ self
+ }
+
+ /// Run tests
+ pub fn test(self) -> ! {
+ let mut walk = ignore::WalkBuilder::new(&self.root);
+ walk.standard_filters(false);
+ let tests = walk.build().filter_map(|entry| {
+ let entry = entry.unwrap();
+ let is_dir = entry.file_type().map(|f| f.is_dir()).unwrap_or(false);
+ let path = entry.into_path();
+ if let Some(overrides) = &self.overrides {
+ overrides
+ .matched(&path, is_dir)
+ .is_whitelist()
+ .then(|| path)
+ } else {
+ Some(path)
+ }
+ });
+
+ let tests: Vec<_> = tests
+ .into_iter()
+ .map(|path| {
+ let case = (self.setup)(path);
+ let test = self.test.clone();
+ Trial::test(case.name.clone(), move || {
+ let actual = (test)(&case.fixture)?;
+ let actual = actual.to_string();
+ let actual = crate::Data::text(actual).normalize(NormalizeNewlines);
+ let verify = Verifier::new()
+ .palette(crate::report::Palette::auto())
+ .action(self.action);
+ verify.verify(&case.expected, actual)?;
+ Ok(())
+ })
+ .with_ignored_flag(self.action == Action::Ignore)
+ })
+ .collect();
+
+ let args = libtest_mimic::Arguments::from_args();
+ libtest_mimic::run(&args, tests).exit()
+ }
+}
+
+struct Verifier {
+ palette: crate::report::Palette,
+ action: Action,
+}
+
+impl Verifier {
+ fn new() -> Self {
+ Default::default()
+ }
+
+ fn palette(mut self, palette: crate::report::Palette) -> Self {
+ self.palette = palette;
+ self
+ }
+
+ fn action(mut self, action: Action) -> Self {
+ self.action = action;
+ self
+ }
+
+ fn verify(&self, expected_path: &std::path::Path, actual: crate::Data) -> crate::Result<()> {
+ match self.action {
+ Action::Skip => Ok(()),
+ Action::Ignore => {
+ let _ = self.try_verify(expected_path, actual);
+ Ok(())
+ }
+ Action::Verify => self.try_verify(expected_path, actual),
+ Action::Overwrite => self.try_overwrite(expected_path, actual),
+ }
+ }
+
+ fn try_overwrite(
+ &self,
+ expected_path: &std::path::Path,
+ actual: crate::Data,
+ ) -> crate::Result<()> {
+ actual.write_to(expected_path)?;
+ Ok(())
+ }
+
+ fn try_verify(
+ &self,
+ expected_path: &std::path::Path,
+ actual: crate::Data,
+ ) -> crate::Result<()> {
+ let expected = crate::Data::read_from(expected_path, Some(DataFormat::Text))?
+ .normalize(NormalizeNewlines);
+
+ if expected != actual {
+ let mut buf = String::new();
+ crate::report::write_diff(
+ &mut buf,
+ &expected,
+ &actual,
+ Some(&expected_path.display()),
+ None,
+ self.palette,
+ )
+ .map_err(|e| e.to_string())?;
+ Err(buf.into())
+ } else {
+ Ok(())
+ }
+ }
+}
+
+impl Default for Verifier {
+ fn default() -> Self {
+ Self {
+ palette: crate::report::Palette::auto(),
+ action: Action::Verify,
+ }
+ }
+}
+
+pub struct Case {
+ pub name: String,
+ pub fixture: std::path::PathBuf,
+ pub expected: std::path::PathBuf,
+}
diff --git a/vendor/snapbox/src/lib.rs b/vendor/snapbox/src/lib.rs
new file mode 100644
index 000000000..61419fd5e
--- /dev/null
+++ b/vendor/snapbox/src/lib.rs
@@ -0,0 +1,246 @@
+//! # Snapshot testing toolbox
+//!
+//! > When you have to treat your tests like pets, instead of [cattle][trycmd]
+//!
+//! `snapbox` is a snapshot-testing toolbox that is ready to use for verifying output from
+//! - Function return values
+//! - CLI stdout/stderr
+//! - Filesystem changes
+//!
+//! It is also flexible enough to build your own test harness like [trycmd](https://crates.io/crates/trycmd).
+//!
+//! ## Which tool is right
+//!
+//! - [cram](https://bitheap.org/cram/): End-to-end CLI snapshotting agnostic of any programming language
+//! - [trycmd](https://crates.io/crates/trycmd): For running a lot of blunt tests (limited test predicates)
+//! - Particular attention is given to allow the test data to be pulled into documentation, like
+//! with [mdbook](https://rust-lang.github.io/mdBook/)
+//! - `snapbox`: When you want something like `trycmd` in one off
+//! cases or you need to customize `trycmd`s behavior.
+//! - [assert_cmd](https://crates.io/crates/assert_cmd) +
+//! [assert_fs](https://crates.io/crates/assert_fs): Test cases follow a certain pattern but
+//! special attention is needed in how to verify the results.
+//! - Hand-written test cases: for peculiar circumstances
+//!
+//! ## Getting Started
+//!
+//! Testing Functions:
+//! - [`assert_eq`][crate::assert_eq] and [`assert_matches`] for reusing diffing / pattern matching for non-snapshot testing
+//! - [`assert_eq_path`][crate::assert_eq_path] and [`assert_matches_path`] for one-off assertions with the snapshot stored in a file
+//! - [`harness::Harness`] for discovering test inputs and asserting against snapshot files:
+//!
+//! Testing Commands:
+//! - [`cmd::Command`]: Process spawning for testing of non-interactive commands
+//! - [`cmd::OutputAssert`]: Assert the state of a [`Command`][cmd::Command]'s
+//! [`Output`][std::process::Output].
+//!
+//! Testing Filesystem Interactions:
+//! - [`path::PathFixture`]: Working directory for tests
+//! - [`Assert`]: Diff a directory against files present in a pattern directory
+//!
+//! You can also build your own version of these with the lower-level building blocks these are
+//! made of.
+//!
+#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
+//!
+//! # Examples
+//!
+//! [`assert_matches`]
+//! ```rust
+//! snapbox::assert_matches("Hello [..] people!", "Hello many people!");
+//! ```
+//!
+//! [`Assert`]
+//! ```rust,no_run
+//! let actual = "...";
+//! let expected_path = "tests/fixtures/help_output_is_clean.txt";
+//! snapbox::Assert::new()
+//! .action_env("SNAPSHOTS")
+//! .matches_path(expected_path, actual);
+//! ```
+//!
+//! [`harness::Harness`]
+#![cfg_attr(not(feature = "harness"), doc = " ```rust,ignore")]
+#![cfg_attr(feature = "harness", doc = " ```rust,no_run")]
+//! snapbox::harness::Harness::new(
+//! "tests/fixtures/invalid",
+//! setup,
+//! test,
+//! )
+//! .select(["tests/cases/*.in"])
+//! .action_env("SNAPSHOTS")
+//! .test();
+//!
+//! fn setup(input_path: std::path::PathBuf) -> snapbox::harness::Case {
+//! let name = input_path.file_name().unwrap().to_str().unwrap().to_owned();
+//! let expected = input_path.with_extension("out");
+//! snapbox::harness::Case {
+//! name,
+//! fixture: input_path,
+//! expected,
+//! }
+//! }
+//!
+//! fn test(input_path: &std::path::Path) -> Result<usize, Box<dyn std::error::Error>> {
+//! let raw = std::fs::read_to_string(input_path)?;
+//! let num = raw.parse::<usize>()?;
+//!
+//! let actual = num + 10;
+//!
+//! Ok(actual)
+//! }
+//! ```
+//!
+//! [trycmd]: https://docs.rs/trycmd
+
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
+
+mod action;
+mod assert;
+mod data;
+mod error;
+mod substitutions;
+
+pub mod cmd;
+pub mod path;
+pub mod report;
+pub mod utils;
+
+#[cfg(feature = "harness")]
+pub mod harness;
+
+pub use action::Action;
+pub use action::DEFAULT_ACTION_ENV;
+pub use assert::Assert;
+pub use data::Data;
+pub use data::DataFormat;
+pub use data::{Normalize, NormalizeMatches, NormalizeNewlines, NormalizePaths};
+pub use error::Error;
+pub use snapbox_macros::debug;
+pub use substitutions::Substitutions;
+
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+/// 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_matches(expected, output);
+/// ```
+#[track_caller]
+pub fn assert_eq(expected: impl Into<crate::Data>, actual: impl Into<crate::Data>) {
+ Assert::new().eq(expected, actual);
+}
+
+/// 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_matches(expected, output);
+/// ```
+#[track_caller]
+pub fn assert_matches(pattern: impl Into<crate::Data>, actual: impl Into<crate::Data>) {
+ Assert::new().matches(pattern, actual);
+}
+
+/// 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_eq_path(expected_path, output);
+/// ```
+#[track_caller]
+pub fn assert_eq_path(expected_path: impl AsRef<std::path::Path>, actual: impl Into<crate::Data>) {
+ Assert::new()
+ .action_env(DEFAULT_ACTION_ENV)
+ .eq_path(expected_path, actual);
+}
+
+/// 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
+///
+/// Normalization:
+/// - Newlines
+/// - `\` to `/`
+///
+/// ```rust,no_run
+/// let output = "something";
+/// let expected_path = "tests/snapshots/output.txt";
+/// snapbox::assert_matches_path(expected_path, output);
+/// ```
+#[track_caller]
+pub fn assert_matches_path(
+ pattern_path: impl AsRef<std::path::Path>,
+ actual: impl Into<crate::Data>,
+) {
+ Assert::new()
+ .action_env(DEFAULT_ACTION_ENV)
+ .matches_path(pattern_path, actual);
+}
+
+/// Check if a path matches the content of another path, recursively
+///
+/// When the content is text, newlines are normalized.
+///
+/// ```rust,no_run
+/// let output_root = "...";
+/// let expected_root = "tests/snapshots/output.txt";
+/// snapbox::assert_subset_eq(expected_root, output_root);
+/// ```
+#[cfg(feature = "path")]
+#[track_caller]
+pub fn assert_subset_eq(
+ expected_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+) {
+ Assert::new()
+ .action_env(DEFAULT_ACTION_ENV)
+ .subset_eq(expected_root, actual_root);
+}
+
+/// Check if a path matches the pattern of another path, recursively
+///
+/// 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,no_run
+/// let output_root = "...";
+/// let expected_root = "tests/snapshots/output.txt";
+/// snapbox::assert_subset_matches(expected_root, output_root);
+/// ```
+#[cfg(feature = "path")]
+#[track_caller]
+pub fn assert_subset_matches(
+ pattern_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+) {
+ Assert::new()
+ .action_env(DEFAULT_ACTION_ENV)
+ .subset_matches(pattern_root, actual_root);
+}
diff --git a/vendor/snapbox/src/path.rs b/vendor/snapbox/src/path.rs
new file mode 100644
index 000000000..16e4ef653
--- /dev/null
+++ b/vendor/snapbox/src/path.rs
@@ -0,0 +1,686 @@
+//! Initialize working directories and assert on how they've changed
+
+use crate::data::{NormalizeMatches, NormalizeNewlines, NormalizePaths};
+/// Working directory for tests
+#[derive(Debug)]
+pub struct PathFixture(PathFixtureInner);
+
+#[derive(Debug)]
+enum PathFixtureInner {
+ None,
+ Immutable(std::path::PathBuf),
+ #[cfg(feature = "path")]
+ MutablePath(std::path::PathBuf),
+ #[cfg(feature = "path")]
+ MutableTemp {
+ temp: tempfile::TempDir,
+ path: std::path::PathBuf,
+ },
+}
+
+impl PathFixture {
+ pub fn none() -> Self {
+ Self(PathFixtureInner::None)
+ }
+
+ pub fn immutable(target: &std::path::Path) -> Self {
+ Self(PathFixtureInner::Immutable(target.to_owned()))
+ }
+
+ #[cfg(feature = "path")]
+ pub fn mutable_temp() -> Result<Self, crate::Error> {
+ let temp = tempfile::tempdir().map_err(|e| e.to_string())?;
+ // We need to get the `/private` prefix on Mac so variable substitutions work
+ // correctly
+ let path = canonicalize(temp.path())
+ .map_err(|e| format!("Failed to canonicalize {}: {}", temp.path().display(), e))?;
+ Ok(Self(PathFixtureInner::MutableTemp { temp, path }))
+ }
+
+ #[cfg(feature = "path")]
+ pub fn mutable_at(target: &std::path::Path) -> Result<Self, crate::Error> {
+ let _ = std::fs::remove_dir_all(&target);
+ std::fs::create_dir_all(&target)
+ .map_err(|e| format!("Failed to create {}: {}", target.display(), e))?;
+ Ok(Self(PathFixtureInner::MutablePath(target.to_owned())))
+ }
+
+ #[cfg(feature = "path")]
+ pub fn with_template(self, template_root: &std::path::Path) -> Result<Self, crate::Error> {
+ match &self.0 {
+ PathFixtureInner::None | PathFixtureInner::Immutable(_) => {
+ return Err("Sandboxing is disabled".into());
+ }
+ PathFixtureInner::MutablePath(path) | PathFixtureInner::MutableTemp { path, .. } => {
+ crate::debug!(
+ "Initializing {} from {}",
+ path.display(),
+ template_root.display()
+ );
+ copy_template(template_root, path)?;
+ }
+ }
+
+ Ok(self)
+ }
+
+ pub fn is_mutable(&self) -> bool {
+ match &self.0 {
+ PathFixtureInner::None | PathFixtureInner::Immutable(_) => false,
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutablePath(_) => true,
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutableTemp { .. } => true,
+ }
+ }
+
+ pub fn path(&self) -> Option<&std::path::Path> {
+ match &self.0 {
+ PathFixtureInner::None => None,
+ PathFixtureInner::Immutable(path) => Some(path.as_path()),
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutablePath(path) => Some(path.as_path()),
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutableTemp { path, .. } => Some(path.as_path()),
+ }
+ }
+
+ /// Explicitly close to report errors
+ pub fn close(self) -> Result<(), std::io::Error> {
+ match self.0 {
+ PathFixtureInner::None | PathFixtureInner::Immutable(_) => Ok(()),
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutablePath(_) => Ok(()),
+ #[cfg(feature = "path")]
+ PathFixtureInner::MutableTemp { temp, .. } => temp.close(),
+ }
+ }
+}
+
+impl Default for PathFixture {
+ fn default() -> Self {
+ Self::none()
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum PathDiff {
+ Failure(crate::Error),
+ TypeMismatch {
+ expected_path: std::path::PathBuf,
+ actual_path: std::path::PathBuf,
+ expected_type: FileType,
+ actual_type: FileType,
+ },
+ LinkMismatch {
+ expected_path: std::path::PathBuf,
+ actual_path: std::path::PathBuf,
+ expected_target: std::path::PathBuf,
+ actual_target: std::path::PathBuf,
+ },
+ ContentMismatch {
+ expected_path: std::path::PathBuf,
+ actual_path: std::path::PathBuf,
+ expected_content: crate::Data,
+ actual_content: crate::Data,
+ },
+}
+
+impl PathDiff {
+ /// Report differences between `actual_root` and `pattern_root`
+ ///
+ /// Note: Requires feature flag `path`
+ #[cfg(feature = "path")]
+ pub fn subset_eq_iter(
+ pattern_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+ ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> {
+ let pattern_root = pattern_root.into();
+ let actual_root = actual_root.into();
+ Self::subset_eq_iter_inner(pattern_root, actual_root)
+ }
+
+ #[cfg(feature = "path")]
+ pub(crate) fn subset_eq_iter_inner(
+ expected_root: std::path::PathBuf,
+ actual_root: std::path::PathBuf,
+ ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> {
+ let walker = Walk::new(&expected_root);
+ walker.map(move |r| {
+ let expected_path = r.map_err(|e| Self::Failure(e.to_string().into()))?;
+ let rel = expected_path.strip_prefix(&expected_root).unwrap();
+ let actual_path = actual_root.join(rel);
+
+ let expected_type = FileType::from_path(&expected_path);
+ let actual_type = FileType::from_path(&actual_path);
+ if expected_type != actual_type {
+ return Err(Self::TypeMismatch {
+ expected_path,
+ actual_path,
+ expected_type,
+ actual_type,
+ });
+ }
+
+ match expected_type {
+ FileType::Symlink => {
+ let expected_target = std::fs::read_link(&expected_path).ok();
+ let actual_target = std::fs::read_link(&actual_path).ok();
+ if expected_target != actual_target {
+ return Err(Self::LinkMismatch {
+ expected_path,
+ actual_path,
+ expected_target: expected_target.unwrap(),
+ actual_target: actual_target.unwrap(),
+ });
+ }
+ }
+ FileType::File => {
+ let mut actual =
+ crate::Data::read_from(&actual_path, None).map_err(Self::Failure)?;
+
+ let expected = crate::Data::read_from(&expected_path, None)
+ .map(|d| d.normalize(NormalizeNewlines))
+ .map_err(Self::Failure)?;
+
+ actual = actual
+ .try_coerce(expected.format())
+ .normalize(NormalizeNewlines);
+
+ if expected != actual {
+ return Err(Self::ContentMismatch {
+ expected_path,
+ actual_path,
+ expected_content: expected,
+ actual_content: actual,
+ });
+ }
+ }
+ FileType::Dir | FileType::Unknown | FileType::Missing => {}
+ }
+
+ Ok((expected_path, actual_path))
+ })
+ }
+
+ /// Report differences between `actual_root` and `pattern_root`
+ ///
+ /// Note: Requires feature flag `path`
+ #[cfg(feature = "path")]
+ pub fn subset_matches_iter(
+ pattern_root: impl Into<std::path::PathBuf>,
+ actual_root: impl Into<std::path::PathBuf>,
+ substitutions: &crate::Substitutions,
+ ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> + '_ {
+ let pattern_root = pattern_root.into();
+ let actual_root = actual_root.into();
+ Self::subset_matches_iter_inner(pattern_root, actual_root, substitutions)
+ }
+
+ #[cfg(feature = "path")]
+ pub(crate) fn subset_matches_iter_inner(
+ expected_root: std::path::PathBuf,
+ actual_root: std::path::PathBuf,
+ substitutions: &crate::Substitutions,
+ ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> + '_ {
+ let walker = Walk::new(&expected_root);
+ walker.map(move |r| {
+ let expected_path = r.map_err(|e| Self::Failure(e.to_string().into()))?;
+ let rel = expected_path.strip_prefix(&expected_root).unwrap();
+ let actual_path = actual_root.join(rel);
+
+ let expected_type = FileType::from_path(&expected_path);
+ let actual_type = FileType::from_path(&actual_path);
+ if expected_type != actual_type {
+ return Err(Self::TypeMismatch {
+ expected_path,
+ actual_path,
+ expected_type,
+ actual_type,
+ });
+ }
+
+ match expected_type {
+ FileType::Symlink => {
+ let expected_target = std::fs::read_link(&expected_path).ok();
+ let actual_target = std::fs::read_link(&actual_path).ok();
+ if expected_target != actual_target {
+ return Err(Self::LinkMismatch {
+ expected_path,
+ actual_path,
+ expected_target: expected_target.unwrap(),
+ actual_target: actual_target.unwrap(),
+ });
+ }
+ }
+ FileType::File => {
+ let mut actual =
+ crate::Data::read_from(&actual_path, None).map_err(Self::Failure)?;
+
+ let expected = crate::Data::read_from(&expected_path, None)
+ .map(|d| d.normalize(NormalizeNewlines))
+ .map_err(Self::Failure)?;
+
+ actual = actual
+ .try_coerce(expected.format())
+ .normalize(NormalizePaths)
+ .normalize(NormalizeNewlines)
+ .normalize(NormalizeMatches::new(substitutions, &expected));
+
+ if expected != actual {
+ return Err(Self::ContentMismatch {
+ expected_path,
+ actual_path,
+ expected_content: expected,
+ actual_content: actual,
+ });
+ }
+ }
+ FileType::Dir | FileType::Unknown | FileType::Missing => {}
+ }
+
+ Ok((expected_path, actual_path))
+ })
+ }
+}
+
+impl PathDiff {
+ pub fn expected_path(&self) -> Option<&std::path::Path> {
+ match &self {
+ Self::Failure(_msg) => None,
+ Self::TypeMismatch {
+ expected_path,
+ actual_path: _,
+ expected_type: _,
+ actual_type: _,
+ } => Some(expected_path),
+ Self::LinkMismatch {
+ expected_path,
+ actual_path: _,
+ expected_target: _,
+ actual_target: _,
+ } => Some(expected_path),
+ Self::ContentMismatch {
+ expected_path,
+ actual_path: _,
+ expected_content: _,
+ actual_content: _,
+ } => Some(expected_path),
+ }
+ }
+
+ pub fn write(
+ &self,
+ f: &mut dyn std::fmt::Write,
+ palette: crate::report::Palette,
+ ) -> Result<(), std::fmt::Error> {
+ match &self {
+ Self::Failure(msg) => {
+ writeln!(f, "{}", palette.error(msg))?;
+ }
+ Self::TypeMismatch {
+ expected_path,
+ actual_path: _actual_path,
+ expected_type,
+ actual_type,
+ } => {
+ writeln!(
+ f,
+ "{}: Expected {}, was {}",
+ expected_path.display(),
+ palette.info(expected_type),
+ palette.error(actual_type)
+ )?;
+ }
+ Self::LinkMismatch {
+ expected_path,
+ actual_path: _actual_path,
+ expected_target,
+ actual_target,
+ } => {
+ writeln!(
+ f,
+ "{}: Expected {}, was {}",
+ expected_path.display(),
+ palette.info(expected_target.display()),
+ palette.error(actual_target.display())
+ )?;
+ }
+ Self::ContentMismatch {
+ expected_path,
+ actual_path,
+ expected_content,
+ actual_content,
+ } => {
+ crate::report::write_diff(
+ f,
+ expected_content,
+ actual_content,
+ Some(&expected_path.display()),
+ Some(&actual_path.display()),
+ palette,
+ )?;
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn overwrite(&self) -> Result<(), crate::Error> {
+ match self {
+ // Not passing the error up because users most likely want to treat a processing error
+ // differently than an overwrite error
+ Self::Failure(_err) => Ok(()),
+ Self::TypeMismatch {
+ expected_path,
+ actual_path,
+ expected_type: _,
+ actual_type,
+ } => {
+ match actual_type {
+ FileType::Dir => {
+ std::fs::remove_dir_all(expected_path).map_err(|e| {
+ format!("Failed to remove {}: {}", expected_path.display(), e)
+ })?;
+ }
+ FileType::File | FileType::Symlink => {
+ std::fs::remove_file(expected_path).map_err(|e| {
+ format!("Failed to remove {}: {}", expected_path.display(), e)
+ })?;
+ }
+ FileType::Unknown | FileType::Missing => {}
+ }
+ shallow_copy(expected_path, actual_path)
+ }
+ Self::LinkMismatch {
+ expected_path,
+ actual_path,
+ expected_target: _,
+ actual_target: _,
+ } => shallow_copy(expected_path, actual_path),
+ Self::ContentMismatch {
+ expected_path,
+ actual_path: _,
+ expected_content: _,
+ actual_content,
+ } => actual_content.write_to(expected_path),
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum FileType {
+ Dir,
+ File,
+ Symlink,
+ Unknown,
+ Missing,
+}
+
+impl FileType {
+ pub fn from_path(path: &std::path::Path) -> Self {
+ let meta = path.symlink_metadata();
+ match meta {
+ Ok(meta) => {
+ if meta.is_dir() {
+ Self::Dir
+ } else if meta.is_file() {
+ Self::File
+ } else {
+ let target = std::fs::read_link(path).ok();
+ if target.is_some() {
+ Self::Symlink
+ } else {
+ Self::Unknown
+ }
+ }
+ }
+ Err(err) => match err.kind() {
+ std::io::ErrorKind::NotFound => Self::Missing,
+ _ => Self::Unknown,
+ },
+ }
+ }
+}
+
+impl FileType {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Dir => "dir",
+ Self::File => "file",
+ Self::Symlink => "symlink",
+ Self::Unknown => "unknown",
+ Self::Missing => "missing",
+ }
+ }
+}
+
+impl std::fmt::Display for FileType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.as_str().fmt(f)
+ }
+}
+
+/// Recursively walk a path
+///
+/// Note: Ignores `.keep` files
+#[cfg(feature = "path")]
+pub struct Walk {
+ inner: walkdir::IntoIter,
+}
+
+#[cfg(feature = "path")]
+impl Walk {
+ pub fn new(path: &std::path::Path) -> Self {
+ Self {
+ inner: walkdir::WalkDir::new(path).into_iter(),
+ }
+ }
+}
+
+#[cfg(feature = "path")]
+impl Iterator for Walk {
+ type Item = Result<std::path::PathBuf, std::io::Error>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ while let Some(entry) = self.inner.next().map(|e| {
+ e.map(walkdir::DirEntry::into_path)
+ .map_err(std::io::Error::from)
+ }) {
+ if entry.as_ref().ok().and_then(|e| e.file_name())
+ != Some(std::ffi::OsStr::new(".keep"))
+ {
+ return Some(entry);
+ }
+ }
+ None
+ }
+}
+
+/// Copy a template into a [`PathFixture`]
+///
+/// Note: Generally you'll use [`PathFixture::with_template`] instead.
+///
+/// Note: Ignores `.keep` files
+#[cfg(feature = "path")]
+pub fn copy_template(
+ source: impl AsRef<std::path::Path>,
+ dest: impl AsRef<std::path::Path>,
+) -> Result<(), crate::Error> {
+ let source = source.as_ref();
+ let dest = dest.as_ref();
+ let source = canonicalize(source)
+ .map_err(|e| format!("Failed to canonicalize {}: {}", source.display(), e))?;
+ std::fs::create_dir_all(dest)
+ .map_err(|e| format!("Failed to create {}: {}", dest.display(), e))?;
+ let dest = canonicalize(dest)
+ .map_err(|e| format!("Failed to canonicalize {}: {}", dest.display(), e))?;
+
+ for current in Walk::new(&source) {
+ let current = current.map_err(|e| e.to_string())?;
+ let rel = current.strip_prefix(&source).unwrap();
+ let target = dest.join(rel);
+
+ shallow_copy(&current, &target)?;
+ }
+
+ Ok(())
+}
+
+/// Copy a file system entry, without recursing
+fn shallow_copy(source: &std::path::Path, dest: &std::path::Path) -> Result<(), crate::Error> {
+ let meta = source
+ .symlink_metadata()
+ .map_err(|e| format!("Failed to read metadata from {}: {}", source.display(), e))?;
+ if meta.is_dir() {
+ std::fs::create_dir_all(dest)
+ .map_err(|e| format!("Failed to create {}: {}", dest.display(), e))?;
+ } else if meta.is_file() {
+ std::fs::copy(source, dest).map_err(|e| {
+ format!(
+ "Failed to copy {} to {}: {}",
+ source.display(),
+ dest.display(),
+ e
+ )
+ })?;
+ // Avoid a mtime check race where:
+ // - Copy files
+ // - Test checks mtime
+ // - Test writes
+ // - Test checks mtime
+ //
+ // If all of this happens too close to each other, then the second mtime check will think
+ // nothing was written by the test.
+ //
+ // Instead of just setting 1s in the past, we'll just respect the existing mtime.
+ copy_stats(&meta, dest).map_err(|e| {
+ format!(
+ "Failed to copy {} metadata to {}: {}",
+ source.display(),
+ dest.display(),
+ e
+ )
+ })?;
+ } else if let Ok(target) = std::fs::read_link(source) {
+ symlink_to_file(dest, &target)
+ .map_err(|e| format!("Failed to create symlink {}: {}", dest.display(), e))?;
+ }
+
+ Ok(())
+}
+
+#[cfg(feature = "path")]
+fn copy_stats(
+ source_meta: &std::fs::Metadata,
+ dest: &std::path::Path,
+) -> Result<(), std::io::Error> {
+ let src_mtime = filetime::FileTime::from_last_modification_time(source_meta);
+ filetime::set_file_mtime(&dest, src_mtime)?;
+
+ Ok(())
+}
+
+#[cfg(not(feature = "path"))]
+fn copy_stats(
+ _source_meta: &std::fs::Metadata,
+ _dest: &std::path::Path,
+) -> Result<(), std::io::Error> {
+ Ok(())
+}
+
+#[cfg(windows)]
+fn symlink_to_file(link: &std::path::Path, target: &std::path::Path) -> Result<(), std::io::Error> {
+ std::os::windows::fs::symlink_file(target, link)
+}
+
+#[cfg(not(windows))]
+fn symlink_to_file(link: &std::path::Path, target: &std::path::Path) -> Result<(), std::io::Error> {
+ std::os::unix::fs::symlink(target, link)
+}
+
+pub fn resolve_dir(
+ path: impl AsRef<std::path::Path>,
+) -> Result<std::path::PathBuf, std::io::Error> {
+ let path = path.as_ref();
+ let meta = std::fs::symlink_metadata(path)?;
+ if meta.is_dir() {
+ canonicalize(path)
+ } else if meta.is_file() {
+ // Git might checkout symlinks as files
+ let target = std::fs::read_to_string(path)?;
+ let target_path = path.parent().unwrap().join(target);
+ resolve_dir(target_path)
+ } else {
+ canonicalize(path)
+ }
+}
+
+fn canonicalize(path: &std::path::Path) -> Result<std::path::PathBuf, std::io::Error> {
+ #[cfg(feature = "path")]
+ {
+ dunce::canonicalize(path)
+ }
+ #[cfg(not(feature = "path"))]
+ {
+ // Hope for the best
+ Ok(strip_trailing_slash(path).to_owned())
+ }
+}
+
+pub fn strip_trailing_slash(path: &std::path::Path) -> &std::path::Path {
+ path.components().as_path()
+}
+
+pub(crate) fn display_relpath(path: impl AsRef<std::path::Path>) -> String {
+ let path = path.as_ref();
+ let relpath = if let Ok(cwd) = std::env::current_dir() {
+ match path.strip_prefix(cwd) {
+ Ok(path) => path,
+ Err(_) => path,
+ }
+ } else {
+ path
+ };
+ relpath.display().to_string()
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn strips_trailing_slash() {
+ let path = std::path::Path::new("/foo/bar/");
+ let rendered = path.display().to_string();
+ assert_eq!(rendered.as_bytes()[rendered.len() - 1], b'/');
+
+ let stripped = strip_trailing_slash(path);
+ let rendered = stripped.display().to_string();
+ assert_eq!(rendered.as_bytes()[rendered.len() - 1], b'r');
+ }
+
+ #[test]
+ fn file_type_detect_file() {
+ let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
+ dbg!(&path);
+ let actual = FileType::from_path(&path);
+ assert_eq!(actual, FileType::File);
+ }
+
+ #[test]
+ fn file_type_detect_dir() {
+ let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
+ dbg!(path);
+ let actual = FileType::from_path(path);
+ assert_eq!(actual, FileType::Dir);
+ }
+
+ #[test]
+ fn file_type_detect_missing() {
+ let path = std::path::Path::new("this-should-never-exist");
+ dbg!(path);
+ let actual = FileType::from_path(path);
+ assert_eq!(actual, FileType::Missing);
+ }
+}
diff --git a/vendor/snapbox/src/report/color.rs b/vendor/snapbox/src/report/color.rs
new file mode 100644
index 000000000..f1cd363b4
--- /dev/null
+++ b/vendor/snapbox/src/report/color.rs
@@ -0,0 +1,127 @@
+#[derive(Copy, Clone, Debug)]
+#[allow(dead_code)]
+pub struct Palette {
+ pub(crate) info: styled::Style,
+ pub(crate) warn: styled::Style,
+ pub(crate) error: styled::Style,
+ pub(crate) hint: styled::Style,
+ pub(crate) expected: styled::Style,
+ pub(crate) actual: styled::Style,
+}
+
+impl Palette {
+ #[cfg(feature = "color")]
+ pub fn always() -> Self {
+ Self {
+ info: styled::Style(yansi::Style::new(yansi::Color::Green)),
+ warn: styled::Style(yansi::Style::new(yansi::Color::Yellow)),
+ error: styled::Style(yansi::Style::new(yansi::Color::Red)),
+ hint: styled::Style(yansi::Style::new(yansi::Color::Unset).dimmed()),
+ expected: styled::Style(yansi::Style::new(yansi::Color::Green).underline()),
+ actual: styled::Style(yansi::Style::new(yansi::Color::Red).underline()),
+ }
+ }
+
+ #[cfg(not(feature = "color"))]
+ pub fn always() -> Self {
+ Self::never()
+ }
+
+ pub fn never() -> Self {
+ Self {
+ info: Default::default(),
+ warn: Default::default(),
+ error: Default::default(),
+ hint: Default::default(),
+ expected: Default::default(),
+ actual: Default::default(),
+ }
+ }
+
+ pub fn auto() -> Self {
+ if is_colored() {
+ Self::always()
+ } else {
+ Self::never()
+ }
+ }
+
+ pub fn info<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.info.paint(item)
+ }
+
+ pub fn warn<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.warn.paint(item)
+ }
+
+ pub fn error<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.error.paint(item)
+ }
+
+ pub fn hint<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.hint.paint(item)
+ }
+
+ pub fn expected<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.expected.paint(item)
+ }
+
+ pub fn actual<D: std::fmt::Display>(self, item: D) -> Styled<D> {
+ self.actual.paint(item)
+ }
+}
+
+fn is_colored() -> bool {
+ #[cfg(feature = "color")]
+ {
+ concolor::get(concolor::Stream::Either).ansi_color()
+ }
+
+ #[cfg(not(feature = "color"))]
+ {
+ false
+ }
+}
+
+pub(crate) use styled::Style;
+pub use styled::Styled;
+
+#[cfg(feature = "color")]
+mod styled {
+ #[derive(Copy, Clone, Debug, Default)]
+ pub(crate) struct Style(pub(crate) yansi::Style);
+
+ impl Style {
+ pub(crate) fn paint<T: std::fmt::Display>(self, item: T) -> Styled<T> {
+ Styled(self.0.paint(item))
+ }
+ }
+
+ pub struct Styled<D: std::fmt::Display>(yansi::Paint<D>);
+
+ impl<D: std::fmt::Display> std::fmt::Display for Styled<D> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+ }
+}
+
+#[cfg(not(feature = "color"))]
+mod styled {
+ #[derive(Copy, Clone, Debug, Default)]
+ pub(crate) struct Style;
+
+ impl Style {
+ pub(crate) fn paint<T: std::fmt::Display>(self, item: T) -> Styled<T> {
+ Styled(item)
+ }
+ }
+
+ pub struct Styled<D: std::fmt::Display>(D);
+
+ impl<D: std::fmt::Display> std::fmt::Display for Styled<D> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+ }
+}
diff --git a/vendor/snapbox/src/report/diff.rs b/vendor/snapbox/src/report/diff.rs
new file mode 100644
index 000000000..adc9f7935
--- /dev/null
+++ b/vendor/snapbox/src/report/diff.rs
@@ -0,0 +1,384 @@
+pub fn write_diff(
+ writer: &mut dyn std::fmt::Write,
+ expected: &crate::Data,
+ actual: &crate::Data,
+ expected_name: Option<&dyn std::fmt::Display>,
+ actual_name: Option<&dyn std::fmt::Display>,
+ palette: crate::report::Palette,
+) -> Result<(), std::fmt::Error> {
+ #[allow(unused_mut)]
+ let mut rendered = false;
+ #[cfg(feature = "diff")]
+ if let (Some(expected), Some(actual)) = (expected.render(), actual.render()) {
+ write_diff_inner(
+ writer,
+ &expected,
+ &actual,
+ expected_name,
+ actual_name,
+ palette,
+ )?;
+ rendered = true;
+ }
+
+ if !rendered {
+ if let Some(expected_name) = expected_name {
+ writeln!(writer, "{} {}:", expected_name, palette.info("(expected)"))?;
+ } else {
+ writeln!(writer, "{}:", palette.info("Expected"))?;
+ }
+ writeln!(writer, "{}", palette.info(&expected))?;
+ if let Some(actual_name) = actual_name {
+ writeln!(writer, "{} {}:", actual_name, palette.error("(actual)"))?;
+ } else {
+ writeln!(writer, "{}:", palette.error("Actual"))?;
+ }
+ writeln!(writer, "{}", palette.error(&actual))?;
+ }
+ Ok(())
+}
+
+#[cfg(feature = "diff")]
+fn write_diff_inner(
+ writer: &mut dyn std::fmt::Write,
+ expected: &str,
+ actual: &str,
+ expected_name: Option<&dyn std::fmt::Display>,
+ actual_name: Option<&dyn std::fmt::Display>,
+ palette: crate::report::Palette,
+) -> Result<(), std::fmt::Error> {
+ let timeout = std::time::Duration::from_millis(500);
+ let min_elide = 20;
+ let context = 5;
+
+ let changes = similar::TextDiff::configure()
+ .algorithm(similar::Algorithm::Patience)
+ .timeout(timeout)
+ .newline_terminated(false)
+ .diff_lines(expected, actual);
+
+ writeln!(writer)?;
+ if let Some(expected_name) = expected_name {
+ writeln!(
+ writer,
+ "{}",
+ palette.info(format_args!("{:->4} expected: {}", "", expected_name))
+ )?;
+ } else {
+ writeln!(writer, "{}", palette.info(format_args!("--- Expected")))?;
+ }
+ if let Some(actual_name) = actual_name {
+ writeln!(
+ writer,
+ "{}",
+ palette.error(format_args!("{:+>4} actual: {}", "", actual_name))
+ )?;
+ } else {
+ writeln!(writer, "{}", palette.error(format_args!("+++ Actual")))?;
+ }
+ let changes = changes
+ .ops()
+ .iter()
+ .flat_map(|op| changes.iter_inline_changes(op))
+ .collect::<Vec<_>>();
+ let tombstones = if min_elide < changes.len() {
+ let mut tombstones = vec![true; changes.len()];
+
+ let mut counter = context;
+ for (i, change) in changes.iter().enumerate() {
+ match change.tag() {
+ similar::ChangeTag::Insert | similar::ChangeTag::Delete => {
+ counter = context;
+ tombstones[i] = false;
+ }
+ similar::ChangeTag::Equal => {
+ if counter != 0 {
+ tombstones[i] = false;
+ counter -= 1;
+ }
+ }
+ }
+ }
+
+ let mut counter = context;
+ for (i, change) in changes.iter().enumerate().rev() {
+ match change.tag() {
+ similar::ChangeTag::Insert | similar::ChangeTag::Delete => {
+ counter = context;
+ tombstones[i] = false;
+ }
+ similar::ChangeTag::Equal => {
+ if counter != 0 {
+ tombstones[i] = false;
+ counter -= 1;
+ }
+ }
+ }
+ }
+ tombstones
+ } else {
+ Vec::new()
+ };
+
+ let mut elided = false;
+ for (i, change) in changes.into_iter().enumerate() {
+ if tombstones.get(i).copied().unwrap_or(false) {
+ if !elided {
+ let sign = "⋮";
+
+ write!(writer, "{:>4} ", " ",)?;
+ write!(writer, "{:>4} ", " ",)?;
+ writeln!(writer, "{}", palette.hint(sign))?;
+ }
+ elided = true;
+ } else {
+ elided = false;
+ match change.tag() {
+ similar::ChangeTag::Insert => {
+ write_change(writer, change, "+", palette.actual, palette.error, palette)?;
+ }
+ similar::ChangeTag::Delete => {
+ write_change(writer, change, "-", palette.expected, palette.info, palette)?;
+ }
+ similar::ChangeTag::Equal => {
+ write_change(writer, change, "|", palette.hint, palette.hint, palette)?;
+ }
+ }
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(feature = "diff")]
+fn write_change(
+ writer: &mut dyn std::fmt::Write,
+ change: similar::InlineChange<str>,
+ sign: &str,
+ em_style: crate::report::Style,
+ style: crate::report::Style,
+ palette: crate::report::Palette,
+) -> Result<(), std::fmt::Error> {
+ if let Some(index) = change.old_index() {
+ write!(writer, "{:>4} ", palette.hint(index + 1),)?;
+ } else {
+ write!(writer, "{:>4} ", " ",)?;
+ }
+ if let Some(index) = change.new_index() {
+ write!(writer, "{:>4} ", palette.hint(index + 1),)?;
+ } else {
+ write!(writer, "{:>4} ", " ",)?;
+ }
+ write!(writer, "{} ", style.paint(sign))?;
+ for &(emphasized, change) in change.values() {
+ let cur_style = if emphasized { em_style } else { style };
+ write!(writer, "{}", cur_style.paint(change))?;
+ }
+ if change.missing_newline() {
+ writeln!(writer, "{}", em_style.paint("∅"))?;
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[cfg(feature = "diff")]
+ #[test]
+ fn diff_eq() {
+ let expected = "Hello\nWorld\n";
+ let expected_name = "A";
+ let actual = "Hello\nWorld\n";
+ let actual_name = "B";
+ let palette = crate::report::Palette::never();
+
+ let mut actual_diff = String::new();
+ write_diff_inner(
+ &mut actual_diff,
+ expected,
+ actual,
+ Some(&expected_name),
+ Some(&actual_name),
+ palette,
+ )
+ .unwrap();
+ let expected_diff = "
+---- expected: A
+++++ actual: B
+ 1 1 | Hello
+ 2 2 | World
+";
+
+ assert_eq!(expected_diff, actual_diff);
+ }
+
+ #[cfg(feature = "diff")]
+ #[test]
+ fn diff_ne_line_missing() {
+ let expected = "Hello\nWorld\n";
+ let expected_name = "A";
+ let actual = "Hello\n";
+ let actual_name = "B";
+ let palette = crate::report::Palette::never();
+
+ let mut actual_diff = String::new();
+ write_diff_inner(
+ &mut actual_diff,
+ expected,
+ actual,
+ Some(&expected_name),
+ Some(&actual_name),
+ palette,
+ )
+ .unwrap();
+ let expected_diff = "
+---- expected: A
+++++ actual: B
+ 1 1 | Hello
+ 2 - World
+";
+
+ assert_eq!(expected_diff, actual_diff);
+ }
+
+ #[cfg(feature = "diff")]
+ #[test]
+ fn diff_eq_trailing_extra_newline() {
+ let expected = "Hello\nWorld";
+ let expected_name = "A";
+ let actual = "Hello\nWorld\n";
+ let actual_name = "B";
+ let palette = crate::report::Palette::never();
+
+ let mut actual_diff = String::new();
+ write_diff_inner(
+ &mut actual_diff,
+ expected,
+ actual,
+ Some(&expected_name),
+ Some(&actual_name),
+ palette,
+ )
+ .unwrap();
+ let expected_diff = "
+---- expected: A
+++++ actual: B
+ 1 1 | Hello
+ 2 - World∅
+ 2 + World
+";
+
+ assert_eq!(expected_diff, actual_diff);
+ }
+
+ #[cfg(feature = "diff")]
+ #[test]
+ fn diff_eq_trailing_newline_missing() {
+ let expected = "Hello\nWorld\n";
+ let expected_name = "A";
+ let actual = "Hello\nWorld";
+ let actual_name = "B";
+ let palette = crate::report::Palette::never();
+
+ let mut actual_diff = String::new();
+ write_diff_inner(
+ &mut actual_diff,
+ expected,
+ actual,
+ Some(&expected_name),
+ Some(&actual_name),
+ palette,
+ )
+ .unwrap();
+ let expected_diff = "
+---- expected: A
+++++ actual: B
+ 1 1 | Hello
+ 2 - World
+ 2 + World∅
+";
+
+ assert_eq!(expected_diff, actual_diff);
+ }
+
+ #[cfg(feature = "diff")]
+ #[test]
+ fn diff_eq_elided() {
+ let mut expected = String::new();
+ expected.push_str("Hello\n");
+ for i in 0..20 {
+ expected.push_str(&i.to_string());
+ expected.push('\n');
+ }
+ expected.push_str("World\n");
+ for i in 0..20 {
+ expected.push_str(&i.to_string());
+ expected.push('\n');
+ }
+ expected.push_str("!\n");
+ let expected_name = "A";
+
+ let mut actual = String::new();
+ actual.push_str("Goodbye\n");
+ for i in 0..20 {
+ actual.push_str(&i.to_string());
+ actual.push('\n');
+ }
+ actual.push_str("Moon\n");
+ for i in 0..20 {
+ actual.push_str(&i.to_string());
+ actual.push('\n');
+ }
+ actual.push_str("?\n");
+ let actual_name = "B";
+
+ let palette = crate::report::Palette::never();
+
+ let mut actual_diff = String::new();
+ write_diff_inner(
+ &mut actual_diff,
+ &expected,
+ &actual,
+ Some(&expected_name),
+ Some(&actual_name),
+ palette,
+ )
+ .unwrap();
+ let expected_diff = "
+---- expected: A
+++++ actual: B
+ 1 - Hello
+ 1 + Goodbye
+ 2 2 | 0
+ 3 3 | 1
+ 4 4 | 2
+ 5 5 | 3
+ 6 6 | 4
+ ⋮
+ 17 17 | 15
+ 18 18 | 16
+ 19 19 | 17
+ 20 20 | 18
+ 21 21 | 19
+ 22 - World
+ 22 + Moon
+ 23 23 | 0
+ 24 24 | 1
+ 25 25 | 2
+ 26 26 | 3
+ 27 27 | 4
+ ⋮
+ 38 38 | 15
+ 39 39 | 16
+ 40 40 | 17
+ 41 41 | 18
+ 42 42 | 19
+ 43 - !
+ 43 + ?
+";
+
+ assert_eq!(expected_diff, actual_diff);
+ }
+}
diff --git a/vendor/snapbox/src/report/mod.rs b/vendor/snapbox/src/report/mod.rs
new file mode 100644
index 000000000..6c9a238b8
--- /dev/null
+++ b/vendor/snapbox/src/report/mod.rs
@@ -0,0 +1,9 @@
+//! Utilities to report test results to users
+
+mod color;
+mod diff;
+
+pub use color::Palette;
+pub(crate) use color::Style;
+pub use color::Styled;
+pub use diff::write_diff;
diff --git a/vendor/snapbox/src/substitutions.rs b/vendor/snapbox/src/substitutions.rs
new file mode 100644
index 000000000..9c228172b
--- /dev/null
+++ b/vendor/snapbox/src/substitutions.rs
@@ -0,0 +1,420 @@
+use std::borrow::Cow;
+
+/// Match pattern expressions, see [`Assert`][crate::Assert]
+///
+/// Built-in expressions:
+/// - `...` on a line of its own: match multiple complete lines
+/// - `[..]`: match multiple characters within a line
+#[derive(Default, Clone, Debug, PartialEq, Eq)]
+pub struct Substitutions {
+ vars: std::collections::BTreeMap<&'static str, Cow<'static, str>>,
+ unused: std::collections::BTreeSet<&'static str>,
+}
+
+impl Substitutions {
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ pub(crate) fn with_exe() -> Self {
+ let mut substitutions = Self::new();
+ substitutions
+ .insert("[EXE]", std::env::consts::EXE_SUFFIX)
+ .unwrap();
+ substitutions
+ }
+
+ /// Insert an additional match pattern
+ ///
+ /// `key` must be enclosed in `[` and `]`.
+ ///
+ /// ```rust
+ /// let mut subst = snapbox::Substitutions::new();
+ /// subst.insert("[EXE]", std::env::consts::EXE_SUFFIX);
+ /// ```
+ pub fn insert(
+ &mut self,
+ key: &'static str,
+ value: impl Into<Cow<'static, str>>,
+ ) -> Result<(), crate::Error> {
+ let key = validate_key(key)?;
+ let value = value.into();
+ if value.is_empty() {
+ self.unused.insert(key);
+ } else {
+ self.vars
+ .insert(key, crate::utils::normalize_text(value.as_ref()).into());
+ }
+ Ok(())
+ }
+
+ /// Insert additional match patterns
+ ///
+ /// keys must be enclosed in `[` and `]`.
+ pub fn extend(
+ &mut self,
+ vars: impl IntoIterator<Item = (&'static str, impl Into<Cow<'static, str>>)>,
+ ) -> Result<(), crate::Error> {
+ for (key, value) in vars {
+ self.insert(key, value)?;
+ }
+ Ok(())
+ }
+
+ /// Apply match pattern to `input`
+ ///
+ /// If `pattern` matches `input`, then `pattern` is returned.
+ ///
+ /// Otherwise, `input`, with as many patterns replaced as possible, will be returned.
+ ///
+ /// ```rust
+ /// let subst = snapbox::Substitutions::new();
+ /// let output = subst.normalize("Hello World!", "Hello [..]!");
+ /// assert_eq!(output, "Hello [..]!");
+ /// ```
+ pub fn normalize(&self, input: &str, pattern: &str) -> String {
+ normalize(input, pattern, self)
+ }
+
+ fn substitute<'v>(&self, value: &'v str) -> Cow<'v, str> {
+ let mut value = Cow::Borrowed(value);
+ for (var, replace) in self.vars.iter() {
+ debug_assert!(!replace.is_empty());
+ value = Cow::Owned(value.replace(replace.as_ref(), var));
+ }
+ value
+ }
+
+ fn clear<'v>(&self, pattern: &'v str) -> Cow<'v, str> {
+ if pattern.contains('[') {
+ let mut pattern = Cow::Borrowed(pattern);
+ for var in self.unused.iter() {
+ pattern = Cow::Owned(pattern.replace(var, ""));
+ }
+ pattern
+ } else {
+ Cow::Borrowed(pattern)
+ }
+ }
+}
+
+fn validate_key(key: &'static str) -> Result<&'static str, crate::Error> {
+ if !key.starts_with('[') || !key.ends_with(']') {
+ return Err(format!("Key `{}` is not enclosed in []", key).into());
+ }
+
+ if key[1..(key.len() - 1)]
+ .find(|c: char| !c.is_ascii_uppercase())
+ .is_some()
+ {
+ return Err(format!("Key `{}` can only be A-Z but ", key).into());
+ }
+
+ Ok(key)
+}
+
+fn normalize(input: &str, pattern: &str, substitutions: &Substitutions) -> String {
+ if input == pattern {
+ return input.to_owned();
+ }
+
+ let mut normalized: Vec<Cow<str>> = Vec::new();
+ let input_lines: Vec<_> = crate::utils::LinesWithTerminator::new(input).collect();
+ let pattern_lines: Vec<_> = crate::utils::LinesWithTerminator::new(pattern).collect();
+
+ let mut input_index = 0;
+ let mut pattern_index = 0;
+ 'outer: loop {
+ let pattern_line = if let Some(pattern_line) = pattern_lines.get(pattern_index) {
+ *pattern_line
+ } else {
+ normalized.extend(
+ input_lines[input_index..]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ break 'outer;
+ };
+ let next_pattern_index = pattern_index + 1;
+
+ let input_line = if let Some(input_line) = input_lines.get(input_index) {
+ *input_line
+ } else {
+ break 'outer;
+ };
+ let next_input_index = input_index + 1;
+
+ if line_matches(input_line, pattern_line, substitutions) {
+ pattern_index = next_pattern_index;
+ input_index = next_input_index;
+ normalized.push(Cow::Borrowed(pattern_line));
+ continue 'outer;
+ } else if is_line_elide(pattern_line) {
+ let next_pattern_line: &str =
+ if let Some(pattern_line) = pattern_lines.get(next_pattern_index) {
+ pattern_line
+ } else {
+ normalized.push(Cow::Borrowed(pattern_line));
+ break 'outer;
+ };
+ if let Some(future_input_index) = input_lines[input_index..]
+ .iter()
+ .enumerate()
+ .find(|(_, l)| **l == next_pattern_line)
+ .map(|(i, _)| input_index + i)
+ {
+ normalized.push(Cow::Borrowed(pattern_line));
+ pattern_index = next_pattern_index;
+ input_index = future_input_index;
+ continue 'outer;
+ } else {
+ normalized.extend(
+ input_lines[input_index..]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ break 'outer;
+ }
+ } else {
+ // Find where we can pick back up for normalizing
+ for future_input_index in next_input_index..input_lines.len() {
+ let future_input_line = input_lines[future_input_index];
+ if let Some(future_pattern_index) = pattern_lines[next_pattern_index..]
+ .iter()
+ .enumerate()
+ .find(|(_, l)| **l == future_input_line || is_line_elide(**l))
+ .map(|(i, _)| next_pattern_index + i)
+ {
+ normalized.extend(
+ input_lines[input_index..future_input_index]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ pattern_index = future_pattern_index;
+ input_index = future_input_index;
+ continue 'outer;
+ }
+ }
+
+ normalized.extend(
+ input_lines[input_index..]
+ .iter()
+ .copied()
+ .map(|s| substitutions.substitute(s)),
+ );
+ break 'outer;
+ }
+ }
+
+ normalized.join("")
+}
+
+fn is_line_elide(line: &str) -> bool {
+ line == "...\n" || line == "..."
+}
+
+fn line_matches(line: &str, pattern: &str, substitutions: &Substitutions) -> bool {
+ if line == pattern {
+ return true;
+ }
+
+ let subbed = substitutions.substitute(line);
+ let mut line = subbed.as_ref();
+
+ let pattern = substitutions.clear(pattern);
+
+ let mut sections = pattern.split("[..]").peekable();
+ while let Some(section) = sections.next() {
+ if let Some(remainder) = line.strip_prefix(section) {
+ if let Some(next_section) = sections.peek() {
+ if next_section.is_empty() {
+ line = "";
+ } else if let Some(restart_index) = remainder.find(next_section) {
+ line = &remainder[restart_index..];
+ }
+ } else {
+ return remainder.is_empty();
+ }
+ } else {
+ return false;
+ }
+ }
+
+ false
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn empty() {
+ let input = "";
+ let pattern = "";
+ let expected = "";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn literals_match() {
+ let input = "Hello\nWorld";
+ let pattern = "Hello\nWorld";
+ let expected = "Hello\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn pattern_shorter() {
+ let input = "Hello\nWorld";
+ let pattern = "Hello\n";
+ let expected = "Hello\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn input_shorter() {
+ let input = "Hello\n";
+ let pattern = "Hello\nWorld";
+ let expected = "Hello\n";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn all_different() {
+ let input = "Hello\nWorld";
+ let pattern = "Goodbye\nMoon";
+ let expected = "Hello\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn middles_diverge() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "Hello\nMoon\nGoodbye";
+ let expected = "Hello\nWorld\nGoodbye";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn leading_elide() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "...\nGoodbye";
+ let expected = "...\nGoodbye";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn trailing_elide() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "Hello\n...";
+ let expected = "Hello\n...";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn middle_elide() {
+ let input = "Hello\nWorld\nGoodbye";
+ let pattern = "Hello\n...\nGoodbye";
+ let expected = "Hello\n...\nGoodbye";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn post_elide_diverge() {
+ let input = "Hello\nSun\nAnd\nWorld";
+ let pattern = "Hello\n...\nMoon";
+ let expected = "Hello\nSun\nAnd\nWorld";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn post_diverge_elide() {
+ let input = "Hello\nWorld\nGoodbye\nSir";
+ let pattern = "Hello\nMoon\nGoodbye\n...";
+ let expected = "Hello\nWorld\nGoodbye\n...";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn inline_elide() {
+ let input = "Hello\nWorld\nGoodbye\nSir";
+ let pattern = "Hello\nW[..]d\nGoodbye\nSir";
+ let expected = "Hello\nW[..]d\nGoodbye\nSir";
+ let actual = normalize(input, pattern, &Substitutions::new());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn line_matches_cases() {
+ let cases = [
+ ("", "", true),
+ ("", "[..]", true),
+ ("hello", "hello", true),
+ ("hello", "goodbye", false),
+ ("hello", "[..]", true),
+ ("hello", "he[..]", true),
+ ("hello", "go[..]", false),
+ ("hello", "[..]o", true),
+ ("hello", "[..]e", false),
+ ("hello", "he[..]o", true),
+ ("hello", "he[..]e", false),
+ ("hello", "go[..]o", false),
+ ("hello", "go[..]e", false),
+ (
+ "hello world, goodbye moon",
+ "hello [..], goodbye [..]",
+ true,
+ ),
+ (
+ "hello world, goodbye moon",
+ "goodbye [..], goodbye [..]",
+ false,
+ ),
+ (
+ "hello world, goodbye moon",
+ "goodbye [..], hello [..]",
+ false,
+ ),
+ ("hello world, goodbye moon", "hello [..], [..] moon", true),
+ (
+ "hello world, goodbye moon",
+ "goodbye [..], [..] moon",
+ false,
+ ),
+ ("hello world, goodbye moon", "hello [..], [..] world", false),
+ ];
+ for (line, pattern, expected) in cases {
+ let actual = line_matches(line, pattern, &Substitutions::new());
+ assert_eq!(expected, actual, "line={:?} pattern={:?}", line, pattern);
+ }
+ }
+
+ #[test]
+ fn test_validate_key() {
+ let cases = [
+ ("[HELLO", false),
+ ("HELLO]", false),
+ ("[HELLO]", true),
+ ("[hello]", false),
+ ("[HE O]", false),
+ ];
+ for (key, expected) in cases {
+ let actual = validate_key(key).is_ok();
+ assert_eq!(expected, actual, "key={:?}", key);
+ }
+ }
+}
diff --git a/vendor/snapbox/src/utils/lines.rs b/vendor/snapbox/src/utils/lines.rs
new file mode 100644
index 000000000..f56408483
--- /dev/null
+++ b/vendor/snapbox/src/utils/lines.rs
@@ -0,0 +1,31 @@
+#[derive(Clone, Debug)]
+pub struct LinesWithTerminator<'a> {
+ data: &'a str,
+}
+
+impl<'a> LinesWithTerminator<'a> {
+ pub fn new(data: &'a str) -> LinesWithTerminator<'a> {
+ LinesWithTerminator { data }
+ }
+}
+
+impl<'a> Iterator for LinesWithTerminator<'a> {
+ type Item = &'a str;
+
+ #[inline]
+ fn next(&mut self) -> Option<&'a str> {
+ match self.data.find('\n') {
+ None if self.data.is_empty() => None,
+ None => {
+ let line = self.data;
+ self.data = "";
+ Some(line)
+ }
+ Some(end) => {
+ let line = &self.data[..end + 1];
+ self.data = &self.data[end + 1..];
+ Some(line)
+ }
+ }
+ }
+}
diff --git a/vendor/snapbox/src/utils/mod.rs b/vendor/snapbox/src/utils/mod.rs
new file mode 100644
index 000000000..d51924196
--- /dev/null
+++ b/vendor/snapbox/src/utils/mod.rs
@@ -0,0 +1,30 @@
+mod lines;
+
+pub use lines::LinesWithTerminator;
+
+/// Normalize line endings
+pub fn normalize_lines(data: &str) -> String {
+ normalize_lines_chars(data.chars()).collect()
+}
+
+fn normalize_lines_chars(data: impl Iterator<Item = char>) -> impl Iterator<Item = char> {
+ normalize_line_endings::normalized(data)
+}
+
+/// Normalize path separators
+pub fn normalize_paths(data: &str) -> String {
+ normalize_paths_chars(data.chars()).collect()
+}
+
+fn normalize_paths_chars(data: impl Iterator<Item = char>) -> impl Iterator<Item = char> {
+ data.map(|c| if c == '\\' { '/' } else { c })
+}
+
+/// "Smart" text normalization
+///
+/// This includes
+/// - Line endings
+/// - Path separators
+pub fn normalize_text(data: &str) -> String {
+ normalize_paths_chars(normalize_lines_chars(data.chars())).collect()
+}