dir_assert/
assert_paths.rs

1use crate::Error;
2use std::fs::{metadata, FileType};
3use std::io::{BufRead, BufReader};
4use std::{
5    cmp::Ordering,
6    fs::{DirEntry, File},
7    path::Path,
8};
9
10/// Recursively scan contents of two directories and find differences.
11///
12/// eg.:
13/// ```rust,ignore
14/// #[test]
15/// fn should_directories_be_equal() {
16///     let result = assert_paths("actual", "expected");
17///     assert!(result.is_ok());
18/// }
19/// ```
20///
21/// This function is called inside `assert_paths` macro invocation.
22/// It can be used to verify types of errors returned when types differ.
23pub fn assert_paths<PE: AsRef<Path>, PA: AsRef<Path>>(
24    actual: PA,
25    expected: PE,
26) -> Result<(), Vec<Error>> {
27    let expected = expected.as_ref();
28    let actual = actual.as_ref();
29
30    if !expected.exists() {
31        return Err(vec![Error::new_missing_path(expected)]);
32    }
33
34    if !actual.exists() {
35        return Err(vec![Error::new_missing_path(actual)]);
36    }
37
38    if expected.is_file() && actual.is_file() {
39        compare_file(expected, actual).map_err(|err| vec![err])
40    } else if expected.is_dir() && actual.is_dir() {
41        compare_dir_recursive(expected, actual)
42    } else {
43        Err(vec![Error::new_invalid_comparison(expected, actual)])
44    }
45}
46
47fn compare_dir_recursive<PE: AsRef<Path>, PA: AsRef<Path>>(
48    expected: PE,
49    actual: PA,
50) -> Result<(), Vec<Error>> {
51    let mut expected = dir_contents_sorted(&expected)
52        .map_err(|err| vec![err])?
53        .into_iter();
54    let mut actual = dir_contents_sorted(&actual)
55        .map_err(|err| vec![err])?
56        .into_iter();
57
58    let mut errors = Vec::new();
59
60    let mut expected_entry = expected.next();
61    let mut actual_entry = actual.next();
62
63    loop {
64        let (e, a) = match (&expected_entry, &actual_entry) {
65            (None, None) => break,
66            (Some(e), Some(a)) => (e, a),
67            (Some(e), None) => {
68                errors.push(Error::new_extra_expected(e.path()));
69                expected_entry = expected.next();
70                continue;
71            }
72            (None, Some(a)) => {
73                errors.push(Error::new_extra_actual(a.path()));
74                actual_entry = actual.next();
75                continue;
76            }
77        };
78
79        match e.path().file_name().cmp(&a.path().file_name()) {
80            Ordering::Less => {
81                errors.push(Error::new_extra_expected(e.path()));
82                expected_entry = expected.next();
83                continue;
84            }
85            Ordering::Equal => {
86                let e_ft = get_file_type(e).map_err(|err| vec![err])?;
87                let a_ft = get_file_type(a).map_err(|err| vec![err])?;
88
89                if e_ft.is_file() && a_ft.is_file() {
90                    if let Err(err) = compare_file(e.path(), a.path()) {
91                        errors.push(err);
92                    }
93                } else if e_ft.is_dir() && a_ft.is_dir() {
94                    if let Err(err) = compare_dir_recursive(e.path(), a.path()) {
95                        errors.extend_from_slice(&err);
96                    }
97                } else if e_ft.is_symlink() && a_ft.is_symlink() {
98                    if let Err(err) = compare_symlinks(e, a) {
99                        errors.extend_from_slice(&err);
100                    }
101                } else {
102                    errors.push(Error::new_invalid_comparison(e.path(), a.path()))
103                }
104            }
105            Ordering::Greater => {
106                errors.push(Error::new_extra_actual(a.path()));
107                actual_entry = actual.next();
108                continue;
109            }
110        }
111
112        expected_entry = expected.next();
113        actual_entry = actual.next();
114    }
115
116    if errors.is_empty() {
117        Ok(())
118    } else {
119        Err(errors)
120    }
121}
122
123fn compare_symlinks(e: &DirEntry, a: &DirEntry) -> Result<(), Vec<Error>> {
124    let e_m = metadata(e.path()).map_err(|err| {
125        vec![Error::new_critical(format!(
126            "unable to retrieve metadata from expected symlink {:?}, {}",
127            e.path(),
128            err
129        ))]
130    })?;
131    let a_m = metadata(a.path()).map_err(|err| {
132        vec![Error::new_critical(format!(
133            "unable to retrieve metadata from actual symlink {:?}, {}",
134            a.path(),
135            err
136        ))]
137    })?;
138
139    if e_m.is_file() && a_m.is_file() {
140        compare_file(e.path(), a.path()).map_err(|err| vec![err])?;
141    } else if e_m.is_dir() && a_m.is_dir() {
142        compare_dir_recursive(e.path(), a.path())?;
143    } else {
144        return Err(vec![Error::new_invalid_comparison(e.path(), a.path())]);
145    }
146
147    Ok(())
148}
149
150fn get_file_type(path: &DirEntry) -> Result<FileType, Error> {
151    path.file_type().map_err(|err| {
152        Error::new_critical(format!(
153            "unable to retrieve file type from {:?}, {}",
154            path, err
155        ))
156    })
157}
158
159fn dir_contents_sorted<P: AsRef<Path>>(dir: &P) -> Result<Vec<DirEntry>, Error> {
160    let mut dir_contents = std::fs::read_dir(&dir)
161        .map_err(|err| {
162            Error::new_critical(format!("failed reading dir {:?}, {}", dir.as_ref(), err))
163        })?
164        .collect::<Result<Vec<_>, _>>()
165        .map_err(|err| {
166            Error::new_critical(format!(
167                "an IO error occurred when reading dir, {:?}, {}",
168                dir.as_ref(),
169                err
170            ))
171        })?;
172
173    dir_contents.sort_by(|left, right| left.file_name().cmp(&right.file_name()));
174
175    Ok(dir_contents)
176}
177
178fn compare_file<PE: AsRef<Path>, PA: AsRef<Path>>(expected: PE, actual: PA) -> Result<(), Error> {
179    let expected = expected.as_ref();
180    let actual = actual.as_ref();
181
182    let file_e = File::open(expected).map_err(|e| {
183        Error::new_critical(format!(
184            "cannot open expected file {:?}, reason: {}",
185            expected, e
186        ))
187    })?;
188    let file_a = File::open(actual).map_err(|e| {
189        Error::new_critical(format!(
190            "cannot open actual file {:?}, reason: {}",
191            actual, e
192        ))
193    })?;
194
195    let reader_e = BufReader::new(file_e);
196    let reader_a = BufReader::new(file_a);
197
198    for (idx, lines) in reader_e.lines().zip(reader_a.lines()).enumerate() {
199        let (line_e, line_a) = match lines {
200            (Ok(line_e), Ok(line_a)) => (line_e, line_a),
201            (Err(err), _) => {
202                return Err(Error::new_critical(format!(
203                    "failed reading line from {:?}, reason: {}",
204                    expected, err
205                )))
206            }
207            (_, Err(err)) => {
208                return Err(Error::new_critical(format!(
209                    "failed reading line from {:?}, reason: {}",
210                    actual, err
211                )))
212            }
213        };
214
215        if line_e != line_a {
216            return Err(Error::new_file_contents_mismatch(expected, actual, idx));
217        }
218    }
219
220    Ok(())
221}