litcheck_lit/suite/
mod.rs

1mod error;
2mod registry;
3mod set;
4
5pub use self::error::TestSuiteError;
6pub use self::registry::{DefaultTestSuiteRegistry, TestSuiteRegistry};
7pub use self::set::{TestSuiteKey, TestSuiteSet};
8
9use std::{
10    borrow::Cow,
11    collections::BTreeSet,
12    path::{Path, PathBuf},
13    sync::Arc,
14};
15
16use intrusive_collections::RBTreeAtomicLink;
17use litcheck::{
18    diagnostics::{DiagResult, IntoDiagnostic, Report, SourceFile, SourceSpan, WrapErr},
19    Input, StaticCow,
20};
21use parking_lot::Mutex;
22use serde::Deserialize;
23
24use crate::{Config, test::TestConfig};
25
26#[derive(Deserialize)]
27pub struct TestSuite {
28    #[serde(skip, default)]
29    link: RBTreeAtomicLink,
30    /// The unique identifier for this suite, a combination of test suite name
31    /// and the source path from which it was loaded
32    //#[serde(flatten)]
33    //pub id: TestSuiteKey,
34    /// The name of the test suite, may not be empty.
35    pub name: toml::Spanned<Arc<str>>,
36    /// The source path from which the test suite configuration was loaded
37    #[serde(skip, default)]
38    pub path: Option<Arc<Path>>,
39    /// The filesystem path which is the root of the test suite, and will be scanned
40    /// for tests.
41    ///
42    /// If provided, and not absolute, it will be relative to the config file itself.
43    ///
44    /// Defaults to the directory containing the config file.
45    #[serde(default = "empty_spanned_path")]
46    pub source_dir: toml::Spanned<StaticCow<Path>>,
47    /// The filesystem path which will be the working directory for tests that are
48    /// run, and where temporary files will be placed.
49    ///
50    /// If provided, and not absolute, it will be relative to the config file itself.
51    ///
52    /// Defaults to a temporary directory created via [std::env::temp_dir]
53    #[serde(default = "empty_spanned_path")]
54    pub working_dir: toml::Spanned<StaticCow<Path>>,
55    /// If a temporary directory is associated with this suite, it will be stored in
56    /// this field. When the suite is dropped, the temporary directory will be cleaned
57    /// up as well.
58    #[serde(skip, default)]
59    pub temp_dir: Option<tempdir::TempDir>,
60    /// The suite-level configuration used for all tests in this suite.
61    ///
62    /// Local configuration files can be used to override this configuration
63    /// on a directory-by-directory basis.
64    #[serde(flatten)]
65    pub config: Arc<TestConfig>,
66    /// The set of paths relative to `source_dir` which will be searched for tests.
67    ///
68    /// If empty, all of `source_dir` and its children will be searched.
69    #[serde(skip)]
70    search_paths: Mutex<BTreeSet<PathBuf>>,
71}
72impl TestSuite {
73    /// Get the name of this suite as a string
74    #[inline]
75    pub fn name(&self) -> &str {
76        //self.id.name()
77        self.name.as_ref()
78    }
79
80    pub fn span(&self) -> SourceSpan {
81        SourceSpan::from(self.name.span())
82    }
83
84    pub fn id(&self) -> TestSuiteKey {
85        TestSuiteKey::new(self.name.clone(), self.path.clone())
86    }
87
88    #[inline]
89    pub fn source_dir(&self) -> &Path {
90        self.source_dir.as_ref()
91    }
92
93    #[inline]
94    pub fn working_dir(&self) -> &Path {
95        self.working_dir.as_ref()
96    }
97
98    pub fn search_paths(&self) -> parking_lot::MutexGuard<'_, BTreeSet<PathBuf>> {
99        self.search_paths.lock()
100    }
101
102    pub fn filter_by_path<P: Into<PathBuf>>(&self, path: P) {
103        self.search_paths.lock().insert(path.into());
104    }
105
106    /// Load test suite configuration from `input`
107    pub fn parse<P: AsRef<Path>>(path: P, config: &Config) -> DiagResult<Arc<Self>> {
108        let path = path.as_ref();
109        let path = if path.is_absolute() {
110            path.to_path_buf()
111        } else {
112            path.canonicalize().into_diagnostic()?
113        };
114        let source = Input::from(path.as_path())
115            .into_arc_source(false)
116            .into_diagnostic()?;
117        let toml = source.source();
118        let mut suite = toml::from_str::<Self>(toml).map_err(|error| {
119            let span = error.span();
120            Report::new(TestSuiteError::Syntax {
121                span: span.map(SourceSpan::from),
122                error,
123            })
124            .with_source_code(source.clone())
125        })?;
126
127        if suite.name().is_empty() {
128            return Err(
129                Report::new(TestSuiteError::EmptyName { span: suite.span() })
130                    .with_source_code(source.clone()),
131            );
132        }
133        suite.path = Some(path.clone().into());
134
135        let parent_dir = path.parent().unwrap();
136
137        if suite.source_dir() == Path::new("") {
138            *suite.source_dir.get_mut() = Cow::Owned(parent_dir.to_path_buf());
139        }
140
141        if suite.source_dir().is_relative() {
142            let source_dir = parent_dir.join(suite.source_dir.get_ref());
143            *suite.source_dir.get_mut() = Cow::Owned(source_dir);
144        }
145
146        if !suite.source_dir().is_dir() {
147            let span = suite.source_dir.span();
148            return Err(Report::new(TestSuiteError::InvalidSourceDir {
149                span: span.into(),
150                source_dir: suite.source_dir.into_inner().into_owned(),
151            })
152            .with_source_code(source.clone()));
153        }
154
155        if suite.working_dir() == Path::new("") {
156            let temp_dir = tempdir::TempDir::new(suite.name())
157                .into_diagnostic()
158                .wrap_err_with(|| {
159                    format!(
160                        "failed to create temporary directory for test suite at '{}'",
161                        &TestSuiteKey::new(suite.name.clone(), suite.path.clone()),
162                    )
163                })?;
164            *suite.working_dir.get_mut() = Cow::Owned(temp_dir.path().to_path_buf());
165            suite.temp_dir = Some(temp_dir);
166        } else if suite.working_dir().is_relative() {
167            let working_dir = parent_dir.join(suite.working_dir.get_ref());
168            *suite.working_dir.get_mut() = Cow::Owned(working_dir);
169        }
170
171        if !suite.working_dir().is_dir() {
172            let span = suite.working_dir.span();
173            let working_dir = suite.working_dir.into_inner();
174            return Err(Report::new(TestSuiteError::InvalidWorkingDir {
175                span: span.into(),
176                working_dir: working_dir.into_owned(),
177            })
178            .with_source_code(source));
179        }
180
181        // Ensure default test features/substitutions are present
182        let mut default_config = TestConfig::default();
183        default_config.set_default_features(config);
184        default_config.set_default_substitutions(config, &suite, path.as_path());
185        let suite_config = Arc::make_mut(&mut suite.config);
186        suite_config.inherit(&default_config);
187
188        Ok(Arc::new(suite))
189    }
190}
191
192fn empty_spanned_path() -> toml::Spanned<StaticCow<Path>> {
193    toml::Spanned::new(0..0, litcheck::fs::empty_path())
194}