dir_assert/
assert_paths.rs1use 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
10pub 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}