Skip to content

Commit ee1ae73

Browse files
committed
test(common): add assert_dir_eq for easy check of dir structure
Can be used against another directory or against a string that express the expected structure using `*` to tell the depth of each items.
1 parent 119c722 commit ee1ae73

File tree

2 files changed

+354
-0
lines changed

2 files changed

+354
-0
lines changed
+352
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
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+
}

mithril-common/src/test_utils/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod fake_keys;
1515

1616
mod cardano_transactions_builder;
1717
mod certificate_chain_builder;
18+
mod dir_eq;
1819
mod fixture_builder;
1920
mod mithril_fixture;
2021
mod precomputed_kes_key;
@@ -28,6 +29,7 @@ pub use cardano_transactions_builder::CardanoTransactionsBuilder;
2829
pub use certificate_chain_builder::{
2930
CertificateChainBuilder, CertificateChainBuilderContext, CertificateChainingMethod,
3031
};
32+
pub use dir_eq::*;
3133
pub use fixture_builder::{MithrilFixtureBuilder, StakeDistributionGenerationMethod};
3234
pub use mithril_fixture::{MithrilFixture, SignerFixture};
3335
pub use temp_dir::*;

0 commit comments

Comments
 (0)