use std::{
borrow::Cow,
collections::HashMap,
env::{self, VarsOs},
ffi::{OsStr, OsString},
fmt,
fmt::Display,
io,
path::{Path, PathBuf},
};
use thiserror::Error;
pub use self::return_settings::*;
#[macro_use]
mod utils;
mod return_settings;
mod sys;
pub struct Command<Output, Error>
where
Output: 'static,
Error: From<io::Error> + From<UnexpectedExitStatus> + 'static,
{
program: OsString,
arguments: Vec<OsString>,
env_updates: HashMap<OsString, EnvChange>,
working_directory_override: Option<PathBuf>,
expected_exit_status: ExitStatus,
check_exit_status: bool,
inherit_env: bool,
return_settings: Option<Box<dyn OutputMapping<Output = Output, Error = Error>>>,
run_callback: Option<
Box<
dyn FnOnce(
Self,
&dyn OutputMapping<Output = Output, Error = Error>,
) -> Result<ExecResult, io::Error>,
>,
>,
}
impl<Output, Error> Command<Output, Error>
where
Output: 'static,
Error: From<io::Error> + From<UnexpectedExitStatus> + 'static,
{
pub fn new(
program: impl Into<OsString>,
return_settings: impl OutputMapping<Output = Output, Error = Error>,
) -> Self {
Command {
program: program.into(),
arguments: Vec::new(),
env_updates: HashMap::new(),
check_exit_status: true,
inherit_env: true,
expected_exit_status: ExitStatus::Code(0),
return_settings: Some(Box::new(return_settings) as _),
working_directory_override: None,
run_callback: Some(Box::new(sys::actual_exec_exec_replacement_callback)),
}
}
pub fn program(&self) -> &OsStr {
&*self.program
}
pub fn arguments(&self) -> &[OsString] {
&self.arguments
}
pub fn with_arguments<T>(mut self, args: impl IntoIterator<Item = T>) -> Self
where
T: Into<OsString>,
{
self.arguments.extend(args.into_iter().map(|v| v.into()));
self
}
pub fn with_argument(mut self, arg: impl Into<OsString>) -> Self {
self.arguments.push(arg.into());
self
}
pub fn env_updates(&self) -> &HashMap<OsString, EnvChange> {
&self.env_updates
}
pub fn with_env_updates<K, V>(mut self, map: impl IntoIterator<Item = (K, V)>) -> Self
where
K: Into<OsString>,
V: Into<EnvChange>,
{
self.env_updates
.extend(map.into_iter().map(|(k, v)| (k.into(), v.into())));
self
}
pub fn with_env_update(
mut self,
key: impl Into<OsString>,
value: impl Into<EnvChange>,
) -> Self {
self.env_updates.insert(key.into(), value.into());
self
}
pub fn inherit_env(&self) -> bool {
self.inherit_env
}
pub fn with_inherit_env(mut self, do_inherit: bool) -> Self {
self.inherit_env = do_inherit;
self
}
pub fn create_expected_env_iter(&self) -> impl Iterator<Item = (Cow<OsStr>, Cow<OsStr>)> {
let inherit = if self.inherit_env() {
Some(env::vars_os())
} else {
None
};
return ExpectedEnvIter {
self_: self,
inherit,
update: Some(self.env_updates.iter()),
};
struct ExpectedEnvIter<'a, Output, Error>
where
Output: 'static,
Error: From<io::Error> + From<UnexpectedExitStatus> + 'static,
{
self_: &'a Command<Output, Error>,
inherit: Option<VarsOs>,
update: Option<std::collections::hash_map::Iter<'a, OsString, EnvChange>>,
}
impl<'a, O, E> Iterator for ExpectedEnvIter<'a, O, E>
where
O: 'static,
E: From<io::Error> + From<UnexpectedExitStatus> + 'static,
{
type Item = (Cow<'a, OsStr>, Cow<'a, OsStr>);
fn next(&mut self) -> Option<Self::Item> {
loop {
fused_opt_iter_next!(&mut self.inherit, |(key, val)| {
match self.self_.env_updates.get(&key) {
Some(_) => continue,
None => return Some((Cow::Owned(key), Cow::Owned(val))),
}
});
fused_opt_iter_next!(&mut self.update, |(key, change)| {
match change {
EnvChange::Set(val) => {
return Some((Cow::Borrowed(&key), Cow::Borrowed(&val)));
}
EnvChange::Inherit => {
if let Some(val) = env::var_os(&key) {
return Some((Cow::Borrowed(&key), Cow::Owned(val)));
} else {
continue;
}
}
EnvChange::Remove => {
continue;
}
}
});
return None;
}
}
}
}
pub fn working_directory_override(&self) -> Option<&Path> {
self.working_directory_override.as_ref().map(|s| &**s)
}
pub fn with_working_directory_override(
mut self,
wd_override: Option<impl Into<PathBuf>>,
) -> Self {
self.working_directory_override = wd_override.map(Into::into);
self
}
pub fn expected_exit_status(&self) -> ExitStatus {
self.expected_exit_status
}
pub fn with_expected_exit_status(self, exit_status: impl Into<ExitStatus>) -> Self {
let mut cmd = self.with_check_exit_status(true);
cmd.expected_exit_status = exit_status.into();
cmd
}
pub fn check_exit_status(&self) -> bool {
self.check_exit_status
}
pub fn with_check_exit_status(mut self, val: bool) -> Self {
self.check_exit_status = val;
self
}
pub fn will_capture_stdout(&self) -> bool {
self.return_settings
.as_ref()
.expect("Can not be called in a exec_replacement_callback.")
.capture_stdout()
}
pub fn will_capture_stderr(&self) -> bool {
self.return_settings
.as_ref()
.expect("Can not be called in a exec_replacement_callback.")
.capture_stderr()
}
pub fn run(mut self) -> Result<Output, Error> {
let expected_exit_status = self.expected_exit_status;
let check_exit_status = self.check_exit_status;
let return_settings = self
.return_settings
.take()
.expect("run recursively called in exec replacing callback");
let run_callback = self
.run_callback
.take()
.expect("run recursively called in exec replacing callback");
let result = run_callback(self, &*return_settings)?;
if check_exit_status && result.exit_status != expected_exit_status {
return Err(UnexpectedExitStatus {
got: result.exit_status,
expected: expected_exit_status,
}
.into());
} else {
let stdout = if return_settings.capture_stdout() {
result.stdout
} else {
debug_assert!(result.stdout.is_none());
None
};
let stderr = if return_settings.capture_stderr() {
result.stderr
} else {
debug_assert!(result.stderr.is_none());
None
};
let exit_status = result.exit_status;
return_settings.map_output(stdout, stderr, exit_status)
}
}
pub fn with_exec_replacement_callback(
mut self,
callback: impl FnOnce(
Self,
&dyn OutputMapping<Output = Output, Error = Error>,
) -> Result<ExecResult, io::Error>
+ 'static,
) -> Self {
self.run_callback = Some(Box::new(callback));
self
}
}
pub trait OutputMapping: 'static {
type Output: 'static;
type Error: 'static;
fn capture_stdout(&self) -> bool;
fn capture_stderr(&self) -> bool;
fn map_output(
self: Box<Self>,
stdout: Option<Vec<u8>>,
stderr: Option<Vec<u8>>,
exit_status: ExitStatus,
) -> Result<Self::Output, Self::Error>;
}
#[derive(Debug, Error)]
#[error("Unexpected exit status. Got: {got}, Expected: {expected}")]
pub struct UnexpectedExitStatus {
got: ExitStatus,
expected: ExitStatus,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum ExitStatus {
Code(i64),
OsSpecific(OpaqueOsExitStatus),
}
impl ExitStatus {
#[cfg(any(window, unix))]
pub fn successful(&self) -> bool {
match self {
Self::Code(code) if *code == 0 => true,
_ => false,
}
}
}
impl Display for ExitStatus {
fn fmt(&self, fter: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Code(code) => write!(fter, "0x{:X}", code),
Self::OsSpecific(alt) => Display::fmt(alt, fter),
}
}
}
impl Default for ExitStatus {
fn default() -> Self {
Self::Code(0)
}
}
impl From<OpaqueOsExitStatus> for ExitStatus {
fn from(ooes: OpaqueOsExitStatus) -> Self {
ExitStatus::OsSpecific(ooes)
}
}
macro_rules! impl_from_and_partial_eq_for_fitting_int {
($($int:ty),*) => ($(
impl From<$int> for ExitStatus {
fn from(code: $int) -> Self {
Self::Code(code as _)
}
}
impl PartialEq<$int> for ExitStatus {
fn eq(&self, other: &$int) -> bool {
match self {
Self::Code(code) => *code == *other as i64,
Self::OsSpecific(_) => false,
}
}
}
)*);
}
impl_from_and_partial_eq_for_fitting_int!(u8, i8, u16, i16, u32, i32, i64);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct OpaqueOsExitStatus {
#[cfg(not(unix))]
_priv: (),
#[cfg(unix)]
signal: i32,
}
impl OpaqueOsExitStatus {
pub fn target_specific_default() -> Self {
Self {
#[cfg(not(unix))]
_priv: (),
#[cfg(unix)]
signal: 9,
}
}
#[cfg(unix)]
pub fn signal_number(&self) -> i32 {
self.signal
}
#[cfg(unix)]
pub fn from_signal_number(signal: i32) -> Self {
Self { signal }
}
}
impl Display for OpaqueOsExitStatus {
fn fmt(&self, fter: &mut fmt::Formatter) -> fmt::Result {
#[cfg(not(unix))]
{
fter.write_str("NO_EXIT_CODE")
}
#[cfg(unix)]
{
write!(fter, "signal({})", self.signal)
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EnvChange {
Remove,
Set(OsString),
Inherit,
}
impl From<&Self> for EnvChange {
fn from(borrow: &Self) -> Self {
borrow.clone()
}
}
impl From<&OsString> for EnvChange {
fn from(val: &OsString) -> Self {
EnvChange::Set(val.clone())
}
}
impl From<OsString> for EnvChange {
fn from(val: OsString) -> Self {
EnvChange::Set(val)
}
}
impl From<&OsStr> for EnvChange {
fn from(val: &OsStr) -> Self {
EnvChange::Set(val.into())
}
}
impl From<String> for EnvChange {
fn from(val: String) -> Self {
EnvChange::Set(val.into())
}
}
impl From<&str> for EnvChange {
fn from(val: &str) -> Self {
EnvChange::Set(val.into())
}
}
#[derive(Debug, Default)]
pub struct ExecResult {
pub exit_status: ExitStatus,
pub stdout: Option<Vec<u8>>,
pub stderr: Option<Vec<u8>>,
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use thiserror::Error;
#[derive(Debug, Error)]
enum TestCommandError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
UnexpectedExitStatus(#[from] UnexpectedExitStatus),
#[error("TestCase error: {0}")]
Prop(TestCaseError),
}
impl From<TestCaseError> for TestCommandError {
fn from(prop_err: TestCaseError) -> Self {
Self::Prop(prop_err)
}
}
impl TestCommandError {
pub fn unwrap_prop(self) -> TestCaseError {
match self {
Self::Io(err) => panic!("unexpected io error: {:?}", err),
Self::UnexpectedExitStatus(err) => panic!("unexpected exit status: {:?}", err),
Self::Prop(prop_err) => return prop_err,
}
}
}
struct TestOutputMapping {
capture_stdout: bool,
capture_stderr: bool,
}
impl OutputMapping for TestOutputMapping {
type Output = bool;
type Error = TestCommandError;
fn capture_stdout(&self) -> bool {
self.capture_stdout
}
fn capture_stderr(&self) -> bool {
self.capture_stderr
}
fn map_output(
self: Box<Self>,
stdout: Option<Vec<u8>>,
stderr: Option<Vec<u8>>,
_exit_status: super::ExitStatus,
) -> Result<Self::Output, Self::Error> {
(|| {
prop_assert_eq!(stdout.is_some(), self.capture_stdout());
prop_assert_eq!(stderr.is_some(), self.capture_stderr());
Ok(())
})()?;
Ok(true)
}
}
mod Command {
#![allow(non_snake_case)]
mod new {
use super::super::super::*;
use proptest::prelude::*;
#[test]
fn comp_can_be_created_using_str_string_os_str_or_os_string() {
Command::new("ls", ReturnNothing);
Command::new("ls".to_owned(), ReturnNothing);
Command::new(OsString::from("ls"), ReturnNothing);
Command::new(OsStr::new("ls"), ReturnNothing);
}
#[test]
fn comp_when_creating_command_different_capture_modes_can_be_used() {
Command::new("foo", ReturnNothing);
Command::new("foo", ReturnStdout);
Command::new("foo", ReturnStderr);
Command::new("foo", ReturnStdoutAndErr);
}
proptest! {
#[test]
fn the_used_program_can_be_queried(s in any::<OsString>()) {
let s = OsStr::new(&*s);
let cmd = Command::new(s, ReturnNothing);
prop_assert_eq!(&*cmd.program(), s)
}
}
}
mod arguments {
use super::super::super::*;
use proptest::prelude::*;
use std::{collections::HashSet, iter};
#[test]
fn default_arguments_are_empty() {
let cmd = Command::new("foo", ReturnNothing);
assert!(cmd.arguments().is_empty());
}
#[test]
fn comp_arguments_can_be_set_from_iterables() {
Command::new("foo", ReturnNothing).with_arguments(Vec::<OsString>::new());
Command::new("foo", ReturnNothing).with_arguments(HashSet::<OsString>::new());
Command::new("foo", ReturnNothing).with_arguments(&[] as &[OsString]);
}
proptest! {
#[test]
fn new_arguments_can_be_added(
cmd in any::<OsString>(),
argument in any::<OsString>(),
arguments in proptest::collection::vec(any::<OsString>(), 0..5),
arguments2 in proptest::collection::vec(any::<OsString>(), 0..5)
) {
let cmd = OsStr::new(&*cmd);
let cmd = Command::new(cmd, ReturnNothing)
.with_arguments(&arguments);
prop_assert_eq!(cmd.arguments(), &arguments);
let cmd = cmd.with_argument(&argument);
prop_assert_eq!(
cmd.arguments().iter().collect::<Vec<_>>(),
arguments.iter().chain(iter::once(&argument)).collect::<Vec<_>>()
);
let cmd = cmd.with_arguments(&arguments2);
prop_assert_eq!(
cmd.arguments().iter().collect::<Vec<_>>(),
arguments.iter()
.chain(iter::once(&argument))
.chain(arguments2.iter())
.collect::<Vec<_>>()
);
}
}
}
mod run {
use super::super::super::*;
#[test]
fn run_can_lead_to_and_io_error() {
let res = Command::new("foo", ReturnNothing)
.with_exec_replacement_callback(|_, _| {
Err(io::Error::new(io::ErrorKind::Other, "random"))
})
.run();
res.unwrap_err();
}
#[test]
fn return_no_error_if_the_command_has_zero_exit_status() {
let res = Command::new("foo", ReturnNothing)
.with_exec_replacement_callback(move |_, _| {
Ok(ExecResult {
exit_status: 0.into(),
..Default::default()
})
})
.run();
res.unwrap();
}
}
mod ReturnSetting {
use super::super::super::*;
use super::super::TestOutputMapping;
use proptest::prelude::*;
#[test]
fn comp_command_must_only_be_generic_over_the_output() {
if false {
let mut _cmd = Command::new("foo", ReturnNothing);
_cmd = Command::new("foo", ReturnNothingAlt);
}
struct ReturnNothingAlt;
impl OutputMapping for ReturnNothingAlt {
type Output = ();
type Error = CommandExecutionError;
fn capture_stdout(&self) -> bool {
false
}
fn capture_stderr(&self) -> bool {
false
}
fn map_output(
self: Box<Self>,
_stdout: Option<Vec<u8>>,
_stderr: Option<Vec<u8>>,
_exit_status: ExitStatus,
) -> Result<Self::Output, Self::Error> {
unimplemented!()
}
}
}
#[test]
fn allow_custom_errors() {
let _result: MyError = Command::new("foo", ReturnError)
.with_exec_replacement_callback(|_, _| {
Ok(ExecResult {
exit_status: 0.into(),
..Default::default()
})
})
.run()
.unwrap_err();
struct ReturnError;
impl OutputMapping for ReturnError {
type Output = ();
type Error = MyError;
fn capture_stdout(&self) -> bool {
false
}
fn capture_stderr(&self) -> bool {
false
}
fn map_output(
self: Box<Self>,
_stdout: Option<Vec<u8>>,
_stderr: Option<Vec<u8>>,
_exit_status: ExitStatus,
) -> Result<Self::Output, Self::Error> {
Err(MyError::BarFoot)
}
}
#[derive(Debug, Error)]
enum MyError {
#[error("FooBar")]
BarFoot,
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
UnexpectedExitStatus(#[from] UnexpectedExitStatus),
}
}
#[should_panic]
#[test]
fn returning_stdout_which_should_not_be_captured_triggers_a_debug_assertion() {
let _ = Command::new("foo", ReturnNothing)
.with_check_exit_status(false)
.with_exec_replacement_callback(|_, _| {
Ok(ExecResult {
exit_status: 1.into(),
stdout: Some(Vec::new()),
..Default::default()
})
})
.run();
}
#[should_panic]
#[test]
fn returning_stderr_which_should_not_be_captured_triggers_a_debug_assertion() {
let _ = Command::new("foo", ReturnNothing)
.with_check_exit_status(false)
.with_exec_replacement_callback(|_, _| {
Ok(ExecResult {
exit_status: 1.into(),
stderr: Some(Vec::new()),
..Default::default()
})
})
.run();
}
proptest! {
#[test]
fn only_pass_stdout_stderr_to_map_output_if_return_settings_indicate_they_capture_it(
capture_stdout in proptest::bool::ANY,
capture_stderr in proptest::bool::ANY
) {
let res = Command::new("foo", TestOutputMapping { capture_stdout, capture_stderr })
.with_exec_replacement_callback(move |_,_| {
Ok(ExecResult {
exit_status: 0.into(),
stdout: if capture_stdout { Some(Vec::new()) } else { None },
stderr: if capture_stderr { Some(Vec::new()) } else { None }
})
})
.run()
.map_err(|e| e.unwrap_prop())?;
assert!(res);
}
#[test]
fn command_provides_a_getter_to_check_if_stdout_and_err_will_be_captured(
capture_stdout in proptest::bool::ANY,
capture_stderr in proptest::bool::ANY
) {
let cmd = Command::new("foo", TestOutputMapping { capture_stdout, capture_stderr });
prop_assert_eq!(cmd.will_capture_stdout(), capture_stdout);
prop_assert_eq!(cmd.will_capture_stderr(), capture_stderr);
}
#[test]
fn capture_hints_are_available_in_the_callback(
capture_stdout in proptest::bool::ANY,
capture_stderr in proptest::bool::ANY
) {
Command::new("foo", TestOutputMapping { capture_stdout, capture_stderr })
.with_exec_replacement_callback(move |_cmd, return_settings| {
assert_eq!(return_settings.capture_stdout(), capture_stdout);
assert_eq!(return_settings.capture_stderr(), capture_stderr);
Ok(ExecResult {
exit_status: 0.into(),
stdout: if capture_stdout { Some(Vec::new()) } else { None },
stderr: if capture_stderr { Some(Vec::new()) } else { None }
})
})
.run()
.unwrap();
}
}
}
mod environment {
use super::super::super::*;
use proptest::prelude::*;
#[test]
fn by_default_no_environment_updates_are_done() {
let cmd = Command::new("foo", ReturnNothing);
assert!(cmd.env_updates().is_empty());
}
#[test]
fn create_expected_env_iter_includes_the_current_env_by_default() {
let process_env = env::vars_os()
.into_iter()
.map(|(k, v)| (Cow::Owned(k), Cow::Owned(v)))
.collect::<HashMap<_, _>>();
let cmd = Command::new("foo", ReturnNothing);
let created_map = cmd.create_expected_env_iter().collect::<HashMap<_, _>>();
assert_eq!(process_env, created_map);
}
#[test]
fn by_default_env_is_inherited() {
let cmd = Command::new("foo", ReturnNothing);
assert_eq!(cmd.inherit_env(), true);
assert_ne!(cmd.create_expected_env_iter().count(), 0);
}
#[test]
fn inheritance_of_env_variables_can_be_disabled() {
let cmd = Command::new("foo", ReturnNothing).with_inherit_env(false);
assert_eq!(cmd.inherit_env(), false);
assert_eq!(cmd.create_expected_env_iter().count(), 0);
}
proptest! {
#[test]
fn new_env_variables_can_be_added(
cmd in any::<OsString>(),
variable in any::<OsString>(),
value in any::<OsString>(),
map1 in proptest::collection::hash_map(
any::<OsString>(),
any::<OsString>().prop_map(|s| EnvChange::Set(s)),
0..4
),
map2 in proptest::collection::hash_map(
any::<OsString>(),
any::<OsString>().prop_map(|s| EnvChange::Set(s)),
0..4
),
) {
let cmd = Command::new(cmd, ReturnNothing)
.with_env_updates(&map1);
prop_assert_eq!(cmd.env_updates(), &map1);
let cmd = cmd.with_env_update(&variable, &value);
let mut n_map = map1.clone();
n_map.insert(variable, EnvChange::Set(value));
prop_assert_eq!(cmd.env_updates(), &n_map);
let cmd = cmd.with_env_updates(&map2);
for (key, value) in &map2 {
n_map.insert(key.into(), value.into());
}
prop_assert_eq!(cmd.env_updates(), &n_map);
}
#[test]
fn env_variables_can_be_set_to_be_removed_from_inherited_env(
cmd in any::<OsString>(),
rem_key in proptest::sample::select(env::vars_os().map(|(k,_v)| k).collect::<Vec<_>>())
) {
let cmd = Command::new(cmd, ReturnNothing).with_env_update(rem_key.clone(), EnvChange::Remove);
prop_assert_eq!(cmd.env_updates().get(&rem_key), Some(&EnvChange::Remove));
let produced_env = cmd.create_expected_env_iter()
.map(|(k,v)| (k.into_owned(), v.into_owned()))
.collect::<HashMap<OsString, OsString>>();
prop_assert_eq!(produced_env.get(&rem_key), None);
}
#[test]
fn env_variables_can_be_set_to_be_replaced_from_inherited_env(
cmd in any::<OsString>(),
rem_key in proptest::sample::select(env::vars_os().map(|(k,_v)| k).collect::<Vec<_>>()),
replacement in any::<OsString>()
) {
let cmd = Command::new(cmd, ReturnNothing).with_env_update(rem_key.clone(), EnvChange::Set(replacement.clone()));
let expect = EnvChange::Set(replacement.clone());
prop_assert_eq!(cmd.env_updates().get(&rem_key), Some(&expect));
let produced_env = cmd.create_expected_env_iter()
.map(|(k,v)| (k.into_owned(), v.into_owned()))
.collect::<HashMap<OsString, OsString>>();
prop_assert_eq!(produced_env.get(&rem_key), Some(&replacement));
}
#[test]
fn env_variables_can_be_set_to_inherit_even_if_inheritance_is_disabled(
cmd in any::<OsString>(),
inherit in proptest::sample::select(env::vars_os().map(|(k,_v)| k).collect::<Vec<_>>()),
) {
let expected_val = env::var_os(&inherit);
let cmd = Command::new(cmd, ReturnNothing)
.with_inherit_env(false)
.with_env_update(&inherit, EnvChange::Inherit);
assert_eq!(cmd.create_expected_env_iter().count(), 1);
let got_value = cmd.create_expected_env_iter().find(|(k,_v)| &*k==&*inherit)
.map(|(_k,v)| v);
assert_eq!(
expected_val.as_ref().map(|v|&**v),
got_value.as_ref().map(|v|&**v)
);
}
#[test]
fn env_variables_can_be_set_to_inherit_even_if_inheritance_is_disabled_2(
cmd in any::<OsString>(),
inherit in proptest::sample::select(env::vars_os().map(|(k,_v)| k).collect::<Vec<_>>()),
) {
let expected_val = env::var_os(&inherit);
let cmd = Command::new(cmd, ReturnNothing)
.with_env_update(&inherit, EnvChange::Inherit)
.with_inherit_env(false);
assert_eq!(cmd.create_expected_env_iter().count(), 1);
let got_value = cmd.create_expected_env_iter().find(|(k,_v)| &*k==&*inherit)
.map(|(_k,v)| v);
assert_eq!(
expected_val.as_ref().map(|v|&**v),
got_value.as_ref().map(|v|&**v)
);
}
#[test]
fn setting_inherit_does_not_affect_anything_if_we_anyway_inherit_all(
cmd in any::<OsString>(),
pointless_inherit in proptest::sample::select(env::vars_os().map(|(k,_v)| k).collect::<Vec<_>>()),
) {
const NON_EXISTING_VAR_KEY: &'static str = "____MAPPED_COMMAND__THIS_SHOULD_NOT_EXIST_AS_ENV_VARIABLE____";
assert_eq!(env::var_os(NON_EXISTING_VAR_KEY), None);
let expected_values = env::vars_os()
.map(|(k,v)| (Cow::Owned(k), Cow::Owned(v)))
.collect::<HashMap<_,_>>();
let cmd = Command::new(cmd, ReturnNothing)
.with_env_update(&pointless_inherit, EnvChange::Inherit)
.with_env_update(NON_EXISTING_VAR_KEY, EnvChange::Inherit);
let values = cmd.create_expected_env_iter().collect::<HashMap<_,_>>();
assert!(!values.contains_key(OsStr::new(NON_EXISTING_VAR_KEY)));
assert_eq!(expected_values.len(), values.len());
assert_eq!(
expected_values.get(&pointless_inherit),
values.get(&*pointless_inherit)
);
}
}
}
mod working_directory {
use super::super::super::*;
use crate::utils::opt_arbitrary_path_buf;
use proptest::prelude::*;
#[test]
fn by_default_no_explicit_working_directory_is_set() {
let cmd = Command::new("foo", ReturnNothing);
assert_eq!(cmd.working_directory_override(), None);
}
proptest! {
#[test]
fn the_working_directory_can_be_changed(
cmd in any::<OsString>(),
wd_override in opt_arbitrary_path_buf(),
wd_override2 in opt_arbitrary_path_buf()
) {
let cmd = Command::new(cmd, ReturnNothing)
.with_working_directory_override(wd_override.as_ref());
assert_eq!(cmd.working_directory_override(), wd_override.as_ref().map(|i|&**i));
let cmd = cmd.with_working_directory_override(wd_override2.as_ref());
assert_eq!(cmd.working_directory_override(), wd_override2.as_ref().map(|i|&**i));
}
}
}
mod exit_status_checking {
use super::super::super::*;
use proptest::prelude::*;
#[test]
fn by_default_the_expected_exit_status_is_0() {
let cmd = Command::new("foo", ReturnNothing);
assert_eq!(cmd.expected_exit_status(), 0);
}
#[test]
fn by_default_exit_status_checking_is_enabled() {
let cmd = Command::new("foo", ReturnNothing);
assert_eq!(cmd.check_exit_status(), true);
}
#[test]
fn setting_check_exit_status_to_false_disables_it() {
Command::new("foo", ReturnNothing)
.with_check_exit_status(false)
.with_exec_replacement_callback(|_, _| {
Ok(ExecResult {
exit_status: 1.into(),
..Default::default()
})
})
.run()
.unwrap();
}
#[test]
fn you_can_expect_no_exit_status_to_be_returned() {
let cmd = Command::new("foo", ReturnNothing).with_expected_exit_status(
ExitStatus::OsSpecific(OpaqueOsExitStatus::target_specific_default()),
);
assert_eq!(
cmd.expected_exit_status(),
ExitStatus::OsSpecific(OpaqueOsExitStatus::target_specific_default())
);
}
#[test]
fn setting_the_expected_exit_status_will_enable_checking() {
let cmd = Command::new("foo", ReturnNothing)
.with_check_exit_status(false)
.with_expected_exit_status(0);
assert_eq!(cmd.check_exit_status(), true);
}
proptest! {
#[test]
fn return_an_error_if_the_command_has_non_zero_exit_status(
cmd in any::<OsString>(),
exit_status in prop_oneof!(..0, 1..).prop_map(ExitStatus::from)
) {
let res = Command::new(cmd, ReturnNothing)
.with_exec_replacement_callback(move |_,_| {
Ok(ExecResult {
exit_status,
..Default::default()
})
})
.run();
res.unwrap_err();
}
#[test]
fn replacing_the_expected_exit_status_causes_error_on_different_exit_status(
exit_status in -5..6,
offset in prop_oneof!(-100..0, 1..101)
) {
let res = Command::new("foo", ReturnNothing)
.with_expected_exit_status(exit_status)
.with_exec_replacement_callback(move |cmd,_| {
assert_eq!(cmd.expected_exit_status(), exit_status);
Ok(ExecResult {
exit_status: ExitStatus::from(exit_status + offset),
..Default::default()
})
})
.run();
match res {
Err(CommandExecutionError::UnexpectedExitStatus(UnexpectedExitStatus {got, expected})) => {
assert_eq!(expected, exit_status);
assert_eq!(got, exit_status+offset);
},
_ => panic!("Unexpected Result: {:?}", res)
}
}
#[test]
fn exit_status_checking_can_be_disabled_and_enabled(
change1 in proptest::bool::ANY,
change2 in proptest::bool::ANY,
) {
let cmd = Command::new("foo", ReturnNothing)
.with_check_exit_status(change1);
assert_eq!(cmd.check_exit_status(), change1);
let cmd = cmd.with_check_exit_status(change2);
assert_eq!(cmd.check_exit_status(), change2);
}
}
}
mod exec_replacement_callback {
use std::{cell::RefCell, rc::Rc};
use super::super::super::*;
#[test]
fn program_execution_can_be_replaced_with_an_callback() {
let was_run = Rc::new(RefCell::new(false));
let was_run_ = was_run.clone();
let cmd = Command::new("some_cmd", ReturnStdoutAndErr)
.with_exec_replacement_callback(move |for_cmd, _| {
*(*was_run_).borrow_mut() = true;
assert_eq!(&*for_cmd.program(), "some_cmd");
Ok(ExecResult {
exit_status: 0.into(),
stdout: Some("result=12".to_owned().into()),
stderr: Some(Vec::new()),
})
});
let res = cmd.run().unwrap();
assert_eq!(*was_run.borrow_mut(), true);
assert_eq!(&*res.stdout, "result=12".as_bytes());
assert_eq!(&*res.stderr, "".as_bytes());
}
}
}
mod ExitStatus {
#![allow(non_snake_case)]
mod display_fmt {
use crate::{ExitStatus, OpaqueOsExitStatus};
#[test]
fn format_exit_status_as_hex() {
let exit_status = ExitStatus::from(0x7Fi32);
assert_eq!(&format!("{}", exit_status), "0x7F");
}
#[test]
fn format_negative_exit_status_as_hex() {
let exit_status = ExitStatus::Code(-1i32 as u32 as _);
assert_eq!(&format!("{}", exit_status), "0xFFFFFFFF");
}
#[test]
#[cfg(unix)]
fn display_for_non_exit_code_on_unix() {
let signal = OpaqueOsExitStatus::from_signal_number(9);
assert_eq!(&format!("{}", signal), "signal(9)");
}
}
mod new {
use crate::ExitStatus;
#[test]
fn can_be_create_from_many_numbers() {
let status = ExitStatus::from(12u8);
assert_eq!(status, ExitStatus::Code(12));
let status = ExitStatus::from(-12i8);
assert_eq!(status, ExitStatus::Code(-12));
let status = ExitStatus::from(12u16);
assert_eq!(status, ExitStatus::Code(12));
let status = ExitStatus::from(-12i16);
assert_eq!(status, ExitStatus::Code(-12));
let status = ExitStatus::from(u32::MAX);
assert_eq!(status, ExitStatus::Code(u32::MAX as i64));
let status = ExitStatus::from(-1i32);
assert_eq!(status, ExitStatus::Code(-1));
let status = ExitStatus::from(-13i64);
assert_eq!(status, ExitStatus::Code(-13));
}
#[test]
fn can_compare_to_many_numbers() {
let status = ExitStatus::from(12u8);
assert_eq!(status, 12u8);
let status = ExitStatus::from(-12i8);
assert_eq!(status, -12i8);
let status = ExitStatus::from(12u16);
assert_eq!(status, 12u16);
let status = ExitStatus::from(-12i16);
assert_eq!(status, -12i16);
let status = ExitStatus::from(u32::MAX);
assert_eq!(status, u32::MAX);
let status = ExitStatus::from(-1i32);
assert_eq!(status, -1i32);
let status = ExitStatus::from(-13i64);
assert_eq!(status, -13i64);
}
}
}
#[cfg(unix)]
mod signal_number {
use proptest::prelude::*;
use crate::OpaqueOsExitStatus;
proptest! {
#[test]
fn from_to_signal_number(
nr in any::<i32>()
) {
let exit_status = OpaqueOsExitStatus::from_signal_number(nr);
assert_eq!(exit_status.signal_number(), nr);
}
}
}
}