diff --git a/src/cargo/ops/cargo_run.rs b/src/cargo/ops/cargo_run.rs index 170e038852b..2e136bb17e1 100644 --- a/src/cargo/ops/cargo_run.rs +++ b/src/cargo/ops/cargo_run.rs @@ -1,7 +1,8 @@ use std::path::Path; -use ops::{self, CompileFilter, Packages}; +use ops::{self, CompileFilter, Packages, MessageFormat}; use util::{self, human, CargoResult, ProcessError}; +use util::machine_message::{self, RunProfile}; use core::Workspace; pub fn run(ws: &Workspace, @@ -64,6 +65,14 @@ pub fn run(ws: &Workspace, let mut process = compile.target_process(exe, &pkg)?; process.args(args).cwd(config.cwd()); - config.shell().status("Running", process.to_string())?; - Ok(process.exec_replace().err()) + if config.print_run() { + if options.message_format != MessageFormat::Json { + bail!("CARGO_PRINT_RUN requires --message-format=json") + } + machine_message::emit(RunProfile::new(&process)); + Ok(None) + } else { + config.shell().status("Running", process.to_string())?; + Ok(process.exec_replace().err()) + } } diff --git a/src/cargo/ops/cargo_test.rs b/src/cargo/ops/cargo_test.rs index 1de4370908d..d7e3b064273 100644 --- a/src/cargo/ops/cargo_test.rs +++ b/src/cargo/ops/cargo_test.rs @@ -1,7 +1,8 @@ use std::ffi::{OsString, OsStr}; -use ops::{self, Compilation}; +use ops::{self, Compilation, MessageFormat}; use util::{self, CargoResult, CargoTestError, Test, ProcessError}; +use util::machine_message::{self, RunProfile}; use core::Workspace; pub struct TestOptions<'a> { @@ -95,14 +96,22 @@ fn run_unit_tests(options: &TestOptions, config.shell().concise(|shell| { shell.status("Running", to_display.display().to_string()) })?; - config.shell().verbose(|shell| { - shell.status("Running", cmd.to_string()) - })?; - if let Err(e) = cmd.exec() { - errors.push(e); - if !options.no_fail_fast { - return Ok((Test::UnitTest(kind.clone(), test.clone()), errors)) + if config.print_run() { + if options.compile_opts.message_format != MessageFormat::Json { + bail!("CARGO_PRINT_RUN requires --message-format=json") + } + machine_message::emit(RunProfile::new(&cmd)); + } else { + config.shell().verbose(|shell| { + shell.status("Running", cmd.to_string()) + })?; + + if let Err(e) = cmd.exec() { + errors.push(e); + if !options.no_fail_fast { + return Ok((Test::UnitTest(kind.clone(), test.clone()), errors)) + } } } } @@ -116,8 +125,9 @@ fn run_doc_tests(options: &TestOptions, let mut errors = Vec::new(); let config = options.compile_opts.config; - // We don't build/rust doctests if target != host - if config.rustc()?.host != compilation.target { + // We don't build/rust doctests if target != host or + // if we are not supposed to run binaries. + if config.rustc()?.host != compilation.target || config.print_run() { return Ok((Test::Doc, errors)); } diff --git a/src/cargo/util/config.rs b/src/cargo/util/config.rs index a715306637c..0295a575781 100644 --- a/src/cargo/util/config.rs +++ b/src/cargo/util/config.rs @@ -33,6 +33,10 @@ pub struct Config { extra_verbose: Cell, frozen: Cell, locked: Cell, + /// When this is set, Cargo does not run binaries + /// during `cargo run`/`cargo test`. Instead it + /// prints what it should have run otherwise. + print_run: Cell, } impl Config { @@ -50,6 +54,7 @@ impl Config { extra_verbose: Cell::new(false), frozen: Cell::new(false), locked: Cell::new(false), + print_run: Cell::new(false), } } @@ -361,6 +366,7 @@ impl Config { self.extra_verbose.set(extra_verbose); self.frozen.set(frozen); self.locked.set(locked); + self.print_run.set(env::var("CARGO_PRINT_RUN").is_ok()); Ok(()) } @@ -377,6 +383,10 @@ impl Config { !self.frozen.get() && !self.locked.get() } + pub fn print_run(&self) -> bool { + self.print_run.get() + } + fn load_values(&self) -> CargoResult> { let mut cfg = CV::Table(HashMap::new(), PathBuf::from(".")); diff --git a/src/cargo/util/machine_message.rs b/src/cargo/util/machine_message.rs index 1d4f33a86da..2a27c381bbb 100644 --- a/src/cargo/util/machine_message.rs +++ b/src/cargo/util/machine_message.rs @@ -1,7 +1,12 @@ +use std::path::{PathBuf, Path}; +use std::collections::HashMap; +use std::borrow::Cow; + use serde::ser; use serde_json::{self, Value}; use core::{PackageId, Target, Profile}; +use util::ProcessBuilder; pub trait Message: ser::Serialize { fn reason(&self) -> &str; @@ -55,3 +60,36 @@ impl<'a> Message for BuildScript<'a> { "build-script-executed" } } + +#[derive(Serialize)] +pub struct RunProfile<'a> { + pub program:PathBuf, + pub args: Vec>, + pub env: HashMap<&'a str, Option>>, + pub cwd: Option<&'a Path>, +} + +impl<'a> RunProfile<'a> { + pub fn new(process: &ProcessBuilder) -> RunProfile { + let program = if let Some(cwd) = process.get_cwd() { + cwd.join(process.get_program()) + } else { + PathBuf::from(process.get_program()) + }; + assert!(program.is_absolute(), "Running program by relative path without cwd"); + RunProfile { + program: program, + args: process.get_args().iter().map(|s| s.to_string_lossy()).collect(), + env: process.get_envs().iter().map(|(k, v)| { + (k.as_str(), v.as_ref().map(|s| s.to_string_lossy())) + }).collect(), + cwd: process.get_cwd(), + } + } +} + +impl<'a> Message for RunProfile<'a> { + fn reason(&self) -> &str { + "run-profile" + } +} diff --git a/src/cargo/util/process_builder.rs b/src/cargo/util/process_builder.rs index 26f872ed459..f8b32f83f27 100644 --- a/src/cargo/util/process_builder.rs +++ b/src/cargo/util/process_builder.rs @@ -57,6 +57,10 @@ impl ProcessBuilder { self } + pub fn get_program(&self) -> &OsString { + &self.program + } + pub fn get_args(&self) -> &[OsString] { &self.args } diff --git a/src/doc/environment-variables.md b/src/doc/environment-variables.md index 97bdb38cb51..290dabfd744 100644 --- a/src/doc/environment-variables.md +++ b/src/doc/environment-variables.md @@ -28,6 +28,13 @@ configuration values, as described in [that documentation][config-env] [config-env]: config.html#environment-variables +There are special environmental variables which are not read from `.cargo/config`: + +* `CARGO_PRINT_RUN` - If set, Cargo print information about binaries it is about to + run during `cargo test` and `cargo run`, without actually executing the binaries. + This is intended for integration with tools wrapping Cargo and IDEs. + + # Environment variables Cargo sets for crates Cargo exposes these environment variables to your crate when it is compiled. diff --git a/tests/cargotest/support/mod.rs b/tests/cargotest/support/mod.rs index 2e3e6d21c8c..df3fd859f14 100644 --- a/tests/cargotest/support/mod.rs +++ b/tests/cargotest/support/mod.rs @@ -426,13 +426,14 @@ impl Execs { } if let Some(ref objects) = self.expect_json { - let lines = match str::from_utf8(&actual.stdout) { + let stdout = match str::from_utf8(&actual.stdout) { Err(..) => return Err("stdout was not utf8 encoded".to_owned()), - Ok(stdout) => stdout.lines().collect::>(), + Ok(stdout) => stdout, }; + let lines = stdout.lines().collect::>();; if lines.len() != objects.len() { - return Err(format!("expected {} json lines, got {}", - objects.len(), lines.len())); + return Err(format!("expected {} json lines, got {}:\n {}", + objects.len(), lines.len(), stdout)); } for (obj, line) in objects.iter().zip(lines) { self.match_json(obj, line)?; diff --git a/tests/print_run.rs b/tests/print_run.rs new file mode 100644 index 00000000000..07958ca99db --- /dev/null +++ b/tests/print_run.rs @@ -0,0 +1,168 @@ +extern crate cargo; +extern crate cargotest; +extern crate hamcrest; + +use cargotest::support::{project, execs}; +use hamcrest::{assert_that, existing_file}; + +#[test] +fn single_bin() { + let p = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.0.1" + authors = [] + "#) + .file("src/main.rs", r#" + fn main() { println!("hello"); } + "#); + + assert_that(p.cargo_process("run").env("CARGO_PRINT_RUN", "1").arg("--message-format=json"), + execs().with_status(0) + .with_stderr(&"\ +[COMPILING] foo v0.0.1 ([..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]") + .with_json(r#" +{ + "features": [], + "filenames": ["[..]target[/]debug[/]foo"], + "fresh": false, + "package_id": "foo 0.0.1 ([..])", + "profile": "{...}", + "reason": "compiler-artifact", + "target": "{...}" +} + +{ + "reason": "run-profile", + "program": "[..][/]foo[/]target[/]debug[/]foo", + "env": { + "LD_LIBRARY_PATH": "[..]", + "CARGO_PKG_VERSION_PRE": "", + "CARGO_PKG_VERSION_PATCH": "1", + "CARGO_PKG_VERSION_MINOR": "0", + "CARGO_PKG_VERSION_MAJOR": "0", + "CARGO_PKG_VERSION": "0.0.1", + "CARGO_PKG_NAME": "foo", + "CARGO_PKG_HOMEPAGE": "", + "CARGO_PKG_DESCRIPTION": "", + "CARGO_PKG_AUTHORS": "", + "CARGO_MANIFEST_DIR": "[..]", + "CARGO": "[..]" + }, + "cwd": "[..]", + "args": [] +} +"#)); + assert_that(&p.bin("foo"), existing_file()); +} + +#[test] +fn several_tests() { + let p = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.0.1" + authors = [] + "#) + .file("src/lib.rs", r#" + /// Doctest are not executed with wrap + /// ``` + /// assert!(false); + /// ``` + pub fn f() { } + + #[test] fn test_in_lib() {} + "#) + .file("tests/bar.rs", r#" + #[test] fn test_bar() {} + "#) + .file("tests/baz.rs", r#" + #[test] fn test_baz() {} + "#); + + let env = r#"{ + "LD_LIBRARY_PATH": "[..]", + "CARGO_PKG_VERSION_PRE": "", + "CARGO_PKG_VERSION_PATCH": "1", + "CARGO_PKG_VERSION_MINOR": "0", + "CARGO_PKG_VERSION_MAJOR": "0", + "CARGO_PKG_VERSION": "0.0.1", + "CARGO_PKG_NAME": "foo", + "CARGO_PKG_HOMEPAGE": "", + "CARGO_PKG_DESCRIPTION": "", + "CARGO_PKG_AUTHORS": "", + "CARGO_MANIFEST_DIR": "[..]", + "CARGO": "[..]" + }"#; + let artifact = r#"{ + "features": [], + "filenames": "{...}", + "fresh": false, + "package_id": "foo 0.0.1 ([..])", + "profile": "{...}", + "reason": "compiler-artifact", + "target": "{...}" +}"#; + assert_that(p.cargo_process("test").env("CARGO_PRINT_RUN", "1").arg("--message-format=json"), + execs().with_status(0) + .with_json(&format!(r#" +{artifact} + +{artifact} + +{artifact} + +{artifact} + +{{ + "reason": "run-profile", + "program": "[..]bar-[..]", + "env": {env}, + "cwd": "[..]", + "args": [] +}} + +{{ + "reason": "run-profile", + "program": "[..]baz-[..]", + "env": {env}, + "cwd": "[..]", + "args": [] +}} + +{{ + "reason": "run-profile", + "program": "[..]foo-[..]", + "env": {env}, + "cwd": "[..]", + "args": [] +}} +"#, env=env, artifact=artifact))); +} + +#[test] +fn print_run_needs_json() { + let p = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.0.1" + authors = [] + "#) + .file("src/main.rs", r#" + fn main() { println!("hello"); } + "#); + p.build(); + + assert_that(p.cargo("run").env("CARGO_PRINT_RUN", "1"), + execs().with_status(101).with_stderr_contains("\ +[ERROR] CARGO_PRINT_RUN requires --message-format=json")); + + assert_that(p.cargo("test").env("CARGO_PRINT_RUN", "1"), + execs().with_status(101).with_stderr_contains("\ +[ERROR] CARGO_PRINT_RUN requires --message-format=json")); + +} \ No newline at end of file