mod engine;
mod script;
pub use self::script::{InvalidTestScriptError, TestScript};
use std::path::Path;
use litcheck::{
diagnostics::{reporting::PrintDiagnostic, DiagResult, IntoDiagnostic, WrapErr},
Input,
};
use serde::Deserialize;
use crate::{
config::{ScopedSubstitutionSet, SubstitutionSet},
format::TestFormat,
test::{DefaultTestRegistry, Test, TestRegistry, TestResult, TestStatus},
Config,
};
#[derive(Default, Clone, Debug, Deserialize)]
pub struct ShTest {
#[serde(default)]
shell: Option<String>,
#[serde(default)]
pipefail: bool,
#[serde(default)]
extra_substitutions: SubstitutionSet,
}
impl ShTest {
#[allow(unused)]
pub fn new(shell: String) -> Self {
Self {
shell: Some(shell),
..Default::default()
}
}
#[inline]
fn shell(&self) -> &str {
self.shell.as_deref().unwrap_or("sh")
}
}
impl TestFormat for ShTest {
#[inline(always)]
fn name(&self) -> &'static str {
"shtest"
}
#[inline(always)]
fn registry(&self) -> &dyn TestRegistry {
&DefaultTestRegistry
}
fn execute(&self, test: &Test, config: &Config) -> DiagResult<TestResult> {
let script_source = Input::from(test.source_path())
.into_arc_source(false)
.into_diagnostic()
.wrap_err("failed to read test file")?;
let mut script = match TestScript::parse_source(&script_source) {
Ok(script) => script,
Err(err) => {
let err = err.with_source_code(script_source);
let buf = format!("{}", PrintDiagnostic::new(err));
return Ok(TestResult::new(TestStatus::Unresolved).with_stderr(buf.into_bytes()));
}
};
if let Some(missing_features) = test.config.missing_features(&script.requires) {
return Ok(
TestResult::new(TestStatus::Unsupported).with_stderr(missing_features.into_bytes())
);
}
if let Some(unsupported_features) = test.config.unsupported_features(&script.unsupported) {
return Ok(TestResult::new(TestStatus::Unsupported)
.with_stderr(unsupported_features.into_bytes()));
}
if config.no_execute {
log::debug!("--no-execute was set, automatically passing test");
return Ok(TestResult::new(TestStatus::Pass));
}
let test_filename = test.path.file_name().unwrap();
let test_dir = test.path.parent().unwrap_or_else(|| Path::new(""));
let test_temp_dir = test.suite.working_dir().join(test_dir);
let test_name = test_filename.to_str().unwrap();
let temp = std::fs::create_dir_all(&test_temp_dir)
.and_then(|_| tempdir::TempDir::new_in(&test_temp_dir, test_name))
.expect("failed to create temporary directory");
let temp_dir = temp.path();
let base_temp_file = temp_dir.join(test_filename);
let temp_file = base_temp_file.with_extension(format!(
"{}.tmp",
base_temp_file.extension().unwrap().to_str().unwrap()
));
let mut substitutions = ScopedSubstitutionSet::new(&test.config.substitutions);
substitutions.extend(
self.extra_substitutions
.iter()
.map(|(k, v)| (k.clone(), v.clone())),
);
substitutions.extend([
("%s", test.absolute_path.to_string_lossy().into_owned()),
(
"%S",
test.absolute_path
.parent()
.unwrap()
.to_string_lossy()
.into_owned(),
),
("%t", temp_file.to_string_lossy().into_owned()),
(
"%basename_t",
temp_file
.file_stem()
.unwrap()
.to_string_lossy()
.into_owned(),
),
]);
substitutions.insert("%%", "%");
script
.apply_substitutions(&mut substitutions)
.map_err(|err| err.with_source_code(script_source.clone()))?;
let mut result = engine::run_test(&script, test, config, self)
.map_err(|err| err.with_source_code(script_source))?;
let expected_to_fail = if test.xfail_not {
false
} else {
script
.xfails
.iter()
.any(|condition| condition.evaluate(&test.config.available_features))
};
if expected_to_fail {
match result.status() {
TestStatus::Pass | TestStatus::FlakyPass => {
result.status = TestStatus::Xpass;
}
TestStatus::Fail => {
result.status = TestStatus::Xfail;
}
_ => (),
}
}
Ok(result)
}
}