From fec3ce921c8399a00cd31909216f94fbe0578be6 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sat, 5 Dec 2015 03:22:54 +0300 Subject: [PATCH 1/5] Introduce cargo metadata subcommand Most of the work was done by @dan-t in #1225 and by @winger in #1434 Fixes #2193 --- src/bin/cargo.rs | 1 + src/bin/metadata.rs | 65 +++++++++++++ src/cargo/ops/cargo_output_metadata.rs | 126 +++++++++++++++++++++++++ src/cargo/ops/mod.rs | 2 + tests/test_cargo_metadata.rs | 107 +++++++++++++++++++++ tests/tests.rs | 1 + 6 files changed, 302 insertions(+) create mode 100644 src/bin/metadata.rs create mode 100644 src/cargo/ops/cargo_output_metadata.rs create mode 100644 tests/test_cargo_metadata.rs diff --git a/src/bin/cargo.rs b/src/bin/cargo.rs index 62556279d59..cc095eb73e6 100644 --- a/src/bin/cargo.rs +++ b/src/bin/cargo.rs @@ -73,6 +73,7 @@ macro_rules! each_subcommand{ $mac!(install); $mac!(locate_project); $mac!(login); + $mac!(metadata); $mac!(new); $mac!(owner); $mac!(package); diff --git a/src/bin/metadata.rs b/src/bin/metadata.rs new file mode 100644 index 00000000000..41a4dbd83a8 --- /dev/null +++ b/src/bin/metadata.rs @@ -0,0 +1,65 @@ +extern crate cargo; +extern crate docopt; +extern crate rustc_serialize; +extern crate toml; + +use std::path::PathBuf; + +use cargo::ops::{output_metadata, OutputTo, OutputMetadataOptions}; +use cargo::util::important_paths::find_root_manifest_for_wd; +use cargo::util::{CliResult, CliError, Config}; + +#[derive(RustcDecodable)] +struct Options { + flag_features: Vec, + flag_manifest_path: Option, + flag_no_default_features: bool, + flag_output_format: String, + flag_output_path: Option, + flag_verbose: bool, + flag_quiet: bool, + flag_color: Option, +} + +pub const USAGE: &'static str = " +Output the resolved dependencies of a project, the concrete used versions +including overrides, in machine-readable format. + +Usage: + cargo metadata [options] + +Options: + -h, --help Print this message + -o, --output-path PATH Path the output is written to, otherwise stdout is used + -f, --output-format FMT Output format [default: toml] + Valid values: toml, json + --features FEATURES Space-separated list of features + --no-default-features Do not include the `default` feature + --manifest-path PATH Path to the manifest + -v, --verbose Use verbose output + -q, --quiet No output printed to stdout + --color WHEN Coloring: auto, always, never +"; + +pub fn execute(options: Options, config: &Config) -> CliResult> { + try!(config.shell().set_verbosity(options.flag_verbose, options.flag_quiet)); + try!(config.shell().set_color_config(options.flag_color.as_ref().map(|s| &s[..]))); + let manifest = try!(find_root_manifest_for_wd(options.flag_manifest_path, config.cwd())); + + let output_to = match options.flag_output_path { + Some(path) => OutputTo::File(PathBuf::from(path)), + None => OutputTo::StdOut + }; + + let options = OutputMetadataOptions { + features: options.flag_features, + manifest_path: &manifest, + no_default_features: options.flag_no_default_features, + output_format: options.flag_output_format, + output_to: output_to, + }; + + output_metadata(options, config) + .map(|_| None) + .map_err(|err| CliError::from_boxed(err, 101)) +} diff --git a/src/cargo/ops/cargo_output_metadata.rs b/src/cargo/ops/cargo_output_metadata.rs new file mode 100644 index 00000000000..38f084af294 --- /dev/null +++ b/src/cargo/ops/cargo_output_metadata.rs @@ -0,0 +1,126 @@ +use std::ascii::AsciiExt; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use core::resolver::Resolve; +use core::{Source, Package}; +use ops; +use rustc_serialize::json; +use sources::PathSource; +use toml; +use util::config::Config; +use util::{paths, CargoResult}; + + +/// Where the dependencies should be written to. +pub enum OutputTo { + File(PathBuf), + StdOut, +} + +pub struct OutputMetadataOptions<'a> { + pub features: Vec, + pub output_format: String, + pub output_to: OutputTo, + pub manifest_path: &'a Path, + pub no_default_features: bool, +} + +/// Loads the manifest, resolves the dependencies of the project to the concrete +/// used versions - considering overrides - and writes all dependencies in a TOML +/// format to stdout or the specified file. +/// +/// The TOML format is e.g.: +/// ```toml +/// root = "libA" +/// +/// [packages.libA] +/// dependencies = ["libB"] +/// path = "/home/user/.cargo/registry/src/jackfan.us.kg-1ecc6299db9ec823/libA-0.1" +/// version = "0.1" +/// +/// [packages.libB] +/// dependencies = [] +/// path = "/home/user/.cargo/registry/src/jackfan.us.kg-1ecc6299db9ec823/libB-0.4" +/// version = "0.4" +/// +/// [packages.libB.features] +/// featureA = ["featureB"] +/// featureB = [] +/// ``` +pub fn output_metadata(opt: OutputMetadataOptions, config: &Config) -> CargoResult<()> { + let deps = try!(resolve_dependencies(opt.manifest_path, + config, + opt.features, + opt.no_default_features)); + let (resolved_deps, packages) = deps; + + #[derive(RustcEncodable)] + struct RootPackageInfo<'a> { + name: &'a str, + version: String, + features: Option<&'a HashMap>>, + } + + #[derive(RustcEncodable)] + struct ExportInfo<'a> { + root: RootPackageInfo<'a>, + packages: Vec<&'a Package>, + } + + let mut output = ExportInfo { + root: RootPackageInfo { + name: resolved_deps.root().name(), + version: format!("{}", resolved_deps.root().version()), + features: None, + }, + packages: Vec::new(), + }; + + for package in packages.iter() { + output.packages.push(&package); + if package.package_id() == resolved_deps.root() { + let features = package.manifest().summary().features(); + if !features.is_empty() { + output.root.features = Some(features); + } + } + } + + let serialized_str = match &opt.output_format.to_ascii_uppercase()[..] { + "TOML" => toml::encode_str(&output), + "JSON" => try!(json::encode(&output)), + _ => bail!("unknown format: {}, supported formats are TOML, JSON.", + opt.output_format), + }; + + match opt.output_to { + OutputTo::StdOut => println!("{}", serialized_str), + OutputTo::File(ref path) => try!(paths::write(path, serialized_str.as_bytes())) + } + + Ok(()) +} + +/// Loads the manifest and resolves the dependencies of the project to the +/// concrete used versions. Afterwards available overrides of dependencies are applied. +fn resolve_dependencies(manifest: &Path, + config: &Config, + features: Vec, + no_default_features: bool) + -> CargoResult<(Resolve, Vec)> { + let mut source = try!(PathSource::for_path(manifest.parent().unwrap(), config)); + try!(source.update()); + + let package = try!(source.root_package()); + + let deps = try!(ops::resolve_dependencies(&package, + config, + Some(Box::new(source)), + features, + no_default_features)); + + let (packages, resolve_with_overrides, _) = deps; + + Ok((resolve_with_overrides, packages)) +} diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 806d3921a6e..b5c1efec65a 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -23,6 +23,7 @@ pub use self::registry::{modify_owners, yank, OwnersOptions}; pub use self::cargo_fetch::{fetch, get_resolved_packages}; pub use self::cargo_pkgid::pkgid; pub use self::resolve::{resolve_pkg, resolve_with_previous}; +pub use self::cargo_output_metadata::{output_metadata, OutputTo, OutputMetadataOptions}; mod cargo_clean; mod cargo_compile; @@ -31,6 +32,7 @@ mod cargo_fetch; mod cargo_generate_lockfile; mod cargo_install; mod cargo_new; +mod cargo_output_metadata; mod cargo_package; mod cargo_pkgid; mod cargo_read_manifest; diff --git a/tests/test_cargo_metadata.rs b/tests/test_cargo_metadata.rs new file mode 100644 index 00000000000..f491737f842 --- /dev/null +++ b/tests/test_cargo_metadata.rs @@ -0,0 +1,107 @@ +use std::fs::File; +use std::io::prelude::*; + +use hamcrest::{assert_that, existing_file, is, equal_to}; +use support::{project, execs, basic_bin_manifest}; + + +fn setup() { +} + +test!(cargo_metadata_simple { + let p = project("foo") + .file("Cargo.toml", &basic_bin_manifest("foo")); + + assert_that(p.cargo_process("metadata"), execs().with_stdout(r#" +[[packages]] +dependencies = [] +id = "foo 0.5.0 [..]" +manifest_path = "[..]Cargo.toml" +name = "foo" +version = "0.5.0" + +[packages.features] + +[[packages.targets]] +kind = ["bin"] +name = "foo" +src_path = "src[..]foo.rs" + +[root] +name = "foo" +version = "0.5.0" + +"#)); +}); + + +test!(cargo_metadata_simple_json { + let p = project("foo") + .file("Cargo.toml", &basic_bin_manifest("foo")); + + assert_that(p.cargo_process("metadata").arg("-f").arg("json"), execs().with_stdout(r#" + { + "root": { + "name": "foo", + "version": "0.5.0", + "features": null + }, + "packages": [ + { + "name": "foo", + "version": "0.5.0", + "id": "foo[..]", + "source": null, + "dependencies": [], + "targets": [ + { + "kind": [ + "bin" + ], + "name": "foo", + "src_path": "src[..]foo.rs" + } + ], + "features": {}, + "manifest_path": "[..]Cargo.toml" + } + ] + }"#.split_whitespace().collect::())); +}); + +test!(cargo_metadata_with_invalid_manifest { + let p = project("foo") + .file("Cargo.toml", ""); + + assert_that(p.cargo_process("metadata"), execs().with_status(101) + .with_stderr("\ +failed to parse manifest at `[..]` + +Caused by: + no `package` or `project` section found.")) +}); + +test!(cargo_metadata_with_invalid_output_format { + let p = project("foo") + .file("Cargo.toml", &basic_bin_manifest("foo")); + + assert_that(p.cargo_process("metadata").arg("--output-format").arg("XML"), + execs().with_status(101) + .with_stderr("unknown format: XML, supported formats are TOML, JSON.")) +}); + +test!(cargo_metadata_simple_file { + let p = project("foo") + .file("Cargo.toml", &basic_bin_manifest("foo")); + + assert_that(p.cargo_process("metadata").arg("--output-path").arg("metadata.toml"), + execs().with_status(0)); + + let outputfile = p.root().join("metadata.toml"); + assert_that(&outputfile, existing_file()); + + let mut output = String::new(); + File::open(&outputfile).unwrap().read_to_string(&mut output).unwrap(); + + assert_that(output[..].contains(r#"name = "foo""#), is(equal_to(true))); +}); diff --git a/tests/tests.rs b/tests/tests.rs index a435f91b860..43a97fb636b 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -51,6 +51,7 @@ mod test_cargo_freshness; mod test_cargo_generate_lockfile; mod test_cargo_init; mod test_cargo_install; +mod test_cargo_metadata; mod test_cargo_new; mod test_cargo_package; mod test_cargo_profiles; From fe7291ad7b6003ab0d6669eeb55a603eb163d636 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 21 Dec 2015 18:05:26 +0300 Subject: [PATCH 2/5] metadata: serialize Resolve --- src/cargo/ops/cargo_output_metadata.rs | 47 +++------ tests/test_cargo_metadata.rs | 126 +++++++++++++++++++++++-- 2 files changed, 132 insertions(+), 41 deletions(-) diff --git a/src/cargo/ops/cargo_output_metadata.rs b/src/cargo/ops/cargo_output_metadata.rs index 38f084af294..6d4415e5190 100644 --- a/src/cargo/ops/cargo_output_metadata.rs +++ b/src/cargo/ops/cargo_output_metadata.rs @@ -1,5 +1,4 @@ use std::ascii::AsciiExt; -use std::collections::HashMap; use std::path::{Path, PathBuf}; use core::resolver::Resolve; @@ -53,40 +52,13 @@ pub fn output_metadata(opt: OutputMetadataOptions, config: &Config) -> CargoResu config, opt.features, opt.no_default_features)); - let (resolved_deps, packages) = deps; + let (packages, resolve) = deps; - #[derive(RustcEncodable)] - struct RootPackageInfo<'a> { - name: &'a str, - version: String, - features: Option<&'a HashMap>>, - } - - #[derive(RustcEncodable)] - struct ExportInfo<'a> { - root: RootPackageInfo<'a>, - packages: Vec<&'a Package>, - } - - let mut output = ExportInfo { - root: RootPackageInfo { - name: resolved_deps.root().name(), - version: format!("{}", resolved_deps.root().version()), - features: None, - }, - packages: Vec::new(), + let output = ExportInfo { + packages: &packages, + resolve: &resolve, }; - for package in packages.iter() { - output.packages.push(&package); - if package.package_id() == resolved_deps.root() { - let features = package.manifest().summary().features(); - if !features.is_empty() { - output.root.features = Some(features); - } - } - } - let serialized_str = match &opt.output_format.to_ascii_uppercase()[..] { "TOML" => toml::encode_str(&output), "JSON" => try!(json::encode(&output)), @@ -102,13 +74,20 @@ pub fn output_metadata(opt: OutputMetadataOptions, config: &Config) -> CargoResu Ok(()) } +#[derive(RustcEncodable)] +struct ExportInfo<'a> { + packages: &'a [Package], + resolve: &'a Resolve, +} + + /// Loads the manifest and resolves the dependencies of the project to the /// concrete used versions. Afterwards available overrides of dependencies are applied. fn resolve_dependencies(manifest: &Path, config: &Config, features: Vec, no_default_features: bool) - -> CargoResult<(Resolve, Vec)> { + -> CargoResult<(Vec, Resolve)> { let mut source = try!(PathSource::for_path(manifest.parent().unwrap(), config)); try!(source.update()); @@ -122,5 +101,5 @@ fn resolve_dependencies(manifest: &Path, let (packages, resolve_with_overrides, _) = deps; - Ok((resolve_with_overrides, packages)) + Ok((packages, resolve_with_overrides)) } diff --git a/tests/test_cargo_metadata.rs b/tests/test_cargo_metadata.rs index f491737f842..7ae3df66995 100644 --- a/tests/test_cargo_metadata.rs +++ b/tests/test_cargo_metadata.rs @@ -2,6 +2,8 @@ use std::fs::File; use std::io::prelude::*; use hamcrest::{assert_that, existing_file, is, equal_to}; +use rustc_serialize::json::Json; +use support::registry::Package; use support::{project, execs, basic_bin_manifest}; @@ -27,7 +29,11 @@ kind = ["bin"] name = "foo" src_path = "src[..]foo.rs" -[root] +[resolve] +package = [] + +[resolve.root] +dependencies = [] name = "foo" version = "0.5.0" @@ -41,11 +47,6 @@ test!(cargo_metadata_simple_json { assert_that(p.cargo_process("metadata").arg("-f").arg("json"), execs().with_stdout(r#" { - "root": { - "name": "foo", - "version": "0.5.0", - "features": null - }, "packages": [ { "name": "foo", @@ -65,10 +66,121 @@ test!(cargo_metadata_simple_json { "features": {}, "manifest_path": "[..]Cargo.toml" } - ] + ], + "resolve": { + "package": [], + "root": { + "name": "foo", + "version": "0.5.0", + "source": null, + "dependencies" : [] + }, + "metadata": null + } }"#.split_whitespace().collect::())); }); + +test!(cargo_metadata_with_deps { + let p = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.5.0" + authors = [] + license = "MIT" + description = "foo" + + [[bin]] + name = "foo" + + [dependencies] + bar = "*" + "#); + Package::new("baz", "0.0.1").publish(); + Package::new("bar", "0.0.1").dep("baz", "0.0.1").publish(); + + assert_that(p.cargo_process("metadata") + .arg("--output-path").arg("metadata.json") + .arg("-f").arg("json"), + + execs().with_status(0)); + + let outputfile = p.root().join("metadata.json"); + assert_that(&outputfile, existing_file()); + + let mut output = String::new(); + File::open(&outputfile).unwrap().read_to_string(&mut output).unwrap(); + let result = Json::from_str(&output).unwrap(); + println!("{}", result.pretty()); + + let packages = result.find("packages") + .and_then(|o| o.as_array()) + .expect("no packages"); + + assert_that(packages.len(), is(equal_to(3))); + + let root = result.find_path(&["resolve", "root"]) + .and_then(|o| o.as_object()) + .expect("no root"); + + // source is null because foo is root + let foo_id_start = format!("{} {}", root["name"].as_string().unwrap(), + root["version"].as_string().unwrap()); + let foo_name = packages.iter().find(|o| { + o.find("id").and_then(|i| i.as_string()).unwrap() + .starts_with(&foo_id_start) + }).and_then(|p| p.find("name")) + .and_then(|n| n.as_string()) + .expect("no root package"); + assert_that(foo_name, is(equal_to("foo"))); + + let foo_deps = root["dependencies"].as_array().expect("no root deps"); + assert_that(foo_deps.len(), is(equal_to(1))); + + let bar = &foo_deps[0].as_string().expect("bad root dep"); + + let check_name_for_id = |id: &str, expected_name: &str| { + let name = packages.iter().find(|o| { + id == o.find("id").and_then(|i| i.as_string()).unwrap() + }).and_then(|p| p.find("name")) + .and_then(|n| n.as_string()) + .expect(&format!("no {} in packages", expected_name)); + + assert_that(name, is(equal_to(expected_name))); + }; + + let find_deps = |id: &str| -> Vec<_> { + result.find_path(&["resolve", "package"]) + .and_then(|o| o.as_array()).expect("resolve.package is not an array") + .iter() + .find(|o| { + let o = o.as_object().expect("package is not an object"); + let o_id = format!("{} {} ({})", + o["name"].as_string().unwrap(), + o["version"].as_string().unwrap(), + o["source"].as_string().unwrap()); + id == o_id + }) + .and_then(|o| o.find("dependencies")) + .and_then(|o| o.as_array()) + .and_then(|a| a.iter() + .map(|o| o.as_string()) + .collect()) + .expect(&format!("no deps for {}", id)) + }; + + + check_name_for_id(bar, "bar"); + let bar_deps = find_deps(&bar); + assert_that(bar_deps.len(), is(equal_to(1))); + + let baz = &bar_deps[0]; + check_name_for_id(baz, "baz"); + let baz_deps = find_deps(&baz); + assert_that(baz_deps.len(), is(equal_to(0))); +}); + test!(cargo_metadata_with_invalid_manifest { let p = project("foo") .file("Cargo.toml", ""); From 16e4d72d4ebde4c86378d747ba439348d36b685d Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 15 Jan 2016 02:47:58 +0300 Subject: [PATCH 3/5] metadata: simplify --- src/bin/metadata.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/bin/metadata.rs b/src/bin/metadata.rs index 41a4dbd83a8..599cce242ba 100644 --- a/src/bin/metadata.rs +++ b/src/bin/metadata.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use cargo::ops::{output_metadata, OutputTo, OutputMetadataOptions}; use cargo::util::important_paths::find_root_manifest_for_wd; -use cargo::util::{CliResult, CliError, Config}; +use cargo::util::{CliResult, Config}; #[derive(RustcDecodable)] struct Options { @@ -59,7 +59,6 @@ pub fn execute(options: Options, config: &Config) -> CliResult> { output_to: output_to, }; - output_metadata(options, config) - .map(|_| None) - .map_err(|err| CliError::from_boxed(err, 101)) + try!(output_metadata(options, config)); + Ok(None) } From 874fe2638a9a5596d8c97c2e86a482150e87a130 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sat, 23 Jan 2016 14:54:35 +0300 Subject: [PATCH 4/5] metadata: add version to format --- src/bin/metadata.rs | 28 +++++++++++++++----------- src/cargo/ops/cargo_output_metadata.rs | 9 +++++++-- tests/test_cargo_metadata.rs | 6 ++++-- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/bin/metadata.rs b/src/bin/metadata.rs index 599cce242ba..805d2fac147 100644 --- a/src/bin/metadata.rs +++ b/src/bin/metadata.rs @@ -11,14 +11,15 @@ use cargo::util::{CliResult, Config}; #[derive(RustcDecodable)] struct Options { + flag_color: Option, flag_features: Vec, + flag_format_version: u32, flag_manifest_path: Option, flag_no_default_features: bool, flag_output_format: String, flag_output_path: Option, - flag_verbose: bool, flag_quiet: bool, - flag_color: Option, + flag_verbose: bool, } pub const USAGE: &'static str = " @@ -29,16 +30,18 @@ Usage: cargo metadata [options] Options: - -h, --help Print this message - -o, --output-path PATH Path the output is written to, otherwise stdout is used - -f, --output-format FMT Output format [default: toml] - Valid values: toml, json - --features FEATURES Space-separated list of features - --no-default-features Do not include the `default` feature - --manifest-path PATH Path to the manifest - -v, --verbose Use verbose output - -q, --quiet No output printed to stdout - --color WHEN Coloring: auto, always, never + -h, --help Print this message + -o, --output-path PATH Path the output is written to, otherwise stdout is used + -f, --output-format FMT Output format [default: toml] + Valid values: toml, json + --features FEATURES Space-separated list of features + --no-default-features Do not include the `default` feature + --manifest-path PATH Path to the manifest + --format-version VERSION Format version [default: 1] + Valid values: 1 + -v, --verbose Use verbose output + -q, --quiet No output printed to stdout + --color WHEN Coloring: auto, always, never "; pub fn execute(options: Options, config: &Config) -> CliResult> { @@ -57,6 +60,7 @@ pub fn execute(options: Options, config: &Config) -> CliResult> { no_default_features: options.flag_no_default_features, output_format: options.flag_output_format, output_to: output_to, + version: options.flag_format_version, }; try!(output_metadata(options, config)); diff --git a/src/cargo/ops/cargo_output_metadata.rs b/src/cargo/ops/cargo_output_metadata.rs index 6d4415e5190..72d4ab50adc 100644 --- a/src/cargo/ops/cargo_output_metadata.rs +++ b/src/cargo/ops/cargo_output_metadata.rs @@ -10,6 +10,7 @@ use toml; use util::config::Config; use util::{paths, CargoResult}; +const VERSION: u32 = 1; /// Where the dependencies should be written to. pub enum OutputTo { @@ -19,10 +20,11 @@ pub enum OutputTo { pub struct OutputMetadataOptions<'a> { pub features: Vec, - pub output_format: String, - pub output_to: OutputTo, pub manifest_path: &'a Path, pub no_default_features: bool, + pub output_format: String, + pub output_to: OutputTo, + pub version: u32, } /// Loads the manifest, resolves the dependencies of the project to the concrete @@ -54,9 +56,11 @@ pub fn output_metadata(opt: OutputMetadataOptions, config: &Config) -> CargoResu opt.no_default_features)); let (packages, resolve) = deps; + assert_eq!(opt.version, VERSION); let output = ExportInfo { packages: &packages, resolve: &resolve, + version: VERSION, }; let serialized_str = match &opt.output_format.to_ascii_uppercase()[..] { @@ -78,6 +82,7 @@ pub fn output_metadata(opt: OutputMetadataOptions, config: &Config) -> CargoResu struct ExportInfo<'a> { packages: &'a [Package], resolve: &'a Resolve, + version: u32, } diff --git a/tests/test_cargo_metadata.rs b/tests/test_cargo_metadata.rs index 7ae3df66995..4dc307c92c2 100644 --- a/tests/test_cargo_metadata.rs +++ b/tests/test_cargo_metadata.rs @@ -14,7 +14,8 @@ test!(cargo_metadata_simple { let p = project("foo") .file("Cargo.toml", &basic_bin_manifest("foo")); - assert_that(p.cargo_process("metadata"), execs().with_stdout(r#" + assert_that(p.cargo_process("metadata"), execs().with_stdout(r#"version = 1 + [[packages]] dependencies = [] id = "foo 0.5.0 [..]" @@ -76,7 +77,8 @@ test!(cargo_metadata_simple_json { "dependencies" : [] }, "metadata": null - } + }, + "version": 1 }"#.split_whitespace().collect::())); }); From b24bf7e6a42027b140eb136a336b3444d879658c Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 25 Jan 2016 00:16:33 +0300 Subject: [PATCH 5/5] metadata: use existing infra for JSON output --- src/bin/metadata.rs | 22 +- src/cargo/ops/cargo_output_metadata.rs | 70 ++---- src/cargo/ops/mod.rs | 2 +- tests/support/mod.rs | 76 ++++++ tests/test_cargo_metadata.rs | 320 +++++++++++-------------- 5 files changed, 239 insertions(+), 251 deletions(-) diff --git a/src/bin/metadata.rs b/src/bin/metadata.rs index 805d2fac147..433ed75d3cc 100644 --- a/src/bin/metadata.rs +++ b/src/bin/metadata.rs @@ -3,9 +3,7 @@ extern crate docopt; extern crate rustc_serialize; extern crate toml; -use std::path::PathBuf; - -use cargo::ops::{output_metadata, OutputTo, OutputMetadataOptions}; +use cargo::ops::{output_metadata, OutputMetadataOptions, ExportInfo}; use cargo::util::important_paths::find_root_manifest_for_wd; use cargo::util::{CliResult, Config}; @@ -16,8 +14,6 @@ struct Options { flag_format_version: u32, flag_manifest_path: Option, flag_no_default_features: bool, - flag_output_format: String, - flag_output_path: Option, flag_quiet: bool, flag_verbose: bool, } @@ -31,9 +27,6 @@ Usage: Options: -h, --help Print this message - -o, --output-path PATH Path the output is written to, otherwise stdout is used - -f, --output-format FMT Output format [default: toml] - Valid values: toml, json --features FEATURES Space-separated list of features --no-default-features Do not include the `default` feature --manifest-path PATH Path to the manifest @@ -44,25 +37,18 @@ Options: --color WHEN Coloring: auto, always, never "; -pub fn execute(options: Options, config: &Config) -> CliResult> { +pub fn execute(options: Options, config: &Config) -> CliResult> { try!(config.shell().set_verbosity(options.flag_verbose, options.flag_quiet)); try!(config.shell().set_color_config(options.flag_color.as_ref().map(|s| &s[..]))); let manifest = try!(find_root_manifest_for_wd(options.flag_manifest_path, config.cwd())); - let output_to = match options.flag_output_path { - Some(path) => OutputTo::File(PathBuf::from(path)), - None => OutputTo::StdOut - }; - let options = OutputMetadataOptions { features: options.flag_features, manifest_path: &manifest, no_default_features: options.flag_no_default_features, - output_format: options.flag_output_format, - output_to: output_to, version: options.flag_format_version, }; - try!(output_metadata(options, config)); - Ok(None) + let result = try!(output_metadata(options, config)); + Ok(Some(result)) } diff --git a/src/cargo/ops/cargo_output_metadata.rs b/src/cargo/ops/cargo_output_metadata.rs index 72d4ab50adc..33be1857f6f 100644 --- a/src/cargo/ops/cargo_output_metadata.rs +++ b/src/cargo/ops/cargo_output_metadata.rs @@ -1,55 +1,27 @@ -use std::ascii::AsciiExt; -use std::path::{Path, PathBuf}; +use std::path::Path; use core::resolver::Resolve; use core::{Source, Package}; use ops; -use rustc_serialize::json; use sources::PathSource; -use toml; use util::config::Config; -use util::{paths, CargoResult}; +use util::CargoResult; const VERSION: u32 = 1; -/// Where the dependencies should be written to. -pub enum OutputTo { - File(PathBuf), - StdOut, -} - pub struct OutputMetadataOptions<'a> { pub features: Vec, pub manifest_path: &'a Path, pub no_default_features: bool, - pub output_format: String, - pub output_to: OutputTo, pub version: u32, } /// Loads the manifest, resolves the dependencies of the project to the concrete -/// used versions - considering overrides - and writes all dependencies in a TOML -/// format to stdout or the specified file. -/// -/// The TOML format is e.g.: -/// ```toml -/// root = "libA" -/// -/// [packages.libA] -/// dependencies = ["libB"] -/// path = "/home/user/.cargo/registry/src/jackfan.us.kg-1ecc6299db9ec823/libA-0.1" -/// version = "0.1" -/// -/// [packages.libB] -/// dependencies = [] -/// path = "/home/user/.cargo/registry/src/jackfan.us.kg-1ecc6299db9ec823/libB-0.4" -/// version = "0.4" -/// -/// [packages.libB.features] -/// featureA = ["featureB"] -/// featureB = [] -/// ``` -pub fn output_metadata(opt: OutputMetadataOptions, config: &Config) -> CargoResult<()> { +/// used versions - considering overrides - and writes all dependencies in a JSON +/// format to stdout. +pub fn output_metadata<'a>(opt: OutputMetadataOptions, + config: &'a Config) + -> CargoResult { let deps = try!(resolve_dependencies(opt.manifest_path, config, opt.features, @@ -57,31 +29,17 @@ pub fn output_metadata(opt: OutputMetadataOptions, config: &Config) -> CargoResu let (packages, resolve) = deps; assert_eq!(opt.version, VERSION); - let output = ExportInfo { - packages: &packages, - resolve: &resolve, + Ok(ExportInfo { + packages: packages, + resolve: resolve, version: VERSION, - }; - - let serialized_str = match &opt.output_format.to_ascii_uppercase()[..] { - "TOML" => toml::encode_str(&output), - "JSON" => try!(json::encode(&output)), - _ => bail!("unknown format: {}, supported formats are TOML, JSON.", - opt.output_format), - }; - - match opt.output_to { - OutputTo::StdOut => println!("{}", serialized_str), - OutputTo::File(ref path) => try!(paths::write(path, serialized_str.as_bytes())) - } - - Ok(()) + }) } #[derive(RustcEncodable)] -struct ExportInfo<'a> { - packages: &'a [Package], - resolve: &'a Resolve, +pub struct ExportInfo { + packages: Vec, + resolve: Resolve, version: u32, } diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index b5c1efec65a..b9eb78ac3da 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -23,7 +23,7 @@ pub use self::registry::{modify_owners, yank, OwnersOptions}; pub use self::cargo_fetch::{fetch, get_resolved_packages}; pub use self::cargo_pkgid::pkgid; pub use self::resolve::{resolve_pkg, resolve_with_previous}; -pub use self::cargo_output_metadata::{output_metadata, OutputTo, OutputMetadataOptions}; +pub use self::cargo_output_metadata::{output_metadata, OutputMetadataOptions, ExportInfo}; mod cargo_clean; mod cargo_compile; diff --git a/tests/support/mod.rs b/tests/support/mod.rs index f57a283a061..7bd68151872 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -10,6 +10,7 @@ use std::process::Output; use std::str; use std::usize; +use rustc_serialize::json::Json; use url::Url; use hamcrest as ham; use cargo::util::ProcessBuilder; @@ -271,6 +272,7 @@ pub struct Execs { expect_exit_code: Option, expect_stdout_contains: Vec, expect_stderr_contains: Vec, + expect_json: Option, } impl Execs { @@ -299,6 +301,11 @@ impl Execs { self } + pub fn with_json(mut self, expected: &str) -> Execs { + self.expect_json = Some(Json::from_str(expected).unwrap()); + self + } + fn match_output(&self, actual: &Output) -> ham::MatchResult { self.match_status(actual) .and(self.match_stdout(actual)) @@ -330,6 +337,10 @@ impl Execs { try!(self.match_std(Some(expect), &actual.stderr, "stderr", &actual.stdout, true)); } + + if let Some(ref expect_json) = self.expect_json { + try!(self.match_json(expect_json, &actual.stdout)); + } Ok(()) } @@ -379,6 +390,27 @@ impl Execs { } + fn match_json(&self, expected: &Json, stdout: &[u8]) -> ham::MatchResult { + let stdout = match str::from_utf8(stdout) { + Err(..) => return Err("stdout was not utf8 encoded".to_owned()), + Ok(stdout) => stdout, + }; + + let actual = match Json::from_str(stdout) { + Err(..) => return Err(format!("Invalid json {}", stdout)), + Ok(actual) => actual, + }; + + match find_mismatch(expected, &actual) { + Some((expected_part, actual_part)) => Err(format!( + "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n", + expected.pretty(), actual.pretty(), + expected_part.pretty(), actual_part.pretty() + )), + None => Ok(()), + } + } + fn diff_lines<'a>(&self, actual: str::Lines<'a>, expected: str::Lines<'a>, partial: bool) -> Vec { let actual = actual.take(if partial { @@ -419,6 +451,49 @@ fn lines_match(expected: &str, mut actual: &str) -> bool { actual.is_empty() || expected.ends_with("[..]") } +// Compares JSON object for approximate equality. +// You can use `[..]` wildcard in strings (useful for OS dependent things such as paths). +// Arrays are sorted before comparison. +fn find_mismatch<'a>(expected: &'a Json, actual: &'a Json) -> Option<(&'a Json, &'a Json)> { + use rustc_serialize::json::Json::*; + match (expected, actual) { + (&I64(l), &I64(r)) if l == r => None, + (&F64(l), &F64(r)) if l == r => None, + (&U64(l), &U64(r)) if l == r => None, + (&Boolean(l), &Boolean(r)) if l == r => None, + (&String(ref l), &String(ref r)) if lines_match(l, r) => None, + (&Array(ref l), &Array(ref r)) => { + if l.len() != r.len() { + return Some((expected, actual)); + } + + fn sorted(xs: &Vec) -> Vec<&Json> { + let mut result = xs.iter().collect::>(); + // `unwrap` should be safe because JSON spec does not allow NaNs + result.sort_by(|x, y| x.partial_cmp(y).unwrap()); + result + } + + sorted(l).iter().zip(sorted(r)) + .filter_map(|(l, r)| find_mismatch(l, r)) + .nth(0) + } + (&Object(ref l), &Object(ref r)) => { + let same_keys = l.len() == r.len() && l.keys().all(|k| r.contains_key(k)); + if !same_keys { + return Some((expected, actual)); + } + + l.values().zip(r.values()) + .filter_map(|(l, r)| find_mismatch(l, r)) + .nth(0) + } + (&Null, &Null) => None, + _ => Some((expected, actual)), + } + +} + struct ZipAll { first: I1, second: I2, @@ -486,6 +561,7 @@ pub fn execs() -> Execs { expect_exit_code: None, expect_stdout_contains: Vec::new(), expect_stderr_contains: Vec::new(), + expect_json: None, } } diff --git a/tests/test_cargo_metadata.rs b/tests/test_cargo_metadata.rs index 4dc307c92c2..09afbb2fc1d 100644 --- a/tests/test_cargo_metadata.rs +++ b/tests/test_cargo_metadata.rs @@ -1,85 +1,48 @@ -use std::fs::File; -use std::io::prelude::*; - -use hamcrest::{assert_that, existing_file, is, equal_to}; -use rustc_serialize::json::Json; +use hamcrest::{assert_that, existing_file}; use support::registry::Package; use support::{project, execs, basic_bin_manifest}; -fn setup() { -} +fn setup() {} test!(cargo_metadata_simple { let p = project("foo") .file("Cargo.toml", &basic_bin_manifest("foo")); - assert_that(p.cargo_process("metadata"), execs().with_stdout(r#"version = 1 - -[[packages]] -dependencies = [] -id = "foo 0.5.0 [..]" -manifest_path = "[..]Cargo.toml" -name = "foo" -version = "0.5.0" - -[packages.features] - -[[packages.targets]] -kind = ["bin"] -name = "foo" -src_path = "src[..]foo.rs" - -[resolve] -package = [] - -[resolve.root] -dependencies = [] -name = "foo" -version = "0.5.0" - -"#)); -}); - - -test!(cargo_metadata_simple_json { - let p = project("foo") - .file("Cargo.toml", &basic_bin_manifest("foo")); - - assert_that(p.cargo_process("metadata").arg("-f").arg("json"), execs().with_stdout(r#" - { - "packages": [ - { - "name": "foo", - "version": "0.5.0", - "id": "foo[..]", - "source": null, - "dependencies": [], - "targets": [ - { - "kind": [ - "bin" - ], - "name": "foo", - "src_path": "src[..]foo.rs" - } - ], - "features": {}, - "manifest_path": "[..]Cargo.toml" - } - ], - "resolve": { - "package": [], - "root": { - "name": "foo", - "version": "0.5.0", - "source": null, - "dependencies" : [] - }, - "metadata": null + assert_that(p.cargo_process("metadata"), execs().with_json(r#" + { + "packages": [ + { + "name": "foo", + "version": "0.5.0", + "id": "foo[..]", + "source": null, + "dependencies": [], + "targets": [ + { + "kind": [ + "bin" + ], + "name": "foo", + "src_path": "src[..]foo.rs" + } + ], + "features": {}, + "manifest_path": "[..]Cargo.toml" + } + ], + "resolve": { + "package": [], + "root": { + "name": "foo", + "version": "0.5.0", + "source": null, + "dependencies" : [] }, - "version": 1 - }"#.split_whitespace().collect::())); + "metadata": null + }, + "version": 1 + }"#)); }); @@ -102,85 +65,115 @@ test!(cargo_metadata_with_deps { Package::new("baz", "0.0.1").publish(); Package::new("bar", "0.0.1").dep("baz", "0.0.1").publish(); - assert_that(p.cargo_process("metadata") - .arg("--output-path").arg("metadata.json") - .arg("-f").arg("json"), - - execs().with_status(0)); - - let outputfile = p.root().join("metadata.json"); - assert_that(&outputfile, existing_file()); - - let mut output = String::new(); - File::open(&outputfile).unwrap().read_to_string(&mut output).unwrap(); - let result = Json::from_str(&output).unwrap(); - println!("{}", result.pretty()); - - let packages = result.find("packages") - .and_then(|o| o.as_array()) - .expect("no packages"); - - assert_that(packages.len(), is(equal_to(3))); - - let root = result.find_path(&["resolve", "root"]) - .and_then(|o| o.as_object()) - .expect("no root"); - - // source is null because foo is root - let foo_id_start = format!("{} {}", root["name"].as_string().unwrap(), - root["version"].as_string().unwrap()); - let foo_name = packages.iter().find(|o| { - o.find("id").and_then(|i| i.as_string()).unwrap() - .starts_with(&foo_id_start) - }).and_then(|p| p.find("name")) - .and_then(|n| n.as_string()) - .expect("no root package"); - assert_that(foo_name, is(equal_to("foo"))); - - let foo_deps = root["dependencies"].as_array().expect("no root deps"); - assert_that(foo_deps.len(), is(equal_to(1))); - - let bar = &foo_deps[0].as_string().expect("bad root dep"); - - let check_name_for_id = |id: &str, expected_name: &str| { - let name = packages.iter().find(|o| { - id == o.find("id").and_then(|i| i.as_string()).unwrap() - }).and_then(|p| p.find("name")) - .and_then(|n| n.as_string()) - .expect(&format!("no {} in packages", expected_name)); - - assert_that(name, is(equal_to(expected_name))); - }; - - let find_deps = |id: &str| -> Vec<_> { - result.find_path(&["resolve", "package"]) - .and_then(|o| o.as_array()).expect("resolve.package is not an array") - .iter() - .find(|o| { - let o = o.as_object().expect("package is not an object"); - let o_id = format!("{} {} ({})", - o["name"].as_string().unwrap(), - o["version"].as_string().unwrap(), - o["source"].as_string().unwrap()); - id == o_id - }) - .and_then(|o| o.find("dependencies")) - .and_then(|o| o.as_array()) - .and_then(|a| a.iter() - .map(|o| o.as_string()) - .collect()) - .expect(&format!("no deps for {}", id)) - }; - - - check_name_for_id(bar, "bar"); - let bar_deps = find_deps(&bar); - assert_that(bar_deps.len(), is(equal_to(1))); - - let baz = &bar_deps[0]; - check_name_for_id(baz, "baz"); - let baz_deps = find_deps(&baz); - assert_that(baz_deps.len(), is(equal_to(0))); + assert_that(p.cargo_process("metadata").arg("-q"), execs().with_json(r#" + { + "packages": [ + { + "dependencies": [], + "features": {}, + "id": "baz 0.0.1 (registry+file:[..])", + "manifest_path": "[..]Cargo.toml", + "name": "baz", + "source": "registry+file:[..]", + "targets": [ + { + "kind": [ + "lib" + ], + "name": "baz", + "src_path": "[..]lib.rs" + } + ], + "version": "0.0.1" + }, + { + "dependencies": [ + { + "features": [], + "kind": null, + "name": "baz", + "optional": false, + "req": "^0.0.1", + "source": "registry+file:[..]", + "target": null, + "uses_default_features": true + } + ], + "features": {}, + "id": "bar 0.0.1 (registry+file:[..])", + "manifest_path": "[..]Cargo.toml", + "name": "bar", + "source": "registry+file:[..]", + "targets": [ + { + "kind": [ + "lib" + ], + "name": "bar", + "src_path": "[..]lib.rs" + } + ], + "version": "0.0.1" + }, + { + "dependencies": [ + { + "features": [], + "kind": null, + "name": "bar", + "optional": false, + "req": "*", + "source": "registry+file:[..]", + "target": null, + "uses_default_features": true + } + ], + "features": {}, + "id": "foo 0.5.0 (path+file:[..]foo)", + "manifest_path": "[..]Cargo.toml", + "name": "foo", + "source": null, + "targets": [ + { + "kind": [ + "bin" + ], + "name": "foo", + "src_path": "[..]foo.rs" + } + ], + "version": "0.5.0" + } + ], + "resolve": { + "metadata": null, + "package": [ + { + "dependencies": [ + "baz 0.0.1 (registry+file:[..])" + ], + "name": "bar", + "source": "registry+file:[..]", + "version": "0.0.1" + }, + { + "dependencies": [], + "name": "baz", + "source": "registry+file:[..]", + "version": "0.0.1" + } + ], + "root": { + "dependencies": [ + "bar 0.0.1 (registry+file:[..])" + ], + "name": "foo", + "source": null, + "version": "0.5.0" + } + }, + "version": 1 + }"#)); }); test!(cargo_metadata_with_invalid_manifest { @@ -194,28 +187,3 @@ failed to parse manifest at `[..]` Caused by: no `package` or `project` section found.")) }); - -test!(cargo_metadata_with_invalid_output_format { - let p = project("foo") - .file("Cargo.toml", &basic_bin_manifest("foo")); - - assert_that(p.cargo_process("metadata").arg("--output-format").arg("XML"), - execs().with_status(101) - .with_stderr("unknown format: XML, supported formats are TOML, JSON.")) -}); - -test!(cargo_metadata_simple_file { - let p = project("foo") - .file("Cargo.toml", &basic_bin_manifest("foo")); - - assert_that(p.cargo_process("metadata").arg("--output-path").arg("metadata.toml"), - execs().with_status(0)); - - let outputfile = p.root().join("metadata.toml"); - assert_that(&outputfile, existing_file()); - - let mut output = String::new(); - File::open(&outputfile).unwrap().read_to_string(&mut output).unwrap(); - - assert_that(output[..].contains(r#"name = "foo""#), is(equal_to(true))); -});