Skip to content

Allow to intercept binaries invoked by Cargo #3866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/cargo/ops/cargo_run.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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())
}
}
30 changes: 20 additions & 10 deletions src/cargo/ops/cargo_test.rs
Original file line number Diff line number Diff line change
@@ -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> {
Expand Down Expand Up @@ -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))
}
}
}
}
Expand All @@ -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));
}

Expand Down
10 changes: 10 additions & 0 deletions src/cargo/util/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ pub struct Config {
extra_verbose: Cell<bool>,
frozen: Cell<bool>,
locked: Cell<bool>,
/// 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<bool>,
}

impl Config {
Expand All @@ -50,6 +54,7 @@ impl Config {
extra_verbose: Cell::new(false),
frozen: Cell::new(false),
locked: Cell::new(false),
print_run: Cell::new(false),
}
}

Expand Down Expand Up @@ -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(())
}
Expand All @@ -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<HashMap<String, ConfigValue>> {
let mut cfg = CV::Table(HashMap::new(), PathBuf::from("."));

Expand Down
38 changes: 38 additions & 0 deletions src/cargo/util/machine_message.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Cow<'a, str>>,
pub env: HashMap<&'a str, Option<Cow<'a, str>>>,
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"
}
}
4 changes: 4 additions & 0 deletions src/cargo/util/process_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ impl ProcessBuilder {
self
}

pub fn get_program(&self) -> &OsString {
&self.program
}

pub fn get_args(&self) -> &[OsString] {
&self.args
}
Expand Down
7 changes: 7 additions & 0 deletions src/doc/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 5 additions & 4 deletions tests/cargotest/support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>(),
Ok(stdout) => stdout,
};
let lines = stdout.lines().collect::<Vec<_>>();;
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)?;
Expand Down
168 changes: 168 additions & 0 deletions tests/print_run.rs
Original file line number Diff line number Diff line change
@@ -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"));

}