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