Skip to main content

litcheck_lit/formats/sh/
mod.rs

1mod engine;
2mod script;
3
4pub use self::script::{InvalidTestScriptError, TestScript};
5
6use std::path::Path;
7
8use litcheck::{
9    diagnostics::{DiagResult, IntoDiagnostic, WrapErr},
10    reporting::PrintDiagnostic,
11    Input,
12};
13use serde::Deserialize;
14
15use crate::{
16    config::{ScopedSubstitutionSet, SubstitutionSet},
17    format::TestFormat,
18    test::{DefaultTestRegistry, Test, TestRegistry, TestResult, TestStatus},
19    Config,
20};
21
22/// ShTest is a format with one file per test.
23///
24/// This is the primary format for regression tests as described in the LLVM
25/// [testing guide](http://llvm.org/docs/TestingGuide.md).
26///
27/// The ShTest files contain some number of shell-like command pipelines, along
28/// with assertions about what should be in the output.
29#[derive(Default, Clone, Debug, Deserialize)]
30pub struct ShTest {
31    /// Users can override the default shell, `sh`, to be used when invoking commands.
32    #[serde(default)]
33    shell: Option<String>,
34    #[serde(default)]
35    pipefail: bool,
36    #[serde(default)]
37    extra_substitutions: SubstitutionSet,
38}
39impl ShTest {
40    #[allow(unused)]
41    pub fn new(shell: String) -> Self {
42        Self {
43            shell: Some(shell),
44            ..Default::default()
45        }
46    }
47
48    #[inline]
49    fn shell(&self) -> &str {
50        self.shell.as_deref().unwrap_or("sh")
51    }
52}
53impl TestFormat for ShTest {
54    #[inline(always)]
55    fn name(&self) -> &'static str {
56        "shtest"
57    }
58
59    #[inline(always)]
60    fn registry(&self) -> &dyn TestRegistry {
61        &DefaultTestRegistry
62    }
63
64    fn execute(&self, test: &Test, config: &Config) -> DiagResult<TestResult> {
65        let script_source = Input::from(test.source_path())
66            .into_source(false, config.source_manager())
67            .into_diagnostic()
68            .wrap_err("failed to read test file")?;
69        let mut script = match TestScript::parse(script_source.clone()) {
70            Ok(script) => script,
71            Err(err) => {
72                let buf = format!("{}", PrintDiagnostic::new(err));
73                return Ok(TestResult::new(TestStatus::Unresolved).with_stderr(buf.into_bytes()));
74            }
75        };
76
77        log::trace!(target: "lit:shtest", "compiled test script (no substitutions): {script:#?}");
78
79        // Enforce requires
80        if let Some(missing_features) = test.config.missing_features(&script.requires) {
81            return Ok(
82                TestResult::new(TestStatus::Unsupported).with_stderr(missing_features.into_bytes())
83            );
84        }
85
86        // Enforce unsupported
87        if let Some(unsupported_features) = test.config.unsupported_features(&script.unsupported) {
88            return Ok(TestResult::new(TestStatus::Unsupported)
89                .with_stderr(unsupported_features.into_bytes()));
90        }
91
92        if config.options.no_execute {
93            log::debug!(target: "lit:shtest", "--no-execute was set, automatically passing test");
94            return Ok(TestResult::new(TestStatus::Pass));
95        }
96
97        // Get the file and directory paths for this test, relative to the test suite directory
98        let test_filename = test.path.file_name().unwrap();
99        let test_dir = test.path.parent().unwrap_or_else(|| Path::new(""));
100        // Generate a temporary directory for this test, relative to the suite working directory
101        let test_temp_dir = test.suite.working_dir().join(test_dir);
102        let test_name = test_filename.to_str().unwrap();
103        let temp = std::fs::create_dir_all(&test_temp_dir)
104            .and_then(|_| tempdir::TempDir::new_in(&test_temp_dir, test_name))
105            .expect("failed to create temporary directory");
106        // Generate a temporary filename for this test, by appending .tmp to the source filename
107        let temp_dir = temp.path();
108        let base_temp_file = temp_dir.join(test_filename);
109        let temp_file = base_temp_file.with_extension(format!(
110            "{}.tmp",
111            base_temp_file.extension().unwrap().to_str().unwrap()
112        ));
113
114        // Construct substitutions
115        let mut substitutions = ScopedSubstitutionSet::new(&test.config.substitutions);
116        substitutions.extend(
117            self.extra_substitutions
118                .iter()
119                .map(|(k, v)| (k.clone(), v.clone())),
120        );
121        substitutions.extend([
122            ("%s", test.absolute_path.to_string_lossy().into_owned()),
123            (
124                "%S",
125                test.absolute_path
126                    .parent()
127                    .unwrap()
128                    .to_string_lossy()
129                    .into_owned(),
130            ),
131            ("%t", temp_file.to_string_lossy().into_owned()),
132            (
133                "%basename_t",
134                temp_file
135                    .file_stem()
136                    .unwrap()
137                    .to_string_lossy()
138                    .into_owned(),
139            ),
140        ]);
141        substitutions.insert("%%", "%");
142        script
143            .apply_substitutions(&mut substitutions)
144            .map_err(|err| err.with_source_code(script_source.clone()))?;
145
146        log::trace!(target: "lit:shtest", "compiled test script and applied substitutions: {script:#?}");
147
148        let mut result = engine::run_test(&script, test, config, self)
149            .map_err(|err| err.with_source_code(script_source))?;
150        let expected_to_fail = if test.xfail_not {
151            false
152        } else {
153            script
154                .xfails
155                .iter()
156                .any(|condition| condition.evaluate(&test.config.available_features))
157        };
158
159        if expected_to_fail {
160            match result.status() {
161                TestStatus::Pass | TestStatus::FlakyPass => {
162                    result.status = TestStatus::Xpass;
163                }
164                TestStatus::Fail => {
165                    result.status = TestStatus::Xfail;
166                }
167                _ => (),
168            }
169        }
170
171        Ok(result)
172    }
173}