| //! Initialize working directories and assert on how they've changed |
| |
| #[doc(inline)] |
| pub use crate::cargo_rustc_current_dir; |
| #[doc(inline)] |
| pub use crate::current_dir; |
| #[doc(inline)] |
| pub use crate::current_rs; |
| |
| #[cfg(feature = "path")] |
| 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::try_read_from(&actual_path, None).map_err(Self::Failure)?; |
| |
| let expected = |
| crate::Data::read_from(&expected_path, None).normalize(NormalizeNewlines); |
| |
| actual = actual |
| .coerce_to(expected.intended_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, true) |
| } |
| |
| #[cfg(feature = "path")] |
| pub(crate) fn subset_matches_iter_inner( |
| expected_root: std::path::PathBuf, |
| actual_root: std::path::PathBuf, |
| substitutions: &crate::Substitutions, |
| normalize_paths: bool, |
| ) -> 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::try_read_from(&actual_path, None).map_err(Self::Failure)?; |
| |
| let expected = |
| crate::Data::read_from(&expected_path, None).normalize(NormalizeNewlines); |
| |
| actual = actual.coerce_to(expected.intended_format()); |
| if normalize_paths { |
| actual = actual.normalize(NormalizePaths); |
| } |
| actual = actual |
| .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_content.source().unwrap()), |
| } |
| } |
| } |
| |
| #[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); |
| } |
| } |