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::fs::File;
use std::io::{Seek, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::Mutex;
#[derive(Default)]
pub struct Runcmd {
env: HashMap<OsString, OsString>,
paths: Vec<OsString>,
exitcode: Option<i32>,
stdin: Stdin,
stdout: Vec<u8>,
stderr: Vec<u8>,
}
#[derive(Debug, Default)]
enum Stdin {
#[default]
None,
Exact(Vec<u8>),
File(PathBuf),
}
static FORK_LOCK: Mutex<()> = Mutex::new(());
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("stdin", &format!("{:?}", self.stdin))
.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_DATADIR_ROOT: PathBuf = PathBuf::from("\0USE_DATADIR_ROOT");
}
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(""))),
)
}
pub fn with_forklock<F, R>(&mut self, func: F) -> Result<R, StepError>
where
F: FnOnce() -> Result<R, StepError>,
{
let _lock = FORK_LOCK.lock().map_err(|e| e.to_string())?;
let res = func()?;
Ok(res)
}
}
#[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_DATADIR_ROOT, 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 datadir = context.with(
|datadir: &Datadir| Ok(datadir.base_path().to_path_buf()),
false,
)?;
let mut proc = Command::new(&argv0);
let args = shell_words::split(args)?;
proc.args(&args);
if dirname != USE_DATADIR_ROOT.as_path() {
proc.current_dir(datadir.join(dirname));
} else {
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 mut tmp_file = tempfile::tempfile()?;
let stdin_file = context.with(
|runcmd: &Runcmd| {
let file = match &runcmd.stdin {
Stdin::None => tmp_file,
Stdin::Exact(data) => {
tmp_file.write_all(data)?;
tmp_file.rewind()?;
tmp_file
}
Stdin::File(filename) => {
eprintln!("Stdin::File: filename={}", filename.display());
File::open(filename)?
}
};
Ok(file)
},
false,
)?;
proc.stdin(Stdio::from(stdin_file));
let path = context.with(|runcmd: &Runcmd| Ok(runcmd.join_paths()?), false)?;
proc.env("PATH", &path);
println!("PATH: {}", path.to_string_lossy());
proc.stdout(Stdio::piped()).stderr(Stdio::piped());
let child = context.with_mut(
|runcmd: &mut Runcmd| runcmd.with_forklock(|| Ok(proc.spawn()?)),
false,
)?;
let mut output = child.wait_with_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,
};
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]
#[context(Datadir)]
#[context(Runcmd)]
pub fn stdin_from_file(context: &ScenarioContext, file: &Path) {
let filename: PathBuf = context.with(
|datadir: &Datadir| datadir.canonicalise_filename(file),
false,
)?;
context.with_mut(
|runcmd: &mut Runcmd| {
eprintln!("filename: {}", filename.display());
runcmd.stdin = Stdin::File(filename);
Ok(())
},
true,
)?;
}
#[step]
pub fn stdin_is(runcmd: &mut Runcmd, text: &str) {
eprintln!("runcmd: {runcmd:#?}");
eprintln!("text: {text:#?}");
runcmd.stdin = Stdin::Exact(text.as_bytes().to_vec());
}
#[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:?}"));
}
}
#[step]
pub fn set_environment_variable(runcmd: &mut Runcmd, variable: &str, value: &str) {
runcmd.env.insert(variable.into(), value.into());
}
#[step]
pub fn unset_environment_variable(runcmd: &mut Runcmd, variable: &str) {
let variable = OsStr::new(variable);
runcmd.env.remove(variable);
}