use super::{Action, ActionError};
use duct_sh::sh_dangerous;
use log::{debug, error};
use thiserror::Error;
pub struct ScriptAction {
directory: String,
command: String,
}
#[derive(Debug, Error)]
pub enum ScriptError {
#[error("the script cannot run: {0}")]
ScriptFailure(#[from] std::io::Error),
#[error("the script returned non-zero exit code {0} with message: {1}")]
NonZeroExitcode(i32, String),
#[error("the script returned invalid characters")]
NonUtf8Return,
}
impl From<ScriptError> for ActionError {
fn from(value: ScriptError) -> Self {
match value {
ScriptError::ScriptFailure(_)
| ScriptError::NonZeroExitcode(_, _)
| ScriptError::NonUtf8Return => ActionError::FailedAction(value.to_string()),
}
}
}
impl ScriptAction {
pub fn new(directory: String, command: String) -> Self {
ScriptAction { directory, command }
}
fn run_inner(&self) -> Result<String, ScriptError> {
let output = sh_dangerous(&self.command)
.stderr_to_stdout()
.stdout_capture()
.dir(&self.directory)
.unchecked()
.run()?;
let output_str =
std::str::from_utf8(&output.stdout).map_err(|_| ScriptError::NonUtf8Return)?;
let output_str = output_str.trim_end().to_string();
if output.status.success() {
Ok(output_str)
} else {
Err(ScriptError::NonZeroExitcode(
output.status.code().unwrap_or(-1),
output_str,
))
}
}
}
impl Action for ScriptAction {
fn run(&self) -> Result<(), ActionError> {
debug!(
"Running script: {} in directory {}.",
self.command, self.directory
);
match self.run_inner() {
Ok(result) => {
debug!("Command success, output:");
result.lines().for_each(|line| {
debug!("{line}");
});
Ok(())
}
Err(err) => {
error!("Failed: {err}.");
Err(err.into())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_create_new_script() {
let action = ScriptAction::new(String::from("."), String::from("echo test"));
assert_eq!("echo test", action.command);
assert_eq!(".", action.directory);
}
#[test]
fn it_should_run_the_script() -> Result<(), ScriptError> {
let action = ScriptAction::new(String::from("."), String::from("echo test"));
let output = action.run_inner()?;
assert_eq!("test", output);
Ok(())
}
#[test]
fn it_should_catch_error_output() -> Result<(), ScriptError> {
let action = ScriptAction::new(String::from("."), String::from("echo err >&2"));
let output = action.run_inner()?;
assert_eq!("err", output);
Ok(())
}
#[test]
fn it_should_fail_if_the_script_fails() -> Result<(), ScriptError> {
let action = ScriptAction::new(String::from("."), String::from("false"));
let result = action.run_inner();
assert!(
matches!(result, Err(ScriptError::NonZeroExitcode(1, _))),
"{result:?} should match non zero exit code"
);
Ok(())
}
#[test]
fn it_should_fail_if_the_script_returns_non_utf8() -> Result<(), ScriptError> {
let action =
ScriptAction::new(String::from("."), String::from("/bin/echo -e '\\xc3\\x28'"));
let result = action.run_inner();
assert!(
matches!(result, Err(ScriptError::NonUtf8Return)),
"{result:?} should match non utf8 return"
);
Ok(())
}
}