From 855928582aa08bccd77e8f83c068b9fe8a336442 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Sat, 27 Jan 2018 16:09:33 -0700 Subject: [PATCH 1/7] refactor: Split out Error --- src/error.rs | 12 ++++++++++++ src/lib.rs | 17 ++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 src/error.rs diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..6235a39 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,12 @@ +use std::io; +use std::path; + +use walkdir; + +/// The various errors that can happen when diffing two directories +#[derive(Debug)] +pub enum Error { + Io(io::Error), + StripPrefix(path::StripPrefixError), + WalkDir(walkdir::Error), +} diff --git a/src/lib.rs b/src/lib.rs index 1cce8bd..b87eb98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,13 +20,9 @@ use std::cmp::Ordering; use walkdir::{DirEntry, WalkDir}; -/// 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), -} +mod error; + +pub use error::Error; /// Are the contents of two directories different? /// @@ -45,10 +41,9 @@ pub fn is_different, B: AsRef>(a_base: A, b_base: B) -> Res 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())?) - { + 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())?) { return Ok(true); } } From c2bd2e6a8cea7a3ed6a572cf56ad5e6728ce8040 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Sat, 27 Jan 2018 16:55:59 -0700 Subject: [PATCH 2/7] refactor: Simplfy iter type --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b87eb98..6e0abba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,11 +51,11 @@ pub fn is_different, B: AsRef>(a_base: A, b_base: B) -> Res Ok(!a_walker.next().is_none() || !b_walker.next().is_none()) } -fn walk_dir>(path: P) -> std::iter::Skip { +fn walk_dir>(path: P) -> walkdir::IntoIter { WalkDir::new(path) .sort_by(compare_by_file_name) + .min_depth(1) .into_iter() - .skip(1) } fn compare_by_file_name(a: &DirEntry, b: &DirEntry) -> Ordering { From 69153f9eb933fc6b044d96df6d356900fbc108f2 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Sat, 27 Jan 2018 17:18:54 -0700 Subject: [PATCH 3/7] rest of error --- src/error.rs | 18 ++++++++++++++++++ src/lib.rs | 18 ------------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/error.rs b/src/error.rs index 6235a39..70a19ca 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,3 +10,21 @@ pub enum Error { StripPrefix(path::StripPrefixError), WalkDir(walkdir::Error), } + +impl From for Error { + fn from(e: io::Error) -> Error { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: path::StripPrefixError) -> Error { + Error::StripPrefix(e) + } +} + +impl From for Error { + fn from(e: walkdir::Error) -> Error { + Error::WalkDir(e) + } +} diff --git a/src/lib.rs b/src/lib.rs index 6e0abba..7e42961 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,21 +70,3 @@ fn read_to_vec>(file: P) -> Result, std::io::Error> { 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) - } -} From f606aeeb8da5582ceb0c9a2ec2f15ed1c4e8d653 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 30 Jan 2018 20:00:05 -0700 Subject: [PATCH 4/7] refactor: Using a dir-diff iter This is a step towards publicly exposing it. We do lose the optimization from the last major PR. We should still be better than the original code though. --- src/error.rs | 8 --- src/iter.rs | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 44 ++++++-------- 3 files changed, 181 insertions(+), 33 deletions(-) create mode 100644 src/iter.rs diff --git a/src/error.rs b/src/error.rs index 70a19ca..5ac4e6f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,4 @@ use std::io; -use std::path; use walkdir; @@ -7,7 +6,6 @@ use walkdir; #[derive(Debug)] pub enum Error { Io(io::Error), - StripPrefix(path::StripPrefixError), WalkDir(walkdir::Error), } @@ -17,12 +15,6 @@ impl From for Error { } } -impl From for Error { - fn from(e: path::StripPrefixError) -> Error { - Error::StripPrefix(e) - } -} - impl From for Error { fn from(e: walkdir::Error) -> Error { Error::WalkDir(e) diff --git a/src/iter.rs b/src/iter.rs new file mode 100644 index 0000000..f901d7c --- /dev/null +++ b/src/iter.rs @@ -0,0 +1,162 @@ +use std::fs; +use std::path; + +use walkdir; + +use error::Error; + +type WalkIter = walkdir::IntoIter; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DirDiff { + left: path::PathBuf, + right: path::PathBuf, +} + +impl DirDiff { + 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, + } + } +} + +#[derive(Debug, Clone)] +pub struct DirEntry { + path: path::PathBuf, + file_type: Option, +} + +impl DirEntry { + 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) + } + + pub fn path(&self) -> &path::Path { + self.path.as_path() + } + + pub fn file_type(&self) -> Option { + self.file_type + } +} + +#[derive(Debug, Clone)] +pub struct DiffEntry { + left: DirEntry, + right: DirEntry, +} + +impl DiffEntry { + pub fn left(&self) -> &DirEntry { + &self.left + } + + pub fn right(&self) -> &DirEntry { + &self.right + } +} + +#[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, Error> { + 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 7e42961..5d62a38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,12 +15,10 @@ extern crate walkdir; use std::fs::File; use std::io::prelude::*; -use std::path::Path; -use std::cmp::Ordering; - -use walkdir::{DirEntry, WalkDir}; +use std::path::{Path, PathBuf}; mod error; +mod iter; pub use error::Error; @@ -33,33 +31,29 @@ pub use error::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); +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) { + let entry = entry?; + let left = entry.left(); + let right = entry.right(); - for (a, b) in (&mut a_walker).zip(&mut b_walker) { - let a = a?; - let b = b?; + // Covers missing files because We know that entry can never be missing on both sides + if left.file_type() != right.file_type() { + return Ok(true); + } - 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())?) { + let are_files = left.file_type() + .expect("exists because of above `file_type` check") + .is_file(); + if are_files && read_to_vec(left.path())? != read_to_vec(right.path())? { return Ok(true); } } - Ok(!a_walker.next().is_none() || !b_walker.next().is_none()) -} - -fn walk_dir>(path: P) -> walkdir::IntoIter { - WalkDir::new(path) - .sort_by(compare_by_file_name) - .min_depth(1) - .into_iter() -} - -fn compare_by_file_name(a: &DirEntry, b: &DirEntry) -> Ordering { - a.file_name().cmp(b.file_name()) + Ok(false) } fn read_to_vec>(file: P) -> Result, std::io::Error> { From 90c50d9573399832de8ddcecd8c9cdd8d3e7ed47 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 30 Jan 2018 20:14:53 -0700 Subject: [PATCH 5/7] fix(Error): Clarify underlying errors This is more so making room for other kinds of errors that will be added. BREAKING CHANGE: `is_different` now returns `IoError` instead of `Error`. --- src/error.rs | 19 +++++++++++-------- src/iter.rs | 12 ++++++------ src/lib.rs | 6 +++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/error.rs b/src/error.rs index 5ac4e6f..14067db 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,21 +2,24 @@ use std::io; use walkdir; -/// The various errors that can happen when diffing two directories +/// IO errors preventing diffing from happening. #[derive(Debug)] -pub enum Error { +pub struct IoError(InnerIoError); + +#[derive(Debug)] +enum InnerIoError { Io(io::Error), WalkDir(walkdir::Error), } -impl From for Error { - fn from(e: io::Error) -> Error { - Error::Io(e) +impl From for IoError { + fn from(e: io::Error) -> IoError { + IoError(InnerIoError::Io(e)) } } -impl From for Error { - fn from(e: walkdir::Error) -> Error { - Error::WalkDir(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 index f901d7c..2dd27b4 100644 --- a/src/iter.rs +++ b/src/iter.rs @@ -3,7 +3,7 @@ use std::path; use walkdir; -use error::Error; +use error::IoError; type WalkIter = walkdir::IntoIter; @@ -30,7 +30,7 @@ impl DirDiff { } impl IntoIterator for DirDiff { - type Item = Result; + type Item = Result; type IntoIter = IntoIter; @@ -53,14 +53,14 @@ pub struct DirEntry { } impl DirEntry { - pub(self) fn exists(path: path::PathBuf) -> Result { + 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 { + pub(self) fn missing(path: path::PathBuf) -> Result { let file_type = None; let s = Self { path, file_type }; Ok(s) @@ -100,7 +100,7 @@ pub struct IntoIter { } impl IntoIter { - fn transposed_next(&mut self) -> Result, Error> { + fn transposed_next(&mut self) -> Result, IoError> { if let Some(entry) = self.left_walk.next() { let entry = entry?; let entry_path = entry.path(); @@ -149,7 +149,7 @@ impl IntoIter { } impl Iterator for IntoIter { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { let item = self.transposed_next(); diff --git a/src/lib.rs b/src/lib.rs index 5d62a38..05b6de4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ use std::path::{Path, PathBuf}; mod error; mod iter; -pub use error::Error; +pub use error::IoError; /// Are the contents of two directories different? /// @@ -31,7 +31,7 @@ pub use error::Error; /// /// assert!(dir_diff::is_different("dir/a", "dir/b").unwrap()); /// ``` -pub fn is_different(left_root: L, right_root: R) -> Result +pub fn is_different(left_root: L, right_root: R) -> Result where L: Into, R: Into { @@ -56,7 +56,7 @@ pub fn is_different(left_root: L, right_root: R) -> Result Ok(false) } -fn read_to_vec>(file: P) -> Result, std::io::Error> { +fn read_to_vec>(file: P) -> Result, IoError> { let mut data = Vec::new(); let mut file = File::open(file.as_ref())?; From 85e571195d3e232d1e59775cf9c9350baf52f33a Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 31 Jan 2018 14:31:29 -0700 Subject: [PATCH 6/7] feat: Expose a per-path diffing iter Fixes #11 by giving users access to more detailed error information if they use `DirDiff`'s `IntoIter` instead of `is_different`. This makes it possible to workaround several existing issues: - #9 by providing your own line-ending agnostic checks - #6 by providing a file comparison function that handles larger files. --- src/error.rs | 158 +++++++++++++++++++++++++++++++++++++++++++++++++- src/iter.rs | 161 +++++++++++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 31 ++-------- 3 files changed, 313 insertions(+), 37 deletions(-) diff --git a/src/error.rs b/src/error.rs index 14067db..3a46861 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,15 +1,171 @@ +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)] +#[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 { diff --git a/src/iter.rs b/src/iter.rs index 2dd27b4..74449a4 100644 --- a/src/iter.rs +++ b/src/iter.rs @@ -1,12 +1,16 @@ +use std::io::prelude::*; +use std::ffi; use std::fs; 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, @@ -14,6 +18,8 @@ pub struct DirDiff { } 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 @@ -46,13 +52,48 @@ impl IntoIterator for DirDiff { } } -#[derive(Debug, Clone)] +/// 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()); @@ -65,32 +106,132 @@ impl DirEntry { let s = Self { path, file_type }; Ok(s) } - - pub fn path(&self) -> &path::Path { - self.path.as_path() - } - - pub fn file_type(&self) -> Option { - self.file_type - } } -#[derive(Debug, Clone)] +/// 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 { + let left = Self::read_to_vec(self.left.path())?; + let right = Self::read_to_vec(self.right.path())?; + Ok(left == right) + } + + fn read_to_vec(file: &path::Path) -> Result, IoError> { + let mut data = Vec::new(); + let mut file = fs::File::open(file)?; + + file.read_to_end(&mut data)?; + + Ok(data) + } } +/// 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, diff --git a/src/lib.rs b/src/lib.rs index 05b6de4..268c95b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,14 +13,14 @@ extern crate walkdir; -use std::fs::File; -use std::io::prelude::*; -use std::path::{Path, PathBuf}; - mod error; mod iter; +use std::path::PathBuf; + pub use error::IoError; +pub use error::{AssertionKind, AssertionError}; +pub use iter::{DirDiff, DirEntry, DiffEntry, IntoIter}; /// Are the contents of two directories different? /// @@ -36,31 +36,10 @@ pub fn is_different(left_root: L, right_root: R) -> Result R: Into { for entry in iter::DirDiff::new(left_root, right_root) { - let entry = entry?; - let left = entry.left(); - let right = entry.right(); - - // Covers missing files because We know that entry can never be missing on both sides - if left.file_type() != right.file_type() { - return Ok(true); - } - - let are_files = left.file_type() - .expect("exists because of above `file_type` check") - .is_file(); - if are_files && read_to_vec(left.path())? != read_to_vec(right.path())? { + if entry?.assert().is_err() { return Ok(true); } } Ok(false) } - -fn read_to_vec>(file: P) -> Result, IoError> { - let mut data = Vec::new(); - let mut file = File::open(file.as_ref())?; - - file.read_to_end(&mut data)?; - - Ok(data) -} From 0b9d6c16a36927666f1889114c70467a51b0d52e Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 31 Jan 2018 15:25:50 -0700 Subject: [PATCH 7/7] fix: Support diffing large files Fixes #6 --- src/iter.rs | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/iter.rs b/src/iter.rs index 74449a4..55d2f54 100644 --- a/src/iter.rs +++ b/src/iter.rs @@ -1,6 +1,7 @@ use std::io::prelude::*; use std::ffi; use std::fs; +use std::io; use std::path; use walkdir; @@ -214,18 +215,35 @@ impl DiffEntry { } fn content_matches(&self) -> Result { - let left = Self::read_to_vec(self.left.path())?; - let right = Self::read_to_vec(self.right.path())?; - Ok(left == right) - } + const CAP: usize = 1024; + + let left_file = fs::File::open(self.left.path())?; + let mut left_buf = io::BufReader::with_capacity(CAP, left_file); - fn read_to_vec(file: &path::Path) -> Result, IoError> { - let mut data = Vec::new(); - let mut file = fs::File::open(file)?; + let right_file = fs::File::open(self.right.path())?; + let mut right_buf = io::BufReader::with_capacity(CAP, right_file); - file.read_to_end(&mut data)?; + 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(data) + Ok(true) } }