Add cargo {test,bench} -p <spec>

This functionality allows running tests and benchmarks on any upstream
dependencies in the dependency graph. This is most useful for path sources all
developed in tandem (see Servo for instance).

In terms of built artifacts, this will actually preserve as many artifacts as
possible. That means that if you test a low-level dependency with the high-level
artifacts already built, the high-level artifacts will not get removed. This
means that it's possible to accidentally have a low-level dependency to depend
on a higher level one just because it's lib is picked up via -L, but this is
generally a necessary evil to get testing to not rebuild packages too often.

Closes #483
This commit is contained in:
Alex Crichton 2014-09-23 18:10:27 -07:00
parent 69bc3d055a
commit f97cef0c30
11 changed files with 195 additions and 63 deletions

View file

@ -13,20 +13,27 @@ Usage:
cargo bench [options] [--] [<args>...]
Options:
-h, --help Print this message
--no-run Compile, but don't run benchmarks
-j N, --jobs N The number of jobs to run in parallel
--features FEATURES Space-separated list of features to also build
--no-default-features Do not build the `default` feature
--target TRIPLE Build for the target triple
--manifest-path PATH Path to the manifest to build benchmarks for
-v, --verbose Use verbose output
-h, --help Print this message
--no-run Compile, but don't run benchmarks
-p SPEC, --package SPEC Package to run benchmarks for
-j N, --jobs N The number of jobs to run in parallel
--features FEATURES Space-separated list of features to also build
--no-default-features Do not build the `default` feature
--target TRIPLE Build for the target triple
--manifest-path PATH Path to the manifest to build benchmarks for
-v, --verbose Use verbose output
All of the trailing arguments are passed to the benchmark binaries generated
for filtering benchmarks and generally providing options configuring how they
run.
If the --package argument is given, then SPEC is a package id specification
which indicates which package should be benchmarked. If it is not given, then
the current package is benchmarked. For more information on SPEC and its format,
see the `cargo help pkgid` command.
", flag_jobs: Option<uint>, flag_target: Option<String>,
flag_manifest_path: Option<String>, flag_features: Vec<String>)
flag_manifest_path: Option<String>, flag_features: Vec<String>,
flag_package: Option<String>)
pub fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
let root = try!(find_root_manifest_for_cwd(options.flag_manifest_path));
@ -42,7 +49,7 @@ pub fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>
dev_deps: true,
features: options.flag_features.as_slice(),
no_default_features: options.flag_no_default_features,
spec: None,
spec: options.flag_package.as_ref().map(|s| s.as_slice()),
},
};

View file

