//! Step library for running subprocesses as part of scenarios use regex::RegexBuilder; pub use super::datadir::Datadir; pub use crate::prelude::*; use std::collections::HashMap; use std::env; use std::ffi::{OsStr, OsString}; use std::io::Write; use std::path::PathBuf; use std::process::{Command, Stdio}; #[derive(Default)] pub struct Runcmd { env: HashMap, // push to "prepend", order reversed when added to env paths: Vec, // The following are the result of any executed command exitcode: Option, stdout: Vec, stderr: Vec, } // Note, this prefix requires that the injection env vars must have // names which are valid unicode (and ideally ASCII) const ENV_INJECTION_PREFIX: &str = "SUBPLOT_ENV_"; #[cfg(not(windows))] static DEFAULT_PATHS: &[&str] = &["/usr/bin", "/bin"]; // Note, this comes from https://www.computerhope.com/issues/ch000549.htm#defaultpath #[cfg(windows)] static DEFAULT_PATHS: &[&str] = &[ r"%SystemRoot%\system32", r"%SystemRoot%", r"%SystemRoot%\System32\Wbem", ]; // This us used internally to force CWD for running commands const USE_CWD: &str = "\0USE_CWD"; impl ContextElement for Runcmd { fn scenario_starts(&mut self) -> StepResult { self.env.drain(); self.paths.drain(..); self.env.insert("SHELL".into(), "/bin/sh".into()); self.env.insert( "PATH".into(), env::var_os("PATH") .map(Ok) .unwrap_or_else(|| env::join_paths(DEFAULT_PATHS.iter()))?, ); // Having assembled the 'default' environment, override it with injected // content from the calling environment. for (k, v) in env::vars_os() { if let Some(k) = k.to_str() { if let Some(k) = k.strip_prefix(ENV_INJECTION_PREFIX) { self.env.insert(k.into(), v); } } } Ok(()) } } impl Runcmd { pub fn prepend_to_path>(&mut self, element: S) { self.paths.push(element.into()); } } #[step] pub fn helper_script(context: &Datadir, script: SubplotDataFile) { context .open_write(script.name())? .write_all(script.data())?; } #[step] pub fn helper_srcdir_path(context: &mut Runcmd) { context.prepend_to_path(env!("CARGO_MANIFEST_DIR")); } #[step] #[context(Datadir)] #[context(Runcmd)] pub fn run(context: &ScenarioContext, argv0: &str, args: &str) { try_to_run::call(context, argv0, args)?; exit_code_is::call(context, 0)?; } #[step] #[context(Datadir)] #[context(Runcmd)] pub fn run_in(context: &ScenarioContext, dirname: &str, argv0: &str, args: &str) { try_to_run_in::call(context, dirname, argv0, args)?; exit_code_is::call(context, 0)?; } #[step] #[context(Datadir)] #[context(Runcmd)] pub fn try_to_run(context: &ScenarioContext, argv0: &str, args: &str) { try_to_run_in::call(context, USE_CWD, argv0, args)?; } #[step] #[context(Datadir)] #[context(Runcmd)] pub fn try_to_run_in(context: &ScenarioContext, dirname: &str, argv0: &str, args: &str) { // This is the core of runcmd and is how we handle things let argv0: PathBuf = if argv0.starts_with('.') { context.with( |datadir: &Datadir| datadir.canonicalise_filename(argv0), false, )? } else { argv0.into() }; let mut datadir = context.with( |datadir: &Datadir| Ok(datadir.base_path().to_path_buf()), false, )?; if dirname != USE_CWD { datadir = datadir.join(dirname); } let mut proc = Command::new(argv0); proc.args(&shell_words::split(args)?); proc.current_dir(&datadir); proc.env("HOME", &datadir); proc.env("TMPDIR", &datadir); let mut curpath = context.with( |runcmd: &Runcmd| { let mut curpath = None; for (k, v) in runcmd.env.iter() { proc.env(k, v); if k == "PATH" { curpath = Some(v.to_owned()); } } Ok(curpath) }, false, )?; let path = context.with( |runcmd: &Runcmd| { Ok(env::join_paths( runcmd .paths .iter() .rev() .map(PathBuf::from) .chain(env::split_paths( curpath.as_deref().unwrap_or_else(|| OsStr::new("")), )), )?) }, false, )?; proc.env("PATH", path); proc.stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); let mut output = proc.output()?; context.with_mut( |runcmd: &mut Runcmd| { std::mem::swap(&mut runcmd.stdout, &mut output.stdout); std::mem::swap(&mut runcmd.stderr, &mut output.stderr); runcmd.exitcode = output.status.code(); Ok(()) }, false, )?; } #[step] pub fn exit_code_is(context: &Runcmd, exit: i32) { if context.exitcode != Some(exit) { throw!(format!( "expected exit code {}, but had {:?}", exit, context.exitcode )); } } #[step] pub fn exit_code_is_not(context: &Runcmd, exit: i32) { if context.exitcode.is_none() || context.exitcode == Some(exit) { throw!(format!("Expected exit code to not equal {}", exit)); } } #[step] #[context(Runcmd)] pub fn exit_code_is_zero(context: &ScenarioContext) { exit_code_is::call(context, 0) } #[step] #[context(Runcmd)] pub fn exit_code_is_nonzero(context: &ScenarioContext) { exit_code_is_not::call(context, 0) } enum Stream { Stdout, Stderr, } enum MatchKind { Exact, Contains, Regex, } #[throws(StepError)] fn check_matches(runcmd: &Runcmd, which: Stream, how: MatchKind, against: &str) -> bool { let stream = match which { Stream::Stdout => &runcmd.stdout, Stream::Stderr => &runcmd.stderr, }; let against = if matches!(how, MatchKind::Regex) { against.to_string() } else { unescape::unescape(against).ok_or("unable to unescape input")? }; match how { MatchKind::Exact => stream.as_slice() == against.as_bytes(), MatchKind::Contains => stream .windows(against.len()) .any(|window| window == against.as_bytes()), MatchKind::Regex => { let stream = String::from_utf8_lossy(stream); let regex = RegexBuilder::new(&against).multi_line(true).build()?; regex.is_match(&stream) } } } #[step] pub fn stdout_is(runcmd: &Runcmd, text: &str) { if !check_matches(runcmd, Stream::Stdout, MatchKind::Exact, text)? { throw!(format!("stdout is not {:?}", text)); } } #[step] pub fn stdout_isnt(runcmd: &Runcmd, text: &str) { if check_matches(runcmd, Stream::Stdout, MatchKind::Exact, text)? { throw!(format!("stdout is exactly {:?}", text)); } } #[step] pub fn stderr_is(runcmd: &Runcmd, text: &str) { if !check_matches(runcmd, Stream::Stderr, MatchKind::Exact, text)? { throw!(format!("stderr is not {:?}", text)); } } #[step] pub fn stderr_isnt(runcmd: &Runcmd, text: &str) { if check_matches(runcmd, Stream::Stderr, MatchKind::Exact, text)? { throw!(format!("stderr is exactly {:?}", text)); } } #[step] pub fn stdout_contains(runcmd: &Runcmd, text: &str) { if !check_matches(runcmd, Stream::Stdout, MatchKind::Contains, text)? { throw!(format!("stdout does not contain {:?}", text)); } } #[step] pub fn stdout_doesnt_contain(runcmd: &Runcmd, text: &str) { if check_matches(runcmd, Stream::Stdout, MatchKind::Contains, text)? { throw!(format!("stdout contains {:?}", text)); } } #[step] pub fn stderr_contains(runcmd: &Runcmd, text: &str) { if !check_matches(runcmd, Stream::Stderr, MatchKind::Contains, text)? { throw!(format!("stderr does not contain {:?}", text)); } } #[step] pub fn stderr_doesnt_contain(runcmd: &Runcmd, text: &str) { if check_matches(runcmd, Stream::Stderr, MatchKind::Contains, text)? { throw!(format!("stderr contains {:?}", text)); } } #[step] pub fn stdout_matches_regex(runcmd: &Runcmd, regex: &str) { if !check_matches(runcmd, Stream::Stdout, MatchKind::Regex, regex)? { throw!(format!("stdout does not match {:?}", regex)); } } #[step] pub fn stdout_doesnt_match_regex(runcmd: &Runcmd, regex: &str) { if check_matches(runcmd, Stream::Stdout, MatchKind::Regex, regex)? { throw!(format!("stdout matches {:?}", regex)); } } #[step] pub fn stderr_matches_regex(runcmd: &Runcmd, regex: &str) { if !check_matches(runcmd, Stream::Stderr, MatchKind::Regex, regex)? { throw!(format!("stderr does not match {:?}", regex)); } } #[step] pub fn stderr_doesnt_match_regex(runcmd: &Runcmd, regex: &str) { if check_matches(runcmd, Stream::Stderr, MatchKind::Regex, regex)? { throw!(format!("stderr matches {:?}", regex)); } }