From 7a81c5ff0ebc4afbe7e75e219aad3f470f6e1b11 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 25 May 2018 14:01:29 +0200 Subject: [PATCH 1/2] test: Assert::example --- Cargo.toml | 2 +- .../example_fixture.rs | 0 src/bin/bin_fixture.rs | 36 +++++++++++++++++++ tests/cargo.rs | 14 +++++++- 4 files changed, 50 insertions(+), 2 deletions(-) rename src/bin/assert_fixture.rs => examples/example_fixture.rs (100%) create mode 100644 src/bin/bin_fixture.rs diff --git a/Cargo.toml b/Cargo.toml index 3ef4098..a1d7f74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ categories = ["development-tools::testing"] keywords = ["cli", "testing", "assert"] [[bin]] -name = "assert_fixture" +name = "bin_fixture" [dependencies] colored = "1.5" diff --git a/src/bin/assert_fixture.rs b/examples/example_fixture.rs similarity index 100% rename from src/bin/assert_fixture.rs rename to examples/example_fixture.rs diff --git a/src/bin/bin_fixture.rs b/src/bin/bin_fixture.rs new file mode 100644 index 0000000..66a0fb4 --- /dev/null +++ b/src/bin/bin_fixture.rs @@ -0,0 +1,36 @@ +extern crate failure; + +use std::env; +use std::io; +use std::io::Write; +use std::process; + +use failure::ResultExt; + +fn run() -> Result<(), failure::Error> { + if let Ok(text) = env::var("stdout") { + println!("{}", text); + } + if let Ok(text) = env::var("stderr") { + eprintln!("{}", text); + } + + let code = env::var("exit") + .ok() + .map(|v| v.parse::()) + .map_or(Ok(None), |r| r.map(Some)) + .context("Invalid exit code")? + .unwrap_or(0); + process::exit(code); +} + +fn main() { + let code = match run() { + Ok(_) => 0, + Err(ref e) => { + write!(&mut io::stderr(), "{}", e).expect("writing to stderr won't fail"); + 1 + } + }; + process::exit(code); +} diff --git a/tests/cargo.rs b/tests/cargo.rs index f476ee1..76698e4 100644 --- a/tests/cargo.rs +++ b/tests/cargo.rs @@ -13,7 +13,19 @@ fn main_binary() { #[test] fn cargo_binary() { - assert_cli::Assert::cargo_binary("assert_fixture") + assert_cli::Assert::cargo_binary("bin_fixture") + .with_env(assert_cli::Environment::inherit().insert("stdout", "42")) + .stdout() + .is("42") + .stderr() + .is("") + .unwrap(); +} + +#[test] +fn cargo_example() { + assert_cli::Assert::example("example_fixture") + .unwrap() .with_env(assert_cli::Environment::inherit().insert("stdout", "42")) .stdout() .is("42") From d892f965b4517d770d77a2520c8c7071067a96f0 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 25 May 2018 13:34:53 +0200 Subject: [PATCH 2/2] fix(cargo): Avoid `cargo run` This switches us from using `cargo run` to `cargo build`, reading where the binary is placed, and callin that instead. Fixes #95 because the user changing the `CWD` doesn't impact `cargo build`. Fixes #79 because there is no `cargo` output when capturing the user's stdout/stderr. Fixes #51 because the user changing the environment doesn't impact `cargo build`. This is a step towards working around #100 because we can allow a user to cache the result of `cargo build`. --- Cargo.toml | 1 + build.rs | 16 +++++ src/assert.rs | 187 ++++++++++++++++++++++--------------------------- src/cargo.rs | 115 ++++++++++++++++++++++++++++++ src/lib.rs | 3 + tests/cargo.rs | 38 ++++++++++ 6 files changed, 256 insertions(+), 104 deletions(-) create mode 100644 build.rs create mode 100644 src/cargo.rs diff --git a/Cargo.toml b/Cargo.toml index a1d7f74..0f60937 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ colored = "1.5" difference = "2.0" failure = "0.1" failure_derive = "0.1" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" environment = "0.1" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..88cbe33 --- /dev/null +++ b/build.rs @@ -0,0 +1,16 @@ +use std::env; +use std::fs; +use std::io::Write; +use std::path; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + // env::ARCH doesn't include full triple, and AFAIK there isn't a nicer way of getting the full triple + // (see lib.rs for the rest of this hack) + let out = path::PathBuf::from(env::var_os("OUT_DIR").expect("run within cargo")) + .join("current_target.txt"); + let default_target = env::var("TARGET").expect("run as cargo build script"); + let mut file = fs::File::create(out).unwrap(); + file.write_all(default_target.as_bytes()).unwrap(); +} diff --git a/src/assert.rs b/src/assert.rs index 5ab3b6b..f6bad73 100644 --- a/src/assert.rs +++ b/src/assert.rs @@ -1,4 +1,3 @@ -use std::default; use std::ffi::{OsStr, OsString}; use std::io::Write; use std::path::PathBuf; @@ -11,6 +10,35 @@ use failure::Fail; use errors::*; use output::{Content, Output, OutputKind, OutputPredicate}; +use cargo; + +#[derive(Deserialize)] +struct MessageTarget<'a> { + #[serde(borrow)] + crate_types: Vec<&'a str>, + #[serde(borrow)] + kind: Vec<&'a str>, +} + +#[derive(Deserialize)] +struct MessageFilter<'a> { + #[serde(borrow)] + reason: &'a str, + target: MessageTarget<'a>, + #[serde(borrow)] + filenames: Vec<&'a str>, +} + +fn filenames(msg: cargo::Message, kind: &str) -> Option { + let filter: MessageFilter = msg.convert().ok()?; + if filter.reason != "compiler-artifact" || filter.target.crate_types != ["bin"] + || filter.target.kind != [kind] + { + None + } else { + Some(filter.filenames[0].to_owned()) + } +} /// Assertions for a specific command. #[derive(Debug)] @@ -25,80 +53,72 @@ pub struct Assert { stdin_contents: Option>, } -impl default::Default for Assert { - /// Construct an assert using `cargo run --` as command. +impl Assert { + /// Run the crate's main binary. /// /// Defaults to asserting _successful_ execution. - fn default() -> Self { - Assert { - cmd: vec![ - "cargo", - "run", - #[cfg(not(debug_assertions))] - "--release", - "--quiet", - "--", - ].into_iter() - .map(OsString::from) - .collect(), + pub fn main_binary() -> Result { + let cargo = cargo::Cargo::new().build().current_release(); + let bins: Vec<_> = cargo.exec()?.filter_map(|m| filenames(m, "bin")).collect(); + if bins.is_empty() { + bail!("No binaries in crate"); + } else if bins.len() != 1 { + bail!("Ambiguous which binary is intended: {:?}", bins); + } + let bin = bins[0].as_str(); + let cmd = Self { + cmd: vec![bin].into_iter().map(OsString::from).collect(), env: Environment::inherit(), current_dir: None, expect_success: Some(true), expect_exit_code: None, expect_output: vec![], stdin_contents: None, - } - } -} - -impl Assert { - /// Run the crate's main binary. - /// - /// Defaults to asserting _successful_ execution. - pub fn main_binary() -> Self { - Assert::default() + }; + Ok(cmd) } /// Run a specific binary of the current crate. /// /// Defaults to asserting _successful_ execution. - pub fn cargo_binary>(name: S) -> Self { - Assert { - cmd: vec![ - OsStr::new("cargo"), - OsStr::new("run"), - #[cfg(not(debug_assertions))] - OsStr::new("--release"), - OsStr::new("--quiet"), - OsStr::new("--bin"), - name.as_ref(), - OsStr::new("--"), - ].into_iter() - .map(OsString::from) - .collect(), - ..Self::default() - } + pub fn cargo_binary>(name: S) -> Result { + let cargo = cargo::Cargo::new().build().bin(name).current_release(); + let bins: Vec<_> = cargo.exec()?.filter_map(|m| filenames(m, "bin")).collect(); + assert_eq!(bins.len(), 1); + let bin = bins[0].as_str(); + let cmd = Self { + cmd: vec![bin].into_iter().map(OsString::from).collect(), + env: Environment::inherit(), + current_dir: None, + expect_success: Some(true), + expect_exit_code: None, + expect_output: vec![], + stdin_contents: None, + }; + Ok(cmd) } /// Run a specific example of the current crate. /// /// Defaults to asserting _successful_ execution. - pub fn example>(name: S) -> Self { - Assert { - cmd: vec![ - OsStr::new("cargo"), - OsStr::new("run"), - #[cfg(not(debug_assertions))] - OsStr::new("--release"), - OsStr::new("--quiet"), - OsStr::new("--example"), - name.as_ref(), - OsStr::new("--"), - ].into_iter() - .map(OsString::from) - .collect(), - ..Self::default() - } + pub fn example>(name: S) -> Result { + let cargo = cargo::Cargo::new().build().example(name).current_release(); + let bins: Vec<_> = cargo + .exec()? + .filter_map(|m| filenames(m, "example")) + .collect(); + assert_eq!(bins.len(), 1); + let bin = bins[0].as_str(); + let cmd = Self { + cmd: vec![bin].into_iter().map(OsString::from).collect(), + env: Environment::inherit(), + current_dir: None, + expect_success: Some(true), + expect_exit_code: None, + expect_output: vec![], + stdin_contents: None, + }; + Ok(cmd) } /// Run a custom command. @@ -116,7 +136,12 @@ impl Assert { pub fn command>(cmd: &[S]) -> Self { Assert { cmd: cmd.into_iter().map(OsString::from).collect(), - ..Self::default() + env: Environment::inherit(), + current_dir: None, + expect_success: Some(true), + expect_exit_code: None, + expect_output: vec![], + stdin_contents: None, } } @@ -559,52 +584,6 @@ mod test { Assert::command(&["printenv"]) } - #[test] - fn main_binary_default_uses_active_profile() { - let assert = Assert::main_binary(); - - let expected = if cfg!(debug_assertions) { - OsString::from("cargo run --quiet -- ") - } else { - OsString::from("cargo run --release --quiet -- ") - }; - - assert_eq!( - expected, - assert - .cmd - .into_iter() - .fold(OsString::from(""), |mut cmd, token| { - cmd.push(token); - cmd.push(" "); - cmd - }) - ); - } - - #[test] - fn cargo_binary_default_uses_active_profile() { - let assert = Assert::cargo_binary("hello"); - - let expected = if cfg!(debug_assertions) { - OsString::from("cargo run --quiet --bin hello -- ") - } else { - OsString::from("cargo run --release --quiet --bin hello -- ") - }; - - assert_eq!( - expected, - assert - .cmd - .into_iter() - .fold(OsString::from(""), |mut cmd, token| { - cmd.push(token); - cmd.push(" "); - cmd - }) - ); - } - #[test] fn take_ownership() { let x = Environment::inherit(); diff --git a/src/cargo.rs b/src/cargo.rs new file mode 100644 index 0000000..b74e01b --- /dev/null +++ b/src/cargo.rs @@ -0,0 +1,115 @@ +use std::ffi; +use std::process; +use std::str; +use std::vec; + +use failure; +use serde; +use serde_json; + +const CURRENT_TARGET: &str = include_str!(concat!(env!("OUT_DIR"), "/current_target.txt")); + +#[derive(Debug)] +pub struct Cargo { + cmd: process::Command, +} + +impl Cargo { + pub fn new() -> Self { + Self { + cmd: process::Command::new("cargo"), + } + } + + pub fn arg>(mut self, arg: S) -> Self { + self.cmd.arg(arg); + self + } + + pub fn build(mut self) -> CargoBuild { + self.cmd.arg("build").arg("--message-format=json"); + CargoBuild { cmd: self.cmd } + } +} + +pub struct CargoBuild { + cmd: process::Command, +} + +impl CargoBuild { + pub fn new() -> Self { + Cargo::new().build() + } + + pub fn quiet(self) -> Self { + self.arg("--quiet") + } + + pub fn bin>(self, name: S) -> Self { + self.arg("--bin").arg(name) + } + + pub fn example>(self, name: S) -> Self { + self.arg("--example").arg(name) + } + + pub fn release(self) -> Self { + self.arg("--release") + } + + #[cfg(debug_assertions)] + pub fn current_release(self) -> Self { + self + } + + #[cfg(not(debug_assertions))] + pub fn current_release(self) -> Self { + self.release() + } + + pub fn target>(self, triplet: S) -> Self { + self.arg("--target").arg(triplet) + } + + pub fn current_taget(self) -> Self { + self.target(CURRENT_TARGET) + } + + pub fn arg>(mut self, arg: S) -> Self { + self.cmd.arg(arg); + self + } + + pub fn exec(mut self) -> Result { + let output = self.cmd.output()?; + if !output.status.success() { + bail!("{}", String::from_utf8_lossy(&output.stderr)); + } + + let messages: Vec = str::from_utf8(&output.stdout) + .expect("json to be UTF-8") + .split('\n') + .map(|s| Message { + content: s.to_owned(), + }) + .collect(); + + Ok(messages.into_iter()) + } +} + +pub type MessageItr = vec::IntoIter; + +pub struct Message { + content: String, +} + +impl Message { + pub fn convert<'a, T>(&'a self) -> Result + where + T: serde::Deserialize<'a>, + { + let data = serde_json::from_str(self.content.as_str())?; + Ok(data) + } +} diff --git a/src/lib.rs b/src/lib.rs index 9b055b4..54ea71a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,6 +124,8 @@ extern crate environment; extern crate failure; #[macro_use] extern crate failure_derive; +#[macro_use] +extern crate serde; extern crate serde_json; mod errors; @@ -134,6 +136,7 @@ mod macros; pub use macros::flatten_escaped_string; mod assert; +mod cargo; mod diff; mod output; diff --git a/tests/cargo.rs b/tests/cargo.rs index 76698e4..43de31f 100644 --- a/tests/cargo.rs +++ b/tests/cargo.rs @@ -3,6 +3,7 @@ extern crate assert_cli; #[test] fn main_binary() { assert_cli::Assert::main_binary() + .unwrap() .with_env(assert_cli::Environment::inherit().insert("stdout", "42")) .stdout() .is("42") @@ -11,9 +12,22 @@ fn main_binary() { .unwrap(); } +#[test] +fn main_binary_with_empty_env() { + assert_cli::Assert::main_binary() + .unwrap() + .with_env(assert_cli::Environment::empty().insert("stdout", "42")) + .stdout() + .is("42") + .stderr() + .is("") + .unwrap(); +} + #[test] fn cargo_binary() { assert_cli::Assert::cargo_binary("bin_fixture") + .unwrap() .with_env(assert_cli::Environment::inherit().insert("stdout", "42")) .stdout() .is("42") @@ -22,6 +36,18 @@ fn cargo_binary() { .unwrap(); } +#[test] +fn cargo_binary_with_empty_env() { + assert_cli::Assert::cargo_binary("bin_fixture") + .unwrap() + .with_env(assert_cli::Environment::empty().insert("stdout", "42")) + .stdout() + .is("42") + .stderr() + .is("") + .unwrap(); +} + #[test] fn cargo_example() { assert_cli::Assert::example("example_fixture") @@ -33,3 +59,15 @@ fn cargo_example() { .is("") .unwrap(); } + +#[test] +fn cargo_example_with_empty_env() { + assert_cli::Assert::example("example_fixture") + .unwrap() + .with_env(assert_cli::Environment::empty().insert("stdout", "42")) + .stdout() + .is("42") + .stderr() + .is("") + .unwrap(); +}