testdata_rt/
globbing.rs

1use std::collections::HashSet;
2use std::path::{Path, PathBuf, StripPrefixError};
3
4use path_slash::PathBufExt as _;
5use path_slash::PathExt as _;
6use thiserror::Error as StdError;
7use walkdir::WalkDir;
8
9use crate::fixtures::TestFile;
10use crate::patterns::{GlobParseError, GlobPattern};
11
12/// Represents the glob error.
13#[derive(Debug, StdError)]
14pub enum GlobError {
15    #[error("Error during walk: {0}")]
16    Walkdir(#[from] walkdir::Error),
17    #[error("Cannot compute relative path from {} to {}", .1.display(), .2.display())]
18    StripPrefix(#[source] StripPrefixError, PathBuf, PathBuf),
19    #[error("Got a non-utf8 path: {0:?}")]
20    InvalidPath(PathBuf),
21}
22
23/// Configurations for finding test files in a file-based test.
24#[derive(Debug, Clone)]
25#[non_exhaustive]
26pub struct GlobSpec {
27    /// Serching root. Defaults to `.`.
28    pub root: PathBuf,
29    /// List of arguments in the order of appearence.
30    pub args: Vec<ArgSpec>,
31}
32
33impl GlobSpec {
34    /// Creates the default glob configuration.
35    pub fn new() -> Self {
36        Self {
37            root: PathBuf::from("."),
38            args: Vec::new(),
39        }
40    }
41
42    /// Builder utility to set `self.root`.
43    pub fn root(mut self, root: &Path) -> Self {
44        self.root = root.to_owned();
45        self
46    }
47
48    /// Builder utility to set `self.args`.
49    pub fn arg(mut self, arg: ArgSpec) -> Self {
50        self.args.push(arg);
51        self
52    }
53
54    /// Searches for the test files.
55    pub fn glob(&self) -> Result<Vec<String>, GlobError> {
56        self.glob_from(Path::new(""))
57    }
58    /// Searches for the test files, with custom working directory.
59    pub fn glob_from(&self, cwd: &Path) -> Result<Vec<String>, GlobError> {
60        let root = cwd.join(&self.root);
61        let mut stems = HashSet::new();
62        for prefix in &self.prefixes() {
63            let walk_root = root.join(PathBuf::from_slash(prefix));
64            for entry in WalkDir::new(&walk_root).sort_by_file_name() {
65                let entry = entry?;
66                let file_name = entry.path().strip_prefix(&root).map_err(|e| {
67                    GlobError::StripPrefix(e, root.clone(), entry.path().to_owned())
68                })?;
69                let file_name = file_name
70                    .to_slash()
71                    .ok_or_else(|| GlobError::InvalidPath(entry.path().to_owned()))?;
72                for arg in &self.args {
73                    for stem in arg.glob.do_match(&file_name) {
74                        stems.insert(stem.to_owned());
75                    }
76                }
77            }
78        }
79        let sorted_stems = {
80            let mut sorted_stems = stems.into_iter().collect::<Vec<_>>();
81            sorted_stems.sort();
82            sorted_stems
83        };
84
85        Ok(sorted_stems)
86    }
87
88    /// Helper function that does `GlobSpec::glob` and set differene.
89    pub fn glob_diff(
90        &self,
91        known_stems: &[String],
92    ) -> Result<(Vec<String>, Vec<String>), GlobError> {
93        let stems = self.glob()?;
94        let missing_stems = {
95            let stems = stems.iter().collect::<HashSet<_>>();
96            known_stems
97                .iter()
98                .cloned()
99                .filter(|stem| !stems.contains(stem))
100                .collect::<Vec<_>>()
101        };
102        let extra_stems = {
103            let known_stems = known_stems.iter().collect::<HashSet<_>>();
104            stems
105                .iter()
106                .cloned()
107                .filter(|stem| !known_stems.contains(stem))
108                .collect::<Vec<_>>()
109        };
110        Ok((extra_stems, missing_stems))
111    }
112
113    /// Assigns a specific test name to get the path(s) to the file.
114    pub fn expand(&self, stem: &str) -> Option<Vec<TestFile>> {
115        let mut test_files = Vec::new();
116        for arg in &self.args {
117            let paths = arg
118                .glob
119                .subst(stem)
120                .iter()
121                .map(|stem| self.root.join(PathBuf::from_slash(stem)))
122                .collect::<Vec<_>>();
123            if paths.is_empty() {
124                return None;
125            }
126            test_files.push(TestFile { paths });
127        }
128        if test_files.iter().any(|f| f.exists()) {
129            Some(test_files)
130        } else {
131            None
132        }
133    }
134
135    fn prefixes(&self) -> Vec<String> {
136        let mut prefixes = Vec::new();
137        for arg in &self.args {
138            prefixes.extend_from_slice(&arg.glob.prefixes());
139        }
140        for prefix in &mut prefixes {
141            let pos = prefix.rfind('/').unwrap_or(0);
142            prefix.truncate(pos);
143            prefix.push('/');
144        }
145        prefixes.sort();
146        let mut last = 0;
147        for i in 1..prefixes.len() {
148            if prefixes[i].starts_with(&prefixes[last]) {
149                prefixes[i].clear();
150            } else {
151                last = i;
152            }
153        }
154        // prefixes.drain_filter(|elem| elem.is_empty());
155        prefixes = prefixes
156            .into_iter()
157            .filter(|elem| !elem.is_empty())
158            .collect::<Vec<_>>();
159        for p in &mut prefixes {
160            p.pop();
161        }
162        prefixes
163    }
164}
165
166/// Configuration for a specific argument in a file-based test.
167#[derive(Debug, Clone)]
168#[non_exhaustive]
169pub struct ArgSpec {
170    pub glob: GlobPattern,
171}
172
173impl ArgSpec {
174    pub fn new(glob: &str) -> Self {
175        Self::parse(glob).unwrap()
176    }
177
178    pub fn parse(glob: &str) -> Result<Self, GlobParseError> {
179        Ok(Self {
180            glob: glob.parse()?,
181        })
182    }
183}