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 #[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 #[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 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}