litcheck_lit/suite/registry/
default.rs1use std::{collections::BTreeMap, path::Path, sync::Arc};
2
3use litcheck::{
4 diagnostics::{DiagResult, IntoDiagnostic, Report, WrapErr},
5 fs::{self, PathPrefixTree},
6};
7
8use crate::{
9 format::TestFormat,
10 suite::{TestSuite, TestSuiteKey, TestSuiteSet},
11 test::{Test, TestConfig, TestList},
12 Config, LitError,
13};
14
15use super::TestSuiteRegistry;
16
17#[derive(Default)]
18pub struct DefaultTestSuiteRegistry {
19 suites: TestSuiteSet,
21 tests_by_suite: BTreeMap<TestSuiteKey, TestList>,
23 config_cache: PathPrefixTree<TestSuiteKey>,
27 local_config_cache: PathPrefixTree<Arc<TestConfig>>,
28}
29impl TestSuiteRegistry for DefaultTestSuiteRegistry {
30 fn load(&mut self, config: &Config) -> DiagResult<()> {
31 let cwd = std::env::current_dir().expect("unable to access current working directory");
32
33 self.clear();
34
35 for input in config.tests.iter().map(Path::new) {
39 let input_path = input
40 .canonicalize()
41 .into_diagnostic()
42 .wrap_err("test input does not exist")?;
43 if !input_path.starts_with(&cwd) {
44 return Err(Report::new(LitError::TestPath(input_path)));
45 }
46 let search_root = if input_path.starts_with(&cwd) {
47 &cwd
48 } else {
49 &input_path
50 };
51
52 if let Some(suite) = self.find_nearest_suite(&input_path, search_root, config)? {
53 log::debug!("resolved input {} to {}", input.display(), &suite.id());
54 let suite_source_dir = suite.source_dir();
55 let filter_path = input_path.strip_prefix(suite_source_dir).expect(
56 "test suite source directory should have been an ancestor of the input path",
57 );
58 if filter_path != Path::new("") {
59 log::debug!(
60 "filtering suite '{}' by path: '{}'",
61 &suite.id(),
62 filter_path.display()
63 );
64 suite.filter_by_path(filter_path);
65 }
66 } else {
67 log::warn!("unable to locate test suite for {}", input.display());
68 }
69 }
70
71 if self.suites.is_empty() {
72 return Ok(());
73 }
74
75 self.load_tests(config)?;
76
77 Ok(())
78 }
79
80 fn is_empty(&self) -> bool {
81 !self.tests_by_suite.values().any(|suite| !suite.is_empty())
82 }
83
84 fn num_suites(&self) -> usize {
85 self.tests_by_suite.len()
86 }
87
88 fn num_tests(&self) -> usize {
89 self.tests_by_suite.values().map(|suite| suite.len()).sum()
90 }
91
92 fn size_of_suite(&self, id: &TestSuiteKey) -> usize {
93 self.tests_by_suite[id].len()
94 }
95
96 #[inline(always)]
97 fn get(&self, id: &TestSuiteKey) -> Arc<TestSuite> {
98 self.suites.get(id).unwrap()
99 }
100
101 fn get_by_path(&self, path: &Path) -> Option<Arc<TestSuite>> {
103 self.config_cache
104 .get(path)
105 .and_then(|key| self.suites.get(key))
106 }
107
108 fn tests(&self) -> impl Iterator<Item = Arc<Test>> + '_ {
109 self.tests_by_suite.values().flat_map(|suite| suite.iter())
110 }
111
112 fn suites(&self) -> impl Iterator<Item = Arc<TestSuite>> + '_ {
113 self.suites.iter()
114 }
115}
116impl IntoIterator for DefaultTestSuiteRegistry {
117 type Item = Arc<Test>;
118 type IntoIter = <TestList as IntoIterator>::IntoIter;
119
120 fn into_iter(mut self) -> Self::IntoIter {
121 let mut tests = self
122 .tests_by_suite
123 .pop_first()
124 .map(|(_, tests)| tests)
125 .unwrap_or_default();
126 while let Some((_, ts)) = self.tests_by_suite.pop_first() {
127 tests.append(ts);
128 }
129 tests.into_iter()
130 }
131}
132impl rayon::iter::IntoParallelIterator for DefaultTestSuiteRegistry {
133 type Item = Arc<Test>;
134 type Iter = <TestList as rayon::iter::IntoParallelIterator>::Iter;
135
136 #[inline]
137 fn into_par_iter(self) -> Self::Iter {
138 Self::into_iter(self)
139 }
140}
141impl DefaultTestSuiteRegistry {
142 fn clear(&mut self) {
143 self.local_config_cache.clear();
144 self.config_cache.clear();
145 self.tests_by_suite.clear();
146 self.suites.clear();
147 }
148
149 pub fn find_nearest_suite<P: AsRef<Path>>(
157 &mut self,
158 path: P,
159 cwd: &Path,
160 config: &Config,
161 ) -> DiagResult<Option<Arc<TestSuite>>> {
162 let path = path.as_ref();
163 if path.is_dir() {
168 if let Some(cache_key) = self.config_cache.get(path) {
169 return Ok(Some(self.get(cache_key)));
170 }
171
172 let mut found =
173 fs::search_directory(path, false, |entry| entry.file_name() == "lit.suite.toml");
174
175 if let Some(entry) = found.next() {
176 let entry = entry
177 .into_diagnostic()
178 .wrap_err("found test suite config, but it could not be read")?;
179 return self.load_without_cache(entry.path(), config).map(Some);
180 }
181 }
182
183 self.find_nearest_suite_containing(path, cwd, config)
184 }
185
186 fn find_nearest_suite_containing(
187 &mut self,
188 path: &Path,
189 cwd: &Path,
190 config: &Config,
191 ) -> DiagResult<Option<Arc<TestSuite>>> {
192 if let Some(cache_key) = self.config_cache.nearest_ancestor(path) {
197 return Ok(Some(self.get(cache_key.into_value())));
198 }
199
200 let dir = if path.is_dir() {
201 path
202 } else {
203 path.parent().expect("expected canonical file path")
204 };
205
206 for ancestor in dir.ancestors() {
209 let is_cwd = ancestor == cwd;
210 let mut found = fs::search_directory(ancestor, false, |entry| {
211 entry.file_name() == "lit.suite.toml"
212 });
213 if let Some(entry) = found.next() {
214 let entry = entry.into_diagnostic()?;
215 let suite = self.load_without_cache(entry.path(), config)?;
216 if dir.starts_with(suite.source_dir()) {
219 return Ok(Some(suite));
220 }
221 }
222
223 if is_cwd {
225 break;
226 }
227 }
228
229 Ok(None)
230 }
231
232 fn load_without_cache(&mut self, path: &Path, config: &Config) -> DiagResult<Arc<TestSuite>> {
233 let suite = TestSuite::parse(path, config)?;
234 self.suites.insert(suite.clone());
235 self.tests_by_suite.insert(suite.id(), TestList::default());
236 self.config_cache.insert(path, suite.id());
238 self.config_cache.insert(suite.source_dir(), suite.id());
240
241 Ok(suite)
242 }
243
244 fn load_tests(&mut self, config: &Config) -> DiagResult<()> {
250 log::debug!("loading tests into registry..");
251
252 for suite in self.suites.iter() {
253 log::debug!("loading tests for '{}'", &suite.id());
254 let tests = self
257 .tests_by_suite
258 .get_mut(&suite.id())
259 .expect("invalid test suite key");
260 tests.clear();
261
262 let search_paths = {
264 let mut lock = suite.search_paths();
265 core::mem::take(&mut *lock)
266 };
267 if search_paths.is_empty() {
268 log::debug!(
269 "no search paths given, searching entire suite source directory for tests"
270 );
271 let found = suite.config.format.registry().all(suite.clone())?;
272 log::debug!("found {} tests", found.len());
273 tests.append(found);
274 } else {
275 log::debug!(
276 "{} search paths were given, searching just those paths for tests",
277 search_paths.len()
278 );
279 for path in search_paths.iter() {
280 log::debug!("searching {} for tests..", path.display());
281 let local_config =
284 get_local_config(&suite, path, config, &mut self.local_config_cache)?;
285 let found = local_config.format.registry().find(
286 path,
287 suite.clone(),
288 local_config.clone(),
289 )?;
290 log::debug!("found {} tests", found.len());
291 tests.append(found);
292 }
293 }
294
295 *suite.search_paths() = search_paths;
296 }
297
298 log::debug!("loading complete!");
299
300 Ok(())
301 }
302}
303
304fn get_local_config(
305 suite: &TestSuite,
306 path_in_suite: &Path,
307 lit: &Config,
308 local_config_cache: &mut PathPrefixTree<Arc<TestConfig>>,
309) -> DiagResult<Arc<TestConfig>> {
310 let source_dir = suite.source_dir();
311 let path = source_dir.join(path_in_suite);
312 let path = if path.is_dir() {
313 path
314 } else {
315 path.parent().unwrap().to_path_buf()
316 };
317
318 if let Some(config) = local_config_cache.get(&path) {
320 return Ok(config.clone());
321 }
322
323 let (mut config, search_root_in_suite) =
329 if let Some(entry) = local_config_cache.nearest_ancestor(&path) {
330 (
331 entry.data.clone(),
332 entry.path.strip_prefix(source_dir).unwrap().to_path_buf(),
333 )
334 } else {
335 (suite.config.clone(), path_in_suite.to_path_buf())
336 };
337
338 let mut path = source_dir.to_path_buf();
339 for component in search_root_in_suite.components() {
340 path.push(AsRef::<Path>::as_ref(&component));
341
342 let mut found =
343 fs::search_directory(&path, false, |entry| entry.file_name() == "lit.local.toml");
344 if let Some(entry) = found.next() {
345 let entry = entry.into_diagnostic()?;
346 let mut cfg = TestConfig::parse(entry.path())?;
347 cfg.inherit(&config);
348 cfg.set_default_substitutions(lit, suite, &path);
349 cfg.set_default_features(lit);
350 let cfg = Arc::<TestConfig>::from(cfg);
351 local_config_cache.insert(&path, cfg.clone());
352 config = cfg;
353 }
354 }
355
356 local_config_cache.insert(&path, config.clone());
358 Ok(config)
359}