use regex::RegexBuilder;
pub use super::datadir::Datadir;
pub use crate::prelude::*;
use std::collections::HashMap;
use std::env::{self, JoinPathsError};
use std::ffi::{OsStr, OsString};
use std::fmt::Debug;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
#[derive(Default)]
pub struct Runcmd {
env: HashMap<OsString, OsString>,
paths: Vec<OsString>,
exitcode: Option<i32>,
stdout: Vec<u8>,
stderr: Vec<u8>,
}
impl Debug for Runcmd {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Runcmd")
.field("env", &self.env)
.field("paths", &self.paths)
.field("exitcode", &self.exitcode)
.field("stdout", &String::from_utf8_lossy(&self.stdout))
.field("stderr", &String::from_utf8_lossy(&self.stderr))
.finish()
}
}
const ENV_INJECTION_PREFIX: &str = "SUBPLOT_ENV_";
#[cfg(not(windows))]
static DEFAULT_PATHS: &[&str] = &["/usr/bin", "/bin"];
#[cfg(windows)]
static DEFAULT_PATHS: &[&str] = &[
r"%SystemRoot%\system32",
r"%SystemRoot%",
r"%SystemRoot%\System32\Wbem",
];
lazy_static! {
static ref USE_CWD: PathBuf = PathBuf::from("\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()))?,
);
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<S: Into<OsString>>(&mut self, element: S) {
self.paths.push(element.into());
}
pub fn stdout_as_string(&self) -> String {
String::from_utf8_lossy(&self.stdout).into_owned()
}
pub fn stderr_as_string(&self) -> String {
String::from_utf8_lossy(&self.stderr).into_owned()
}
pub fn setenv<K: Into<OsString>, V: Into<OsString>>(&mut self, key: K, value: V) {
self.env.insert(key.into(), value.into());
}
pub fn getenv<K: AsRef<OsStr>>(&self, key: K) -> Option<&OsStr> {
self.env.get(key.as_ref()).map(OsString::as_os_str)
}
pub fn unsetenv<K: AsRef<OsStr>>(&mut self, key: K) -> bool {
self.env.remove(key.as_ref()).is_some()
}
pub fn join_paths(&self) -> Result<OsString, JoinPathsError> {
let curpath = self
.env
.get(OsStr::new("PATH"))
.map(|s| s.as_os_str())
.unwrap_or_else(|| OsStr::new(""));
env::join_paths(
self.paths
.iter()
.rev()
.map(PathBuf::from)
.chain(env::split_paths(curpath).filter(|p| p != Path::new(""))),
)
}
}
#[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: &Path, 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: &Path, argv0: &str, args: &str) {
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.as_path() {
datadir = datadir.join(dirname);
}
let mut proc = Command::new(&argv0);
let args = shell_words::split(args)?;
proc.args(&args);
proc.current_dir(&datadir);
println!(
"Running `{}` with args {:?}\nRunning in {}",
argv0.display(),
args,
datadir.display()
);
proc.env("HOME", &datadir);
proc.env("TMPDIR", &datadir);
context.with(
|runcmd: &Runcmd| {
for (k, v) in runcmd
.env
.iter()
.filter(|(k, _)| k.to_str() != Some("PATH"))
{
println!("ENV: {} = {}", k.to_string_lossy(), v.to_string_lossy());
proc.env(k, v);
}
Ok(())
},
false,
)?;
let path = context.with(|runcmd: &Runcmd| Ok(runcmd.join_paths()?), false)?;
proc.env("PATH", &path);
println!("PATH: {}", path.to_string_lossy());
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();
println!("Exit code: {}", runcmd.exitcode.unwrap_or(-1));
println!(
"Stdout:\n{}\nStderr:\n{}\n",
runcmd.stdout_as_string(),
runcmd.stderr_as_string()
);
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:?}"));
}
}