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#[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#[derive(Debug, Clone)]
25#[non_exhaustive]
26pub struct GlobSpec {
27 pub root: PathBuf,
29 pub args: Vec<ArgSpec>,
31}
32
33impl GlobSpec {
34 pub fn new() -> Self {
36 Self {
37 root: PathBuf::from("."),
38 args: Vec::new(),
39 }
40 }
41
42 pub fn root(mut self, root: &Path) -> Self {
44 self.root = root.to_owned();
45 self
46 }
47
48 pub fn arg(mut self, arg: ArgSpec) -> Self {
50 self.args.push(arg);
51 self
52 }
53
54 pub fn glob(&self) -> Result<Vec<String>, GlobError> {
56 self.glob_from(Path::new(""))
57 }
58 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 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 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 = 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#[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}