blob: c834778af31edffa026d83bb68025b196bb75b43 [file] [log] [blame]
extern crate cargo_metadata;
extern crate semver;
#[macro_use]
extern crate serde_json;
use camino::Utf8PathBuf;
use cargo_metadata::{
workspace_default_members_is_missing, ArtifactDebuginfo, CargoOpt, DependencyKind, Edition,
Message, Metadata, MetadataCommand,
};
/// Output from oldest version ever supported (1.24).
///
/// This intentionally has as many null fields as possible.
/// 1.8 is when metadata was introduced.
/// Older versions not supported because the following are required:
/// - `workspace_members` added in 1.13
/// - `target_directory` added in 1.19
/// - `workspace_root` added in 1.24
const JSON_OLD_MINIMAL: &str = r#"
{
"packages": [
{
"name": "foo",
"version": "0.1.0",
"id": "foo 0.1.0 (path+file:///foo)",
"license": null,
"license_file": null,
"description": null,
"source": null,
"dependencies": [
{
"name": "somedep",
"source": null,
"req": "^1.0",
"kind": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null
}
],
"targets": [
{
"kind": [
"bin"
],
"crate_types": [
"bin"
],
"name": "foo",
"src_path": "/foo/src/main.rs"
}
],
"features": {},
"manifest_path": "/foo/Cargo.toml"
}
],
"workspace_members": [
"foo 0.1.0 (path+file:///foo)"
],
"resolve": null,
"target_directory": "/foo/target",
"version": 1,
"workspace_root": "/foo"
}
"#;
#[test]
fn old_minimal() {
let meta: Metadata = serde_json::from_str(JSON_OLD_MINIMAL).unwrap();
assert_eq!(meta.packages.len(), 1);
let pkg = &meta.packages[0];
assert_eq!(pkg.name, "foo");
assert_eq!(pkg.version, semver::Version::parse("0.1.0").unwrap());
assert_eq!(pkg.authors.len(), 0);
assert_eq!(pkg.id.to_string(), "foo 0.1.0 (path+file:///foo)");
assert_eq!(pkg.description, None);
assert_eq!(pkg.license, None);
assert_eq!(pkg.license_file, None);
assert_eq!(pkg.default_run, None);
assert_eq!(pkg.rust_version, None);
assert_eq!(pkg.dependencies.len(), 1);
let dep = &pkg.dependencies[0];
assert_eq!(dep.name, "somedep");
assert_eq!(dep.source, None);
assert_eq!(dep.req, semver::VersionReq::parse("^1.0").unwrap());
assert_eq!(dep.kind, DependencyKind::Normal);
assert!(!dep.optional);
assert!(dep.uses_default_features);
assert_eq!(dep.features.len(), 0);
assert!(dep.target.is_none());
assert_eq!(dep.rename, None);
assert_eq!(dep.registry, None);
assert_eq!(pkg.targets.len(), 1);
let target = &pkg.targets[0];
assert_eq!(target.name, "foo");
assert_eq!(target.kind, vec!["bin"]);
assert_eq!(target.crate_types, vec!["bin"]);
assert_eq!(target.required_features.len(), 0);
assert_eq!(target.src_path, "/foo/src/main.rs");
assert_eq!(target.edition, Edition::E2015);
assert!(target.doctest);
assert!(target.test);
assert!(target.doc);
assert_eq!(pkg.features.len(), 0);
assert_eq!(pkg.manifest_path, "/foo/Cargo.toml");
assert_eq!(pkg.categories.len(), 0);
assert_eq!(pkg.keywords.len(), 0);
assert_eq!(pkg.readme, None);
assert_eq!(pkg.repository, None);
assert_eq!(pkg.homepage, None);
assert_eq!(pkg.documentation, None);
assert_eq!(pkg.edition, Edition::E2015);
assert_eq!(pkg.metadata, serde_json::Value::Null);
assert_eq!(pkg.links, None);
assert_eq!(pkg.publish, None);
assert_eq!(meta.workspace_members.len(), 1);
assert_eq!(
meta.workspace_members[0].to_string(),
"foo 0.1.0 (path+file:///foo)"
);
assert!(meta.resolve.is_none());
assert_eq!(meta.workspace_root, "/foo");
assert_eq!(meta.workspace_metadata, serde_json::Value::Null);
assert_eq!(meta.target_directory, "/foo/target");
assert!(workspace_default_members_is_missing(
&meta.workspace_default_members
));
let serialized = serde_json::to_value(meta).unwrap();
assert!(!serialized
.as_object()
.unwrap()
.contains_key("workspace_default_members"));
}
macro_rules! sorted {
($e:expr) => {{
let mut v = $e.clone();
v.sort();
v
}};
}
fn cargo_version() -> semver::Version {
let output = std::process::Command::new("cargo")
.arg("-V")
.output()
.expect("Failed to exec cargo.");
let out = std::str::from_utf8(&output.stdout)
.expect("invalid utf8")
.trim();
let split: Vec<&str> = out.split_whitespace().collect();
assert!(split.len() >= 2, "cargo -V output is unexpected: {}", out);
let mut ver = semver::Version::parse(split[1]).expect("cargo -V semver could not be parsed");
// Don't care about metadata, it is awkward to compare.
ver.pre = semver::Prerelease::EMPTY;
ver.build = semver::BuildMetadata::EMPTY;
ver
}
#[derive(serde::Deserialize, PartialEq, Eq, Debug)]
struct WorkspaceMetadata {
testobject: TestObject,
}
#[derive(serde::Deserialize, PartialEq, Eq, Debug)]
struct TestObject {
myvalue: String,
}
#[test]
fn all_the_fields() {
// All the fields currently generated as of 1.60. This tries to exercise as
// much as possible.
let ver = cargo_version();
let minimum = semver::Version::parse("1.56.0").unwrap();
if ver < minimum {
// edition added in 1.30
// rename added in 1.31
// links added in 1.33
// doctest added in 1.37
// publish added in 1.39
// dep_kinds added in 1.41
// test added in 1.47
// homepage added in 1.49
// documentation added in 1.49
// doc added in 1.50
// path added in 1.51
// default_run added in 1.55
// rust_version added in 1.58
// workspace_default_members added in 1.71
eprintln!("Skipping all_the_fields test, cargo {} is too old.", ver);
return;
}
let meta = MetadataCommand::new()
.manifest_path("tests/all/Cargo.toml")
.exec()
.unwrap();
assert_eq!(meta.workspace_root.file_name().unwrap(), "all");
assert_eq!(
serde_json::from_value::<WorkspaceMetadata>(meta.workspace_metadata.clone()).unwrap(),
WorkspaceMetadata {
testobject: TestObject {
myvalue: "abc".to_string()
}
}
);
assert_eq!(meta.workspace_members.len(), 1);
assert!(meta.workspace_members[0].to_string().starts_with("all"));
if ver >= semver::Version::parse("1.71.0").unwrap() {
assert_eq!(&*meta.workspace_default_members, &meta.workspace_members);
}
assert_eq!(meta.packages.len(), 9);
let all = meta.packages.iter().find(|p| p.name == "all").unwrap();
assert_eq!(all.version, semver::Version::parse("0.1.0").unwrap());
assert_eq!(all.authors, vec!["Jane Doe <user@example.com>"]);
assert!(all.id.to_string().starts_with("all"));
assert_eq!(all.description, Some("Package description.".to_string()));
assert_eq!(all.license, Some("MIT/Apache-2.0".to_string()));
assert_eq!(all.license_file, Some(Utf8PathBuf::from("LICENSE")));
assert!(all.license_file().unwrap().ends_with("tests/all/LICENSE"));
assert_eq!(all.publish, Some(vec![]));
assert_eq!(all.links, Some("foo".to_string()));
assert_eq!(all.default_run, Some("otherbin".to_string()));
if ver >= semver::Version::parse("1.58.0").unwrap() {
assert_eq!(
all.rust_version,
Some(semver::Version::parse("1.56.0").unwrap())
);
}
assert_eq!(all.dependencies.len(), 8);
let bitflags = all
.dependencies
.iter()
.find(|d| d.name == "bitflags")
.unwrap();
assert_eq!(
bitflags.source,
Some("registry+https://github.com/rust-lang/crates.io-index".to_string())
);
assert!(bitflags.optional);
assert_eq!(bitflags.req, semver::VersionReq::parse("^1.0").unwrap());
let path_dep = all
.dependencies
.iter()
.find(|d| d.name == "path-dep")
.unwrap();
assert_eq!(path_dep.source, None);
assert_eq!(path_dep.kind, DependencyKind::Normal);
assert_eq!(path_dep.req, semver::VersionReq::parse("*").unwrap());
assert_eq!(
path_dep.path.as_ref().map(|p| p.ends_with("path-dep")),
Some(true),
);
all.dependencies
.iter()
.find(|d| d.name == "namedep")
.unwrap();
let featdep = all
.dependencies
.iter()
.find(|d| d.name == "featdep")
.unwrap();
assert_eq!(featdep.features, vec!["i128"]);
assert!(!featdep.uses_default_features);
let renamed = all
.dependencies
.iter()
.find(|d| d.name == "oldname")
.unwrap();
assert_eq!(renamed.rename, Some("newname".to_string()));
let devdep = all
.dependencies
.iter()
.find(|d| d.name == "devdep")
.unwrap();
assert_eq!(devdep.kind, DependencyKind::Development);
let bdep = all.dependencies.iter().find(|d| d.name == "bdep").unwrap();
assert_eq!(bdep.kind, DependencyKind::Build);
let windep = all
.dependencies
.iter()
.find(|d| d.name == "windep")
.unwrap();
assert_eq!(
windep.target.as_ref().map(|x| x.to_string()),
Some("cfg(windows)".to_string())
);
macro_rules! get_file_name {
($v:expr) => {
all.targets
.iter()
.find(|t| t.src_path.file_name().unwrap() == $v)
.unwrap()
};
}
assert_eq!(all.targets.len(), 8);
let lib = get_file_name!("lib.rs");
assert_eq!(lib.name, "all");
assert_eq!(sorted!(lib.kind), vec!["cdylib", "rlib", "staticlib"]);
assert_eq!(
sorted!(lib.crate_types),
vec!["cdylib", "rlib", "staticlib"]
);
assert_eq!(lib.required_features.len(), 0);
assert_eq!(lib.edition, Edition::E2018);
assert!(lib.doctest);
assert!(lib.test);
assert!(lib.doc);
let main = get_file_name!("main.rs");
assert_eq!(main.crate_types, vec!["bin"]);
assert_eq!(main.kind, vec!["bin"]);
assert!(!main.doctest);
assert!(main.test);
assert!(main.doc);
let otherbin = get_file_name!("otherbin.rs");
assert_eq!(otherbin.edition, Edition::E2015);
assert!(!otherbin.doc);
let reqfeat = get_file_name!("reqfeat.rs");
assert_eq!(reqfeat.required_features, vec!["feat2"]);
let ex1 = get_file_name!("ex1.rs");
assert_eq!(ex1.kind, vec!["example"]);
assert!(!ex1.test);
let t1 = get_file_name!("t1.rs");
assert_eq!(t1.kind, vec!["test"]);
let b1 = get_file_name!("b1.rs");
assert_eq!(b1.kind, vec!["bench"]);
let build = get_file_name!("build.rs");
assert_eq!(build.kind, vec!["custom-build"]);
if ver >= semver::Version::parse("1.60.0").unwrap() {
// 1.60 now reports optional dependencies within the features table
assert_eq!(all.features.len(), 4);
assert_eq!(all.features["bitflags"], vec!["dep:bitflags"]);
} else {
assert_eq!(all.features.len(), 3);
}
assert_eq!(all.features["feat1"].len(), 0);
assert_eq!(all.features["feat2"].len(), 0);
assert_eq!(sorted!(all.features["default"]), vec!["bitflags", "feat1"]);
assert!(all.manifest_path.ends_with("all/Cargo.toml"));
assert_eq!(all.categories, vec!["command-line-utilities"]);
assert_eq!(all.keywords, vec!["cli"]);
assert_eq!(all.readme, Some(Utf8PathBuf::from("README.md")));
assert!(all.readme().unwrap().ends_with("tests/all/README.md"));
assert_eq!(
all.repository,
Some("https://github.com/oli-obk/cargo_metadata/".to_string())
);
assert_eq!(
all.homepage,
Some("https://github.com/oli-obk/cargo_metadata/".to_string())
);
assert_eq!(
all.documentation,
Some("https://docs.rs/cargo_metadata/".to_string())
);
assert_eq!(all.edition, Edition::E2018);
assert_eq!(
all.metadata,
json!({
"docs": {
"rs": {
"all-features": true,
"default-target": "x86_64-unknown-linux-gnu",
"rustc-args": ["--example-rustc-arg"]
}
}
})
);
let resolve = meta.resolve.as_ref().unwrap();
assert!(resolve
.root
.as_ref()
.unwrap()
.to_string()
.starts_with("all"));
assert_eq!(resolve.nodes.len(), 9);
let path_dep = resolve
.nodes
.iter()
.find(|n| n.id.to_string().starts_with("path-dep"))
.unwrap();
assert_eq!(path_dep.deps.len(), 0);
assert_eq!(path_dep.dependencies.len(), 0);
assert_eq!(path_dep.features.len(), 0);
let bitflags = resolve
.nodes
.iter()
.find(|n| n.id.to_string().starts_with("bitflags"))
.unwrap();
assert_eq!(bitflags.features, vec!["default"]);
let featdep = resolve
.nodes
.iter()
.find(|n| n.id.to_string().starts_with("featdep"))
.unwrap();
assert_eq!(featdep.features, vec!["i128"]);
let all = resolve
.nodes
.iter()
.find(|n| n.id.to_string().starts_with("all"))
.unwrap();
assert_eq!(all.dependencies.len(), 8);
assert_eq!(all.deps.len(), 8);
let newname = all.deps.iter().find(|d| d.name == "newname").unwrap();
assert!(newname.pkg.to_string().starts_with("oldname"));
// Note the underscore here.
let path_dep = all.deps.iter().find(|d| d.name == "path_dep").unwrap();
assert!(path_dep.pkg.to_string().starts_with("path-dep"));
assert_eq!(path_dep.dep_kinds.len(), 1);
let kind = &path_dep.dep_kinds[0];
assert_eq!(kind.kind, DependencyKind::Normal);
assert!(kind.target.is_none());
let namedep = all
.deps
.iter()
.find(|d| d.name == "different_name")
.unwrap();
assert!(namedep.pkg.to_string().starts_with("namedep"));
assert_eq!(sorted!(all.features), vec!["bitflags", "default", "feat1"]);
let bdep = all.deps.iter().find(|d| d.name == "bdep").unwrap();
assert_eq!(bdep.dep_kinds.len(), 1);
let kind = &bdep.dep_kinds[0];
assert_eq!(kind.kind, DependencyKind::Build);
assert!(kind.target.is_none());
let devdep = all.deps.iter().find(|d| d.name == "devdep").unwrap();
assert_eq!(devdep.dep_kinds.len(), 1);
let kind = &devdep.dep_kinds[0];
assert_eq!(kind.kind, DependencyKind::Development);
assert!(kind.target.is_none());
let windep = all.deps.iter().find(|d| d.name == "windep").unwrap();
assert_eq!(windep.dep_kinds.len(), 1);
let kind = &windep.dep_kinds[0];
assert_eq!(kind.kind, DependencyKind::Normal);
assert_eq!(
kind.target.as_ref().map(|x| x.to_string()),
Some("cfg(windows)".to_string())
);
let serialized = serde_json::to_value(meta).unwrap();
if ver >= semver::Version::parse("1.71.0").unwrap() {
assert!(serialized.as_object().unwrap()["workspace_default_members"]
.as_array()
.is_some());
} else {
assert!(!serialized
.as_object()
.unwrap()
.contains_key("workspace_default_members"));
}
}
#[test]
fn alt_registry() {
// This is difficult to test (would need to set up a custom index).
// Just manually check the JSON is handled.
let json = r#"
{
"packages": [
{
"name": "alt",
"version": "0.1.0",
"id": "alt 0.1.0 (path+file:///alt)",
"source": null,
"dependencies": [
{
"name": "alt2",
"source": "registry+https://example.com",
"req": "^0.1",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": "https://example.com"
}
],
"targets": [
{
"kind": [
"lib"
],
"crate_types": [
"lib"
],
"name": "alt",
"src_path": "/alt/src/lib.rs",
"edition": "2018"
}
],
"features": {},
"manifest_path": "/alt/Cargo.toml",
"metadata": null,
"authors": [],
"categories": [],
"keywords": [],
"readme": null,
"repository": null,
"edition": "2018",
"links": null
}
],
"workspace_members": [
"alt 0.1.0 (path+file:///alt)"
],
"resolve": null,
"target_directory": "/alt/target",
"version": 1,
"workspace_root": "/alt"
}
"#;
let meta: Metadata = serde_json::from_str(json).unwrap();
assert_eq!(meta.packages.len(), 1);
let alt = &meta.packages[0];
let deps = &alt.dependencies;
assert_eq!(deps.len(), 1);
let dep = &deps[0];
assert_eq!(dep.registry, Some("https://example.com".to_string()));
}
#[test]
fn current_dir() {
let meta = MetadataCommand::new()
.current_dir("tests/all/namedep")
.exec()
.unwrap();
let namedep = meta.packages.iter().find(|p| p.name == "namedep").unwrap();
assert!(namedep.name.starts_with("namedep"));
}
#[test]
fn parse_stream_is_robust() {
// Proc macros can print stuff to stdout, which naturally breaks JSON messages.
// Let's check that we don't die horribly in this case, and report an error.
let json_output = r##"{"reason":"compiler-artifact","package_id":"chatty 0.1.0 (path+file:///chatty-macro/chatty)","manifest_path":"chatty-macro/Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"chatty","src_path":"/chatty-macro/chatty/src/lib.rs","edition":"2018","doctest":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/chatty-macro/target/debug/deps/libchatty-f2adcff24cdf3bb2.so"],"executable":null,"fresh":false}
Evil proc macro was here!
{"reason":"compiler-artifact","package_id":"chatty-macro 0.1.0 (path+file:///chatty-macro)","manifest_path":"chatty-macro/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"chatty-macro","src_path":"/chatty-macro/src/lib.rs","edition":"2018","doctest":true},"profile":{"opt_level":"0","debuginfo":2,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["/chatty-macro/target/debug/libchatty_macro.rlib","/chatty-macro/target/debug/deps/libchatty_macro-cb5956ed52a11fb6.rmeta"],"executable":null,"fresh":false}
"##;
let mut n_messages = 0;
let mut text = String::new();
for message in cargo_metadata::Message::parse_stream(json_output.as_bytes()) {
let message = message.unwrap();
match message {
cargo_metadata::Message::TextLine(line) => text = line,
_ => n_messages += 1,
}
}
assert_eq!(n_messages, 2);
assert_eq!(text, "Evil proc macro was here!");
}
#[test]
fn advanced_feature_configuration() {
fn build_features<F: FnOnce(&mut MetadataCommand) -> &mut MetadataCommand>(
func: F,
) -> Vec<String> {
let mut meta = MetadataCommand::new();
let meta = meta.manifest_path("tests/all/Cargo.toml");
let meta = func(meta);
let meta = meta.exec().unwrap();
let resolve = meta.resolve.as_ref().unwrap();
let all = resolve
.nodes
.iter()
.find(|n| n.id.to_string().starts_with("all"))
.unwrap();
all.features.clone()
}
// Default behavior; tested above
let default_features = build_features(|meta| meta);
assert_eq!(
sorted!(default_features),
vec!["bitflags", "default", "feat1"]
);
// Manually specify the same default features
let manual_features = build_features(|meta| {
meta.features(CargoOpt::NoDefaultFeatures)
.features(CargoOpt::SomeFeatures(vec![
"feat1".into(),
"bitflags".into(),
]))
});
assert_eq!(sorted!(manual_features), vec!["bitflags", "feat1"]);
// Multiple SomeFeatures is same as one longer SomeFeatures
let manual_features = build_features(|meta| {
meta.features(CargoOpt::NoDefaultFeatures)
.features(CargoOpt::SomeFeatures(vec!["feat1".into()]))
.features(CargoOpt::SomeFeatures(vec!["feat2".into()]))
});
assert_eq!(sorted!(manual_features), vec!["feat1", "feat2"]);
// No features + All features == All features
let all_features = build_features(|meta| {
meta.features(CargoOpt::AllFeatures)
.features(CargoOpt::NoDefaultFeatures)
});
assert_eq!(
sorted!(all_features),
vec!["bitflags", "default", "feat1", "feat2"]
);
// The '--all-features' flag supersedes other feature flags
let all_flag_variants = build_features(|meta| {
meta.features(CargoOpt::SomeFeatures(vec!["feat2".into()]))
.features(CargoOpt::NoDefaultFeatures)
.features(CargoOpt::AllFeatures)
});
assert_eq!(sorted!(all_flag_variants), sorted!(all_features));
}
#[test]
fn depkind_to_string() {
assert_eq!(DependencyKind::Normal.to_string(), "normal");
assert_eq!(DependencyKind::Development.to_string(), "dev");
assert_eq!(DependencyKind::Build.to_string(), "build");
assert_eq!(DependencyKind::Unknown.to_string(), "Unknown");
}
#[test]
fn basic_workspace_root_package_exists() {
// First try with dependencies
let meta = MetadataCommand::new()
.manifest_path("tests/basic_workspace/Cargo.toml")
.exec()
.unwrap();
assert_eq!(meta.root_package().unwrap().name, "ex_bin");
// Now with no_deps, it should still work exactly the same
let meta = MetadataCommand::new()
.manifest_path("tests/basic_workspace/Cargo.toml")
.no_deps()
.exec()
.unwrap();
assert_eq!(
meta.root_package()
.expect("workspace root still exists when no_deps used")
.name,
"ex_bin"
);
}
#[test]
fn debuginfo_variants() {
// Checks behavior for the different debuginfo variants.
let variants = [
("0", ArtifactDebuginfo::None),
("1", ArtifactDebuginfo::Limited),
("2", ArtifactDebuginfo::Full),
(
"\"line-directives-only\"",
ArtifactDebuginfo::LineDirectivesOnly,
),
("\"line-tables-only\"", ArtifactDebuginfo::LineTablesOnly),
("3", ArtifactDebuginfo::UnknownInt(3)),
(
"\"abc\"",
ArtifactDebuginfo::UnknownString("abc".to_string()),
),
("null", ArtifactDebuginfo::None),
];
for (value, expected) in variants {
let s = r#"{"reason":"compiler-artifact","package_id":"cargo_metadata 0.16.0 (path+file:////cargo_metadata)","manifest_path":"/cargo_metadata/Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cargo_metadata","src_path":"/cargo_metadata/src/lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":DEBUGINFO,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["/cargo_metadata/target/debug/deps/libcargo_metadata-27f582f7187b9a2c.rmeta"],"executable":null,"fresh":false}"#;
let message: Message = serde_json::from_str(&s.replace("DEBUGINFO", value)).unwrap();
match message {
Message::CompilerArtifact(artifact) => {
assert_eq!(artifact.profile.debuginfo, expected);
let de_s = serde_json::to_string(&artifact.profile.debuginfo).unwrap();
// Note: Roundtrip does not retain null value.
if value == "null" {
assert_eq!(artifact.profile.debuginfo.to_string(), "0");
assert_eq!(de_s, "0");
} else {
assert_eq!(
artifact.profile.debuginfo.to_string(),
value.trim_matches('"')
);
assert_eq!(de_s, value);
}
}
_ => panic!("unexpected {:?}", message),
}
}
}
#[test]
#[should_panic = "WorkspaceDefaultMembers should only be dereferenced on Cargo versions >= 1.71"]
fn missing_workspace_default_members() {
let meta: Metadata = serde_json::from_str(JSON_OLD_MINIMAL).unwrap();
let _ = &*meta.workspace_default_members;
}