@ -11,26 +11,26 @@ docopt!(Options, "
Compile a local package and all of its dependencies
Usage:
cargo build [options] [<spec>]
cargo build [options]
Options:
-h, --help Print this message
-j N, --jobs N The number of jobs to run in parallel
--release Build artifacts in release mode, with optimizations
--features FEATURES Space-separated list of features to also build
--no-default-features Do not build the `default` feature
--target TRIPLE Build for the target triple
--manifest-path PATH Path to the manifest to compile
-v, --verbose Use verbose output
-h, --help Print this message
-p SPEC, --package SPEC Package to run benchmarks for
-j N, --jobs N The number of jobs to run in parallel
--release Build artifacts in release mode, with optimizations
--features FEATURES Space-separated list of features to also build
--no-default-features Do not build the `default` feature
--target TRIPLE Build for the target triple
--manifest-path PATH Path to the manifest to compile
-v, --verbose Use verbose output
If <spec> is given, then only the package specified will be build (along with
all its dependencies). If <spec> is not given, then the current package will be
built.
For more information about the format of <spec>, see `cargo help pkgid`.
If the --package argument is given, then SPEC is a package id specification
which indicates which package should be built. If it is not given, then the
current package built tested. For more information on SPEC and its format, see the
`cargo help pkgid` command.
", flag_jobs: Option<uint>, flag_target: Option<String>,
flag_manifest_path: Option<String>, flag_features: Vec<String>,
arg_spec: Option<String>)
flag_package: Option<String>)
pub fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
debug!("executing; cmd=cargo-build; args={}", os::args());
@ -52,7 +52,7 @@ pub fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>
dev_deps: false,
features: options.flag_features.as_slice(),
no_default_features: options.flag_no_default_features,
spec: options.arg_spec.as_ref().map(|s| s.as_slice()),
spec: options.flag_package.as_ref().map(|s| s.as_slice()),
};
ops::compile(&root, &mut opts).map(|_| None).map_err(|err| {

View file

@ -10,21 +10,20 @@ docopt!(Options, "
Remove artifacts that cargo has generated in the past
Usage:
cargo clean [options] [<spec>]
cargo clean [options]
Options:
-h, --help Print this message
--manifest-path PATH Path to the manifest to the package to clean
--target TRIPLE Target triple to clean output for (default all)
-v, --verbose Use verbose output
-h, --help Print this message
-p SPEC, --package SPEC Package to run benchmarks for
--manifest-path PATH Path to the manifest to the package to clean
--target TRIPLE Target triple to clean output for (default all)
-v, --verbose Use verbose output
If <spec> is provided, then it is interpreted as a package id specification and
only the output for the package specified will be removed. If <spec> is not
provided, then all output from cargo will be cleaned out. Note that a lockfile
must exist for <spec> to be given.
For more information about <spec>, see `cargo help pkgid`.
", flag_manifest_path: Option<String>, arg_spec: Option<String>,
If the --package argument is given, then SPEC is a package id specification
which indicates which package's artifacts should be cleaned out. If it is not
given, then all packages' artifacts are removed. For more information on SPEC
and its format, see the `cargo help pkgid` command.
", flag_manifest_path: Option<String>, flag_package: Option<String>,
flag_target: Option<String>)
pub fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
@ -34,7 +33,7 @@ pub fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>
let root = try!(find_root_manifest_for_cwd(options.flag_manifest_path));
let mut opts = ops::CleanOptions {
shell: shell,
spec: options.arg_spec.as_ref().map(|s| s.as_slice()),
spec: options.flag_package.as_ref().map(|s| s.as_slice()),
target: options.flag_target.as_ref().map(|s| s.as_slice()),
};
ops::clean(&root, &mut opts).map(|_| None).map_err(|err| {

View file

@ -13,19 +13,26 @@ Usage:
cargo test [options] [--] [<args>...]
Options:
-h, --help Print this message
--no-run Compile, but don't run tests
-j N, --jobs N The number of jobs to run in parallel
--features FEATURES Space-separated list of features to also build
--no-default-features Do not build the `default` feature
--target TRIPLE Build for the target triple
--manifest-path PATH Path to the manifest to build tests for
-v, --verbose Use verbose output
-h, --help Print this message
--no-run Compile, but don't run tests
-p SPEC, --package SPEC Package to run tests for
-j N, --jobs N The number of jobs to run in parallel
--features FEATURES Space-separated list of features to also build
--no-default-features Do not build the `default` feature
--target TRIPLE Build for the target triple
--manifest-path PATH Path to the manifest to build tests for
-v, --verbose Use verbose output
All of the trailing arguments are passed to the test binaries generated for
filtering tests and generally providing options configuring how they run.
If the --package argument is given, then SPEC is a package id specification
which indicates which package should be tested. If it is not given, then the
current package is tested. For more information on SPEC and its format, see the
`cargo help pkgid` command.
", flag_jobs: Option<uint>, flag_target: Option<String>,
flag_manifest_path: Option<String>, flag_features: Vec<String>)
flag_manifest_path: Option<String>, flag_features: Vec<String>,
flag_package: Option<String>)
pub fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
let root = try!(find_root_manifest_for_cwd(options.flag_manifest_path));
@ -41,7 +48,7 @@ pub fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>
dev_deps: true,
features: options.flag_features.as_slice(),
no_default_features: options.flag_no_default_features,
spec: None,
spec: options.flag_package.as_ref().map(|s| s.as_slice()),
},
};

View file

