litcheck_lit/formats/sh/
mod.rs1mod 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#[derive(Default, Clone, Debug, Deserialize)]
30pub struct ShTest {
31 #[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 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 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 let test_filename = test.path.file_name().unwrap();
99 let test_dir = test.path.parent().unwrap_or_else(|| Path::new(""));
100 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 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 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}