mithril_common/test_utils/
dir_eq.rs1use std::cmp::Ordering;
2use std::fmt::Debug;
3use std::path::{Path, PathBuf};
4
5use walkdir::WalkDir;
6
7#[derive(Debug, Clone)]
9pub struct DirStructure {
10 content: String,
11}
12
13impl DirStructure {
14 pub fn from_path<P: AsRef<Path>>(path: P) -> Self {
16 let mut content = String::new();
17 let mut is_first_entry = true;
18
19 for dir_entry in WalkDir::new(path)
20 .sort_by(|l, r| {
21 (!l.file_type().is_dir())
22 .cmp(&!r.file_type().is_dir())
23 .then(l.file_name().cmp(r.file_name()))
24 })
25 .into_iter()
26 .filter_entry(|e| e.file_type().is_file() || e.file_type().is_dir())
27 .flatten()
28 .skip(1)
30 {
31 if !is_first_entry {
32 content.push('\n')
33 } else {
34 is_first_entry = false;
35 }
36
37 let suffix = if dir_entry.file_type().is_dir() {
38 "/"
39 } else {
40 ""
41 };
42
43 content.push_str(&format!(
44 "{} {}{suffix}",
45 "*".repeat(dir_entry.depth()),
46 dir_entry.file_name().to_string_lossy()
47 ));
48 }
49
50 Self { content }
51 }
52
53 fn trimmed_lines(&self) -> Vec<&str> {
54 self.content.lines().map(|l| l.trim_start()).collect()
55 }
56
57 pub fn diff(&self, other: &Self) -> String {
59 let mut self_lines = self.trimmed_lines();
60 let mut other_lines = other.trimmed_lines();
61
62 let left_padding = self_lines.iter().map(|l| l.len()).max().unwrap_or(0);
63
64 match self_lines.len().cmp(&other_lines.len()) {
67 Ordering::Less => {
68 let padding = vec![""; other_lines.len() - self_lines.len()];
69 self_lines.extend(padding);
70 }
71 Ordering::Greater => {
72 let padding = vec![""; self_lines.len() - other_lines.len()];
73 other_lines.extend(padding);
74 }
75 Ordering::Equal => {}
76 }
77
78 self_lines
79 .into_iter()
80 .zip(other_lines)
81 .map(|(left, right)| {
82 if left == right {
83 format!("= {left}")
84 } else {
85 format!("! {left:<left_padding$} </> {right}")
86 }
87 })
88 .collect::<Vec<_>>()
89 .join("\n")
90 }
91}
92
93impl From<&Path> for DirStructure {
94 fn from(path: &Path) -> Self {
95 Self::from_path(path)
96 }
97}
98
99impl PartialEq for DirStructure {
100 fn eq(&self, other: &Self) -> bool {
101 self.trimmed_lines() == other.trimmed_lines()
102 }
103}
104
105impl From<PathBuf> for DirStructure {
106 fn from(path: PathBuf) -> Self {
107 Self::from_path(path)
108 }
109}
110
111impl From<&PathBuf> for DirStructure {
112 fn from(path: &PathBuf) -> Self {
113 Self::from_path(path)
114 }
115}
116
117impl From<String> for DirStructure {
118 fn from(content: String) -> Self {
119 Self { content }
120 }
121}
122
123impl From<&str> for DirStructure {
124 fn from(str: &str) -> Self {
125 str.to_string().into()
126 }
127}
128
129#[macro_export]
156macro_rules! assert_dir_eq {
157 ($dir: expr, $expected_structure: expr) => {
158 $crate::test_utils::assert_dir_eq!($dir, $expected_structure, "");
159 };
160 ($dir: expr, $expected_structure: expr, $($arg:tt)+) => {
161 let actual = $crate::test_utils::DirStructure::from_path($dir);
162 let expected = $crate::test_utils::DirStructure::from($expected_structure);
163 let comment = format!($($arg)+);
164 assert!(
165 actual == expected,
166 "{}Directory `{}` does not match expected structure:
167{}",
168 if comment.is_empty() { String::new() } else { format!("{}:\n", comment) },
169 $dir.display(),
170 actual.diff(&expected)
171 );
172 };
173}
174pub use assert_dir_eq;
175
176#[cfg(test)]
177mod tests {
178 use std::fs::{create_dir, File};
179
180 use crate::test_utils::temp_dir_create;
181
182 use super::*;
183
184 fn create_multiple_dirs<P: AsRef<Path>>(dirs: &[P]) {
185 for dir in dirs {
186 create_dir(dir).unwrap();
187 }
188 }
189
190 fn create_multiple_files<P: AsRef<Path>>(files: &[P]) {
191 for file in files {
192 File::create(file).unwrap();
193 }
194 }
195
196 #[test]
197 fn path_to_dir_structure() {
198 let test_dir = temp_dir_create!();
199
200 assert_eq!("", DirStructure::from(&test_dir).content);
201
202 create_dir(test_dir.join("folder1")).unwrap();
203 assert_eq!("* folder1/", DirStructure::from(&test_dir).content);
204
205 File::create(test_dir.join("folder1").join("file")).unwrap();
206 assert_eq!(
207 "* folder1/
208** file",
209 DirStructure::from(&test_dir).content
210 );
211
212 create_multiple_dirs(&[
213 test_dir.join("folder2"),
214 test_dir.join("folder2").join("f_subfolder"),
215 test_dir.join("folder2").join("1_subfolder"),
216 ]);
217 create_multiple_files(&[
218 test_dir.join("folder2").join("xyz"),
219 test_dir.join("folder2").join("abc"),
220 test_dir.join("folder2").join("100"),
221 test_dir.join("folder2").join("20"),
222 test_dir.join("folder2").join("300"),
223 test_dir.join("main_folder_file"),
224 ]);
225 assert_eq!(
226 "* folder1/
227** file
228* folder2/
229** 1_subfolder/
230** f_subfolder/
231** 100
232** 20
233** 300
234** abc
235** xyz
236* main_folder_file",
237 DirStructure::from(&test_dir).content
238 );
239 }
240
241 #[test]
242 fn dir_structure_diff() {
243 let structure = DirStructure {
244 content: "* line 1\n* line 2".to_string(),
245 };
246
247 assert_eq!(
248 "= * line 1
249= * line 2",
250 structure.diff(&structure)
251 );
252 assert_eq!(
253 "! </> * line 1
254! </> * line 2",
255 DirStructure {
256 content: String::new(),
257 }
258 .diff(&structure)
259 );
260 assert_eq!(
261 "= * line 1
262! * line 2 </> ",
263 structure.diff(&DirStructure {
264 content: "* line 1".to_string(),
265 })
266 );
267 assert_eq!(
268 "! * line 1 </> * line a
269= * line 2
270! </> * line b",
271 structure.diff(&DirStructure {
272 content: "* line a\n* line 2\n* line b".to_string(),
273 })
274 );
275 }
276
277 #[test]
278 fn trim_whitespaces_at_lines_start() {
279 let structure = DirStructure {
280 content: " * line1
281 * line 2"
282 .to_string(),
283 };
284
285 assert_eq!(vec!["* line1", "* line 2"], structure.trimmed_lines());
286 }
287
288 #[test]
289 fn dir_eq_single_file() {
290 let test_dir = temp_dir_create!();
291 File::create(test_dir.join("file")).unwrap();
292 assert_dir_eq!(&test_dir, "* file");
293 }
294
295 #[test]
296 fn dir_eq_single_dir() {
297 let test_dir = temp_dir_create!();
298 create_dir(test_dir.join("folder")).unwrap();
299 assert_dir_eq!(&test_dir, "* folder/");
300 }
301
302 #[test]
303 fn can_compare_two_path() {
304 let test_dir = temp_dir_create!();
305 let left_dir = test_dir.join("left");
306 let right_dir = test_dir.join("right");
307
308 create_multiple_dirs(&[&left_dir, &right_dir]);
309 create_multiple_files(&[left_dir.join("file"), right_dir.join("file")]);
310
311 assert_dir_eq!(&left_dir, right_dir);
312 }
313
314 #[test]
315 fn can_provide_additional_comment() {
316 let test_dir = temp_dir_create!();
317 assert_dir_eq!(&test_dir, "", "additional comment: {}", "formatted");
318 }
319
320 #[test]
321 fn dir_eq_multiple_files_and_dirs() {
322 let test_dir = temp_dir_create!();
323 let first_subfolder = test_dir.join("folder 1");
324 let second_subfolder = test_dir.join("folder 2");
325
326 create_multiple_dirs(&[&first_subfolder, &second_subfolder]);
327 create_multiple_files(&[
328 test_dir.join("xyz"),
329 test_dir.join("abc"),
330 test_dir.join("100"),
331 test_dir.join("20"),
332 test_dir.join("300"),
333 first_subfolder.join("file 1"),
334 first_subfolder.join("file 2"),
335 second_subfolder.join("file 3"),
336 ]);
337
338 assert_dir_eq!(
339 &test_dir,
340 "* folder 1/
341 ** file 1
342 ** file 2
343 * folder 2/
344 ** file 3
345 * 100
346 * 20
347 * 300
348 * abc
349 * xyz"
350 );
351 }
352}