xvc_walker/
lib.rs

1//! Xvc walker traverses directory trees with ignore rules.
2//!
3//! Ignore rules are similar to [.gitignore](https://git-scm.com/docs/gitignore) and child
4//! directories are not traversed if ignored.
5//!
6//! [walk_parallel] function is the most useful element in this module.
7//! It walks and sends [PathMetadata] through a channel, also updating the ignore rules and sending
8//! them.
9#![warn(missing_docs)]
10#![forbid(unsafe_code)]
11pub mod abspath;
12pub mod error;
13pub mod ignore_rules;
14pub mod notify;
15pub mod pattern;
16pub mod sync;
17pub mod walk_parallel;
18pub mod walk_serial;
19
20pub use pattern::MatchResult;
21pub use pattern::PathKind;
22pub use pattern::Pattern;
23pub use pattern::PatternEffect;
24pub use pattern::PatternRelativity;
25pub use pattern::Source;
26
27pub use walk_parallel::walk_parallel;
28pub use walk_serial::walk_serial;
29
30pub use abspath::AbsolutePath;
31pub use error::{Error, Result};
32
33pub use ignore_rules::IgnoreRules;
34pub use ignore_rules::SharedIgnoreRules;
35
36pub use notify::make_watcher;
37pub use std::hash::Hash;
38pub use sync::{PathSync, PathSyncSingleton};
39use xvc_logging::warn;
40
41pub use notify::PathEvent;
42pub use notify::RecommendedWatcher;
43
44pub use fast_glob::Glob;
45
46use xvc_logging::watch;
47
48use std::{
49    fmt::Debug,
50    fs::{self, Metadata},
51    path::{Path, PathBuf},
52};
53
54use anyhow::anyhow;
55
56static MAX_THREADS_PARALLEL_WALK: usize = 8;
57
58/// Combine a path and its metadata in a single struct
59#[derive(Debug, Clone)]
60pub struct PathMetadata {
61    /// path
62    pub path: PathBuf,
63    /// metadata
64    pub metadata: Metadata,
65}
66
67/// What's the ignore file name and should we add directories to the result?
68#[derive(Debug, Clone)]
69pub struct WalkOptions {
70    /// The ignore filename (`.gitignore`, `.xvcignore`, `.ignore`, etc.) or `None` for not
71    /// ignoring anything.
72    pub ignore_filename: Option<String>,
73    /// Should the results include directories themselves?
74    /// Note that they are always traversed, but may not be listed if we're only interested in
75    /// actual files.
76    pub include_dirs: bool,
77}
78
79impl WalkOptions {
80    /// Instantiate a Git repository walker that uses `.gitignore` as ignore file name and includes
81    /// directories in results.
82    pub fn gitignore() -> Self {
83        Self {
84            ignore_filename: Some(".gitignore".into()),
85            include_dirs: true,
86        }
87    }
88
89    /// Instantiate a Xvc repository walker that uses `.xvcignore` as ignore file name and includes
90    /// directories in results.
91    pub fn xvcignore() -> Self {
92        Self {
93            ignore_filename: Some(".xvcignore".into()),
94            include_dirs: true,
95        }
96    }
97
98    /// Return options with `include_dirs` turned off.
99    /// `WalkOptions::xvcignore().without_dirs()` specifies a `xvcignore` walker that only lists
100    /// files.
101    pub fn without_dirs(self) -> Self {
102        Self {
103            ignore_filename: self.ignore_filename,
104            include_dirs: false,
105        }
106    }
107    /// Return the same option with `include_dirs` turned on.
108    pub fn with_dirs(self) -> Self {
109        Self {
110            ignore_filename: self.ignore_filename,
111            include_dirs: true,
112        }
113    }
114}
115
116/// Build the ignore rules with the given directory
117pub fn build_ignore_patterns(
118    given: &str,
119    ignore_root: &Path,
120    ignore_filename: &str,
121) -> Result<IgnoreRules> {
122    watch!(ignore_filename);
123    watch!(ignore_root);
124
125    let ignore_rules = IgnoreRules::from_global_patterns(ignore_root, Some(ignore_filename), given);
126
127    let dirs_under = |p: &Path| -> Vec<PathBuf> {
128        p.read_dir()
129            .unwrap()
130            .filter_map(|p| {
131                if let Ok(p) = p {
132                    if p.path().is_dir() {
133                        Some(p.path())
134                    } else {
135                        None
136                    }
137                } else {
138                    None
139                }
140            })
141            .filter_map(|p| match ignore_rules.check(&p) {
142                MatchResult::NoMatch | MatchResult::Whitelist => Some(p),
143                MatchResult::Ignore => None,
144            })
145            .collect()
146    };
147
148    let mut dir_stack: Vec<PathBuf> = vec![ignore_root.to_path_buf()];
149
150    let ignore_fn = ignore_rules.ignore_filename.as_deref().unwrap();
151
152    while let Some(dir) = dir_stack.pop() {
153        watch!(dir);
154        let ignore_filename = dir.join(ignore_fn);
155        watch!(ignore_filename);
156        if ignore_filename.is_file() {
157            let ignore_content = fs::read_to_string(&ignore_filename)?;
158            let new_patterns =
159                content_to_patterns(ignore_root, Some(&ignore_filename), &ignore_content);
160            ignore_rules.add_patterns(new_patterns)?;
161        }
162        let mut new_dirs = dirs_under(&dir);
163        watch!(new_dirs);
164        dir_stack.append(&mut new_dirs);
165        watch!(dir_stack);
166    }
167
168    Ok(ignore_rules)
169}
170
171/// convert a set of rules in `content` to glob patterns.
172/// patterns may come from `source`.
173/// the root directory of all search is in `ignore_root`.
174pub fn content_to_patterns(
175    ignore_root: &Path,
176    source: Option<&Path>,
177    content: &str,
178) -> Vec<Pattern> {
179    let patterns: Vec<Pattern> = content
180        .lines()
181        .enumerate()
182        // A line starting with # serves as a comment. Put a backslash ("\") in front of the first hash for patterns that begin with a hash.
183        .filter(|(_, line)| !(line.trim().is_empty() || line.starts_with('#')))
184        // Trailing spaces are ignored unless they are quoted with backslash ("\").
185        .map(|(i, line)| {
186            if !line.ends_with("\\ ") {
187                (i, line.trim_end())
188            } else {
189                (i, line)
190            }
191        })
192        // if source file is not given, set the source Global
193        .map(|(i, line)| {
194            (
195                line,
196                match source {
197                    Some(p) => Source::File {
198                        path: p
199                            .strip_prefix(ignore_root)
200                            .expect("path must be within ignore_root")
201                            .to_path_buf(),
202                        line: (i + 1),
203                    },
204                    None => Source::Global,
205                },
206            )
207        })
208        .map(|(line, source)| Pattern::new(source, line))
209        .collect();
210
211    patterns
212}
213
214/// Updates the ignore rules from a given directory.
215///
216/// Gets ignore filename from the ignore rules, concatenates it with the directory path and reads
217/// the file if it exists. Then updates the ignore rules with the new patterns.
218pub fn update_ignore_rules(dir: &Path, ignore_rules: &IgnoreRules) -> Result<()> {
219    if let Some(ref ignore_filename) = ignore_rules.ignore_filename {
220        let ignore_root = &ignore_rules.root;
221        let ignore_path = dir.join(ignore_filename);
222        if ignore_path.is_file() {
223            let new_patterns: Vec<Pattern> = {
224                let content = fs::read_to_string(&ignore_path)?;
225                content_to_patterns(ignore_root, Some(ignore_path).as_deref(), &content)
226            };
227
228            ignore_rules.add_patterns(new_patterns)?;
229        }
230    }
231    Ok(())
232}
233/// Return all childs of a directory regardless of any ignore rules
234/// If there is an error to obtain the metadata, error is added to the element instead
235pub fn directory_list(dir: &Path) -> Result<Vec<Result<PathMetadata>>> {
236    let elements = dir
237        .read_dir()
238        .map_err(|e| anyhow!("Error reading directory: {:?}, {:?}", dir, e))?;
239    let mut child_paths = Vec::<Result<PathMetadata>>::new();
240
241    for entry in elements {
242        match entry {
243            Err(err) => child_paths.push(Err(Error::from(anyhow!(
244                "Error reading entry in dir {:?} {:?}",
245                dir,
246                err
247            )))),
248            Ok(entry) => match entry.metadata() {
249                Err(err) => child_paths.push(Err(Error::from(anyhow!(
250                    "Error getting metadata {:?} {:?}",
251                    entry,
252                    err
253                )))),
254                Ok(md) => {
255                    child_paths.push(Ok(PathMetadata {
256                        path: entry.path(),
257                        metadata: md.clone(),
258                    }));
259                }
260            },
261        }
262    }
263    Ok(child_paths)
264}
265
266#[cfg(test)]
267mod tests {
268
269    use super::*;
270
271    use log::LevelFilter;
272    use test_case::test_case;
273
274    use crate::error::Result;
275    use crate::AbsolutePath;
276    use xvc_test_helper::*;
277
278    #[test_case("!mydir/*/file" => matches PatternEffect::Whitelist ; "t1159938339")]
279    #[test_case("!mydir/myfile" => matches PatternEffect::Whitelist ; "t1302522194")]
280    #[test_case("!myfile" => matches PatternEffect::Whitelist ; "t3599739725")]
281    #[test_case("!myfile/" => matches PatternEffect::Whitelist ; "t389990097")]
282    #[test_case("/my/file" => matches PatternEffect::Ignore ; "t3310011546")]
283    #[test_case("mydir/*" => matches PatternEffect::Ignore ; "t1461510927")]
284    #[test_case("mydir/file" => matches PatternEffect::Ignore; "t4096563949")]
285    #[test_case("myfile" => matches PatternEffect::Ignore; "t4042406621")]
286    #[test_case("myfile*" => matches PatternEffect::Ignore ; "t3367706249")]
287    #[test_case("myfile/" => matches PatternEffect::Ignore ; "t1204466627")]
288    fn test_pattern_effect(line: &str) -> PatternEffect {
289        let pat = Pattern::new(Source::Global, line);
290        pat.effect
291    }
292
293    #[test_case("", "!mydir/*/file" => matches PatternRelativity::RelativeTo { directory } if directory.is_empty() ; "t500415168")]
294    #[test_case("", "!mydir/myfile" => matches PatternRelativity::RelativeTo {directory} if directory.is_empty() ; "t1158125354")]
295    #[test_case("dir/", "!mydir/*/file" => matches PatternRelativity::RelativeTo { directory } if directory == "/dir" ; "t3052699971")]
296    #[test_case("dir/", "!mydir/myfile" => matches PatternRelativity::RelativeTo {directory} if directory == "/dir" ; "t885029019")]
297    #[test_case("", "!myfile" => matches PatternRelativity::Anywhere; "t3101661374")]
298    #[test_case("", "!myfile/" => matches PatternRelativity::Anywhere ; "t3954695505")]
299    #[test_case("", "/my/file" => matches PatternRelativity::RelativeTo { directory } if directory.is_empty() ; "t1154256567")]
300    #[test_case("", "mydir/*" => matches PatternRelativity::RelativeTo { directory } if directory.is_empty() ; "t865348822")]
301    #[test_case("", "mydir/file" => matches PatternRelativity::RelativeTo { directory } if directory.is_empty() ; "t809589695")]
302    #[test_case("root/", "/my/file" => matches PatternRelativity::RelativeTo { directory } if directory == "/root" ; "t7154256567")]
303    #[test_case("root/", "mydir/*" => matches PatternRelativity::RelativeTo { directory } if directory == "/root" ; "t765348822")]
304    #[test_case("root/", "mydir/file" => matches PatternRelativity::RelativeTo { directory } if directory == "/root" ; "t709589695")]
305    #[test_case("", "myfile" => matches PatternRelativity::Anywhere; "t949952742")]
306    #[test_case("", "myfile*" => matches PatternRelativity::Anywhere ; "t2212007572")]
307    #[test_case("", "myfile/" => matches PatternRelativity::Anywhere; "t900104620")]
308    fn test_pattern_relativity(dir: &str, line: &str) -> PatternRelativity {
309        let source = Source::File {
310            path: PathBuf::from(dir).join(".gitignore"),
311            line: 1,
312        };
313        let pat = Pattern::new(source, line);
314        pat.relativity
315    }
316
317    #[test_case("", "!mydir/*/file" => matches PathKind::Any ; "t4069397926")]
318    #[test_case("", "!mydir/myfile" => matches PathKind::Any ; "t206435934")]
319    #[test_case("", "!myfile" => matches PathKind::Any ; "t4262638148")]
320    #[test_case("", "!myfile/" => matches PathKind::Directory ; "t214237847")]
321    #[test_case("", "/my/file" => matches PathKind::Any ; "t187692643")]
322    #[test_case("", "mydir/*" => matches PathKind::Any ; "t1159784957")]
323    #[test_case("", "mydir/file" => matches PathKind::Any ; "t2011171465")]
324    #[test_case("", "myfile" => matches PathKind::Any ; "t167946945")]
325    #[test_case("", "myfile*" => matches PathKind::Any ; "t3091563211")]
326    #[test_case("", "myfile/" => matches PathKind::Directory ; "t1443554623")]
327    fn test_path_kind(dir: &str, line: &str) -> PathKind {
328        let source = Source::File {
329            path: PathBuf::from(dir).join(".gitignore"),
330            line: 1,
331        };
332        let pat = Pattern::new(source, line);
333        pat.path_kind
334    }
335
336    #[test_case("" => 0)]
337    #[test_case("myfile" => 1)]
338    #[test_case("mydir/myfile" => 1)]
339    #[test_case("mydir/myfile\n!myfile" => 2)]
340    #[test_case("mydir/myfile\n/another" => 2)]
341    #[test_case("mydir/myfile\n\n\nanother" => 2)]
342    #[test_case("#comment\nmydir/myfile\n\n\nanother" => 2)]
343    #[test_case("#mydir/myfile" => 0)]
344    fn test_content_to_patterns_count(contents: &str) -> usize {
345        let patterns = content_to_patterns(Path::new(""), None, contents);
346        patterns.len()
347    }
348
349    fn create_patterns(root: &str, dir: Option<&str>, patterns: &str) -> Vec<Pattern> {
350        content_to_patterns(Path::new(root), dir.map(Path::new), patterns)
351    }
352
353    fn new_dir_with_ignores(
354        root: &str,
355        dir: Option<&str>,
356        initial_patterns: &str,
357    ) -> Result<IgnoreRules> {
358        let patterns = create_patterns(root, dir, initial_patterns);
359        let initialized = IgnoreRules::empty(&PathBuf::from(root), None);
360
361        initialized.add_patterns(patterns)?;
362        Ok(initialized)
363    }
364
365    #[test_case(".", "" ; "empty_dwi")]
366    #[test_case("dir", "myfile")]
367    #[test_case("dir", "mydir/myfile")]
368    #[test_case("dir", "mydir/myfile\n!myfile")]
369    #[test_case("dir", "mydir/myfile\n/another")]
370    #[test_case("dir", "mydir/myfile\n\n\nanother")]
371    #[test_case("dir", "#comment\nmydir/myfile\n\n\nanother")]
372    #[test_case("dir", "#mydir/myfile" ; "single ignored lined")]
373    fn test_dir_with_ignores(dir: &str, contents: &str) {
374        new_dir_with_ignores(dir, None, contents).unwrap();
375    }
376
377    #[test_case("/dir", "/mydir/myfile/" => matches PatternRelativity::RelativeTo { directory } if directory == "/dir" ; "t868594159")]
378    #[test_case("/dir", "mydir" => matches PatternRelativity::Anywhere ; "t4030766779")]
379    #[test_case("/dir/", "mydir/myfile" => matches PatternRelativity::RelativeTo { directory } if directory == "/dir" ; "t2043231107")]
380    #[test_case("dir", "myfile" => matches PatternRelativity::Anywhere; "t871610344" )]
381    #[test_case("dir/", "mydir/myfile" => matches PatternRelativity::RelativeTo { directory } if directory == "/dir" ; "t21398102")]
382    #[test_case("dir/", "myfile" => matches PatternRelativity::Anywhere ; "t1846637197")]
383    #[test_case("dir//", "/mydir/myfile" => matches PatternRelativity::RelativeTo { directory } if directory == "/dir" ; "t2556287848")]
384    fn test_path_relativity(dir: &str, pattern: &str) -> PatternRelativity {
385        let source = Source::File {
386            path: PathBuf::from(format!("{dir}/.gitignore")),
387            line: 1,
388        };
389        let pattern = Pattern::new(source, pattern);
390        pattern.relativity
391    }
392    // ---- tests::test_pattern_line::t1242345310 stdout ----
393    // thread 'tests::test_pattern_line::t1242345310' panicked at walker/src/lib.rs:391:5:
394    // assertion `left == right` failed
395    //   left: "myfile"
396    //  right: "**/myfile"
397    //
398    // ---- tests::test_pattern_line::t1142345310 stdout ----
399    // thread 'tests::test_pattern_line::t1142345310' panicked at walker/src/lib.rs:391:5:
400    // assertion `left == right` failed
401    //   left: "myfile"
402    //  right: "**/myfile"
403    // note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
404    //
405    // ---- tests::test_pattern_line::t1427001291 stdout ----
406    // thread 'tests::test_pattern_line::t1427001291' panicked at walker/src/lib.rs:391:5:
407    // assertion `left == right` failed
408    //   left: "myfile"
409    //  right: "/**/myfile"
410    //
411    // ---- tests::test_pattern_line::t21199018562 stdout ----
412    // thread 'tests::test_pattern_line::t21199018562' panicked at walker/src/lib.rs:391:5:
413    // assertion `left == right` failed
414    //   left: "mydir/myfile"
415    //  right: "/**/mydir/myfile"
416    //
417    // ---- tests::test_pattern_line::t21199018162 stdout ----
418    // thread 'tests::test_pattern_line::t21199018162' panicked at walker/src/lib.rs:391:5:
419    // assertion `left == right` failed
420    //   left: "mydir/myfile"
421    //  right: "/**/mydir/myfile"
422    //
423    // ---- tests::test_pattern_line::t31199018162 stdout ----
424    // thread 'tests::test_pattern_line::t31199018162' panicked at walker/src/lib.rs:391:5:
425    // assertion `left == right` failed
426    //   left: "myfile.*"
427    //  right: "**/myfile.*"
428    //
429    #[test_case("dir", "myfile" => "**/myfile" ; "t1242345310")]
430    #[test_case("dir", "/myfile" => "/**/myfile" ; "t3427001291")]
431    #[test_case("dir", "myfile/" => "**/myfile/**" ; "t759151905")]
432    #[test_case("dir", "mydir/myfile" => "/**/mydir/myfile" ; "t21199018562")]
433    #[test_case("dir", "/my/file.*" => "/**/my/file.*" ; "t61199018162")]
434    #[test_case("dir", "/mydir/**.*" => "/**/mydir/**.*" ; "t47199018162")]
435    fn test_pattern_line(dir: &str, pattern: &str) -> String {
436        let source = Source::File {
437            path: PathBuf::from(format!("{dir}.gitignore")),
438            line: 1,
439        };
440        let pattern = Pattern::new(source, pattern);
441        pattern.glob
442    }
443
444    // Blank file tests
445    #[test_case("", "#mydir/myfile", ""  => matches MatchResult::NoMatch ; "t01")]
446    #[test_case("", "", ""  => matches MatchResult::NoMatch ; "t02" )]
447    #[test_case("", "\n\n  \n", ""  => matches MatchResult::NoMatch; "t03"  )]
448    #[test_case("", "dir-0001", ""  => matches MatchResult::NoMatch ; "t04" )]
449    #[test_case("", "dir-0001/file-0001.bin", ""  => matches MatchResult::NoMatch ; "t05" )]
450    #[test_case("", "dir-0001/*", ""  => matches MatchResult::NoMatch ; "t06" )]
451    #[test_case("", "dir-0001/**", ""  => matches MatchResult::NoMatch ; "t07" )]
452    #[test_case("", "dir-0001/dir-0001**", ""  => matches MatchResult::NoMatch ; "t08" )]
453    #[test_case("", "dir-0001/dir-00*", ""  => matches MatchResult::NoMatch ; "t09" )]
454    #[test_case("", "dir-00**/", ""  => matches MatchResult::NoMatch ; "t10" )]
455    #[test_case("", "dir-00**/*/file-0001.bin", ""  => matches MatchResult::NoMatch ; "t11" )]
456    #[test_case("", "dir-00**/*/*.bin", ""  => matches MatchResult::NoMatch ; "t12" )]
457    #[test_case("", "dir-00**/", ""  => matches MatchResult::NoMatch ; "t13" )]
458    #[test_case("", "#mydir/myfile", ""  => matches MatchResult::NoMatch ; "t148864489901")]
459    // No Match Tests
460    #[test_case("", "", "dir-0001/file-0002.bin"  => matches MatchResult::NoMatch ; "t172475356002" )]
461    #[test_case("", "\n\n  \n", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch; "t8688937603"  )]
462    #[test_case("", "dir-0001", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t132833780304" )]
463    #[test_case("", "dir-0001/file-0001.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t173193800505" )]
464    #[test_case("", "dir-0001/dir-0001**", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t318664043308" )]
465    #[test_case("", "dir-0001/dir-00*", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t269908728009" )]
466    #[test_case("", "dir-00**/*/file-0001.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t142240004811" )]
467    #[test_case("", "dir-00**/*/*.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t414921892712" )]
468    #[test_case("", "dir-00**/", "dir-0001/file-0002.bin" => matches MatchResult::Ignore; "t256322548613" )]
469    // Ignore tests
470    #[test_case("", "dir-0001/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t3378553489" )]
471    #[test_case("", "dir-0001/file-0001.*", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t3449646229" )]
472    #[test_case("", "dir-0001/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t1232001745" )]
473    #[test_case("", "dir-0001/*", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t2291655464" )]
474    #[test_case("", "dir-0001/**/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t355659763" )]
475    #[test_case("", "dir-0001/**", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t1888678340" )]
476    #[test_case("", "dir-000?/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t1603222532" )]
477    #[test_case("", "dir-000?/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t2528090273" )]
478    #[test_case("", "dir-*/*", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t3141482339" )]
479    // Whitelist Tests
480    #[test_case("", "!dir-0001", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t2963495371" )]
481    #[test_case("", "!dir-0001/file-0001.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t3935333051" )]
482    #[test_case("", "!dir-0001/dir-0001**", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t3536143628" )]
483    #[test_case("", "!dir-0001/dir-00*", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t4079058836" )]
484    #[test_case("", "!dir-00**/", "dir-0001/file-0002.bin" => matches MatchResult::Whitelist ; "t3713155445" )]
485    #[test_case("", "!dir-00**/*/file-0001.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t1434153118" )]
486    #[test_case("", "!dir-00**/*/*.bin", "dir-0001/file-0002.bin" => matches MatchResult::NoMatch ; "t1650195998" )]
487    #[test_case("", "!dir-0001/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t1569068369" )]
488    #[test_case("", "!dir-0001/file-0001.*", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t2919165396" )]
489    #[test_case("", "!dir-0001/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t2682012728" )]
490    #[test_case("", "!dir-0001/*", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t4009543743" )]
491    #[test_case("", "!dir-0001/**/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t3333689486" )]
492    #[test_case("", "!dir-0001/**", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t4259364613" )]
493    #[test_case("", "!dir-000?/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t3424909626" )]
494    #[test_case("", "!dir-000?/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t3741545053" )]
495    #[test_case("", "!dir-*/*", "dir-0001/file-0001.bin" => matches MatchResult::Whitelist ; "t1793504005" )]
496    // Ignore in child dir
497    #[test_case("dir-0001", "/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t1295565113" )]
498    #[test_case("dir-0001", "/file-0001.*", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t4048655621" )]
499    #[test_case("dir-0001", "/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t2580936986" )]
500    #[test_case("dir-0001", "/*", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t109602877" )]
501    #[test_case("dir-0001", "/**/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t112292599" )]
502    #[test_case("dir-0001", "/**", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t1323958164" )]
503    #[test_case("dir-0001", "/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t4225367752" )]
504    #[test_case("dir-0001", "/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::Ignore ; "t3478922394" )]
505    // NoMatch in child_dir
506    #[test_case("dir-0002", "/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t345532514" )]
507    #[test_case("dir-0002", "/file-0001.*", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t1313276210" )]
508    #[test_case("dir-0002", "/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t657078396" )]
509    #[test_case("dir-0002", "/*", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t2456576806" )]
510    #[test_case("dir-0002", "/**/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t2629832143" )]
511    #[test_case("dir-0002", "/**", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t2090580478" )]
512    #[test_case("dir-0002", "/file-0001.bin", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t1588943529" )]
513    #[test_case("dir-0002", "/*.bin", "dir-0001/file-0001.bin" => matches MatchResult::NoMatch ; "t371313784" )]
514    fn test_match_result(dir: &str, contents: &str, path: &str) -> MatchResult {
515        test_logging(LevelFilter::Trace);
516
517        let root = create_directory_hierarchy(false).unwrap();
518        let source_file = format!("{root}/{dir}/.gitignore");
519        let path = root.as_ref().join(path).to_owned();
520        let dwi =
521            new_dir_with_ignores(root.to_str().unwrap(), Some(&source_file), contents).unwrap();
522
523        dwi.check(&path)
524    }
525
526    // TODO: Patterns shouldn't have / prefix, but an appropriate PathKind
527    #[test_case(true => matches Ok(_); "this is to refresh the dir for each test run")]
528    // This builds a directory hierarchy to run the tests
529    fn create_directory_hierarchy(force: bool) -> Result<AbsolutePath> {
530        let temp_dir: PathBuf = seeded_temp_dir("xvc-walker", Some(20220615));
531
532        if force && temp_dir.exists() {
533            fs::remove_dir_all(&temp_dir)?;
534        }
535
536        if !temp_dir.exists() {
537            // in parallel tests, sometimes this fail
538            fs::create_dir(&temp_dir)?;
539            create_directory_tree(&temp_dir, 10, 10, 1000, None)?;
540            // root/dir1 may have another tree
541            let level_1 = &temp_dir.join("dir-0001");
542            create_directory_tree(level_1, 10, 10, 1000, None)?;
543            // and another level
544            let level_2 = &level_1.join("dir-0001");
545            create_directory_tree(level_2, 10, 10, 1000, None)?;
546        }
547
548        Ok(AbsolutePath::from(temp_dir))
549    }
550}