Skip to main content

litcheck_lit/test/
config.rs

1use std::{collections::BTreeMap, path::Path};
2
3use litcheck::{
4    diagnostics::{DiagResult, Diagnostic, IntoDiagnostic, Report, SourceSpan},
5    fs::PatternSet,
6    Input, StaticCow,
7};
8use serde::Deserialize;
9
10use crate::{
11    config::{BooleanExpr, FeatureSet, SubstitutionSet},
12    format::SelectedTestFormat,
13    Config,
14};
15
16#[derive(Debug, Diagnostic, thiserror::Error)]
17pub enum TestConfigError {
18    #[error("invalid syntax in configuration file")]
19    #[diagnostic()]
20    Syntax {
21        #[label("{error}")]
22        span: SourceSpan,
23        #[source]
24        error: toml::de::Error,
25    },
26}
27
28/// This is the type represents a local lit test configuration file
29#[derive(Default, Clone, Deserialize)]
30pub struct TestConfig {
31    /// The test format which will be used to discover and run tests in the suite
32    ///
33    /// Defaults to the `ShTest` format, see [crate::formats::ShTest] for details.
34    #[serde(default)]
35    pub format: SelectedTestFormat,
36    /// For test formats which scan directories for tests, this is a list of glob
37    /// patterns used to find test files, e.g. `*.rs` would search for any test files
38    /// ending in `.rs` in any of the search paths.
39    ///
40    /// See [glob::Pattern] docs for the full syntax available for glob patterns.
41    ///
42    /// By default all text files found in the search path for tests are considered
43    /// valid tests, unless they are prefixed with `.`, e.g. `.gitignore`.
44    #[serde(default)]
45    pub patterns: PatternSet,
46    /// Like `patterns`, but the given globs are used to reject potential test files matched by
47    /// `patterns` which should not be treated as tests.
48    ///
49    /// You should prefer to exclude subdirectories of a suite by using explicit search paths, and
50    /// excluding the directories that you don't want to search (so as to avoid traversing them in
51    /// the first place) - but in those cases where it is more convenient to use exclusion patterns,
52    /// this option is here to help.
53    #[serde(default)]
54    pub exclude: PatternSet,
55    /// Environment variables to set when executing tests
56    #[serde(default)]
57    pub env: BTreeMap<StaticCow<str>, StaticCow<str>>,
58    /// The set of substitutions which can be used in a test script.
59    ///
60    /// The substitutions will be replaced prior to running the test.
61    #[serde(default)]
62    pub substitutions: SubstitutionSet,
63    /// The set of feature strings available for these tests
64    #[serde(default)]
65    pub available_features: FeatureSet,
66}
67impl TestConfig {
68    /// Parse local test suite configuration from `input`, inheriting from `parent` where applicable
69    pub fn parse<P: AsRef<Path>>(path: P, config: &Config) -> DiagResult<Box<Self>> {
70        let path = path.as_ref();
71        let path = if path.is_absolute() {
72            path.to_path_buf()
73        } else {
74            path.canonicalize().into_diagnostic()?
75        };
76        let source = Input::from(path)
77            .into_source(false, config.source_manager())
78            .into_diagnostic()?;
79        toml::from_str::<Self>(source.as_str())
80            .map(Box::new)
81            .map_err(|error| {
82                let span = error.span().unwrap_or(0..0);
83                Report::new(TestConfigError::Syntax {
84                    span: SourceSpan::from_range_unchecked(source.id(), span),
85                    error,
86                })
87                .with_source_code(source)
88            })
89    }
90
91    /// Inherits values from `parent` that are empty/default in `self`.
92    pub fn inherit(&mut self, parent: &Self) {
93        if matches!(self.format, SelectedTestFormat::Default) {
94            self.format = parent.format.clone();
95        }
96
97        if self.patterns.is_empty() {
98            self.patterns = parent.patterns.clone();
99        }
100
101        if self.exclude.is_empty() {
102            self.exclude = parent.exclude.clone();
103        }
104
105        if !parent.env.is_empty() {
106            let env = core::mem::replace(&mut self.env, parent.env.clone());
107            self.env.extend(env);
108        }
109
110        if !parent.substitutions.is_empty() {
111            let subs = core::mem::replace(&mut self.substitutions, parent.substitutions.clone());
112            self.substitutions.extend(subs);
113        }
114
115        if !parent.available_features.is_empty() {
116            let features = core::mem::replace(
117                &mut self.available_features,
118                parent.available_features.clone(),
119            );
120            self.available_features.extend(features);
121        }
122    }
123
124    pub fn set_default_features(&mut self, config: &Config) {
125        use target_lexicon::*;
126
127        let host = config.host();
128        let target = config.target();
129
130        self.available_features
131            .insert(format!("system-{}", &host.operating_system));
132        self.available_features.insert(format!("host={}", &host));
133        self.available_features
134            .insert(format!("target={}", &target));
135        if host == target {
136            self.available_features.insert("native");
137        }
138        match target.architecture {
139            Architecture::X86_64 | Architecture::X86_64h => {
140                self.available_features.insert("target-x86_64");
141                match target.operating_system {
142                    OperatingSystem::Darwin(_) => {
143                        self.available_features.insert("x86_64-apple");
144                    }
145                    OperatingSystem::Linux => {
146                        self.available_features.insert("x86_64-linux");
147                    }
148                    _ => (),
149                }
150            }
151            Architecture::X86_32(_) => {
152                self.available_features.insert("target-x86");
153            }
154            Architecture::Aarch64(_) => {
155                self.available_features.insert("target-aarch64");
156            }
157            Architecture::Arm(_) => {
158                self.available_features.insert("target-arm");
159            }
160            arch => {
161                self.available_features.insert(format!("target-{}", arch));
162            }
163        }
164        match target.operating_system {
165            OperatingSystem::Darwin(_) | OperatingSystem::MacOSX { .. } => {
166                self.available_features.insert("system-linker-mach-o");
167            }
168            _ => (),
169        }
170    }
171
172    pub fn set_default_substitutions(
173        &mut self,
174        _config: &Config,
175        suite: &super::TestSuite,
176        source_dir: &Path,
177    ) {
178        // This binary is a multi-call executable containing both lit and filecheck,
179        // so get the path that we were invoked with (or that the OS chose to give us),
180        // and depending on how it was invoked,
181        let mut exe =
182            std::env::current_exe().expect("unable to detect lit/filecheck executable path");
183        let (filecheck, not) = if exe.ends_with("litcheck") {
184            // We are running lit, so a filename of 'litcheck'
185            // means that lit was invoked explicitly, thus we
186            // should use a substitution that invokes filecheck
187            // explicitly
188            let filecheck = StaticCow::Owned(format!("{} filecheck", exe.display()));
189            let not = StaticCow::Owned(format!("{} not", exe.display()));
190            (filecheck, not)
191        } else if exe.ends_with("lit") {
192            // We must have been invoked as a symlink, in
193            // which case the filecheck symlink is in the
194            // same directory, we just need to update the
195            // executable name
196            exe.set_file_name("filecheck");
197            let filecheck = StaticCow::Owned(exe.to_string_lossy().into_owned());
198            exe.set_file_name("not");
199            let not = StaticCow::Owned(exe.to_string_lossy().into_owned());
200            (filecheck, not)
201        } else {
202            // We're probably running as a test executable right now,
203            // so just use 'filecheck' as the substitution
204            (StaticCow::Borrowed("filecheck"), StaticCow::Borrowed("not"))
205        };
206        self.substitutions.insert(
207            "(?<before>^|\\s)[Ff]ile[Cc]heck(?<after>$|\\s)",
208            format!("$before{filecheck}$after"),
209        );
210        self.substitutions.insert(
211            "(?<before>^|\\s)not(?<after>$|\\s)",
212            format!("$before{not}$after"),
213        );
214
215        self.substitutions
216            .insert(r"%\{pathsep\}", if cfg!(windows) { ";" } else { ":" });
217
218        let test_root = source_dir
219            .components()
220            .next()
221            .unwrap()
222            .as_os_str()
223            .to_string_lossy()
224            .into_owned();
225        self.substitutions.insert(r"%\{fs-src-root\}", test_root);
226        let temp_root = suite
227            .working_dir()
228            .components()
229            .next()
230            .unwrap()
231            .as_os_str()
232            .to_string_lossy()
233            .into_owned();
234        self.substitutions.insert(r"%\{fs-tmp-root\}", temp_root);
235        self.substitutions
236            .insert(r"%\{fs-sep\}", std::path::MAIN_SEPARATOR_STR);
237    }
238
239    #[inline]
240    pub fn missing_features<F: AsRef<BooleanExpr>>(&self, required: &[F]) -> Option<String> {
241        self.available_features.missing_features(required)
242    }
243
244    #[inline]
245    pub fn unsupported_features<F: AsRef<BooleanExpr>>(&self, unsupported: &[F]) -> Option<String> {
246        self.available_features.unsupported_features(unsupported)
247    }
248}