Skip to main content

snapbox/dir/
diff.rs

1#[cfg(feature = "dir")]
2use crate::filter::{Filter as _, FilterNewlines, FilterPaths, NormalizeToExpected};
3
4#[derive(Clone, Debug, PartialEq, Eq)]
5pub enum PathDiff {
6    Failure(crate::assert::Error),
7    TypeMismatch {
8        expected_path: std::path::PathBuf,
9        actual_path: std::path::PathBuf,
10        expected_type: FileType,
11        actual_type: FileType,
12    },
13    LinkMismatch {
14        expected_path: std::path::PathBuf,
15        actual_path: std::path::PathBuf,
16        expected_target: std::path::PathBuf,
17        actual_target: std::path::PathBuf,
18    },
19    ContentMismatch {
20        expected_path: std::path::PathBuf,
21        actual_path: std::path::PathBuf,
22        expected_content: crate::Data,
23        actual_content: crate::Data,
24    },
25}
26
27impl PathDiff {
28    /// Report differences between `actual_root` and `pattern_root`
29    ///
30    /// Note: Requires feature flag `path`
31    #[cfg(feature = "dir")]
32    pub fn subset_eq_iter(
33        pattern_root: impl Into<std::path::PathBuf>,
34        actual_root: impl Into<std::path::PathBuf>,
35    ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> {
36        let pattern_root = pattern_root.into();
37        let actual_root = actual_root.into();
38        Self::subset_eq_iter_inner(pattern_root, actual_root)
39    }
40
41    #[cfg(feature = "dir")]
42    pub(crate) fn subset_eq_iter_inner(
43        expected_root: std::path::PathBuf,
44        actual_root: std::path::PathBuf,
45    ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> {
46        let walker = crate::dir::Walk::new(&expected_root);
47        walker.map(move |r| {
48            let expected_path = r.map_err(|e| Self::Failure(e.to_string().into()))?;
49            let rel = expected_path.strip_prefix(&expected_root).unwrap();
50            let actual_path = actual_root.join(rel);
51
52            let expected_type = FileType::from_path(&expected_path);
53            let actual_type = FileType::from_path(&actual_path);
54            if expected_type != actual_type {
55                return Err(Self::TypeMismatch {
56                    expected_path,
57                    actual_path,
58                    expected_type,
59                    actual_type,
60                });
61            }
62
63            match expected_type {
64                FileType::Symlink => {
65                    let expected_target = std::fs::read_link(&expected_path).ok();
66                    let actual_target = std::fs::read_link(&actual_path).ok();
67                    if expected_target != actual_target {
68                        return Err(Self::LinkMismatch {
69                            expected_path,
70                            actual_path,
71                            expected_target: expected_target.unwrap(),
72                            actual_target: actual_target.unwrap(),
73                        });
74                    }
75                }
76                FileType::File => {
77                    let mut actual =
78                        crate::Data::try_read_from(&actual_path, None).map_err(Self::Failure)?;
79
80                    let expected =
81                        FilterNewlines.filter(crate::Data::read_from(&expected_path, None));
82
83                    actual = FilterNewlines.filter(actual.coerce_to(expected.intended_format()));
84
85                    if expected != actual {
86                        return Err(Self::ContentMismatch {
87                            expected_path,
88                            actual_path,
89                            expected_content: expected,
90                            actual_content: actual,
91                        });
92                    }
93                }
94                FileType::Dir | FileType::Unknown | FileType::Missing => {}
95            }
96
97            Ok((expected_path, actual_path))
98        })
99    }
100
101    /// Report differences between `actual_root` and `pattern_root`
102    ///
103    /// Note: Requires feature flag `path`
104    #[cfg(feature = "dir")]
105    pub fn subset_matches_iter(
106        pattern_root: impl Into<std::path::PathBuf>,
107        actual_root: impl Into<std::path::PathBuf>,
108        substitutions: &crate::Redactions,
109    ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> + '_ {
110        let pattern_root = pattern_root.into();
111        let actual_root = actual_root.into();
112        Self::subset_matches_iter_inner(pattern_root, actual_root, substitutions, true)
113    }
114
115    #[cfg(feature = "dir")]
116    pub(crate) fn subset_matches_iter_inner(
117        expected_root: std::path::PathBuf,
118        actual_root: std::path::PathBuf,
119        substitutions: &crate::Redactions,
120        normalize_paths: bool,
121    ) -> impl Iterator<Item = Result<(std::path::PathBuf, std::path::PathBuf), Self>> + '_ {
122        let walker = crate::dir::Walk::new(&expected_root);
123        walker.map(move |r| {
124            let expected_path = r.map_err(|e| Self::Failure(e.to_string().into()))?;
125            let rel = expected_path.strip_prefix(&expected_root).unwrap();
126            let actual_path = actual_root.join(rel);
127
128            let expected_type = FileType::from_path(&expected_path);
129            let actual_type = FileType::from_path(&actual_path);
130            if expected_type != actual_type {
131                return Err(Self::TypeMismatch {
132                    expected_path,
133                    actual_path,
134                    expected_type,
135                    actual_type,
136                });
137            }
138
139            match expected_type {
140                FileType::Symlink => {
141                    let expected_target = std::fs::read_link(&expected_path).ok();
142                    let actual_target = std::fs::read_link(&actual_path).ok();
143                    if expected_target != actual_target {
144                        return Err(Self::LinkMismatch {
145                            expected_path,
146                            actual_path,
147                            expected_target: expected_target.unwrap(),
148                            actual_target: actual_target.unwrap(),
149                        });
150                    }
151                }
152                FileType::File => {
153                    let mut actual =
154                        crate::Data::try_read_from(&actual_path, None).map_err(Self::Failure)?;
155
156                    let expected =
157                        FilterNewlines.filter(crate::Data::read_from(&expected_path, None));
158
159                    actual = actual.coerce_to(expected.intended_format());
160                    if normalize_paths {
161                        actual = FilterPaths.filter(actual);
162                    }
163                    actual = NormalizeToExpected::new()
164                        .redact_with(substitutions)
165                        .normalize(FilterNewlines.filter(actual), &expected);
166
167                    if expected != actual {
168                        return Err(Self::ContentMismatch {
169                            expected_path,
170                            actual_path,
171                            expected_content: expected,
172                            actual_content: actual,
173                        });
174                    }
175                }
176                FileType::Dir | FileType::Unknown | FileType::Missing => {}
177            }
178
179            Ok((expected_path, actual_path))
180        })
181    }
182}
183
184impl PathDiff {
185    pub fn expected_path(&self) -> Option<&std::path::Path> {
186        match &self {
187            Self::Failure(_msg) => None,
188            Self::TypeMismatch {
189                expected_path,
190                actual_path: _,
191                expected_type: _,
192                actual_type: _,
193            } => Some(expected_path),
194            Self::LinkMismatch {
195                expected_path,
196                actual_path: _,
197                expected_target: _,
198                actual_target: _,
199            } => Some(expected_path),
200            Self::ContentMismatch {
201                expected_path,
202                actual_path: _,
203                expected_content: _,
204                actual_content: _,
205            } => Some(expected_path),
206        }
207    }
208
209    pub fn write(
210        &self,
211        f: &mut dyn std::fmt::Write,
212        palette: crate::report::Palette,
213    ) -> Result<(), std::fmt::Error> {
214        match &self {
215            Self::Failure(msg) => {
216                writeln!(f, "{}", palette.error(msg))?;
217            }
218            Self::TypeMismatch {
219                expected_path,
220                actual_path: _actual_path,
221                expected_type,
222                actual_type,
223            } => {
224                writeln!(
225                    f,
226                    "{}: Expected {}, was {}",
227                    expected_path.display(),
228                    palette.info(expected_type),
229                    palette.error(actual_type)
230                )?;
231            }
232            Self::LinkMismatch {
233                expected_path,
234                actual_path: _actual_path,
235                expected_target,
236                actual_target,
237            } => {
238                writeln!(
239                    f,
240                    "{}: Expected {}, was {}",
241                    expected_path.display(),
242                    palette.info(expected_target.display()),
243                    palette.error(actual_target.display())
244                )?;
245            }
246            Self::ContentMismatch {
247                expected_path,
248                actual_path,
249                expected_content,
250                actual_content,
251            } => {
252                crate::report::write_diff(
253                    f,
254                    expected_content,
255                    actual_content,
256                    Some(&expected_path.display()),
257                    Some(&actual_path.display()),
258                    palette,
259                )?;
260            }
261        }
262
263        Ok(())
264    }
265
266    pub fn overwrite(&self) -> Result<(), crate::assert::Error> {
267        match self {
268            // Not passing the error up because users most likely want to treat a processing error
269            // differently than an overwrite error
270            Self::Failure(_err) => Ok(()),
271            Self::TypeMismatch {
272                expected_path,
273                actual_path,
274                expected_type: _,
275                actual_type,
276            } => {
277                match actual_type {
278                    FileType::Dir => {
279                        std::fs::remove_dir_all(expected_path).map_err(|e| {
280                            format!("Failed to remove {}: {}", expected_path.display(), e)
281                        })?;
282                    }
283                    FileType::File | FileType::Symlink => {
284                        std::fs::remove_file(expected_path).map_err(|e| {
285                            format!("Failed to remove {}: {}", expected_path.display(), e)
286                        })?;
287                    }
288                    FileType::Unknown | FileType::Missing => {}
289                }
290                super::shallow_copy(expected_path, actual_path)
291            }
292            Self::LinkMismatch {
293                expected_path,
294                actual_path,
295                expected_target: _,
296                actual_target: _,
297            } => super::shallow_copy(expected_path, actual_path),
298            Self::ContentMismatch {
299                expected_path: _,
300                actual_path: _,
301                expected_content,
302                actual_content,
303            } => actual_content.write_to(expected_content.source().unwrap()),
304        }
305    }
306}
307
308#[derive(Copy, Clone, Debug, PartialEq, Eq)]
309pub enum FileType {
310    Dir,
311    File,
312    Symlink,
313    Unknown,
314    Missing,
315}
316
317impl FileType {
318    pub fn from_path(path: &std::path::Path) -> Self {
319        let meta = path.symlink_metadata();
320        match meta {
321            Ok(meta) => {
322                if meta.is_dir() {
323                    Self::Dir
324                } else if meta.is_file() {
325                    Self::File
326                } else {
327                    let target = std::fs::read_link(path).ok();
328                    if target.is_some() {
329                        Self::Symlink
330                    } else {
331                        Self::Unknown
332                    }
333                }
334            }
335            Err(err) => match err.kind() {
336                std::io::ErrorKind::NotFound => Self::Missing,
337                _ => Self::Unknown,
338            },
339        }
340    }
341}
342
343impl FileType {
344    fn as_str(self) -> &'static str {
345        match self {
346            Self::Dir => "dir",
347            Self::File => "file",
348            Self::Symlink => "symlink",
349            Self::Unknown => "unknown",
350            Self::Missing => "missing",
351        }
352    }
353}
354
355impl std::fmt::Display for FileType {
356    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357        self.as_str().fmt(f)
358    }
359}