blob: 89c6c929c1d33a36a2dbc73695656eb2aa2824a5 [file] [log] [blame] [edit]
//! Interact with `cargo`
/// The absolute path to a binary target's executable.
///
/// The `bin_target_name` is the name of the binary
/// target, exactly as-is.
///
/// **NOTE:** This is only set when building an integration test or benchmark.
///
/// ## Example
///
/// ```rust,no_run
/// #[test]
/// fn cli_tests() {
/// trycmd::TestCases::new()
/// .default_bin_path(trycmd::cargo_bin!("bin-fixture"))
/// .case("tests/cmd/*.trycmd");
/// }
/// ```
#[macro_export]
macro_rules! cargo_bin {
($bin_target_name:expr) => {
::std::path::Path::new(env!(concat!("CARGO_BIN_EXE_", $bin_target_name)))
};
}
#[cfg(feature = "examples")]
pub use examples::{compile_example, compile_examples};
#[cfg(feature = "examples")]
pub(crate) mod examples {
/// Prepare an example for testing
///
/// Unlike [`cargo_bin!`][crate::cargo_bin!], this does not inherit all of the current compiler settings. It
/// will match the current target and profile but will not get feature flags. Pass those arguments
/// to the compiler via `args`.
///
/// ## Example
///
/// ```rust,no_run
/// #[test]
/// fn cli_tests() {
/// trycmd::TestCases::new()
/// .register_bin("example-fixture", trycmd::cargo::compile_example("example-fixture", []))
/// .case("examples/cmd/*.trycmd");
/// }
/// ```
pub fn compile_example<'a>(
target_name: &str,
args: impl IntoIterator<Item = &'a str>,
) -> crate::schema::Bin {
compile_example_path(target_name, args).into()
}
fn compile_example_path<'a>(
target_name: &str,
args: impl IntoIterator<Item = &'a str>,
) -> Result<crate::schema::Bin, crate::Error> {
debug!("Compiling example {}", target_name);
let messages = escargot::CargoBuild::new()
.current_target()
.current_release()
.example(target_name)
.args(args)
.exec()
.map_err(|e| crate::Error::new(e.to_string()))?;
for message in messages {
let message = message.map_err(|e| crate::Error::new(e.to_string()))?;
let message = message
.decode()
.map_err(|e| crate::Error::new(e.to_string()))?;
debug!("Message: {:?}", message);
if let Some(bin) = decode_example_message(&message) {
let (name, bin) = bin?;
assert_eq!(target_name, name);
return Ok(bin);
}
}
Err(crate::Error::new(format!(
"Unknown error building example {}",
target_name
)))
}
/// Prepare all examples for testing
///
/// Unlike [`cargo_bin!`][crate::cargo_bin!], this does not inherit all of the current compiler settings. It
/// will match the current target and profile but will not get feature flags. Pass those arguments
/// to the compiler via `args`.
///
/// ## Example
///
/// ```rust,no_run
/// #[test]
/// fn cli_tests() {
/// trycmd::TestCases::new()
/// .register_bins(trycmd::cargo::compile_examples([]).unwrap())
/// .case("examples/cmd/*.trycmd");
/// }
/// ```
pub fn compile_examples<'a>(
args: impl IntoIterator<Item = &'a str>,
) -> Result<impl Iterator<Item = (String, crate::schema::Bin)>, crate::Error> {
debug!("Compiling examples");
let mut examples = std::collections::BTreeMap::new();
let messages = escargot::CargoBuild::new()
.current_target()
.current_release()
.examples()
.args(args)
.exec()
.map_err(|e| crate::Error::new(e.to_string()))?;
for message in messages {
let message = message.map_err(|e| crate::Error::new(e.to_string()))?;
let message = message
.decode()
.map_err(|e| crate::Error::new(e.to_string()))?;
debug!("Message: {:?}", message);
if let Some(bin) = decode_example_message(&message) {
let (name, bin) = bin?;
examples.insert(name.to_owned(), bin);
}
}
Ok(examples.into_iter())
}
fn decode_example_message<'m>(
message: &'m escargot::format::Message,
) -> Option<Result<(&'m str, crate::schema::Bin), crate::Error>> {
match message {
escargot::format::Message::CompilerMessage(msg) => {
let level = msg.message.level;
if level == escargot::format::diagnostic::DiagnosticLevel::Ice
|| level == escargot::format::diagnostic::DiagnosticLevel::Error
{
let output = msg
.message
.rendered
.as_deref()
.unwrap_or_else(|| msg.message.message.as_ref())
.to_owned();
if is_example_target(&msg.target) {
let bin = crate::schema::Bin::Error(crate::Error::new(output));
Some(Ok((msg.target.name.as_ref(), bin)))
} else {
Some(Err(crate::Error::new(output)))
}
} else {
None
}
}
escargot::format::Message::CompilerArtifact(artifact) => {
if !artifact.profile.test && is_example_target(&artifact.target) {
let path = artifact
.executable
.clone()
.expect("cargo is new enough for this to be present");
let bin = crate::schema::Bin::Path(path.into_owned());
Some(Ok((artifact.target.name.as_ref(), bin)))
} else {
None
}
}
_ => None,
}
}
fn is_example_target(target: &escargot::format::Target) -> bool {
target.crate_types == ["bin"] && target.kind == ["example"]
}
}
/// Look up the path to a cargo-built binary within an integration test.
///
/// **NOTE:** Prefer `trycmd::cargo_bin!` as this makes assumptions about cargo
pub(crate) fn cargo_bin(name: &str) -> std::path::PathBuf {
let file_name = format!("{}{}", name, std::env::consts::EXE_SUFFIX);
let target_dir = target_dir();
target_dir.join(&file_name)
}
// Adapted from
// https://github.com/rust-lang/cargo/blob/485670b3983b52289a2f353d589c57fae2f60f82/tests/testsuite/support/mod.rs#L507
fn target_dir() -> std::path::PathBuf {
std::env::current_exe()
.ok()
.map(|mut path| {
path.pop();
if path.ends_with("deps") {
path.pop();
}
path
})
.unwrap()
}