diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..3a46861 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,181 @@ +use std::fmt; +use std::fs; +use std::io; + +use walkdir; + +use super::iter; + +/// The type of assertion that occurred. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum AssertionKind { + /// One of the two sides is missing. + Missing, + /// The two sides have different types. + FileType, + /// The content of the two sides is different. + Content, +} + +impl AssertionKind { + /// Test if the assertion is from one of the two sides being missing. + pub fn is_missing(self) -> bool { + self == AssertionKind::Missing + } + + /// Test if the assertion is from the two sides having different file types. + pub fn is_file_type(self) -> bool { + self == AssertionKind::FileType + } + + /// Test if the assertion is from the two sides having different content. + pub fn is_content(self) -> bool { + self == AssertionKind::Content + } +} + +/// Error to capture the difference between paths. +#[derive(Debug, Clone)] +pub struct AssertionError { + kind: AssertionKind, + entry: iter::DiffEntry, + msg: Option, + cause: Option, +} + +impl AssertionError { + /// The type of difference detected. + pub fn kind(self) -> AssertionKind { + self.kind + } + + /// Access to the `DiffEntry` for which a difference was detected. + pub fn entry(&self) -> &iter::DiffEntry { + &self.entry + } + + /// Underlying error found when trying to find a difference + pub fn cause(&self) -> Option<&IoError> { + self.cause.as_ref() + } + + /// Add an optional message to display with the error. + pub fn with_msg>(mut self, msg: S) -> Self { + self.msg = Some(msg.into()); + self + } + + /// Add an underlying error found when trying to find a difference. + pub fn with_cause>(mut self, err: E) -> Self { + self.cause = Some(err.into()); + self + } + + pub(crate) fn new(kind: AssertionKind, entry: iter::DiffEntry) -> Self { + Self { + kind, + entry, + msg: None, + cause: None, + } + } +} + +impl fmt::Display for AssertionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.kind { + AssertionKind::Missing => { + write!(f, + "One side is missing: {}\n left: {:?}\n right: {:?}", + self.msg.as_ref().map(String::as_str).unwrap_or(""), + self.entry.left().path(), + self.entry.right().path()) + } + AssertionKind::FileType => { + write!(f, + "File types differ: {}\n left: {:?} is {}\n right: {:?} is {}", + self.msg.as_ref().map(String::as_str).unwrap_or(""), + self.entry.left().path(), + display_file_type(self.entry.left().file_type()), + self.entry.right().path(), + display_file_type(self.entry.right().file_type())) + } + AssertionKind::Content => { + write!(f, + "Content differs: {}\n left: {:?}\n right: {:?}", + self.msg.as_ref().map(String::as_str).unwrap_or(""), + self.entry.left().path(), + self.entry.right().path()) + } + }?; + + if let Some(cause) = self.cause() { + write!(f, "\ncause: {}", cause)?; + } + + Ok(()) + } +} + +fn display_file_type(file_type: Option) -> String { + if let Some(file_type) = file_type { + if file_type.is_file() { + "file".to_owned() + } else if file_type.is_dir() { + "dir".to_owned() + } else { + format!("{:?}", file_type) + } + } else { + "missing".to_owned() + } +} + +/// IO errors preventing diffing from happening. +#[derive(Debug, Clone)] +pub struct IoError(InnerIoError); + +#[derive(Debug)] +enum InnerIoError { + Io(io::Error), + WalkDir(walkdir::Error), + WalkDirEmpty, +} + +impl Clone for InnerIoError { + fn clone(&self) -> Self { + match *self { + InnerIoError::Io(_) | + InnerIoError::WalkDirEmpty => self.clone(), + InnerIoError::WalkDir(_) => InnerIoError::WalkDirEmpty, + } + } +} + +impl fmt::Display for IoError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for InnerIoError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + InnerIoError::Io(ref e) => e.fmt(f), + InnerIoError::WalkDir(ref e) => e.fmt(f), + InnerIoError::WalkDirEmpty => write!(f, "Unknown error when walking"), + } + } +} + +impl From for IoError { + fn from(e: io::Error) -> IoError { + IoError(InnerIoError::Io(e)) + } +} + +impl From for IoError { + fn from(e: walkdir::Error) -> IoError { + IoError(InnerIoError::WalkDir(e)) + } +} diff --git a/src/iter.rs b/src/iter.rs new file mode 100644 index 0000000..55d2f54 --- /dev/null +++ b/src/iter.rs @@ -0,0 +1,321 @@ +use std::io::prelude::*; +use std::ffi; +use std::fs; +use std::io; +use std::path; + +use walkdir; + +use error::IoError; +use error::{AssertionKind, AssertionError}; + +type WalkIter = walkdir::IntoIter; + +/// A builder to create an iterator for recusively diffing two directories. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DirDiff { + left: path::PathBuf, + right: path::PathBuf, +} + +impl DirDiff { + /// Create a builder for recursively diffing two directories, starting at `left_root` and + /// `right_root`. + pub fn new(left_root: L, right_root: R) -> Self + where L: Into, + R: Into + { + Self { + left: left_root.into(), + right: right_root.into(), + } + } + + fn walk(path: &path::Path) -> WalkIter { + walkdir::WalkDir::new(path).min_depth(1).into_iter() + } +} + +impl IntoIterator for DirDiff { + type Item = Result; + + type IntoIter = IntoIter; + + fn into_iter(self) -> IntoIter { + let left_walk = Self::walk(&self.left); + let right_walk = Self::walk(&self.right); + IntoIter { + left_root: self.left, + left_walk, + right_root: self.right, + right_walk, + } + } +} + +/// A potential directory entry. +/// +/// # Differences with `std::fs::DirEntry` +/// +/// This mostly mirrors `DirEntry` in `std::fs` and `walkdir` +/// +/// * The path might not actually exist. In this case, `.file_type()` returns `None`. +/// * Borroed information is returned +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct DirEntry { + path: path::PathBuf, + file_type: Option, +} + +impl DirEntry { + /// The full path that this entry represents. + pub fn path(&self) -> &path::Path { + self.path.as_path() + } + + /// Returns the metadata for the file that this entry points to. + pub fn metadata(&self) -> Result { + let m = fs::metadata(&self.path)?; + Ok(m) + } + + /// Returns the file type for the file that this entry points to. + /// + /// The `Option` is `None` if the file does not exist. + pub fn file_type(&self) -> Option { + self.file_type + } + + /// Returns the file name of this entry. + /// + /// If this entry has no file name (e.g. `/`), then the full path is returned. + pub fn file_name(&self) -> &ffi::OsStr { + self.path + .file_name() + .unwrap_or_else(|| self.path.as_os_str()) + } + + pub(self) fn exists(path: path::PathBuf) -> Result { + let metadata = fs::symlink_metadata(&path)?; + let file_type = Some(metadata.file_type()); + let s = Self { path, file_type }; + Ok(s) + } + + pub(self) fn missing(path: path::PathBuf) -> Result { + let file_type = None; + let s = Self { path, file_type }; + Ok(s) + } +} + +/// To paths to compare. +/// +/// This is the type of value that is yielded from `IntoIter`. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct DiffEntry { + left: DirEntry, + right: DirEntry, +} + +impl DiffEntry { + /// The entry for the left tree. + /// + /// This will always be returned, even if the entry does not exist. See `DirEntry::file_type` + /// to see how to check if the path exists. + pub fn left(&self) -> &DirEntry { + &self.left + } + + /// The entry for the right tree. + /// + /// This will always be returned, even if the entry does not exist. See `DirEntry::file_type` + /// to see how to check if the path exists. + pub fn right(&self) -> &DirEntry { + &self.right + } + + /// Embed the `DiffEntry` into an `AssertionError` for convinience when writing assertions. + pub fn into_error(self, kind: AssertionKind) -> AssertionError { + AssertionError::new(kind, self) + } + + /// Returns an error if the two paths are different. + /// + /// If this default policy does not work for you, you can use the constinuent assertions + /// (e.g. `assert_exists). + pub fn assert(self) -> Result { + match self.file_types() { + (Some(left), Some(right)) => { + if left != right { + Err(self.into_error(AssertionKind::FileType)) + } else if left.is_file() { + // Because of the `left != right` test, we can assume `right` is also a file. + match self.content_matches() { + Ok(true) => Ok(self), + Ok(false) => Err(self.into_error(AssertionKind::Content)), + Err(e) => Err(self.into_error(AssertionKind::Content).with_cause(e)), + } + } else { + Ok(self) + } + } + _ => Err(self.into_error(AssertionKind::Missing)), + } + } + + /// Returns an error iff one of the two paths does not exist. + pub fn assert_exists(self) -> Result { + match self.file_types() { + (Some(_), Some(_)) => Ok(self), + _ => Err(self.into_error(AssertionKind::Missing)), + } + } + + /// Returns an error iff two paths are of different types. + pub fn assert_file_type(self) -> Result { + match self.file_types() { + (Some(left), Some(right)) => { + if left != right { + Err(self.into_error(AssertionKind::FileType)) + } else { + Ok(self) + } + } + _ => Ok(self), + } + } + + /// Returns an error iff the file content of the two paths is different. + /// + /// This is assuming they are both files. + pub fn assert_content(self) -> Result { + if !self.are_files() { + return Ok(self); + } + + match self.content_matches() { + Ok(true) => Ok(self), + Ok(false) => Err(self.into_error(AssertionKind::Content)), + Err(e) => Err(self.into_error(AssertionKind::Content).with_cause(e)), + } + } + + fn file_types(&self) -> (Option, Option) { + let left = self.left.file_type(); + let right = self.right.file_type(); + (left, right) + } + + fn are_files(&self) -> bool { + let (left, right) = self.file_types(); + let left = left.as_ref().map(fs::FileType::is_file).unwrap_or(false); + let right = right.as_ref().map(fs::FileType::is_file).unwrap_or(false); + left && right + } + + fn content_matches(&self) -> Result { + const CAP: usize = 1024; + + let left_file = fs::File::open(self.left.path())?; + let mut left_buf = io::BufReader::with_capacity(CAP, left_file); + + let right_file = fs::File::open(self.right.path())?; + let mut right_buf = io::BufReader::with_capacity(CAP, right_file); + + loop { + let length = { + let left = left_buf.fill_buf()?; + let right = right_buf.fill_buf()?; + if left != right { + return Ok(false); + } + + assert_eq!(left.len(), + right.len(), + "Above check should ensure lengths are the same"); + left.len() + }; + if length == 0 { + break; + } + left_buf.consume(length); + right_buf.consume(length); + } + + Ok(true) + } +} + +/// An iterator for recursively diffing two directories. +/// +/// To create an `IntoIter`, first create the builder `DirDiff` and call `.into_iter()`. +#[derive(Debug)] +pub struct IntoIter { + pub(self) left_root: path::PathBuf, + pub(self) left_walk: WalkIter, + pub(self) right_root: path::PathBuf, + pub(self) right_walk: WalkIter, +} + +impl IntoIter { + fn transposed_next(&mut self) -> Result, IoError> { + if let Some(entry) = self.left_walk.next() { + let entry = entry?; + let entry_path = entry.path(); + + let relative = entry_path + .strip_prefix(&self.left_root) + .expect("WalkDir returns items rooted under left_root"); + let right = self.right_root.join(relative); + let right = if right.exists() { + DirEntry::exists(right) + } else { + DirEntry::missing(right) + }?; + + // Don't use `walkdir::DirEntry` because its `file_type` came from `fs::read_dir` + // which we can't reproduce for `right` + let left = DirEntry::exists(entry_path.to_owned())?; + + let entry = DiffEntry { left, right }; + return Ok(Some(entry)); + } + + while let Some(entry) = self.right_walk.next() { + let entry = entry?; + let entry_path = entry.path(); + + let relative = entry_path + .strip_prefix(&self.right_root) + .expect("WalkDir returns items rooted under right_root"); + let left = self.left_root.join(relative); + // `left.exists()` was covered above + if !left.exists() { + let left = DirEntry::missing(left)?; + + // Don't use `walkdir::DirEntry` because its `file_type` came from `fs::read_dir` + // which we can't reproduce for `left` + let right = DirEntry::exists(entry_path.to_owned())?; + + let entry = DiffEntry { left, right }; + return Ok(Some(entry)); + } + } + + Ok(None) + } +} + +impl Iterator for IntoIter { + type Item = Result; + + fn next(&mut self) -> Option { + let item = self.transposed_next(); + match item { + Ok(Some(i)) => Some(Ok(i)), + Ok(None) => None, + Err(e) => Some(Err(e)), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 1cce8bd..268c95b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,20 +13,14 @@ extern crate walkdir; -use std::fs::File; -use std::io::prelude::*; -use std::path::Path; -use std::cmp::Ordering; +mod error; +mod iter; -use walkdir::{DirEntry, WalkDir}; +use std::path::PathBuf; -/// The various errors that can happen when diffing two directories -#[derive(Debug)] -pub enum Error { - Io(std::io::Error), - StripPrefix(std::path::StripPrefixError), - WalkDir(walkdir::Error), -} +pub use error::IoError; +pub use error::{AssertionKind, AssertionError}; +pub use iter::{DirDiff, DirEntry, DiffEntry, IntoIter}; /// Are the contents of two directories different? /// @@ -37,59 +31,15 @@ pub enum Error { /// /// assert!(dir_diff::is_different("dir/a", "dir/b").unwrap()); /// ``` -pub fn is_different, B: AsRef>(a_base: A, b_base: B) -> Result { - let mut a_walker = walk_dir(a_base); - let mut b_walker = walk_dir(b_base); - - for (a, b) in (&mut a_walker).zip(&mut b_walker) { - let a = a?; - let b = b?; - - if a.depth() != b.depth() || a.file_type() != b.file_type() - || a.file_name() != b.file_name() - || (a.file_type().is_file() && read_to_vec(a.path())? != read_to_vec(b.path())?) - { +pub fn is_different(left_root: L, right_root: R) -> Result + where L: Into, + R: Into +{ + for entry in iter::DirDiff::new(left_root, right_root) { + if entry?.assert().is_err() { return Ok(true); } } - Ok(!a_walker.next().is_none() || !b_walker.next().is_none()) -} - -fn walk_dir>(path: P) -> std::iter::Skip { - WalkDir::new(path) - .sort_by(compare_by_file_name) - .into_iter() - .skip(1) -} - -fn compare_by_file_name(a: &DirEntry, b: &DirEntry) -> Ordering { - a.file_name().cmp(b.file_name()) -} - -fn read_to_vec>(file: P) -> Result, std::io::Error> { - let mut data = Vec::new(); - let mut file = File::open(file.as_ref())?; - - file.read_to_end(&mut data)?; - - Ok(data) -} - -impl From for Error { - fn from(e: std::io::Error) -> Error { - Error::Io(e) - } -} - -impl From for Error { - fn from(e: std::path::StripPrefixError) -> Error { - Error::StripPrefix(e) - } -} - -impl From for Error { - fn from(e: walkdir::Error) -> Error { - Error::WalkDir(e) - } + Ok(false) }