Skip to content

Commit

Permalink
Auto merge of #2196 - matklad:metadata2, r=alexcrichton
Browse files Browse the repository at this point in the history
Most of the work was done by @dan-t in #1225 and by @winger in #1434

Fixes #2193

I failed to properly rebase previous attempts so I just salvaged this from bits and pieces.

@alexcrichton are you sure that the default format should be TOML? I think that TOML is more suitable for humans, and JSON is better (at the moment at least) for tools. Maybe we should default to ~~TOML~~ JSON?
  • Loading branch information
bors committed Jan 25, 2016
2 parents 44c0d7b + b24bf7e commit 859c5d3
Show file tree
Hide file tree
Showing 7 changed files with 391 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/bin/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ macro_rules! each_subcommand{
$mac!(install);
$mac!(locate_project);
$mac!(login);
$mac!(metadata);
$mac!(new);
$mac!(owner);
$mac!(package);
Expand Down
54 changes: 54 additions & 0 deletions src/bin/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
extern crate cargo;
extern crate docopt;
extern crate rustc_serialize;
extern crate toml;

use cargo::ops::{output_metadata, OutputMetadataOptions, ExportInfo};
use cargo::util::important_paths::find_root_manifest_for_wd;
use cargo::util::{CliResult, Config};

#[derive(RustcDecodable)]
struct Options {
flag_color: Option<String>,
flag_features: Vec<String>,
flag_format_version: u32,
flag_manifest_path: Option<String>,
flag_no_default_features: bool,
flag_quiet: bool,
flag_verbose: bool,
}

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
--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<Option<ExportInfo>> {
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 options = OutputMetadataOptions {
features: options.flag_features,
manifest_path: &manifest,
no_default_features: options.flag_no_default_features,
version: options.flag_format_version,
};

let result = try!(output_metadata(options, config));
Ok(Some(result))
}
68 changes: 68 additions & 0 deletions src/cargo/ops/cargo_output_metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::path::Path;

use core::resolver::Resolve;
use core::{Source, Package};
use ops;
use sources::PathSource;
use util::config::Config;
use util::CargoResult;

const VERSION: u32 = 1;

pub struct OutputMetadataOptions<'a> {
pub features: Vec<String>,
pub manifest_path: &'a Path,
pub no_default_features: bool,
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 JSON
/// format to stdout.
pub fn output_metadata<'a>(opt: OutputMetadataOptions,
config: &'a Config)
-> CargoResult<ExportInfo> {
let deps = try!(resolve_dependencies(opt.manifest_path,
config,
opt.features,
opt.no_default_features));
let (packages, resolve) = deps;

assert_eq!(opt.version, VERSION);
Ok(ExportInfo {
packages: packages,
resolve: resolve,
version: VERSION,
})
}

#[derive(RustcEncodable)]
pub struct ExportInfo {
packages: Vec<Package>,
resolve: Resolve,
version: u32,
}


/// 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<String>,
no_default_features: bool)
-> CargoResult<(Vec<Package>, Resolve)> {
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((packages, resolve_with_overrides))
}
2 changes: 2 additions & 0 deletions src/cargo/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, OutputMetadataOptions, ExportInfo};

mod cargo_clean;
mod cargo_compile;
Expand All @@ -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;
Expand Down
76 changes: 76 additions & 0 deletions tests/support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -271,6 +272,7 @@ pub struct Execs {
expect_exit_code: Option<i32>,
expect_stdout_contains: Vec<String>,
expect_stderr_contains: Vec<String>,
expect_json: Option<Json>,
}

impl Execs {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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<String> {
let actual = actual.take(if partial {
Expand Down Expand Up @@ -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<Json>) -> Vec<&Json> {
let mut result = xs.iter().collect::<Vec<_>>();
// `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<I1: Iterator, I2: Iterator> {
first: I1,
second: I2,
Expand Down Expand Up @@ -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,
}
}

Expand Down
Loading

0 comments on commit 859c5d3

Please sign in to comment.