diff options
Diffstat (limited to 'vendor/snapbox/src/path.rs')
-rw-r--r-- | vendor/snapbox/src/path.rs | 686 |
1 files changed, 686 insertions, 0 deletions
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(¤t, &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); + } +} |