@ -56,6 +56,11 @@ pub fn compile(manifest_path: &Path,
log!(4, "compile; manifest-path={}", manifest_path.display());
if spec.is_some() && (no_default_features || features.len() > 0) {
return Err(human("features cannot be modified when the main package \
is not being built"))
}
let mut source = try!(PathSource::for_path(&manifest_path.dir_path()));
try!(source.update());

View file

@ -15,6 +15,17 @@ impl Job {
Job { dirty: dirty, fresh: fresh, desc: desc }
}
/// Create a new job which will run `fresh` if the job is fresh and
/// otherwise not run `dirty`.
///
/// Retains the same signature as `new` for compatibility. This job does not
/// describe itself to the console.
pub fn noop(_dirty: proc():Send -> CargoResult<()>,
fresh: proc():Send -> CargoResult<()>,
_desc: String) -> Job {
Job { dirty: proc() Ok(()), fresh: fresh, desc: String::new() }
}
/// Consumes this job by running it, returning the result of the
/// computation.
pub fn run(self, fresh: Freshness) -> CargoResult<()> {

View file

@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::collections::hashmap::{Occupied, Vacant};
use std::collections::hashmap::{HashMap, HashSet, Occupied, Vacant};
use term::color::YELLOW;
use core::{Package, PackageId, Resolve};
@ -23,6 +22,7 @@ pub struct JobQueue<'a, 'b> {
active: uint,
pending: HashMap<(&'a PackageId, TargetStage), PendingBuild>,
state: HashMap<&'a PackageId, Freshness>,
ignored: HashSet<&'a PackageId>,
}
/// A helper structure for metadata about the state of a building package.
@ -66,6 +66,7 @@ impl<'a, 'b> JobQueue<'a, 'b> {
active: 0,
pending: HashMap::new(),
state: HashMap::new(),
ignored: HashSet::new(),
}
}
@ -85,6 +86,10 @@ impl<'a, 'b> JobQueue<'a, 'b> {
(pkg, jobs));
}
pub fn ignore(&mut self, pkg: &'a Package) {
self.ignored.insert(pkg.get_package_id());
}
/// Execute all jobs necessary to build the dependency graph.
///
/// This function will spawn off `config.jobs()` workers to build all of the
@ -149,7 +154,7 @@ impl<'a, 'b> JobQueue<'a, 'b> {
let amt = if njobs == 0 {1} else {njobs};
let id = pkg.get_package_id().clone();
if stage == StageStart {
if stage == StageStart && !self.ignored.contains(&pkg.get_package_id()) {
match fresh.combine(self.state[pkg.get_package_id()]) {
Fresh => try!(config.shell().verbose(|c| {
c.status("Fresh", pkg)

View file

@ -77,30 +77,30 @@ pub fn compile_targets<'a>(env: &str, targets: &[&'a Target], pkg: &'a Package,
// particular package. No actual work is executed as part of this, that's
// all done later as part of the `execute` function which will run
// everything in order with proper parallelism.
let mut dep_pkgids = HashSet::new();
let mut compiled = HashSet::new();
each_dep(pkg, &cx, |dep| {
dep_pkgids.insert(dep.get_package_id().clone());
compiled.insert(dep.get_package_id().clone());
});
for dep in deps.iter() {
if dep == pkg { continue }
if !dep_pkgids.contains(dep.get_package_id()) { continue }
// Only compile lib targets for dependencies
let targets = dep.get_targets().iter().filter(|target| {
cx.is_relevant_target(*target)
}).collect::<Vec<&Target>>();
if targets.len() == 0 {
if targets.len() == 0 && dep.get_package_id() != resolve.root() {
return Err(human(format!("Package `{}` has no library targets", dep)))
}
try!(compile(targets.as_slice(), dep, &mut cx, &mut queue));
let compiled = compiled.contains(dep.get_package_id());
try!(compile(targets.as_slice(), dep, compiled, &mut cx, &mut queue));
}
if pkg.get_package_id() == resolve.root() {
cx.primary();
}
try!(compile(targets, pkg, &mut cx, &mut queue));
try!(compile(targets, pkg, true, &mut cx, &mut queue));
// Now that we've figured out everything that we're going to do, do it!
try!(queue.execute(cx.config));
@ -109,11 +109,19 @@ pub fn compile_targets<'a>(env: &str, targets: &[&'a Target], pkg: &'a Package,
}
fn compile<'a, 'b>(targets: &[&'a Target], pkg: &'a Package,
compiled: bool,
cx: &mut Context<'a, 'b>,
jobs: &mut JobQueue<'a, 'b>) -> CargoResult<()> {
debug!("compile_pkg; pkg={}; targets={}", pkg, targets);
let _p = profile::start(format!("preparing: {}", pkg));
// Packages/targets which are actually getting compiled are constructed into
// a real job. Packages which are *not* compiled still have their jobs
// executed, but only if the work is fresh. This is to preserve their
// artifacts if any exist.
let job = if compiled {Job::new} else {Job::noop};
if !compiled { jobs.ignore(pkg); }
if targets.is_empty() {
return Ok(())
}
@ -145,7 +153,7 @@ fn compile<'a, 'b>(targets: &[&'a Target], pkg: &'a Package,
for cmd in build_cmds.into_iter() { try!(cmd()) }
dirty()
};
jobs.enqueue(pkg, StageCustomBuild, vec![(Job::new(dirty, fresh, desc),
jobs.enqueue(pkg, StageCustomBuild, vec![(job(dirty, fresh, desc),
freshness)]);
// After the custom command has run, execute rustc for all targets of our
@ -169,7 +177,7 @@ fn compile<'a, 'b>(targets: &[&'a Target], pkg: &'a Package,
try!(fingerprint::prepare_target(cx, pkg, target, kind));
let dirty = proc() { try!(work()); dirty() };
dst.push((Job::new(dirty, fresh, desc), freshness));
dst.push((job(dirty, fresh, desc), freshness));
}
}
jobs.enqueue(pkg, StageLibraries, libs);

View file

@ -607,7 +607,8 @@ test!(recompilation {
COMPILING, p.url())));
// Make sure clean only cleans one dep
assert_that(p.process(cargo_dir().join("cargo")).arg("clean").arg("foo"),
assert_that(p.process(cargo_dir().join("cargo")).arg("clean")
.arg("-p").arg("foo"),
execs().with_stdout(""));
assert_that(p.process(cargo_dir().join("cargo")).arg("build"),
execs().with_stdout(format!("{} foo v0.5.0 ({})\n",

View file

@ -88,11 +88,13 @@ test!(cargo_compile_with_nested_deps_shorthand {
assert_that(p.process(cargo_dir().join("cargo")).arg("clean"),
execs().with_stdout(""));
println!("building baz");
assert_that(p.process(cargo_dir().join("cargo")).arg("build").arg("baz"),
assert_that(p.process(cargo_dir().join("cargo")).arg("build")
.arg("-p").arg("baz"),
execs().with_stdout(format!("{} baz v0.5.0 ({})\n",
COMPILING, p.url())));
println!("building foo");
assert_that(p.process(cargo_dir().join("cargo")).arg("build").arg("foo"),
assert_that(p.process(cargo_dir().join("cargo")).arg("build")
.arg("-p").arg("foo"),
execs().with_stdout(format!("{} bar v0.5.0 ({})\n\
{} foo v0.5.0 ({})\n",
COMPILING, p.url(),

View file

@ -896,3 +896,90 @@ test!(test_no_harness {
compiling = COMPILING, running = RUNNING,
dir = p.url()).as_slice()));
})
test!(selective_testing {
let p = project("foo")
.file("Cargo.toml", r#"
[package]
name = "foo"
version = "0.0.1"
authors = []
[dependencies.d1]
path = "d1"
[dependencies.d2]
path = "d2"
[lib]
name = "foo"
doctest = false
"#)
.file("src/lib.rs", "")
.file("d1/Cargo.toml", r#"
[package]
name = "d1"
version = "0.0.1"
authors = []
[lib]
name = "d1"
doctest = false
"#)
.file("d1/src/lib.rs", "")
.file("d2/Cargo.toml", r#"
[package]
name = "d2"
version = "0.0.1"
authors = []
[lib]
name = "d2"
doctest = false
"#)
.file("d2/src/lib.rs", "");
p.build();
println!("d1");
assert_that(p.process(cargo_dir().join("cargo")).arg("test")
.arg("-p").arg("d1"),
execs().with_status(0)
.with_stderr("")
.with_stdout(format!("\
{compiling} d1 v0.0.1 ({dir})
{running} target[..]d1-[..]
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured\n
", compiling = COMPILING, running = RUNNING,
dir = p.url()).as_slice()));
println!("d2");
assert_that(p.process(cargo_dir().join("cargo")).arg("test")
.arg("-p").arg("d2"),
execs().with_status(0)
.with_stderr("")
.with_stdout(format!("\
{compiling} d2 v0.0.1 ({dir})
{running} target[..]d2-[..]
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured\n
", compiling = COMPILING, running = RUNNING,
dir = p.url()).as_slice()));
println!("whole");
assert_that(p.process(cargo_dir().join("cargo")).arg("test"),
execs().with_status(0)
.with_stderr("")
.with_stdout(format!("\
{compiling} foo v0.0.1 ({dir})
{running} target[..]foo-[..]
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured\n
", compiling = COMPILING, running = RUNNING,
dir = p.url()).as_slice()));
})