|
| 1 | +use std::cmp::Ordering; |
| 2 | +use std::fmt::Debug; |
| 3 | +use std::path::{Path, PathBuf}; |
| 4 | + |
| 5 | +use walkdir::WalkDir; |
| 6 | + |
| 7 | +/// A structure to compare two directories in a human-readable way in tests. |
| 8 | +#[derive(Debug, Clone)] |
| 9 | +pub struct DirStructure { |
| 10 | + content: String, |
| 11 | +} |
| 12 | + |
| 13 | +impl 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 yield 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 | + |
| 93 | +impl From<&Path> for DirStructure { |
| 94 | + fn from(path: &Path) -> Self { |
| 95 | + Self::from_path(path) |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +impl PartialEq for DirStructure { |
| 100 | + fn eq(&self, other: &Self) -> bool { |
| 101 | + self.trimmed_lines() == other.trimmed_lines() |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +impl From<PathBuf> for DirStructure { |
| 106 | + fn from(path: PathBuf) -> Self { |
| 107 | + Self::from_path(path) |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | +impl From<&PathBuf> for DirStructure { |
| 112 | + fn from(path: &PathBuf) -> Self { |
| 113 | + Self::from_path(path) |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +impl From<String> for DirStructure { |
| 118 | + fn from(content: String) -> Self { |
| 119 | + Self { content } |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +impl 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] |
| 156 | +macro_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 | +} |
| 174 | +pub use assert_dir_eq; |
| 175 | + |
| 176 | +#[cfg(test)] |
| 177 | +mod 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 | +} |
0 commit comments