]> git.proxmox.com Git - cargo.git/commitdiff
metadata: use existing infra for JSON output
authorAleksey Kladov <aleksey.kladov@gmail.com>
Sun, 24 Jan 2016 21:16:33 +0000 (00:16 +0300)
committerAleksey Kladov <aleksey.kladov@gmail.com>
Mon, 25 Jan 2016 15:04:12 +0000 (18:04 +0300)
src/bin/metadata.rs
src/cargo/ops/cargo_output_metadata.rs
src/cargo/ops/mod.rs
tests/support/mod.rs
tests/test_cargo_metadata.rs

index 805d2fac147377916460e791fa0534a07279765e..433ed75d3ccfa43483a061af694736e4fcdfcab5 100644 (file)
@@ -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<String>,
     flag_no_default_features: bool,
-    flag_output_format: String,
-    flag_output_path: Option<String>,
     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<Option<()>> {
+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 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))
 }
index 72d4ab50adc67296d8e0ef83b6d1f85ab20aeef8..33be1857f6f7b70e922f37cd5f2c6cc67fff7c1e 100644 (file)
@@ -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<String>,
     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/github.com-1ecc6299db9ec823/libA-0.1"
-/// version = "0.1"
-///
-/// [packages.libB]
-/// dependencies = []
-/// path = "/home/user/.cargo/registry/src/github.com-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<ExportInfo> {
     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<Package>,
+    resolve: Resolve,
     version: u32,
 }
 
index b5c1efec65aa47695f72260134ece63c938b0c7e..b9eb78ac3da3a773f63c50e1491ba6cc7f1d5bd1 100644 (file)
@@ -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;
index f57a283a06159ce9cc80621e8b3efafa439c2716..7bd68151872c97b1d241727037265b112f2fa05f 100644 (file)
@@ -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<i32>,
     expect_stdout_contains: Vec<String>,
     expect_stderr_contains: Vec<String>,
+    expect_json: Option<Json>,
 }
 
 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<String> {
         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<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,
@@ -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,
     }
 }
 
index 4dc307c92c2cd57f35cbbeea17d5f3bbabb9686a..09afbb2fc1d74ba4c3a6d9a6b23cd2287927ef5f 100644 (file)
@@ -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::<String>()));
+            "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)));
-});