#![deny(
clippy::all,
clippy::cargo,
clippy::nursery,
clippy::pedantic,
deprecated_in_future,
future_incompatible,
missing_docs,
nonstandard_style,
rust_2018_idioms,
rustdoc,
warnings,
unused_results,
unused_qualifications,
unused_lifetimes,
unused_import_braces,
unsafe_code,
unreachable_pub,
trivial_casts,
trivial_numeric_casts,
missing_debug_implementations,
missing_copy_implementations
)]
#![warn(variant_size_differences)]
#![allow(clippy::multiple_crate_versions, missing_doc_code_examples)]
#![doc(html_root_url = "https://docs.rs/automaat-processor-shell-command/0.1.0")]
use automaat_core::{Context, Processor};
use serde::{Deserialize, Serialize};
use std::{env, error, fmt, io, path, process};
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ShellCommand {
pub command: String,
pub arguments: Option<Vec<String>>,
pub cwd: Option<String>,
pub paths: Option<Vec<String>>,
}
#[cfg(feature = "juniper")]
#[graphql(name = "ShellCommandInput")]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, juniper::GraphQLInputObject)]
pub struct Input {
command: String,
arguments: Option<Vec<String>>,
cwd: Option<String>,
paths: Option<Vec<String>>,
}
#[cfg(feature = "juniper")]
impl From<Input> for ShellCommand {
fn from(input: Input) -> Self {
Self {
command: input.command,
arguments: input.arguments,
cwd: input.cwd,
paths: input.paths,
}
}
}
impl<'a> Processor<'a> for ShellCommand {
const NAME: &'static str = "Shell Command";
type Error = Error;
type Output = String;
fn validate(&self) -> Result<(), Self::Error> {
fn check_path(path: &str) -> Result<(), Error> {
let path = path::Path::new(path);
path.components().try_for_each(|c| match c {
path::Component::Normal(_) => Ok(()),
_ => Err(Error::Path(
"only sibling or child paths are accessible".into(),
)),
})
}
if let Some(cwd) = &self.cwd {
check_path(cwd)?;
};
if let Some(paths) = &self.paths {
paths.iter().map(String::as_str).try_for_each(check_path)?;
}
Ok(())
}
fn run(&self, context: &Context) -> Result<Option<Self::Output>, Self::Error> {
self.validate()?;
let arguments = match &self.arguments {
None => vec![],
Some(v) => v.iter().map(String::as_str).collect(),
};
let workspace = context.workspace_path();
let cwd = workspace.join(path::Path::new(
self.cwd.as_ref().unwrap_or(&"".to_owned()).as_str(),
));
if let Some(new_paths) = &self.paths {
let paths: Vec<_> = match env::var_os("PATH") {
Some(current_path) => env::split_paths(¤t_path)
.chain(new_paths.iter().map(path::PathBuf::from))
.collect(),
None => new_paths
.iter()
.map(path::Path::new)
.map(|p| workspace.join(p))
.collect(),
};
let path = env::join_paths(paths)?;
env::set_var("PATH", &path);
};
let output = process::Command::new(&self.command)
.current_dir(cwd)
.args(arguments)
.output()?;
if !output.status.success() {
if output.stderr.is_empty() {
return Err(Error::Command(
"unknown error during command execution".into(),
));
};
return Err(Error::Command(
String::from_utf8_lossy(&strip_ansi_escapes::strip(output.stderr)?)
.trim_end()
.to_owned(),
));
}
if output.stdout.is_empty() {
return Ok(None);
};
Ok(Some(
String::from_utf8_lossy(&strip_ansi_escapes::strip(output.stdout)?)
.trim_end()
.to_owned(),
))
}
}
#[derive(Debug)]
pub enum Error {
Command(String),
Io(io::Error),
Path(String),
#[doc(hidden)]
__Unknown, }
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Error::Command(ref err) => write!(f, "Command error: {}", err),
Error::Io(ref err) => write!(f, "IO error: {}", err),
Error::Path(ref err) => write!(f, "Path error: {}", err),
Error::__Unknown => unreachable!(),
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match *self {
Error::Command(_) | Error::Path(_) => None,
Error::Io(ref err) => Some(err),
Error::__Unknown => unreachable!(),
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Error::Io(err)
}
}
impl From<env::JoinPathsError> for Error {
fn from(err: env::JoinPathsError) -> Self {
Error::Path(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn processor_stub() -> ShellCommand {
ShellCommand {
command: "echo".to_owned(),
arguments: None,
cwd: None,
paths: None,
}
}
mod run {
use super::*;
#[test]
fn test_command_without_output() {
let mut processor = processor_stub();
processor.command = "true".to_owned();
let context = Context::new().unwrap();
let output = processor.run(&context).unwrap();
assert!(output.is_none())
}
#[test]
fn test_command_with_output() {
let mut processor = processor_stub();
processor.command = "ps".to_owned();
let context = Context::new().unwrap();
let output = processor.run(&context).unwrap().expect("Some");
dbg!(&output);
assert!(output.contains("PID"))
}
#[test]
fn test_command_with_arguments() {
let mut processor = processor_stub();
processor.command = "echo".to_owned();
processor.arguments = Some(vec!["hello world".to_owned()]);
let context = Context::new().unwrap();
let output = processor.run(&context).unwrap().expect("Some");
assert_eq!(output, "hello world".to_owned())
}
#[test]
#[should_panic]
fn test_command_non_zero_exit_code() {
let mut processor = processor_stub();
processor.command = "false".to_owned();
let context = Context::new().unwrap();
let _ = processor.run(&context).unwrap();
}
#[test]
fn test_command_stderr_output() {
let mut processor = processor_stub();
processor.command = "ls".to_owned();
processor.arguments = Some(vec!["invalid-file".to_owned()]);
let context = Context::new().unwrap();
let error = processor.run(&context).unwrap_err();
assert!(error.to_string().contains("Command error"))
}
#[test]
fn test_invalid_command() {
let mut processor = processor_stub();
processor.command = "doesnotexist".to_owned();
let context = Context::new().unwrap();
let error = processor.run(&context).unwrap_err();
assert_eq!(
error.to_string(),
"IO error: No such file or directory (os error 2)".to_owned()
)
}
}
mod validate {
use super::*;
#[test]
fn test_no_cwd() {
let mut processor = processor_stub();
processor.cwd = None;
processor.validate().unwrap()
}
#[test]
fn test_relative_cwd() {
let mut processor = processor_stub();
processor.cwd = Some("hello/world".to_owned());
processor.validate().unwrap()
}
#[test]
#[should_panic]
fn test_prefix_cwd() {
let mut processor = processor_stub();
processor.cwd = Some("../parent".to_owned());
processor.validate().unwrap()
}
#[test]
#[should_panic]
fn test_absolute_cwd() {
let mut processor = processor_stub();
processor.cwd = Some("/etc".to_owned());
processor.validate().unwrap()
}
#[test]
fn test_no_paths() {
let mut processor = processor_stub();
processor.paths = None;
processor.validate().unwrap()
}
#[test]
fn test_relative_paths() {
let mut processor = processor_stub();
processor.paths = Some(vec!["hello/world".to_owned()]);
processor.validate().unwrap()
}
#[test]
fn test_multiple_valid_paths() {
let mut processor = processor_stub();
processor.paths = Some(vec!["valid/path".to_owned(), "another/path".to_owned()]);
processor.validate().unwrap()
}
#[test]
#[should_panic]
fn test_prefix_paths() {
let mut processor = processor_stub();
processor.paths = Some(vec!["../parent".to_owned()]);
processor.validate().unwrap()
}
#[test]
#[should_panic]
fn test_absolute_paths() {
let mut processor = processor_stub();
processor.paths = Some(vec!["/etc".to_owned()]);
processor.validate().unwrap()
}
#[test]
#[should_panic]
fn test_multiple_paths_one_bad() {
let mut processor = processor_stub();
processor.paths = Some(vec!["valid/path".to_owned(), "/etc".to_owned()]);
processor.validate().unwrap()
}
}
#[test]
fn test_readme_deps() {
version_sync::assert_markdown_deps_updated!("README.md");
}
#[test]
fn test_html_root_url() {
version_sync::assert_html_root_url_updated!("src/lib.rs");
}
}