assert_cmd/
cargo.rs

1//! Simplify running `bin`s in a Cargo project.
2//!
3//! [`CommandCargoExt`] is an extension trait for [`Command`] to easily launch a crate's
4//! binaries.
5//!
6//! # Examples
7//!
8//! Simple case:
9//!
10//! ```rust,no_run
11//! use assert_cmd::prelude::*;
12//!
13//! use std::process::Command;
14//!
15//! let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))
16//!     .unwrap();
17//! let output = cmd.unwrap();
18//! ```
19//!
20//! # Limitations
21//!
22//! - Only works within the context of integration tests.  See [`escargot`] for a more
23//!   flexible API.
24//! - Only reuses your existing feature flags, targets, or build mode.
25//! - Only works with cargo binaries (`cargo test` ensures they are built).
26//!
27//! If you run into these limitations, we recommend trying out [`escargot`]:
28//!
29//! ```rust,no_run
30//! use assert_cmd::prelude::*;
31//!
32//! use std::process::Command;
33//!
34//! let bin_under_test = escargot::CargoBuild::new()
35//!     .bin("bin_fixture")
36//!     .current_release()
37//!     .current_target()
38//!     .run()
39//!     .unwrap();
40//! let mut cmd = bin_under_test.command();
41//! let output = cmd.unwrap();
42//! println!("{:?}", output);
43//! ```
44//!
45//! Notes:
46//! - There is a [noticeable per-call overhead][cargo-overhead] for `CargoBuild`.  We recommend
47//!   caching the binary location (`.path()` instead of `.command()`) with [`lazy_static`].
48//! - `.current_target()` improves platform coverage at the cost of [slower test runs if you don't
49//!   explicitly pass `--target <TRIPLET>` on the command line][first-call].
50//!
51//! [`lazy_static`]: https://crates.io/crates/lazy_static
52//! [`Command`]: std::process::Command
53//! [`escargot`]: https://crates.io/crates/escargot
54//! [cargo-overhead]: https://github.com/assert-rs/assert_cmd/issues/6
55//! [first-call]: https://github.com/assert-rs/assert_cmd/issues/57
56
57use std::env;
58use std::error::Error;
59use std::fmt;
60use std::path;
61use std::process;
62
63#[doc(inline)]
64pub use crate::cargo_bin;
65
66/// Create a [`Command`] for a `bin` in the Cargo project.
67///
68/// `CommandCargoExt` is an extension trait for [`Command`][std::process::Command] to easily launch a crate's
69/// binaries.
70///
71/// See the [`cargo` module documentation][super::cargo] for caveats and workarounds.
72///
73/// # Examples
74///
75/// ```rust,no_run
76/// use assert_cmd::prelude::*;
77///
78/// use std::process::Command;
79///
80/// let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))
81///     .unwrap();
82/// let output = cmd.unwrap();
83/// println!("{:?}", output);
84/// ```
85///
86/// [`Command`]: std::process::Command
87pub trait CommandCargoExt
88where
89    Self: Sized,
90{
91    /// Create a [`Command`] to run a specific binary of the current crate.
92    ///
93    /// See the [`cargo` module documentation][crate::cargo] for caveats and workarounds.
94    ///
95    /// The [`Command`] created with this method may run the binary through a runner, as configured
96    /// in the `CARGO_TARGET_<TRIPLET>_RUNNER` environment variable.  This is useful for running
97    /// binaries that can't be launched directly, such as cross-compiled binaries. When using
98    /// this method with [cross](https://github.com/cross-rs/cross), no extra configuration is
99    /// needed.
100    ///
101    /// **NOTE:** Prefer [`cargo_bin!`] as this makes assumptions about cargo
102    ///
103    /// # Examples
104    ///
105    /// ```rust,no_run
106    /// use assert_cmd::prelude::*;
107    ///
108    /// use std::process::Command;
109    ///
110    /// let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))
111    ///     .unwrap();
112    /// let output = cmd.unwrap();
113    /// println!("{:?}", output);
114    /// ```
115    ///
116    /// ```rust,no_run
117    /// use assert_cmd::prelude::*;
118    ///
119    /// use std::process::Command;
120    ///
121    /// let mut cmd = Command::cargo_bin("bin_fixture")
122    ///     .unwrap();
123    /// let output = cmd.unwrap();
124    /// println!("{:?}", output);
125    /// ```
126    ///
127    /// [`Command`]: std::process::Command
128    fn cargo_bin<S: AsRef<str>>(name: S) -> Result<Self, CargoError>;
129}
130
131impl CommandCargoExt for crate::cmd::Command {
132    fn cargo_bin<S: AsRef<str>>(name: S) -> Result<Self, CargoError> {
133        crate::cmd::Command::cargo_bin(name)
134    }
135}
136
137impl CommandCargoExt for process::Command {
138    fn cargo_bin<S: AsRef<str>>(name: S) -> Result<Self, CargoError> {
139        cargo_bin_cmd(name)
140    }
141}
142
143pub(crate) fn cargo_bin_cmd<S: AsRef<str>>(name: S) -> Result<process::Command, CargoError> {
144    let path = cargo_bin(name);
145    if path.is_file() {
146        if let Some(runner) = cargo_runner() {
147            let mut cmd = process::Command::new(&runner[0]);
148            cmd.args(&runner[1..]).arg(path);
149            Ok(cmd)
150        } else {
151            Ok(process::Command::new(path))
152        }
153    } else {
154        Err(CargoError::with_cause(NotFoundError { path }))
155    }
156}
157
158pub(crate) fn cargo_runner() -> Option<Vec<String>> {
159    let runner_env = format!(
160        "CARGO_TARGET_{}_RUNNER",
161        CURRENT_TARGET.replace('-', "_").to_uppercase()
162    );
163    let runner = env::var(runner_env).ok()?;
164    Some(runner.split(' ').map(str::to_string).collect())
165}
166
167/// Error when finding crate binary.
168#[derive(Debug)]
169pub struct CargoError {
170    cause: Option<Box<dyn Error + Send + Sync + 'static>>,
171}
172
173impl CargoError {
174    /// Wrap the underlying error for passing up.
175    pub fn with_cause<E>(cause: E) -> Self
176    where
177        E: Error + Send + Sync + 'static,
178    {
179        let cause = Box::new(cause);
180        Self { cause: Some(cause) }
181    }
182}
183
184impl Error for CargoError {}
185
186impl fmt::Display for CargoError {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        if let Some(ref cause) = self.cause {
189            writeln!(f, "Cause: {cause}")?;
190        }
191        Ok(())
192    }
193}
194
195/// Error when finding crate binary.
196#[derive(Debug)]
197struct NotFoundError {
198    path: path::PathBuf,
199}
200
201impl Error for NotFoundError {}
202
203impl fmt::Display for NotFoundError {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        writeln!(f, "Cargo command not found: {}", self.path.display())
206    }
207}
208
209// Adapted from
210// https://github.com/rust-lang/cargo/blob/485670b3983b52289a2f353d589c57fae2f60f82/tests/testsuite/support/mod.rs#L507
211fn target_dir() -> path::PathBuf {
212    env::current_exe()
213        .ok()
214        .map(|mut path| {
215            path.pop();
216            if path.ends_with("deps") {
217                path.pop();
218            }
219            path
220        })
221        .expect("this should only be used where a `current_exe` can be set")
222}
223
224/// Look up the path to a cargo-built binary within an integration test.
225///
226/// **NOTE:** Prefer [`cargo_bin!`] as this makes assumptions about cargo
227pub fn cargo_bin<S: AsRef<str>>(name: S) -> path::PathBuf {
228    cargo_bin_str(name.as_ref())
229}
230
231fn cargo_bin_str(name: &str) -> path::PathBuf {
232    let env_var = format!("CARGO_BIN_EXE_{name}");
233    env::var_os(env_var)
234        .map(|p| p.into())
235        .unwrap_or_else(|| target_dir().join(format!("{}{}", name, env::consts::EXE_SUFFIX)))
236}
237
238/// The current process' target triplet.
239const CURRENT_TARGET: &str = include_str!(concat!(env!("OUT_DIR"), "/current_target.txt"));