diff --git a/Cargo.toml b/Cargo.toml index bd83954..14be61d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ travis-ci = { repository = "steveklabnik/dir-diff" } [dependencies] walkdir = "2.0.1" +matches = "0.1.7" diff --git a/src/lib.rs b/src/lib.rs index 1cce8bd..e600da8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,15 +9,15 @@ //! extern crate dir_diff; //! //! assert!(dir_diff::is_different("dir/a", "dir/b").unwrap()); -//! ``` +//! extern crate walkdir; -use std::fs::File; -use std::io::prelude::*; -use std::path::Path; use std::cmp::Ordering; - +use std::io::prelude::Read; +use std::path::Path; +use std::path::PathBuf; +use std::{fs, fs::File}; use walkdir::{DirEntry, WalkDir}; /// The various errors that can happen when diffing two directories @@ -26,6 +26,22 @@ pub enum Error { Io(std::io::Error), StripPrefix(std::path::StripPrefixError), WalkDir(walkdir::Error), + /// One directory has more or less files than the other. + MissingFiles, + /// File name doesn't match. + FileNameMismatch(PathBuf, PathBuf), + /// Binary contetn doesn't match. + BinaryContentMismatch(PathBuf, PathBuf), + /// One file has more or less lines than the other. + FileLengthMismatch(PathBuf, PathBuf), + /// The content of a file doesn't match. + ContentMismatch { + line_number: usize, + a_path: PathBuf, + b_path: PathBuf, + a_content: String, + b_content: String, + }, } /// Are the contents of two directories different? @@ -45,7 +61,8 @@ 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() + 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())?) { @@ -56,6 +73,83 @@ pub fn is_different, B: AsRef>(a_base: A, b_base: B) -> Res Ok(!a_walker.next().is_none() || !b_walker.next().is_none()) } +/// Identify the differences between two directories. +/// +/// # Examples +/// +/// ```no_run +/// extern crate dir_diff; +/// +/// assert_eq!(dir_diff::see_difference("main/dir1", "main/dir1").unwrap(), ()); +/// ``` +pub fn see_difference, B: AsRef>(a_base: A, b_base: B) -> Result<(), Error> { + let mut files_a = walk_dir_and_strip_prefix(&a_base) + .into_iter() + .collect::>(); + let mut files_b = walk_dir_and_strip_prefix(&b_base) + .into_iter() + .collect::>(); + + if files_a.len() != files_b.len() { + return Err(Error::MissingFiles); + } + + files_a.sort(); + files_b.sort(); + + for (a, b) in files_a.into_iter().zip(files_b.into_iter()).into_iter() { + if a != b { + return Err(Error::FileNameMismatch(a, b)); + } + + let full_path_a = &a_base.as_ref().join(&a); + let full_path_b = &b_base.as_ref().join(&b); + + if full_path_a.is_dir() || full_path_b.is_dir() { + continue; + } + + let content_of_a = fs::read(full_path_a)?; + let content_of_b = fs::read(full_path_b)?; + + match ( + String::from_utf8(content_of_a), + String::from_utf8(content_of_b), + ) { + (Err(content_of_a), Err(content_of_b)) => { + if content_of_a.as_bytes() != content_of_b.as_bytes() { + return Err(Error::BinaryContentMismatch(a, b)); + } + } + (Ok(content_of_a), Ok(content_of_b)) => { + let mut a_lines = content_of_a.lines().collect::>(); + let mut b_lines = content_of_b.lines().collect::>(); + + if a_lines.len() != b_lines.len() { + return Err(Error::FileLengthMismatch(a, b)); + } + + for (line_number, (line_a, line_b)) in + a_lines.into_iter().zip(b_lines.into_iter()).enumerate() + { + if line_a != line_b { + return Err(Error::ContentMismatch { + a_path: a, + b_path: b, + a_content: line_a.to_string(), + b_content: line_b.to_string(), + line_number, + }); + } + } + } + _ => return Err(Error::BinaryContentMismatch(a, b)), + } + } + + Ok(()) +} + fn walk_dir>(path: P) -> std::iter::Skip { WalkDir::new(path) .sort_by(compare_by_file_name) @@ -63,6 +157,20 @@ fn walk_dir>(path: P) -> std::iter::Skip { .skip(1) } +/// Iterated through a directory, and strips the prefix of each path. +fn walk_dir_and_strip_prefix<'a, P>(prefix: P) -> impl Iterator +where + P: AsRef + Copy, +{ + WalkDir::new(prefix) + .into_iter() + .filter_map(Result::ok) + .filter_map(move |e| { + let new_path = e.path(); + new_path.strip_prefix(&prefix).map(|e| e.to_owned()).ok() + }) +} + fn compare_by_file_name(a: &DirEntry, b: &DirEntry) -> Ordering { a.file_name().cmp(b.file_name()) } diff --git a/tests/content_mismatch/dir1/test.txt b/tests/content_mismatch/dir1/test.txt new file mode 100644 index 0000000..bad6cf1 --- /dev/null +++ b/tests/content_mismatch/dir1/test.txt @@ -0,0 +1 @@ +testing testing \ No newline at end of file diff --git a/tests/content_mismatch/dir2/test.txt b/tests/content_mismatch/dir2/test.txt new file mode 100644 index 0000000..8882ebd --- /dev/null +++ b/tests/content_mismatch/dir2/test.txt @@ -0,0 +1 @@ +oh no! \ No newline at end of file diff --git a/tests/dir_name_mismatch/dir1/a.txt b/tests/dir_name_mismatch/dir1/a.txt new file mode 100644 index 0000000..f3a3485 --- /dev/null +++ b/tests/dir_name_mismatch/dir1/a.txt @@ -0,0 +1 @@ +text \ No newline at end of file diff --git a/tests/dir_name_mismatch/dir2/a.txt b/tests/dir_name_mismatch/dir2/a.txt new file mode 100644 index 0000000..f3a3485 --- /dev/null +++ b/tests/dir_name_mismatch/dir2/a.txt @@ -0,0 +1 @@ +text \ No newline at end of file diff --git a/tests/file_length_mismatch/dir1/a.txt b/tests/file_length_mismatch/dir1/a.txt new file mode 100644 index 0000000..b6fc4c6 --- /dev/null +++ b/tests/file_length_mismatch/dir1/a.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/tests/file_length_mismatch/dir2/a.txt b/tests/file_length_mismatch/dir2/a.txt new file mode 100644 index 0000000..6d5645c --- /dev/null +++ b/tests/file_length_mismatch/dir2/a.txt @@ -0,0 +1,2 @@ +hello +master \ No newline at end of file diff --git a/tests/file_name_mismatch/dir1/b.txt b/tests/file_name_mismatch/dir1/b.txt new file mode 100644 index 0000000..f3a3485 --- /dev/null +++ b/tests/file_name_mismatch/dir1/b.txt @@ -0,0 +1 @@ +text \ No newline at end of file diff --git a/tests/file_name_mismatch/dir2/a.txt b/tests/file_name_mismatch/dir2/a.txt new file mode 100644 index 0000000..f3a3485 --- /dev/null +++ b/tests/file_name_mismatch/dir2/a.txt @@ -0,0 +1 @@ +text \ No newline at end of file diff --git a/tests/missing_dir/dir1/a.txt b/tests/missing_dir/dir1/a.txt new file mode 100644 index 0000000..9233c1a --- /dev/null +++ b/tests/missing_dir/dir1/a.txt @@ -0,0 +1 @@ +dd \ No newline at end of file diff --git a/tests/missing_dir/dir2/a.txt b/tests/missing_dir/dir2/a.txt new file mode 100644 index 0000000..f3a3485 --- /dev/null +++ b/tests/missing_dir/dir2/a.txt @@ -0,0 +1 @@ +text \ No newline at end of file diff --git a/tests/missing_file/dir1/a.txt b/tests/missing_file/dir1/a.txt new file mode 100644 index 0000000..c844b92 --- /dev/null +++ b/tests/missing_file/dir1/a.txt @@ -0,0 +1 @@ +some text from dir1 \ No newline at end of file diff --git a/tests/missing_file/dir1/b.txt b/tests/missing_file/dir1/b.txt new file mode 100644 index 0000000..b649a9b --- /dev/null +++ b/tests/missing_file/dir1/b.txt @@ -0,0 +1 @@ +some text \ No newline at end of file diff --git a/tests/missing_file/dir2/a.txt b/tests/missing_file/dir2/a.txt new file mode 100644 index 0000000..b649a9b --- /dev/null +++ b/tests/missing_file/dir2/a.txt @@ -0,0 +1 @@ +some text \ No newline at end of file diff --git a/tests/smoke.rs b/tests/smoke.rs index ce4d338..d120509 100644 --- a/tests/smoke.rs +++ b/tests/smoke.rs @@ -1,8 +1,11 @@ extern crate dir_diff; +#[macro_use] +extern crate matches; -use std::path::Path; +use dir_diff::Error::*; use std::fs::create_dir; use std::io::ErrorKind; +use std::path::{Path, PathBuf}; #[test] fn easy_good() { @@ -63,3 +66,125 @@ fn filedepth() { dir_diff::is_different("tests/filedepth/desc/dir1", "tests/filedepth/desc/dir2").unwrap() ); } + +#[test] +fn missing_file() { + assert_matches!( + dir_diff::see_difference("tests/missing_file/dir1", "tests/missing_file/dir2"), + Err(MissingFiles) + ); + + assert_matches!( + dir_diff::see_difference("tests/missing_dir/dir1", "tests/missing_dir/dir2"), + Err(MissingFiles) + ); +} + +#[test] +fn file_length_mismatch() { + assert_matches!( + dir_diff::see_difference( + "tests/file_length_mismatch/dir1", + "tests/file_length_mismatch/dir2", + ), + Err(FileLengthMismatch(_, _)) + ); +} + +#[test] +fn binary_content_mismatch() { + let expected_binary_filename_a = PathBuf::from("rust-logo.png"); + let expected_binary_filename_b = PathBuf::from("rust-logo.png"); + + let result = dir_diff::see_difference("tests/binary/bad/dir1", "tests/binary/bad/dir2"); + assert_matches!(result, Err(BinaryContentMismatch(_, _))); + + let result = result.unwrap_err(); + if let BinaryContentMismatch(a, b) = &result { + if *a != expected_binary_filename_a || *b != expected_binary_filename_b { + let expected = FileNameMismatch(expected_binary_filename_a, expected_binary_filename_b); + panic!("{:?} doesn't match {:?}", &result, expected); + } + }; +} + +#[test] +fn dir_name_mismatch() { + let expected_dir_a = PathBuf::from("dirA"); + let expected_dir_b = PathBuf::from("dirB"); + + let result = dir_diff::see_difference( + "tests/dir_name_mismatch/dir1", + "tests/dir_name_mismatch/dir2", + ); + assert_matches!(result, Err(FileNameMismatch(_, _))); + + let result = result.unwrap_err(); + if let FileNameMismatch(a, b) = &result { + if *a != expected_dir_a || *b != expected_dir_b { + let expected = FileNameMismatch(expected_dir_a, expected_dir_b); + panic!("{:?} doesn't match {:?}", &result, expected); + } + }; +} + +#[test] +fn file_name_mismatch() { + let expected_file_a = PathBuf::from("b.txt"); + let expected_file_b = PathBuf::from("a.txt"); + + let result = dir_diff::see_difference( + "tests/file_name_mismatch/dir1", + "tests/file_name_mismatch/dir2", + ); + assert_matches!(result, Err(FileNameMismatch(_, _))); + + let result = result.unwrap_err(); + if let FileNameMismatch(a, b) = &result { + if *a != expected_file_a || *b != expected_file_b { + let expected = FileNameMismatch(expected_file_a, expected_file_b); + panic!("{:?} doesn't match {:?}", &result, expected); + } + }; +} + +#[test] +fn content_misatch() { + let expected_a_path = PathBuf::from("test.txt"); + let expected_b_path = PathBuf::from("test.txt"); + let expected_a_content = String::from("testing testing"); + let expected_b_content = String::from("oh no!"); + + let result = + dir_diff::see_difference("tests/content_mismatch/dir1", "tests/content_mismatch/dir2"); + + assert_matches!(result, Err(ContentMismatch { .. })); + let result = result.unwrap_err(); + + // Match the ContentMismatch result with th expected values. + if let ContentMismatch { + line_number, + a_path, + b_path, + a_content, + b_content, + } = &result + { + if *line_number != 0 + || *a_path != expected_a_path + || *b_path != expected_b_path + || *a_content != expected_a_content + || *b_content != expected_b_content + { + let expected = ContentMismatch { + line_number: 0, + a_path: expected_a_path, + b_path: expected_b_path, + a_content: expected_a_content, + b_content: expected_b_content, + }; + + panic!("{:?} doesn't match {:?}", &result, expected); + } + } +}