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