mithril_common/test_utils/
dir_eq.rs

1use std::cmp::Ordering;
2use std::fmt::Debug;
3use std::path::{Path, PathBuf};
4
5use walkdir::WalkDir;
6
7/// A structure to compare two directories in a human-readable way in tests.
8#[derive(Debug, Clone)]
9pub struct DirStructure {
10    content: String,
11}
12
13impl DirStructure {
14    /// Creates a new `DirStructure` from a given path by recursively traversing it.
15    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 the first entry as it yields the root directory
29            .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    /// Computes a line-by-line diff between the content of `self` and `other`,
58    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        // Equalize vector lengths by adding empty lines to the shorter one,
65        // else zip will stop at the first missing line
66        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/// Compare a directory against a string representing its expected structure or against another
130/// directory.
131///
132/// When comparing against a string, the string must be formatted as:
133/// - one line per file or directory
134/// - each line starts with a number of `*` representing the entry depth
135/// - directories must end with a `/` (i.e.: `* folder/`)
136/// - order rules are:
137///   - directories then files
138///   - alphanumeric order (i.e.: '20' comes before '3')
139///
140/// Example:
141/// ```no_run
142/// # use mithril_common::test_utils::assert_dir_eq;
143/// # use std::path::PathBuf;
144/// # let path = PathBuf::new();
145/// assert_dir_eq!(
146///   &path,
147///   "* folder_1/
148///    ** file_1
149///    ** file_2
150///    ** subfolder/
151///    *** subfolder_file
152///    * file"
153/// );
154/// ```
155#[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}