mapped_command/lib.rs
1//! A `std::process::Command` replacement which is a bit more flexible and testable.
2//!
3//! For now this is focused on cases which wait until the subprocess is completed
4//! and then map the output (or do not care about the output).
5//!
6//! - by default check the exit status
7//!
8//! - bundle a mapping of the captured stdout/stderr to an result into the command,
9//! i.e. the `Command` type is `Command<Output, Error>` e.g. `Command<Vec<String>, Error>`.
10//!
11//! - implicitly define if stdout/stderr needs to be captured to prevent mistakes
12//! wrt. this, this is done through through the same mechanism which is used to
13//! define how the output is mapped, e.g. `Command::new("ls", ReturnStdoutString)`
14//! will implicitly enabled stdout capturing and disable `stderr` capturing.
15//!
16//! - allow replacing command execution with an callback, this is mainly used to
17//! allow mocking the command.
18//!
19//! - besides allowing to decide weather the sub-process should inherit the environment and
20//! which variables get removed/set/overwritten this type also allows you to whitelist which
21//! env variables should be inherited.
22//!
23//! - do not have `&mut self` pass through based API. This makes it more bothersome to create
24//! functions which create and return commands, which this types intents to make simple so
25//! that you can e.g. have a function like `fn ls_command() -> Command<Vec<String>, Error>`
26//! which returns a command which if run runs the ls command and returns a vector of string
27//! (or an error if spawning, running or utf8 validation fails).
28//!
29//! - be generic over Output and Error type but dynamic over how the captured stdout/err is
30//! mapped to the given `Result<Output, Error>`. This allows you to e.g. at runtime switch
31//! between different function which create a command with the same output but on different
32//! ways (i.e. with different called programs and output mapping, e.g. based on a config
33//! setting).
34//!
35//! # Basic Examples
36//!
37//! ```rust
38//! use mapped_command::{Command, MapStdoutString, ReturnStdoutString, ExecResult, CommandExecutionWithStringOutputError as Error};
39//!
40//! /// Usage: `echo().run()`.
41//! fn echo() -> Command<String, Error> {
42//! // implicitly enables stdout capturing but not stderr capturing
43//! // and converts the captured bytes to string
44//! Command::new("echo", ReturnStdoutString)
45//! }
46//!
47//! /// Usage: `ls_command().run()`.
48//! fn ls_command() -> Command<Vec<String>, Error> {
49//! Command::new("ls", MapStdoutString(|out| {
50//! let lines = out.lines().map(Into::into).collect::<Vec<_>>();
51//! Ok(lines)
52//! }))
53//! }
54//!
55//! fn main() {
56//! let res = ls_command()
57//! //mock
58//! .with_exec_replacement_callback(|_cmd, _rs| {
59//! Ok(ExecResult {
60//! exit_status: 0.into(),
61//! // Some indicates in the mock that stdout was captured, None would mean it was not.
62//! stdout: Some("foo\nbar\ndoor\n".to_owned().into()),
63//! ..Default::default()
64//! })
65//! })
66//! // run, check exit status and map captured outputs
67//! .run()
68//! .unwrap();
69//!
70//! assert_eq!(res, vec!["foo", "bar", "door"]);
71//!
72//! let err = ls_command()
73//! //mock
74//! .with_exec_replacement_callback(|_cmd, _rs| {
75//! Ok(ExecResult {
76//! exit_status: 1.into(),
77//! stdout: Some("foo\nbar\ndoor\n".to_owned().into()),
78//! ..Default::default()
79//! })
80//! })
81//! .run()
82//! .unwrap_err();
83//!
84//! assert_eq!(err.to_string(), "Unexpected exit status. Got: 0x1, Expected: 0x0");
85//! }
86//! ```
87//!
88//! # Handling arguments and environment variables
89//!
90//! ```rust
91//! use mapped_command::{Command,ReturnStdoutString, EnvChange};
92//! # #[cfg(unix)]
93//! # fn main() {
94//! std::env::set_var("FOOBAR", "the foo");
95//! std::env::set_var("DODO", "no no");
96//! let echoed = Command::new("bash", ReturnStdoutString)
97//! .with_arguments(&["-c", "echo $0 ${DODO:-yo} $FOOBAR $BARFOOT $(pwd)", "arg1"])
98//! .with_inherit_env(false)
99//! .with_env_update("BARFOOT", "the bar")
100//! //inherit this even if env inheritance is disabled (it is see above)
101//! .with_env_update("FOOBAR", EnvChange::Inherit)
102//! .with_working_directory_override(Some("/usr"))
103//! .run()
104//! .unwrap();
105//!
106//! assert_eq!(echoed, "arg1 yo the foo the bar /usr\n");
107//! # }
108//! ```
109//!
110use std::{
111 borrow::Cow,
112 collections::HashMap,
113 env::{self, VarsOs},
114 ffi::{OsStr, OsString},
115 fmt,
116 fmt::Display,
117 io,
118 path::{Path, PathBuf},
119};
120use thiserror::Error;
121
122pub use self::return_settings::*;
123
124#[macro_use]
125mod utils;
126mod return_settings;
127mod sys;
128
129/// A alternative to `std::process::Command` see module level documentation.
130pub struct Command<Output, Error>
131where
132 Output: 'static,
133 Error: From<io::Error> + From<UnexpectedExitStatus> + 'static,
134{
135 program: OsString,
136 arguments: Vec<OsString>,
137 env_updates: HashMap<OsString, EnvChange>,
138 working_directory_override: Option<PathBuf>,
139 expected_exit_status: ExitStatus,
140 check_exit_status: bool,
141 inherit_env: bool,
142 return_settings: Option<Box<dyn OutputMapping<Output = Output, Error = Error>>>,
143 run_callback: Option<
144 Box<
145 dyn FnOnce(
146 Self,
147 &dyn OutputMapping<Output = Output, Error = Error>,
148 ) -> Result<ExecResult, io::Error>,
149 >,
150 >,
151}
152
153impl<Output, Error> Command<Output, Error>
154where
155 Output: 'static,
156 Error: From<io::Error> + From<UnexpectedExitStatus> + 'static,
157{
158 /// Create a new command for given program and output mapping.
159 ///
160 /// The output mapping will imply if stdout/stderr is captured and how the
161 /// captured output is mapped to a `Result<Self::Output, Self::Error>`.
162 ///
163 pub fn new(
164 program: impl Into<OsString>,
165 return_settings: impl OutputMapping<Output = Output, Error = Error>,
166 ) -> Self {
167 Command {
168 program: program.into(),
169 arguments: Vec::new(),
170 env_updates: HashMap::new(),
171 check_exit_status: true,
172 inherit_env: true,
173 expected_exit_status: ExitStatus::Code(0),
174 return_settings: Some(Box::new(return_settings) as _),
175 working_directory_override: None,
176 run_callback: Some(Box::new(sys::actual_exec_exec_replacement_callback)),
177 }
178 }
179
180 /// Return the program the command will run.
181 pub fn program(&self) -> &OsStr {
182 &*self.program
183 }
184
185 /// Returns the arguments passed the the program when run.
186 pub fn arguments(&self) -> &[OsString] {
187 &self.arguments
188 }
189
190 /// Returns this command with new arguments added to the end of the argument list
191 pub fn with_arguments<T>(mut self, args: impl IntoIterator<Item = T>) -> Self
192 where
193 T: Into<OsString>,
194 {
195 self.arguments.extend(args.into_iter().map(|v| v.into()));
196 self
197 }
198
199 /// Returns this command with a new argument added to the end of the argument list
200 pub fn with_argument(mut self, arg: impl Into<OsString>) -> Self {
201 self.arguments.push(arg.into());
202 self
203 }
204
205 /// Return a map of all env variables which will be set/overwritten in the subprocess.
206 ///
207 /// # Warning
208 ///
209 /// The keys of env variables have not *not* been evaluated for syntactic validity.
210 /// So the given keys can cause process spawning or calls to [`std::env::set_var`] to
211 /// fail.
212 pub fn env_updates(&self) -> &HashMap<OsString, EnvChange> {
213 &self.env_updates
214 }
215
216 /// Returns this command with the map of env updates updated by given iterator of key value pairs.
217 ///
218 /// If any key from the new map already did exist in the current updates it will
219 /// replace the old key & value.
220 ///
221 /// - Common supported values for keys include `OsString`, `&OsStr`, `String`, `&str`.
222 /// - Common supported values for values include `EnvChange`, `OsString`, `&OsStr`, `String`,
223 /// `&str`
224 ///
225 /// So you can pass in containers like `Vec<(&str, &str)>`, `HashMap<&str, &str>` or
226 /// `HashMap<OsString, EnvChange>`, etc.
227 ///
228 /// # Warning
229 ///
230 /// The keys of env variables will *not* be evaluated for syntactic validity.
231 /// Setting a key invalid on given platform *might* cause the process spawning to
232 /// fail (e.g. using a key lik `"="` or `""`). It also *might* also do other thinks
233 /// like the env variable being passed in but being unaccessible or similar. It's completely
234 /// dependent on the OS and the impl. of `std::process::Command` or whatever is used to
235 /// execute the command.
236 pub fn with_env_updates<K, V>(mut self, map: impl IntoIterator<Item = (K, V)>) -> Self
237 where
238 K: Into<OsString>,
239 V: Into<EnvChange>,
240 {
241 self.env_updates
242 .extend(map.into_iter().map(|(k, v)| (k.into(), v.into())));
243 self
244 }
245
246 /// Returns this command with the map of env updates updated by one key value pair.
247 ///
248 /// If the new key already did exist in the current updates it will replace that
249 /// old key & value.
250 ///
251 /// See [`Command::with_env_updates()`].
252 pub fn with_env_update(
253 mut self,
254 key: impl Into<OsString>,
255 value: impl Into<EnvChange>,
256 ) -> Self {
257 self.env_updates.insert(key.into(), value.into());
258 self
259 }
260
261 /// Returns true if the env of the current process is inherited.
262 ///
263 /// Updates to then environment are applied after the inheritance:
264 ///
265 /// - [`EnvChange::Set`] can be use to override inherited env vars, or
266 /// add new ones if no variable with given key was inherited
267 /// - [`EnvChange::Remove`] can be used to remove an inherited (or previously added)
268 /// env variable
269 /// - [`EnvChange::Inherit`] can be used to state a env variable should be inherited
270 /// even if `inherit_env` is `false`. If `inherit_env` is true this will have no
271 /// effect.
272 pub fn inherit_env(&self) -> bool {
273 self.inherit_env
274 }
275
276 /// Returns this command with a change to weather or the sub-process will inherit env variables.
277 ///
278 /// See [`Command::inherit_env()`] for how this affects the sub-process env.
279 pub fn with_inherit_env(mut self, do_inherit: bool) -> Self {
280 self.inherit_env = do_inherit;
281 self
282 }
283
284 /// Returns a map with all env variables the sub-process spawned by this command would have
285 /// if the current processes env is not changed.
286 ///
287 /// # Site note about `env::set_var()` problems
288 ///
289 /// Note that if you use `std::env::set_var()` in a multi-threaded setup depending on
290 /// the operating system you run this on this can lead to all kind of problem, including
291 /// unexpected race conditions in some situations (especially if `inherit_env(true)` is
292 /// combined with `EnvChange::Inherit` and multiple variables are changed in another thread
293 /// racing with this function and some but not all are covered by `EnvChange::Inherit`).
294 ///
295 /// Given that [`std::env::set_var()`] should strictly be avoided in a multi-threaded context
296 /// this is seen as an acceptable drawback.
297 ///
298 /// Note that this function + `std::env::set_var()` is not unsafe it might just have a
299 /// very unexpected result. Except if `env::set_var()` + reading env races are inherently
300 /// unsafe on your system, in which case this has nothing to do with this function.
301 pub fn create_expected_env_iter(&self) -> impl Iterator<Item = (Cow<OsStr>, Cow<OsStr>)> {
302 let inherit = if self.inherit_env() {
303 Some(env::vars_os())
304 } else {
305 None
306 };
307
308 return ExpectedEnvIter {
309 self_: self,
310 inherit,
311 update: Some(self.env_updates.iter()),
312 };
313
314 //FIXME[rust/generators] use yield base iterator
315 struct ExpectedEnvIter<'a, Output, Error>
316 where
317 Output: 'static,
318 Error: From<io::Error> + From<UnexpectedExitStatus> + 'static,
319 {
320 self_: &'a Command<Output, Error>,
321 inherit: Option<VarsOs>,
322 update: Option<std::collections::hash_map::Iter<'a, OsString, EnvChange>>,
323 }
324
325 impl<'a, O, E> Iterator for ExpectedEnvIter<'a, O, E>
326 where
327 O: 'static,
328 E: From<io::Error> + From<UnexpectedExitStatus> + 'static,
329 {
330 type Item = (Cow<'a, OsStr>, Cow<'a, OsStr>);
331
332 fn next(&mut self) -> Option<Self::Item> {
333 loop {
334 fused_opt_iter_next!(&mut self.inherit, |(key, val)| {
335 match self.self_.env_updates.get(&key) {
336 Some(_) => continue,
337 None => return Some((Cow::Owned(key), Cow::Owned(val))),
338 }
339 });
340 fused_opt_iter_next!(&mut self.update, |(key, change)| {
341 match change {
342 EnvChange::Set(val) => {
343 return Some((Cow::Borrowed(&key), Cow::Borrowed(&val)));
344 }
345 EnvChange::Inherit => {
346 // Mostly used if inherit_var is valse in which case we *should* not
347 // have done aboves loop-part on vars_os. We could "optimize" this to
348 // handle Inherit in aboves loop if we run that loop, but why add that
349 // complexity?
350 if let Some(val) = env::var_os(&key) {
351 return Some((Cow::Borrowed(&key), Cow::Owned(val)));
352 } else {
353 continue;
354 }
355 }
356 EnvChange::Remove => {
357 continue;
358 }
359 }
360 });
361 return None;
362 }
363 }
364 }
365 }
366
367 /// Return the working directory which will be used instead of the current working directory.
368 ///
369 /// If `None` is returned it means no override is set and the working directory will be inherited
370 /// from the spawning process.
371 pub fn working_directory_override(&self) -> Option<&Path> {
372 self.working_directory_override.as_ref().map(|s| &**s)
373 }
374
375 /// Replaces the working directory override.
376 ///
377 /// Setting it to `None` will unset the override making the spawned
378 /// process inherit the working directory from the spawning process.
379 pub fn with_working_directory_override(
380 mut self,
381 wd_override: Option<impl Into<PathBuf>>,
382 ) -> Self {
383 self.working_directory_override = wd_override.map(Into::into);
384 self
385 }
386
387 /// Return which exit status is treated as success.
388 pub fn expected_exit_status(&self) -> ExitStatus {
389 self.expected_exit_status
390 }
391
392 /// Set which exit status is treated as successful.
393 ///
394 /// **This enables exit status checking even if it
395 /// was turned of before.**
396 pub fn with_expected_exit_status(self, exit_status: impl Into<ExitStatus>) -> Self {
397 let mut cmd = self.with_check_exit_status(true);
398 cmd.expected_exit_status = exit_status.into();
399 cmd
400 }
401
402 /// Returns true if the exit status is checked before mapping the output(s).
403 pub fn check_exit_status(&self) -> bool {
404 self.check_exit_status
405 }
406
407 /// Sets if the exit status is checked before mapping the output(s).
408 pub fn with_check_exit_status(mut self, val: bool) -> Self {
409 self.check_exit_status = val;
410 self
411 }
412
413 /// Returns true if stdout will be captured.
414 ///
415 /// # Panics
416 ///
417 /// **If called in a `exec_replacement_callback` this will panic.**
418 pub fn will_capture_stdout(&self) -> bool {
419 self.return_settings
420 .as_ref()
421 .expect("Can not be called in a exec_replacement_callback.")
422 .capture_stdout()
423 }
424
425 /// Returns true if stderr will be captured.
426 ///
427 /// # Panics
428 ///
429 /// **If called in a `exec_replacement_callback` this will panic.**
430 pub fn will_capture_stderr(&self) -> bool {
431 self.return_settings
432 .as_ref()
433 .expect("Can not be called in a exec_replacement_callback.")
434 .capture_stderr()
435 }
436
437 /// Run the command, blocking until completion and then mapping the output.
438 ///
439 /// This will:
440 ///
441 /// 1. run the program with the specified arguments and env variables
442 /// 2. capture the necessary outputs as specified by the output mapping
443 /// 3. if exit status checking was not disabled check the exit status and potentially
444 /// fail.
445 /// 4. if 3 doesn't fail now map captured outputs to a `Result<Output, Error>`
446 ///
447 /// If [`Command::with_exec_replacement_callback()`] is used instead of running the
448 /// program and capturing the output the given callback is called. The callback
449 /// could mock the program execution. The exit status checking and output mapping
450 /// are still done as normal.
451 ///
452 /// # Panics
453 ///
454 /// **This will panic if called in a `exec_replacement_callback`.**
455 pub fn run(mut self) -> Result<Output, Error> {
456 let expected_exit_status = self.expected_exit_status;
457 let check_exit_status = self.check_exit_status;
458 let return_settings = self
459 .return_settings
460 .take()
461 .expect("run recursively called in exec replacing callback");
462 let run_callback = self
463 .run_callback
464 .take()
465 .expect("run recursively called in exec replacing callback");
466
467 let result = run_callback(self, &*return_settings)?;
468
469 if check_exit_status && result.exit_status != expected_exit_status {
470 return Err(UnexpectedExitStatus {
471 got: result.exit_status,
472 expected: expected_exit_status,
473 }
474 .into());
475 } else {
476 let stdout = if return_settings.capture_stdout() {
477 result.stdout
478 } else {
479 debug_assert!(result.stdout.is_none());
480 None
481 };
482 let stderr = if return_settings.capture_stderr() {
483 result.stderr
484 } else {
485 debug_assert!(result.stderr.is_none());
486 None
487 };
488 let exit_status = result.exit_status;
489 return_settings.map_output(stdout, stderr, exit_status)
490 }
491 }
492
493 /// Sets a callback which is called instead of executing the command when running the command.
494 ///
495 /// This is mainly meant to be used for mocking command execution during testing, but can be used for
496 /// other thinks, too. E.g. the current implementation does have a default callback for normally executing
497 /// the command this method was not called.
498 ///
499 ///
500 /// # Implementing Mocks with an exec_replacement_callback
501 ///
502 /// You must not call following methods in the callback:
503 ///
504 /// - [`Command::run()`], recursively calling run will not work.
505 /// - [`Command::will_capture_stdout()`], use the passed in output mapping [`OutputMapping::capture_stdout()`] method instead.
506 /// - [`Command::will_capture_stderr()`], use the passed in output mapping [`OutputMapping::capture_stderr()`] method instead.
507 ///
508 /// An emulation of captured output and exit status is returned as [`ExecResult`] instance:
509 ///
510 /// - Any exit code can be returned including a target specific one,
511 /// the `From<num> for ExitStatus` implementations are useful here.
512 /// - If [`OutputMapping::capture_stdout()`] is `true` then [`ExecResult::stdout`] must be `Some`
513 /// else it must be `None`. Failing to do so will panic on unwrap of debug assertions.
514 /// - If [`OutputMapping::capture_stdout()`] is `true` then [`ExecResult::stdout`] must be `Some`
515 /// else it must be `None`. Failing to do so will panic on unwrap of debug assertions.
516 ///
517 /// If used for mocking in tests you already know if stdout/stderr is assumed to (not) be
518 /// captured so you do not need to access [`OutputMapping::capture_stdout()`]/[`OutputMapping::capture_stdout()`].
519 ///
520 /// Settings like env updates and inheritance can be retrieved from the passed in `Command` instance.
521 ///
522 /// # Implement custom subprocess spawning
523 ///
524 /// *Be aware that if you execute the program in the callback you need to make sure the right program, arguments
525 /// stdout/stderr capture setting and env variables are used. Especially note should be taken to how `EnvChange::Inherit`
526 /// is handled.*
527 ///
528 /// The [`Command::create_expected_env_iter()`] method can be used to find what exact env variables
529 /// are expected to be in the sub-process. Clearing the sub-process env and then setting all env vars
530 /// using [`Command::create_expected_env_iter()`] is not the most efficient but most simple and robust
531 /// to changes way to set the env. It's recommended to be used.
532 ///
533 pub fn with_exec_replacement_callback(
534 mut self,
535 callback: impl FnOnce(
536 Self,
537 &dyn OutputMapping<Output = Output, Error = Error>,
538 ) -> Result<ExecResult, io::Error>
539 + 'static,
540 ) -> Self {
541 self.run_callback = Some(Box::new(callback));
542 self
543 }
544}
545
546/// Trait used to configure what [`Command::run()`] returns.
547pub trait OutputMapping: 'static {
548 /// The output produced by this command, if it is run and doesn't fail.
549 type Output: 'static;
550
551 /// The error produced by this command, if it is run and does fail.
552 type Error: 'static;
553
554 /// Return if stdout needs to be captured for this output mapping `map_output` function.
555 ///
556 /// *This should be a pure function only depending on `&self`.*
557 fn capture_stdout(&self) -> bool;
558
559 /// Return if stderr needs to be captured for this output mapping `map_output` function.
560 ///
561 /// *This should be a pure function only depending on `&self`.*
562 fn capture_stderr(&self) -> bool;
563
564 /// The function called once the command's run completed.
565 ///
566 /// This function is used to convert the captured stdout/stderr
567 /// to an instance of the given `Output` type.
568 ///
569 /// If exist code checking is enabled and fails this function will
570 /// not be called.
571 ///
572 /// If it is disabled this function will be called and the implementation
573 /// can still decide to fail due to an unexpected/bad exit status.
574 fn map_output(
575 self: Box<Self>,
576 stdout: Option<Vec<u8>>,
577 stderr: Option<Vec<u8>>,
578 exit_status: ExitStatus,
579 ) -> Result<Self::Output, Self::Error>;
580}
581
582/// The command failed due to an unexpected exit status.
583///
584/// By default this means the exit status was not 0, but
585/// this can be reconfigured.
586#[derive(Debug, Error)]
587#[error("Unexpected exit status. Got: {got}, Expected: {expected}")]
588pub struct UnexpectedExitStatus {
589 got: ExitStatus,
590 expected: ExitStatus,
591}
592
593/// A ExitStatus type similar to `std::process::ExitStatus` but which can be created (e.g. for testing).
594///
595/// # Display
596///
597/// If there is an exit code it will always be displayed as hexadecimal.
598/// This is done because of two reasons:
599///
600/// - Some platforms allow rather large exit codes which are just very unreadable (and unrecognizable) in decimal formatting.
601/// - The hex format is always bit based which removes confusions around differences between targets having signed and unsigned
602/// exit codes. The drawback is that on platforms which do allow negative exit codes you need to convert the number not just
603/// from hex to decimal but also consider signing wrt. the max supported unsigned size on that platform.
604///
605/// An target specific exit status is displayed in a target specific way.
606/// The non os specific fallback defaults to displaying `NO_exit_status`.
607/// A signal termination exit status on unix will be displayed as e.g.
608/// `signal(9)`.
609///
610///
611/// # Os Support
612///
613/// For now part of this type are only supported for targets of the os families
614/// windows and unix(-like).
615///
616/// If you need support for _any_ other OS feel free to open an issue, I will
617/// add the necessary code path for the methods which are not OS independent
618/// then.
619///
620/// Currently this only affects the [`ExitStatus::successful()`] method.
621///
622/// # Why not `std::process::ExitStatus`?
623///
624/// The standard library and this library have different design goals, most
625/// importantly this library can introduce braking changes while the standard
626/// library ones can't really do so.
627///
628/// Major differences include:
629///
630/// - Just one enum instead of an `.exit_status() -> Option<i32>` accessor.
631/// - Implements `PartialEq<RHS>` for various numbers making testing easier.
632/// - Has a platform independent constructor, `std::process::ExitStatus` has
633/// various platform specific constructors.
634/// - Uses `i64` as exit code to more correctly represents exits codes (see below).
635///
636/// ## Incompatibilities and limitations.
637///
638/// Due to the current structures a various exit codes can be constructed which
639/// are not possible to appear on the target you are currently compiling against.
640/// **This is already true for the std implementation, but with slightly less
641/// constraints in our case.** For example if you target linux you can still
642/// create a exit code > 0xFF but in linux no exit code > 0xFF can be returned (
643/// furthermore returning any exit code > 127 is a cause of unexpected problems
644/// and must be avoided).
645///
646/// Furthermore `std::process::Command` returning a i32 code is a major problem
647/// as it's incompatible with various platforms:
648///
649/// - Windows has a `u32`! exit code, rust std's Command does reinterpret
650/// it as `i32` when returning it which in some cases lead to negative
651/// exit codes even through there *are not negative exit codes on windows*.
652/// Furthermore Fushisa does have a i64 exit status which they currently
653/// can't handle at all. *Be aware that this library still uses
654/// `std::process::Command` internally and a such can't handle this either*.
655/// But we do "fix" the exit code so that an exit code of `u32::MAX` is still
656/// `u32::MAX` and not `-1`!.
657///
658#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
659pub enum ExitStatus {
660 /// The process exited with an exit code.
661 ///
662 /// As this allows any i64 this allows you to create an exit status which can not
663 /// appear on the current target. (This is also possible with the standard libraries
664 /// `ExitStatus`). This makes testing easier and allows you to test cases of exit
665 /// codes which can't appear on your but other platforms.
666 ///
667 /// # Differences to `std::process::ExitStatus`
668 ///
669 /// This uses a `i64` as this allows a more correct representation of exit codes.
670 ///
671 /// *On windows a exit code > `i32::MAX` will be correctly be represented as such
672 /// instead of wrongly being displayed as negative number.*
673 Code(i64),
674
675 /// An exit status which isn't a simple exit code was returned.
676 ///
677 /// On unix if a process was directly terminated via an signal no exit code was
678 /// set but the signal causing the exit is returned (encoded with other values
679 /// in the raw exit status).
680 ///
681 /// Rust represents this separately as depending on the exact unix-like operating
682 /// system it might be encoded in different ways, in some cases instead of a integer
683 /// encoding the status a struct with multiple fields is returned, as such there
684 /// is no correct or reliable way to encode exit an status just as an number.
685 ///
686 /// Be aware that for testability [`OpaqueOsExitStatus`] can be created on all platforms
687 /// even through e.g. on windows there are only exit codes! Note that the exact inner
688 /// implementation of [`OpaqueOsExitStatus`] is platform dependent, but it implements
689 /// [`arbitrary_default()`](OpaqueOsExitStatus::target_specific_default())
690 OsSpecific(OpaqueOsExitStatus),
691}
692
693impl ExitStatus {
694 /// Returns true if the command did succeed.
695 ///
696 /// As not all operating systems use 0 == success we need to have platform
697 /// specific code for all of them. Which is infeasible and as such this is
698 /// only enabled on the unix and window target family. (Note that windows
699 /// and unix are currently the only target families as e.g. linux, all BSD's,
700 /// OsX, iOs are unix-like enough to count as part of the unix family).
701 #[cfg(any(window, unix))]
702 pub fn successful(&self) -> bool {
703 match self {
704 Self::Code(code) if *code == 0 => true,
705 _ => false,
706 }
707 }
708}
709
710impl Display for ExitStatus {
711 fn fmt(&self, fter: &mut fmt::Formatter) -> fmt::Result {
712 match self {
713 Self::Code(code) => write!(fter, "0x{:X}", code),
714 Self::OsSpecific(alt) => Display::fmt(alt, fter),
715 }
716 }
717}
718
719impl Default for ExitStatus {
720 fn default() -> Self {
721 Self::Code(0)
722 }
723}
724
725impl From<OpaqueOsExitStatus> for ExitStatus {
726 fn from(ooes: OpaqueOsExitStatus) -> Self {
727 ExitStatus::OsSpecific(ooes)
728 }
729}
730
731macro_rules! impl_from_and_partial_eq_for_fitting_int {
732 ($($int:ty),*) => ($(
733 impl From<$int> for ExitStatus {
734 fn from(code: $int) -> Self {
735 Self::Code(code as _)
736 }
737 }
738
739 impl PartialEq<$int> for ExitStatus {
740 fn eq(&self, other: &$int) -> bool {
741 match self {
742 Self::Code(code) => *code == *other as i64,
743 Self::OsSpecific(_) => false,
744 }
745 }
746 }
747 )*);
748}
749
750impl_from_and_partial_eq_for_fitting_int!(u8, i8, u16, i16, u32, i32, i64);
751/// A platform specific opaque exit status.
752///
753/// An exit status which is not an exit code, e.g.
754/// on unix the signal which terminated an process
755/// preventing it from exiting with an exit status.
756///
757/// **Warning: Besides [`OpaqueOsExitStatus::target_specific_default()`]
758/// all other methods only exist on _some_ targets but not all.** As such
759/// using them can lead to code which only compiles on some targets.
760#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
761pub struct OpaqueOsExitStatus {
762 #[cfg(not(unix))]
763 _priv: (),
764 #[cfg(unix)]
765 signal: i32,
766}
767
768impl OpaqueOsExitStatus {
769 /// Creates a instance of this type.
770 ///
771 /// This is meant for allowing non-platform specific tests which
772 /// handle the case of a non exit code process exit status.
773 ///
774 /// Platform specific tests likely still are needed as what
775 /// this type means is platform specific.
776 ///
777 /// This will always create the same default value but it's
778 /// a target_specific_value *and* it's picked arbitrary so
779 /// it's not really appropriately to implement [`Default`].
780 /// (To make clear why it isn't consider `u32` would default
781 /// to `246` or similar arbitrary value.)
782 pub fn target_specific_default() -> Self {
783 Self {
784 #[cfg(not(unix))]
785 _priv: (),
786 #[cfg(unix)]
787 signal: 9,
788 }
789 }
790
791 /// Return the signal number which did lead to the process termination.
792 #[cfg(unix)]
793 pub fn signal_number(&self) -> i32 {
794 self.signal
795 }
796
797 /// Create a unix [`OpaqueOsExitStatus`] instance based on the signal code
798 /// causing the non exit code termination.
799 ///
800 /// Like some other aspects you can define (and test) unrealistic signal numbers.
801 /// IMHO this is better (more simple, flexible etc.) then to have a result which
802 /// is potentially target dependent or a implicit target dependent bit masking.
803 ///
804 // E.g. on linux and most (all) unix it's limited to 7 bit (&0x7f) but on at least
805 // OpenBSD the value 0x7F is reserved and doesn't count as signal (the macro for
806 // testing if it exited with an signal excludes it). Also in any case 0 is not a
807 // valid signal either.
808 //
809 // POSIX defines signals as `int` and with this more or less as i32, but this seems to
810 // be because of practical reasons i.e. bitmasking a i32 produces a i32. I do not think
811 // there are any negative signals at all, nor do there seem to be any platforms with more
812 // than a handful of valid signals.
813 #[cfg(unix)]
814 pub fn from_signal_number(signal: i32) -> Self {
815 Self { signal }
816 }
817}
818
819impl Display for OpaqueOsExitStatus {
820 fn fmt(&self, fter: &mut fmt::Formatter) -> fmt::Result {
821 #[cfg(not(unix))]
822 {
823 fter.write_str("NO_EXIT_CODE")
824 }
825 #[cfg(unix)]
826 {
827 write!(fter, "signal({})", self.signal)
828 }
829 }
830}
831
832/// Used to determine how a env variable should be updated.
833#[derive(Debug, Clone, PartialEq)]
834pub enum EnvChange {
835 /// Remove the env value if it normally would have been set
836 ///
837 /// (e.g. because of inherited environment)
838 Remove,
839
840 /// Make sure the env variable will have given value in the sub-process.
841 Set(OsString),
842
843 /// Make sure the env variable is inherited from the process spawning the sub-process.
844 ///
845 /// If environment inheritance is disabled (e.g. using `with_inherit_env(false)`) this
846 /// will cause given values to still be inherited anyway.
847 ///
848 /// If environment inheritance is enabled this won't have any effect.
849 Inherit,
850}
851
852impl From<&Self> for EnvChange {
853 fn from(borrow: &Self) -> Self {
854 borrow.clone()
855 }
856}
857impl From<&OsString> for EnvChange {
858 fn from(val: &OsString) -> Self {
859 EnvChange::Set(val.clone())
860 }
861}
862
863impl From<OsString> for EnvChange {
864 fn from(val: OsString) -> Self {
865 EnvChange::Set(val)
866 }
867}
868
869impl From<&OsStr> for EnvChange {
870 fn from(val: &OsStr) -> Self {
871 EnvChange::Set(val.into())
872 }
873}
874
875impl From<String> for EnvChange {
876 fn from(val: String) -> Self {
877 EnvChange::Set(val.into())
878 }
879}
880
881impl From<&str> for EnvChange {
882 fn from(val: &str) -> Self {
883 EnvChange::Set(val.into())
884 }
885}
886
887/// Type used for `exec_replacement_callback` to return mocked output and exit status.
888#[derive(Debug, Default)]
889pub struct ExecResult {
890 /// The exit status the process did exit with.
891 pub exit_status: ExitStatus,
892
893 /// The stdout output captured during sub-process execution (if any).
894 ///
895 /// This must be `Some` if `stdout` is expected to be captured, it must
896 /// be `None` if it's expected to not be captured.
897 pub stdout: Option<Vec<u8>>,
898
899 /// The stderr output captured during sub-process execution (if any).
900 ///
901 /// This must be `Some` if `stderr` is expected to be captured, it must
902 /// be `None` if it's expected to not be captured.
903 pub stderr: Option<Vec<u8>>,
904}
905
906#[cfg(test)]
907mod tests {
908 use super::*;
909 use proptest::prelude::*;
910 use thiserror::Error;
911
912 #[derive(Debug, Error)]
913 enum TestCommandError {
914 #[error(transparent)]
915 Io(#[from] io::Error),
916
917 #[error(transparent)]
918 UnexpectedExitStatus(#[from] UnexpectedExitStatus),
919
920 #[error("TestCase error: {0}")]
921 Prop(TestCaseError),
922 }
923
924 impl From<TestCaseError> for TestCommandError {
925 fn from(prop_err: TestCaseError) -> Self {
926 Self::Prop(prop_err)
927 }
928 }
929
930 impl TestCommandError {
931 pub fn unwrap_prop(self) -> TestCaseError {
932 match self {
933 Self::Io(err) => panic!("unexpected io error: {:?}", err),
934 Self::UnexpectedExitStatus(err) => panic!("unexpected exit status: {:?}", err),
935 Self::Prop(prop_err) => return prop_err,
936 }
937 }
938 }
939
940 struct TestOutputMapping {
941 capture_stdout: bool,
942 capture_stderr: bool,
943 }
944
945 impl OutputMapping for TestOutputMapping {
946 type Output = bool;
947 type Error = TestCommandError;
948
949 fn capture_stdout(&self) -> bool {
950 self.capture_stdout
951 }
952 fn capture_stderr(&self) -> bool {
953 self.capture_stderr
954 }
955
956 fn map_output(
957 self: Box<Self>,
958 stdout: Option<Vec<u8>>,
959 stderr: Option<Vec<u8>>,
960 _exit_status: super::ExitStatus,
961 ) -> Result<Self::Output, Self::Error> {
962 (|| {
963 prop_assert_eq!(stdout.is_some(), self.capture_stdout());
964 prop_assert_eq!(stderr.is_some(), self.capture_stderr());
965 Ok(())
966 })()?;
967 Ok(true)
968 }
969 }
970
971 mod Command {
972 #![allow(non_snake_case)]
973
974 mod new {
975 use super::super::super::*;
976 use proptest::prelude::*;
977
978 #[test]
979 fn comp_can_be_created_using_str_string_os_str_or_os_string() {
980 Command::new("ls", ReturnNothing);
981 Command::new("ls".to_owned(), ReturnNothing);
982 Command::new(OsString::from("ls"), ReturnNothing);
983 Command::new(OsStr::new("ls"), ReturnNothing);
984 }
985
986 #[test]
987 fn comp_when_creating_command_different_capture_modes_can_be_used() {
988 Command::new("foo", ReturnNothing);
989 Command::new("foo", ReturnStdout);
990 Command::new("foo", ReturnStderr);
991 Command::new("foo", ReturnStdoutAndErr);
992 }
993
994 proptest! {
995 #[test]
996 fn the_used_program_can_be_queried(s in any::<OsString>()) {
997 let s = OsStr::new(&*s);
998 let cmd = Command::new(s, ReturnNothing);
999 prop_assert_eq!(&*cmd.program(), s)
1000 }
1001 }
1002 }
1003
1004 mod arguments {
1005 use super::super::super::*;
1006 use proptest::prelude::*;
1007 use std::{collections::HashSet, iter};
1008
1009 #[test]
1010 fn default_arguments_are_empty() {
1011 let cmd = Command::new("foo", ReturnNothing);
1012 assert!(cmd.arguments().is_empty());
1013 }
1014
1015 #[test]
1016 fn comp_arguments_can_be_set_from_iterables() {
1017 Command::new("foo", ReturnNothing).with_arguments(Vec::<OsString>::new());
1018 Command::new("foo", ReturnNothing).with_arguments(HashSet::<OsString>::new());
1019 Command::new("foo", ReturnNothing).with_arguments(&[] as &[OsString]);
1020 }
1021
1022 proptest! {
1023 #[test]
1024 fn new_arguments_can_be_added(
1025 cmd in any::<OsString>(),
1026 argument in any::<OsString>(),
1027 arguments in proptest::collection::vec(any::<OsString>(), 0..5),
1028 arguments2 in proptest::collection::vec(any::<OsString>(), 0..5)
1029 ) {
1030 let cmd = OsStr::new(&*cmd);
1031 let cmd = Command::new(cmd, ReturnNothing)
1032 .with_arguments(&arguments);
1033 prop_assert_eq!(cmd.arguments(), &arguments);
1034 let cmd = cmd.with_argument(&argument);
1035 prop_assert_eq!(
1036 cmd.arguments().iter().collect::<Vec<_>>(),
1037 arguments.iter().chain(iter::once(&argument)).collect::<Vec<_>>()
1038 );
1039 let cmd = cmd.with_arguments(&arguments2);
1040 prop_assert_eq!(
1041 cmd.arguments().iter().collect::<Vec<_>>(),
1042 arguments.iter()
1043 .chain(iter::once(&argument))
1044 .chain(arguments2.iter())
1045 .collect::<Vec<_>>()
1046 );
1047 }
1048 }
1049 }
1050
1051 mod run {
1052 use super::super::super::*;
1053
1054 #[test]
1055 fn run_can_lead_to_and_io_error() {
1056 let res = Command::new("foo", ReturnNothing)
1057 .with_exec_replacement_callback(|_, _| {
1058 Err(io::Error::new(io::ErrorKind::Other, "random"))
1059 })
1060 .run();
1061
1062 res.unwrap_err();
1063 }
1064
1065 #[test]
1066 fn return_no_error_if_the_command_has_zero_exit_status() {
1067 let res = Command::new("foo", ReturnNothing)
1068 .with_exec_replacement_callback(move |_, _| {
1069 Ok(ExecResult {
1070 exit_status: 0.into(),
1071 ..Default::default()
1072 })
1073 })
1074 .run();
1075
1076 res.unwrap();
1077 }
1078 }
1079
1080 mod ReturnSetting {
1081 use super::super::super::*;
1082 use super::super::TestOutputMapping;
1083 use proptest::prelude::*;
1084
1085 #[test]
1086 fn comp_command_must_only_be_generic_over_the_output() {
1087 if false {
1088 let mut _cmd = Command::new("foo", ReturnNothing);
1089 _cmd = Command::new("foo", ReturnNothingAlt);
1090 }
1091
1092 //---
1093 struct ReturnNothingAlt;
1094 impl OutputMapping for ReturnNothingAlt {
1095 type Output = ();
1096 type Error = CommandExecutionError;
1097 fn capture_stdout(&self) -> bool {
1098 false
1099 }
1100 fn capture_stderr(&self) -> bool {
1101 false
1102 }
1103 fn map_output(
1104 self: Box<Self>,
1105 _stdout: Option<Vec<u8>>,
1106 _stderr: Option<Vec<u8>>,
1107 _exit_status: ExitStatus,
1108 ) -> Result<Self::Output, Self::Error> {
1109 unimplemented!()
1110 }
1111 }
1112 }
1113
1114 #[test]
1115 fn allow_custom_errors() {
1116 let _result: MyError = Command::new("foo", ReturnError)
1117 .with_exec_replacement_callback(|_, _| {
1118 Ok(ExecResult {
1119 exit_status: 0.into(),
1120 ..Default::default()
1121 })
1122 })
1123 .run()
1124 .unwrap_err();
1125
1126 //------------
1127 struct ReturnError;
1128 impl OutputMapping for ReturnError {
1129 type Output = ();
1130 type Error = MyError;
1131 fn capture_stdout(&self) -> bool {
1132 false
1133 }
1134 fn capture_stderr(&self) -> bool {
1135 false
1136 }
1137 fn map_output(
1138 self: Box<Self>,
1139 _stdout: Option<Vec<u8>>,
1140 _stderr: Option<Vec<u8>>,
1141 _exit_status: ExitStatus,
1142 ) -> Result<Self::Output, Self::Error> {
1143 Err(MyError::BarFoot)
1144 }
1145 }
1146 #[derive(Debug, Error)]
1147 enum MyError {
1148 #[error("FooBar")]
1149 BarFoot,
1150
1151 #[error(transparent)]
1152 Io(#[from] io::Error),
1153
1154 #[error(transparent)]
1155 UnexpectedExitStatus(#[from] UnexpectedExitStatus),
1156 }
1157 }
1158
1159 #[should_panic]
1160 #[test]
1161 fn returning_stdout_which_should_not_be_captured_triggers_a_debug_assertion() {
1162 let _ = Command::new("foo", ReturnNothing)
1163 .with_check_exit_status(false)
1164 .with_exec_replacement_callback(|_, _| {
1165 Ok(ExecResult {
1166 exit_status: 1.into(),
1167 stdout: Some(Vec::new()),
1168 ..Default::default()
1169 })
1170 })
1171 .run();
1172 }
1173
1174 #[should_panic]
1175 #[test]
1176 fn returning_stderr_which_should_not_be_captured_triggers_a_debug_assertion() {
1177 let _ = Command::new("foo", ReturnNothing)
1178 .with_check_exit_status(false)
1179 .with_exec_replacement_callback(|_, _| {
1180 Ok(ExecResult {
1181 exit_status: 1.into(),
1182 stderr: Some(Vec::new()),
1183 ..Default::default()
1184 })
1185 })
1186 .run();
1187 }
1188
1189 proptest! {
1190 #[test]
1191 fn only_pass_stdout_stderr_to_map_output_if_return_settings_indicate_they_capture_it(
1192 capture_stdout in proptest::bool::ANY,
1193 capture_stderr in proptest::bool::ANY
1194 ) {
1195 let res = Command::new("foo", TestOutputMapping { capture_stdout, capture_stderr })
1196 .with_exec_replacement_callback(move |_,_| {
1197 Ok(ExecResult {
1198 exit_status: 0.into(),
1199 stdout: if capture_stdout { Some(Vec::new()) } else { None },
1200 stderr: if capture_stderr { Some(Vec::new()) } else { None }
1201 })
1202 })
1203 .run()
1204 .map_err(|e| e.unwrap_prop())?;
1205
1206 assert!(res);
1207 }
1208
1209 #[test]
1210 fn command_provides_a_getter_to_check_if_stdout_and_err_will_be_captured(
1211 capture_stdout in proptest::bool::ANY,
1212 capture_stderr in proptest::bool::ANY
1213 ) {
1214 let cmd = Command::new("foo", TestOutputMapping { capture_stdout, capture_stderr });
1215 prop_assert_eq!(cmd.will_capture_stdout(), capture_stdout);
1216 prop_assert_eq!(cmd.will_capture_stderr(), capture_stderr);
1217 }
1218
1219 #[test]
1220 fn capture_hints_are_available_in_the_callback(
1221 capture_stdout in proptest::bool::ANY,
1222 capture_stderr in proptest::bool::ANY
1223 ) {
1224 Command::new("foo", TestOutputMapping { capture_stdout, capture_stderr })
1225 .with_exec_replacement_callback(move |_cmd, return_settings| {
1226 assert_eq!(return_settings.capture_stdout(), capture_stdout);
1227 assert_eq!(return_settings.capture_stderr(), capture_stderr);
1228 Ok(ExecResult {
1229 exit_status: 0.into(),
1230 stdout: if capture_stdout { Some(Vec::new()) } else { None },
1231 stderr: if capture_stderr { Some(Vec::new()) } else { None }
1232 })
1233 })
1234 .run()
1235 .unwrap();
1236 }
1237 }
1238 }
1239 mod environment {
1240 use super::super::super::*;
1241 use proptest::prelude::*;
1242
1243 #[test]
1244 fn by_default_no_environment_updates_are_done() {
1245 let cmd = Command::new("foo", ReturnNothing);
1246 assert!(cmd.env_updates().is_empty());
1247 }
1248
1249 #[test]
1250 fn create_expected_env_iter_includes_the_current_env_by_default() {
1251 let process_env = env::vars_os()
1252 .into_iter()
1253 .map(|(k, v)| (Cow::Owned(k), Cow::Owned(v)))
1254 .collect::<HashMap<_, _>>();
1255 let cmd = Command::new("foo", ReturnNothing);
1256 let created_map = cmd.create_expected_env_iter().collect::<HashMap<_, _>>();
1257 assert_eq!(process_env, created_map);
1258 }
1259
1260 #[test]
1261 fn by_default_env_is_inherited() {
1262 let cmd = Command::new("foo", ReturnNothing);
1263 assert_eq!(cmd.inherit_env(), true);
1264 //FIXME fluky if there is no single ENV variable set
1265 assert_ne!(cmd.create_expected_env_iter().count(), 0);
1266 }
1267
1268 #[test]
1269 fn inheritance_of_env_variables_can_be_disabled() {
1270 let cmd = Command::new("foo", ReturnNothing).with_inherit_env(false);
1271 assert_eq!(cmd.inherit_env(), false);
1272 assert_eq!(cmd.create_expected_env_iter().count(), 0);
1273 }
1274
1275 proptest! {
1276 #[test]
1277 fn new_env_variables_can_be_added(
1278 cmd in any::<OsString>(),
1279 variable in any::<OsString>(),
1280 value in any::<OsString>(),
1281 map1 in proptest::collection::hash_map(
1282 any::<OsString>(),
1283 any::<OsString>().prop_map(|s| EnvChange::Set(s)),
1284 0..4
1285 ),
1286 map2 in proptest::collection::hash_map(
1287 any::<OsString>(),
1288 any::<OsString>().prop_map(|s| EnvChange::Set(s)),
1289 0..4
1290 ),
1291 ) {
1292 let cmd = Command::new(cmd, ReturnNothing)
1293 .with_env_updates(&map1);
1294
1295 prop_assert_eq!(cmd.env_updates(), &map1);
1296
1297 let cmd = cmd.with_env_update(&variable, &value);
1298
1299 let mut n_map = map1.clone();
1300 n_map.insert(variable, EnvChange::Set(value));
1301 prop_assert_eq!(cmd.env_updates(), &n_map);
1302
1303 let cmd = cmd.with_env_updates(&map2);
1304
1305 for (key, value) in &map2 {
1306 n_map.insert(key.into(), value.into());
1307 }
1308 prop_assert_eq!(cmd.env_updates(), &n_map);
1309 }
1310
1311
1312 //FIXME on CI this test can leak secrets if it fails
1313 #[test]
1314 fn env_variables_can_be_set_to_be_removed_from_inherited_env(
1315 cmd in any::<OsString>(),
1316 rem_key in proptest::sample::select(env::vars_os().map(|(k,_v)| k).collect::<Vec<_>>())
1317 ) {
1318 let cmd = Command::new(cmd, ReturnNothing).with_env_update(rem_key.clone(), EnvChange::Remove);
1319 prop_assert_eq!(cmd.env_updates().get(&rem_key), Some(&EnvChange::Remove));
1320
1321 let produced_env = cmd.create_expected_env_iter()
1322 .map(|(k,v)| (k.into_owned(), v.into_owned()))
1323 .collect::<HashMap<OsString, OsString>>();
1324
1325 prop_assert_eq!(produced_env.get(&rem_key), None);
1326 }
1327
1328 //FIXME on CI this test can leak secrets if it fails
1329 #[test]
1330 fn env_variables_can_be_set_to_be_replaced_from_inherited_env(
1331 cmd in any::<OsString>(),
1332 rem_key in proptest::sample::select(env::vars_os().map(|(k,_v)| k).collect::<Vec<_>>()),
1333 replacement in any::<OsString>()
1334 ) {
1335 let cmd = Command::new(cmd, ReturnNothing).with_env_update(rem_key.clone(), EnvChange::Set(replacement.clone()));
1336 let expect = EnvChange::Set(replacement.clone());
1337 prop_assert_eq!(cmd.env_updates().get(&rem_key), Some(&expect));
1338 let produced_env = cmd.create_expected_env_iter()
1339 .map(|(k,v)| (k.into_owned(), v.into_owned()))
1340 .collect::<HashMap<OsString, OsString>>();
1341
1342 prop_assert_eq!(produced_env.get(&rem_key), Some(&replacement));
1343 }
1344
1345 //FIXME on CI this test can leak secrets if it fails
1346 #[test]
1347 fn env_variables_can_be_set_to_inherit_even_if_inheritance_is_disabled(
1348 cmd in any::<OsString>(),
1349 inherit in proptest::sample::select(env::vars_os().map(|(k,_v)| k).collect::<Vec<_>>()),
1350 ) {
1351 let expected_val = env::var_os(&inherit);
1352 let cmd = Command::new(cmd, ReturnNothing)
1353 .with_inherit_env(false)
1354 .with_env_update(&inherit, EnvChange::Inherit);
1355
1356 assert_eq!(cmd.create_expected_env_iter().count(), 1);
1357 let got_value = cmd.create_expected_env_iter().find(|(k,_v)| &*k==&*inherit)
1358 .map(|(_k,v)| v);
1359 assert_eq!(
1360 expected_val.as_ref().map(|v|&**v),
1361 got_value.as_ref().map(|v|&**v)
1362 );
1363 }
1364
1365 #[test]
1366 fn env_variables_can_be_set_to_inherit_even_if_inheritance_is_disabled_2(
1367 cmd in any::<OsString>(),
1368 inherit in proptest::sample::select(env::vars_os().map(|(k,_v)| k).collect::<Vec<_>>()),
1369 ) {
1370 let expected_val = env::var_os(&inherit);
1371 let cmd = Command::new(cmd, ReturnNothing)
1372 .with_env_update(&inherit, EnvChange::Inherit)
1373 .with_inherit_env(false);
1374
1375 assert_eq!(cmd.create_expected_env_iter().count(), 1);
1376 let got_value = cmd.create_expected_env_iter().find(|(k,_v)| &*k==&*inherit)
1377 .map(|(_k,v)| v);
1378 assert_eq!(
1379 expected_val.as_ref().map(|v|&**v),
1380 got_value.as_ref().map(|v|&**v)
1381 );
1382 }
1383
1384 //FIXME on CI this test can leak secrets if it fails
1385 #[test]
1386 fn setting_inherit_does_not_affect_anything_if_we_anyway_inherit_all(
1387 cmd in any::<OsString>(),
1388 pointless_inherit in proptest::sample::select(env::vars_os().map(|(k,_v)| k).collect::<Vec<_>>()),
1389 ) {
1390 const NON_EXISTING_VAR_KEY: &'static str = "____MAPPED_COMMAND__THIS_SHOULD_NOT_EXIST_AS_ENV_VARIABLE____";
1391 assert_eq!(env::var_os(NON_EXISTING_VAR_KEY), None);
1392
1393 let expected_values = env::vars_os()
1394 .map(|(k,v)| (Cow::Owned(k), Cow::Owned(v)))
1395 .collect::<HashMap<_,_>>();
1396
1397 let cmd = Command::new(cmd, ReturnNothing)
1398 .with_env_update(&pointless_inherit, EnvChange::Inherit)
1399 .with_env_update(NON_EXISTING_VAR_KEY, EnvChange::Inherit);
1400
1401 let values = cmd.create_expected_env_iter().collect::<HashMap<_,_>>();
1402
1403 assert!(!values.contains_key(OsStr::new(NON_EXISTING_VAR_KEY)));
1404 assert_eq!(expected_values.len(), values.len());
1405 assert_eq!(
1406 expected_values.get(&pointless_inherit),
1407 values.get(&*pointless_inherit)
1408 );
1409
1410 }
1411 }
1412 }
1413
1414 mod working_directory {
1415 use super::super::super::*;
1416 use crate::utils::opt_arbitrary_path_buf;
1417 use proptest::prelude::*;
1418
1419 #[test]
1420 fn by_default_no_explicit_working_directory_is_set() {
1421 let cmd = Command::new("foo", ReturnNothing);
1422 assert_eq!(cmd.working_directory_override(), None);
1423 }
1424
1425 proptest! {
1426 #[test]
1427 fn the_working_directory_can_be_changed(
1428 cmd in any::<OsString>(),
1429 wd_override in opt_arbitrary_path_buf(),
1430 wd_override2 in opt_arbitrary_path_buf()
1431 ) {
1432 let cmd = Command::new(cmd, ReturnNothing)
1433 .with_working_directory_override(wd_override.as_ref());
1434
1435 assert_eq!(cmd.working_directory_override(), wd_override.as_ref().map(|i|&**i));
1436
1437 let cmd = cmd.with_working_directory_override(wd_override2.as_ref());
1438 assert_eq!(cmd.working_directory_override(), wd_override2.as_ref().map(|i|&**i));
1439 }
1440 }
1441 }
1442
1443 mod exit_status_checking {
1444 use super::super::super::*;
1445 use proptest::prelude::*;
1446
1447 #[test]
1448 fn by_default_the_expected_exit_status_is_0() {
1449 let cmd = Command::new("foo", ReturnNothing);
1450 assert_eq!(cmd.expected_exit_status(), 0);
1451 }
1452
1453 #[test]
1454 fn by_default_exit_status_checking_is_enabled() {
1455 let cmd = Command::new("foo", ReturnNothing);
1456 assert_eq!(cmd.check_exit_status(), true);
1457 }
1458
1459 #[test]
1460 fn setting_check_exit_status_to_false_disables_it() {
1461 Command::new("foo", ReturnNothing)
1462 .with_check_exit_status(false)
1463 .with_exec_replacement_callback(|_, _| {
1464 Ok(ExecResult {
1465 exit_status: 1.into(),
1466 ..Default::default()
1467 })
1468 })
1469 .run()
1470 .unwrap();
1471 }
1472
1473 #[test]
1474 fn you_can_expect_no_exit_status_to_be_returned() {
1475 let cmd = Command::new("foo", ReturnNothing).with_expected_exit_status(
1476 ExitStatus::OsSpecific(OpaqueOsExitStatus::target_specific_default()),
1477 );
1478
1479 assert_eq!(
1480 cmd.expected_exit_status(),
1481 ExitStatus::OsSpecific(OpaqueOsExitStatus::target_specific_default())
1482 );
1483 }
1484
1485 #[test]
1486 fn setting_the_expected_exit_status_will_enable_checking() {
1487 let cmd = Command::new("foo", ReturnNothing)
1488 .with_check_exit_status(false)
1489 .with_expected_exit_status(0);
1490
1491 assert_eq!(cmd.check_exit_status(), true);
1492 }
1493
1494 proptest! {
1495 #[test]
1496 fn return_an_error_if_the_command_has_non_zero_exit_status(
1497 cmd in any::<OsString>(),
1498 exit_status in prop_oneof!(..0, 1..).prop_map(ExitStatus::from)
1499 ) {
1500 let res = Command::new(cmd, ReturnNothing)
1501 .with_exec_replacement_callback(move |_,_| {
1502 Ok(ExecResult {
1503 exit_status,
1504 ..Default::default()
1505 })
1506 })
1507 .run();
1508
1509 res.unwrap_err();
1510 }
1511
1512 #[test]
1513 fn replacing_the_expected_exit_status_causes_error_on_different_exit_status(
1514 exit_status in -5..6,
1515 offset in prop_oneof!(-100..0, 1..101)
1516 ) {
1517 let res = Command::new("foo", ReturnNothing)
1518 .with_expected_exit_status(exit_status)
1519 .with_exec_replacement_callback(move |cmd,_| {
1520 assert_eq!(cmd.expected_exit_status(), exit_status);
1521 Ok(ExecResult {
1522 exit_status: ExitStatus::from(exit_status + offset),
1523 ..Default::default()
1524 })
1525 })
1526 .run();
1527
1528 match res {
1529 Err(CommandExecutionError::UnexpectedExitStatus(UnexpectedExitStatus {got, expected})) => {
1530 assert_eq!(expected, exit_status);
1531 assert_eq!(got, exit_status+offset);
1532 },
1533 _ => panic!("Unexpected Result: {:?}", res)
1534 }
1535 }
1536
1537 #[test]
1538 fn exit_status_checking_can_be_disabled_and_enabled(
1539 change1 in proptest::bool::ANY,
1540 change2 in proptest::bool::ANY,
1541 ) {
1542 let cmd = Command::new("foo", ReturnNothing)
1543 .with_check_exit_status(change1);
1544
1545 assert_eq!(cmd.check_exit_status(), change1);
1546
1547 let cmd = cmd.with_check_exit_status(change2);
1548 assert_eq!(cmd.check_exit_status(), change2);
1549 }
1550
1551 }
1552 }
1553
1554 mod exec_replacement_callback {
1555 use std::{cell::RefCell, rc::Rc};
1556
1557 use super::super::super::*;
1558
1559 #[test]
1560 fn program_execution_can_be_replaced_with_an_callback() {
1561 let was_run = Rc::new(RefCell::new(false));
1562 let was_run_ = was_run.clone();
1563 let cmd = Command::new("some_cmd", ReturnStdoutAndErr)
1564 .with_exec_replacement_callback(move |for_cmd, _| {
1565 *(*was_run_).borrow_mut() = true;
1566 assert_eq!(&*for_cmd.program(), "some_cmd");
1567 Ok(ExecResult {
1568 exit_status: 0.into(),
1569 stdout: Some("result=12".to_owned().into()),
1570 stderr: Some(Vec::new()),
1571 })
1572 });
1573
1574 let res = cmd.run().unwrap();
1575 assert_eq!(*was_run.borrow_mut(), true);
1576 assert_eq!(&*res.stdout, "result=12".as_bytes());
1577 assert_eq!(&*res.stderr, "".as_bytes());
1578 }
1579 }
1580 }
1581
1582 mod ExitStatus {
1583 #![allow(non_snake_case)]
1584
1585 mod display_fmt {
1586 use crate::{ExitStatus, OpaqueOsExitStatus};
1587
1588 #[test]
1589 fn format_exit_status_as_hex() {
1590 let exit_status = ExitStatus::from(0x7Fi32);
1591 assert_eq!(&format!("{}", exit_status), "0x7F");
1592 }
1593
1594 #[test]
1595 fn format_negative_exit_status_as_hex() {
1596 let exit_status = ExitStatus::Code(-1i32 as u32 as _);
1597 assert_eq!(&format!("{}", exit_status), "0xFFFFFFFF");
1598 }
1599
1600 #[test]
1601 #[cfg(unix)]
1602 fn display_for_non_exit_code_on_unix() {
1603 let signal = OpaqueOsExitStatus::from_signal_number(9);
1604 assert_eq!(&format!("{}", signal), "signal(9)");
1605 }
1606 }
1607
1608 mod new {
1609 use crate::ExitStatus;
1610
1611 #[test]
1612 fn can_be_create_from_many_numbers() {
1613 let status = ExitStatus::from(12u8);
1614 assert_eq!(status, ExitStatus::Code(12));
1615 let status = ExitStatus::from(-12i8);
1616 assert_eq!(status, ExitStatus::Code(-12));
1617 let status = ExitStatus::from(12u16);
1618 assert_eq!(status, ExitStatus::Code(12));
1619 let status = ExitStatus::from(-12i16);
1620 assert_eq!(status, ExitStatus::Code(-12));
1621 let status = ExitStatus::from(u32::MAX);
1622 assert_eq!(status, ExitStatus::Code(u32::MAX as i64));
1623 let status = ExitStatus::from(-1i32);
1624 assert_eq!(status, ExitStatus::Code(-1));
1625 let status = ExitStatus::from(-13i64);
1626 assert_eq!(status, ExitStatus::Code(-13));
1627 }
1628
1629 #[test]
1630 fn can_compare_to_many_numbers() {
1631 let status = ExitStatus::from(12u8);
1632 assert_eq!(status, 12u8);
1633 let status = ExitStatus::from(-12i8);
1634 assert_eq!(status, -12i8);
1635 let status = ExitStatus::from(12u16);
1636 assert_eq!(status, 12u16);
1637 let status = ExitStatus::from(-12i16);
1638 assert_eq!(status, -12i16);
1639 let status = ExitStatus::from(u32::MAX);
1640 assert_eq!(status, u32::MAX);
1641 let status = ExitStatus::from(-1i32);
1642 assert_eq!(status, -1i32);
1643 let status = ExitStatus::from(-13i64);
1644 assert_eq!(status, -13i64);
1645 }
1646 }
1647 }
1648
1649 #[cfg(unix)]
1650 mod signal_number {
1651 use proptest::prelude::*;
1652
1653 use crate::OpaqueOsExitStatus;
1654
1655 proptest! {
1656 #[test]
1657 fn from_to_signal_number(
1658 nr in any::<i32>()
1659 ) {
1660 let exit_status = OpaqueOsExitStatus::from_signal_number(nr);
1661 assert_eq!(exit_status.signal_number(), nr);
1662 }
1663
1664 }
1665 }
1666}