litcheck_lit/test/
config.rs

1use std::{collections::BTreeMap, path::Path};
2
3use litcheck::{
4    diagnostics::{DiagResult, Diagnostic, IntoDiagnostic, Report, SourceFile, 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    /// Environment variables to set when executing tests
47    #[serde(default)]
48    pub env: BTreeMap<StaticCow<str>, StaticCow<str>>,
49    /// The set of substitutions which can be used in a test script.
50    ///
51    /// The substitutions will be replaced prior to running the test.
52    #[serde(default)]
53    pub substitutions: SubstitutionSet,
54    /// The set of feature strings available for these tests
55    #[serde(default)]
56    pub available_features: FeatureSet,
57}
58impl TestConfig {
59    /// Parse local test suite configuration from `input`, inheriting from `parent` where applicable
60    pub fn parse<P: AsRef<Path>>(path: P) -> DiagResult<Box<Self>> {
61        let path = path.as_ref();
62        let path = if path.is_absolute() {
63            path.to_path_buf()
64        } else {
65            path.canonicalize().into_diagnostic()?
66        };
67        let source = Input::from(path).into_source(false).into_diagnostic()?;
68        toml::from_str::<Self>(source.source())
69            .map(Box::new)
70            .map_err(|error| {
71                let span = error.span().unwrap_or(0..0);
72                Report::new(TestConfigError::Syntax {
73                    span: SourceSpan::from(span),
74                    error,
75                })
76                .with_source_code(source)
77            })
78    }
79
80    /// Inherits values from `parent` that are empty/default in `self`.
81    pub fn inherit(&mut self, parent: &Self) {
82        if matches!(self.format, SelectedTestFormat::Default) {
83            self.format = parent.format.clone();
84        }
85
86        if self.patterns.is_empty() {
87            self.patterns = parent.patterns.clone();
88        }
89
90        if !parent.env.is_empty() {
91            let env = core::mem::replace(&mut self.env, parent.env.clone());
92            self.env.extend(env);
93        }
94
95        if !parent.substitutions.is_empty() {
96            let subs = core::mem::replace(&mut self.substitutions, parent.substitutions.clone());
97            self.substitutions.extend(subs);
98        }
99
100        if !parent.available_features.is_empty() {
101            let features = core::mem::replace(
102                &mut self.available_features,
103                parent.available_features.clone(),
104            );
105            self.available_features.extend(features);
106        }
107    }
108
109    pub fn set_default_features(&mut self, config: &Config) {
110        use target_lexicon::*;
111
112        let host = config.host();
113        let target = config.target();
114
115        self.available_features
116            .insert(format!("system-{}", &host.operating_system));
117        self.available_features.insert(format!("host={}", &host));
118        self.available_features
119            .insert(format!("target={}", &target));
120        if host == target {
121            self.available_features.insert("native");
122        }
123        match target.architecture {
124            Architecture::X86_64 | Architecture::X86_64h => {
125                self.available_features.insert("target-x86_64");
126                match target.operating_system {
127                    OperatingSystem::Darwin => {
128                        self.available_features.insert("x86_64-apple");
129                    }
130                    OperatingSystem::Linux => {
131                        self.available_features.insert("x86_64-linux");
132                    }
133                    _ => (),
134                }
135            }
136            Architecture::X86_32(_) => {
137                self.available_features.insert("target-x86");
138            }
139            Architecture::Aarch64(_) => {
140                self.available_features.insert("target-aarch64");
141            }
142            Architecture::Arm(_) => {
143                self.available_features.insert("target-arm");
144            }
145            arch => {
146                self.available_features.insert(format!("target-{}", arch));
147            }
148        }
149        match target.operating_system {
150            OperatingSystem::Darwin | OperatingSystem::MacOSX { .. } => {
151                self.available_features.insert("system-linker-mach-o");
152            }
153            _ => (),
154        }
155    }
156
157    pub fn set_default_substitutions(
158        &mut self,
159        _config: &Config,
160        suite: &super::TestSuite,
161        source_dir: &Path,
162    ) {
163        // This binary is a multi-call executable containing both lit and filecheck,
164        // so get the path that we were invoked with (or that the OS chose to give us),
165        // and depending on how it was invoked,
166        let mut exe =
167            std::env::current_exe().expect("unable to detect lit/filecheck executable path");
168        let filecheck = if exe.ends_with("litcheck") {
169            // We are running lit, so a filename of 'litcheck'
170            // means that lit was invoked explicitly, thus we
171            // should use a substitution that invokes filecheck
172            // explicitly
173            StaticCow::Owned(format!("{} filecheck", exe.display()))
174        } else if exe.ends_with("lit") {
175            // We must have been invoked as a symlink, in
176            // which case the filecheck symlink is in the
177            // same directory, we just need to update the
178            // executable name
179            exe.set_file_name("filecheck");
180            StaticCow::Owned(exe.to_string_lossy().into_owned())
181        } else {
182            // We're probably running as a test executable right now,
183            // so just use 'filecheck' as the substitution
184            StaticCow::Borrowed("filecheck")
185        };
186        self.substitutions.insert("[Ff]ile[Cc]heck", filecheck);
187
188        self.substitutions
189            .insert(r"%\{pathsep\}", if cfg!(windows) { ";" } else { ":" });
190
191        let test_root = source_dir
192            .components()
193            .next()
194            .unwrap()
195            .as_os_str()
196            .to_string_lossy()
197            .into_owned();
198        self.substitutions.insert(r"%\{fs-src-root\}", test_root);
199        let temp_root = suite
200            .working_dir()
201            .components()
202            .next()
203            .unwrap()
204            .as_os_str()
205            .to_string_lossy()
206            .into_owned();
207        self.substitutions.insert(r"%\{fs-tmp-root\}", temp_root);
208        self.substitutions
209            .insert(r"%\{fs-sep\}", std::path::MAIN_SEPARATOR_STR);
210    }
211
212    #[inline]
213    pub fn missing_features<F: AsRef<BooleanExpr>>(&self, required: &[F]) -> Option<String> {
214        self.available_features.missing_features(required)
215    }
216
217    #[inline]
218    pub fn unsupported_features<F: AsRef<BooleanExpr>>(&self, unsupported: &[F]) -> Option<String> {
219        self.available_features.unsupported_features(unsupported)
220    }
221}