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::{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#[derive(Default, Clone, Debug, Deserialize)]
29pub struct ShTest {
30 #[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 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 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 let test_filename = test.path.file_name().unwrap();
97 let test_dir = test.path.parent().unwrap_or_else(|| Path::new(""));
98 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 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 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}