diff --git a/src/errors.rs b/src/errors.rs index 4d08200c..5b007a06 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -49,4 +49,16 @@ pub enum Error { /// The output did not contain any json #[error("could not find any json in the output of `cargo metadata`")] NoJson, + + /// WIP. + #[error("could not collect features")] + Features(FeaturesError), +} + +/// WIP. +#[derive(Debug, thiserror::Error)] +pub enum FeaturesError { + /// WIP. + #[error("Package not found {0}")] + PackageNotFound(String), } diff --git a/src/lib.rs b/src/lib.rs index 5a51e83a..8d5db5dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,12 +93,13 @@ use std::str::{from_utf8, FromStr}; pub use camino; pub use semver; use semver::Version; +use serde::{Deserialize, Deserializer, Serialize}; #[cfg(feature = "builder")] pub use dependency::DependencyBuilder; pub use dependency::{Dependency, DependencyKind}; use diagnostic::Diagnostic; -pub use errors::{Error, Result}; +pub use errors::{Error, FeaturesError, Result}; #[cfg(feature = "unstable")] pub use libtest::TestMessage; #[allow(deprecated)] @@ -112,7 +113,7 @@ pub use messages::{ ArtifactBuilder, ArtifactProfileBuilder, BuildFinishedBuilder, BuildScriptBuilder, CompilerMessageBuilder, }; -use serde::{Deserialize, Deserializer, Serialize}; +pub use visit::{FeatureVisitor, FeatureWalker}; mod dependency; pub mod diagnostic; @@ -120,6 +121,7 @@ mod errors; #[cfg(feature = "unstable")] pub mod libtest; mod messages; +mod visit; /// An "opaque" identifier for a package. /// @@ -211,6 +213,11 @@ impl Metadata { .filter(|&p| self.workspace_default_members.contains(&p.id)) .collect() } + + /// Returns the package for a given name. + pub fn get_package(&self, name: &str) -> Option<&Package> { + self.packages.iter().find(|&package| package.name == name) + } } impl<'a> std::ops::Index<&'a PackageId> for Metadata { @@ -347,6 +354,95 @@ pub struct DepKindInfo { pub target: Option, } +// Implementation copied from `cargo` itself: +// https://docs.rs/cargo/latest/cargo/core/summary/enum.FeatureValue.html +/// The types of a feature and the dependencies it can have. +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum FeatureValue { + /// A feature enabling another feature. + Feature(String), + /// A feature enabling a dependency with `dep:dep_name` syntax. + Dep { + /// The dependency's name. + dep_name: String, + }, + /// A feature enabling a feature on a dependency with `crate_name/feat_name` syntax. + DepFeature { + /// The dependency's name. + dep_name: String, + /// The dependency's feature's name. + dep_feature: String, + /// If `true`, indicates the `?` syntax is used, which means this will + /// not automatically enable the dependency unless the dependency is + /// activated through some other means. + weak: bool, + }, +} + +impl FeatureValue { + // Implementation copied from `cargo` itself: + // https://doc.rust-lang.org/nightly/core/fmt/trait.Display.html#tymethod.fmt + /// Creates a feature from its corresponding string representation. + pub fn new(repr: &str) -> Self { + match repr.split_once('/') { + Some((dep, dep_feat)) => { + let dep_name = dep.strip_suffix('?'); + FeatureValue::DepFeature { + dep_name: dep_name.unwrap_or(dep).to_owned(), + dep_feature: dep_feat.to_owned(), + weak: dep_name.is_some(), + } + } + None => { + if let Some(dep_name) = repr.strip_prefix("dep:") { + FeatureValue::Dep { + dep_name: dep_name.to_owned(), + } + } else { + FeatureValue::Feature(repr.to_owned()) + } + } + } + } +} + +impl std::fmt::Display for FeatureValue { + // Implementation copied from `cargo` itself: + // https://docs.rs/cargo/latest/cargo/core/summary/enum.FeatureValue.html#method.new + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Feature(feat) => write!(f, "{feat}"), + Self::Dep { dep_name } => write!(f, "dep:{dep_name}"), + Self::DepFeature { + dep_name, + dep_feature, + weak, + } => { + let weak = if *weak { "?" } else { "" }; + write!(f, "{dep_name}{weak}/{dep_feature}") + } + } + } +} + +impl<'de> Deserialize<'de> for FeatureValue { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + Ok(Self::new(&String::deserialize(deserializer)?)) + } +} + +impl Serialize for FeatureValue { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "builder", derive(Builder))] #[non_exhaustive] @@ -390,7 +486,7 @@ pub struct Package { pub targets: Vec, /// Features provided by the crate, mapped to the features required by that feature. #[cfg_attr(feature = "builder", builder(default))] - pub features: BTreeMap>, + pub features: BTreeMap>, /// Path containing the `Cargo.toml` pub manifest_path: Utf8PathBuf, /// The [`categories` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-categories-field) as specified in the `Cargo.toml` @@ -514,6 +610,13 @@ impl Package { .join(file) }) } + + /// Returns the dependency for a given name, taking renames into consideration. + pub fn get_dependency(&self, name: &str) -> Option<&Dependency> { + self.dependencies + .iter() + .find(|&dependency| &dependency.name == name) + } } /// The source of a package such as crates.io. diff --git a/src/visit.rs b/src/visit.rs new file mode 100644 index 00000000..2a646c1f --- /dev/null +++ b/src/visit.rs @@ -0,0 +1,225 @@ +use std::collections::BTreeMap; + +use crate::{FeatureValue, Metadata, Package}; + +/// A visitor over a package's features and their dependencies. +pub trait FeatureVisitor { + /// The error type of a walk. + type Error; + + /// Visits a missing dependency. + /// + /// This error should not happen for valid manifests, + /// but can happen when reading `Metadata` from unchecked JSON. + /// + /// Return `Ok(())` to continue the walk, or `Err(…)` to abort it. + fn visit_missing_dependency(&mut self, dep_name: &str) -> Result<(), Self::Error>; + + /// Visits a missing package. + /// + /// This is usually caused by the package being an optional dependency and + /// not having been enabled by the features that were passed to `MetadataCommand`, + /// but it can also happen when reading `Metadata` from unchecked JSON. + /// + /// Return `Ok(())` to continue the walk, or `Err(…)` to abort it. + fn visit_missing_package(&mut self, pkg_name: &str) -> Result<(), Self::Error>; + + /// Visits a feature on `package` that's enabling another feature `feature_name`. + /// + /// Corresponds to features with `feature_name` syntax. + /// + /// Return `Ok()` where `` indicates whether or not to walk + /// the feature's downstream dependencies, or `Err(…)` to abort the walk. + fn visit_feature( + &mut self, + package: &Package, + feature_name: &str, + ) -> Result { + let (..) = (package, feature_name); + Ok(true) + } + + /// Visits a feature on `package` that's enabling its dependency `dep_name`. + /// + /// Corresponds to features with `dep:dep_name` syntax. + /// + /// Return `Ok()` where `` indicates whether or not to walk + /// the feature's downstream dependencies, or `Err(…)` to abort the walk. + fn visit_dep(&mut self, package: &Package, dep_name: &str) -> Result { + let (..) = (package, dep_name); + Ok(true) + } + + /// Visits a feature on `package` that's enabling feature `dep_feature` + /// on its dependency `dep_name`. + /// + /// Corresponds to features with `dep_name/dep_feature`/`dep_name?/dep_feature` syntax. + /// + /// Return `Ok()` where `` indicates whether or not to walk + /// the feature's downstream dependencies, or `Err(…)` to abort the walk. + fn visit_dep_feature( + &mut self, + package: &Package, + dep_name: &str, + dep_feature: &str, + weak: bool, + ) -> Result { + let (..) = (package, dep_name, dep_feature, weak); + Ok(true) + } +} + +/// A type for walking package features and their dependencies. +pub struct FeatureWalker<'a> { + packages_by_name: BTreeMap, +} + +impl<'a> FeatureWalker<'a> { + /// Creates a walker for a given `metadata`. + pub fn new(metadata: &'a Metadata) -> Self { + let packages_by_name = metadata + .packages + .iter() + .map(|package| (package.name.clone(), package)) + .collect(); + Self { packages_by_name } + } + + /// Walks the selected features of a package and their dependencies. + pub fn walk_package_features( + &self, + package: &Package, + feature_names: I, + visitor: &mut V, + ) -> Result<(), V::Error> + where + V: FeatureVisitor, + I: IntoIterator, + F: AsRef, + { + for feature_name in feature_names { + self.walk_feature(package, feature_name.as_ref(), visitor)?; + } + + Ok(()) + } + + fn walk_feature( + &self, + package: &Package, + feature_name: &str, + visitor: &mut V, + ) -> Result<(), V::Error> + where + V: FeatureVisitor, + { + let Some(required_features) = package.features.get(feature_name) else { + return Ok(()); + }; + + if !visitor.visit_feature(package, feature_name)? { + return Ok(()); + } + + for required_feature in required_features { + self.walk_feature_value(package, required_feature, visitor)?; + } + + Ok(()) + } + + fn walk_dep( + &self, + package: &Package, + dep_name: &str, + visitor: &mut V, + ) -> Result<(), V::Error> + where + V: FeatureVisitor, + { + let Some(dependency) = package.get_dependency(dep_name) else { + return visitor.visit_missing_dependency(dep_name); + }; + + if !visitor.visit_dep(package, dep_name)? { + return Ok(()); + } + + let package_name = &dependency.name; + + match self.packages_by_name.get(package_name) { + Some(&dep_package) => { + for dep_feature in dependency.features.iter() { + let dep_feature = FeatureValue::new(dep_feature); + assert!(matches!(dep_feature, FeatureValue::Feature(_))); + self.walk_feature_value(dep_package, &dep_feature, visitor)?; + } + } + None => visitor.visit_missing_package(package_name)?, + } + + Ok(()) + } + + fn walk_dep_feature( + &self, + package: &Package, + dep_name: &str, + dep_feature: &str, + weak: bool, + visitor: &mut V, + ) -> Result<(), V::Error> + where + V: FeatureVisitor, + { + let Some(dependency) = package.get_dependency(dep_name) else { + return visitor.visit_missing_dependency(dep_name); + }; + + if !visitor.visit_dep_feature(package, dep_name, dep_feature, weak)? { + return Ok(()); + } + + let package_name = &dependency.name; + + let Some(&dep_package) = self.packages_by_name.get(package_name) else { + return visitor.visit_missing_package(package_name); + }; + + let dep_feature = FeatureValue::new(dep_feature); + assert!(matches!(dep_feature, FeatureValue::Feature(_))); + self.walk_feature_value(dep_package, &dep_feature, visitor)?; + + if !weak { + for feature_name in dependency.features.iter() { + let dep_feature = FeatureValue::new(feature_name); + assert!(matches!(dep_feature, FeatureValue::Feature(_))); + self.walk_feature_value(dep_package, &dep_feature, visitor)?; + } + } + + Ok(()) + } + + fn walk_feature_value( + &self, + package: &Package, + feature_value: &FeatureValue, + visitor: &mut V, + ) -> Result<(), V::Error> + where + V: FeatureVisitor, + { + match feature_value { + FeatureValue::Feature(feature_name) => { + self.walk_feature(package, feature_name, visitor) + } + FeatureValue::Dep { dep_name } => self.walk_dep(package, dep_name, visitor), + FeatureValue::DepFeature { + dep_name, + dep_feature, + weak, + } => self.walk_dep_feature(package, dep_name, dep_feature, *weak, visitor), + } + } +} diff --git a/tests/all/Cargo.lock b/tests/all/Cargo.lock index 21a1a48b..5017f7a2 100644 --- a/tests/all/Cargo.lock +++ b/tests/all/Cargo.lock @@ -12,6 +12,7 @@ dependencies = [ "featdep", "namedep", "oldname", + "optdep", "path-dep", "windep", ] @@ -42,6 +43,10 @@ version = "0.1.0" name = "oldname" version = "0.1.0" +[[package]] +name = "optdep" +version = "0.1.0" + [[package]] name = "path-dep" version = "0.1.0" diff --git a/tests/all/Cargo.toml b/tests/all/Cargo.toml index 95a6a17b..7e19df9a 100644 --- a/tests/all/Cargo.toml +++ b/tests/all/Cargo.toml @@ -21,7 +21,7 @@ rust-version = "1.56" [package.metadata.docs.rs] all-features = true default-target = "x86_64-unknown-linux-gnu" -rustc-args = [ "--example-rustc-arg" ] +rustc-args = ["--example-rustc-arg"] [dependencies] path-dep = { path = "path-dep" } @@ -29,6 +29,7 @@ namedep = { path = "namedep" } bitflags = { version = "1.0", optional = true } featdep = { path = "featdep", features = ["i128"], default-features = false } newname = { path = "oldname", package = "oldname" } +optdep = { path = "optdep", optional = true } [dev-dependencies] devdep = { path = "devdep" } @@ -43,6 +44,8 @@ windep = { path = "windep" } default = ["feat1", "bitflags"] feat1 = [] feat2 = [] +opt-feat-strong = ["optdep/feat"] +opt-feat-weak = ["optdep?/feat"] [lib] crate-type = ["rlib", "cdylib", "staticlib"] @@ -57,7 +60,19 @@ name = "reqfeat" required-features = ["feat2"] [workspace] -exclude = ["bare-rust-version", "bdep", "benches", "devdep", "examples", "featdep", "namedep", "oldname", "path-dep", "windep"] +exclude = [ + "bare-rust-version", + "bdep", + "benches", + "devdep", + "examples", + "featdep", + "namedep", + "oldname", + "optdep", + "path-dep", + "windep", +] [workspace.metadata.testobject] myvalue = "abc" diff --git a/tests/all/optdep/Cargo.toml b/tests/all/optdep/Cargo.toml new file mode 100644 index 00000000..72fb7cc6 --- /dev/null +++ b/tests/all/optdep/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "optdep" +version = "0.1.0" +edition = "2018" + +[dependencies] + +[features] +feat = [] diff --git a/tests/all/optdep/src/lib.rs b/tests/all/optdep/src/lib.rs new file mode 100644 index 00000000..e69de29b diff --git a/tests/selftest.rs b/tests/selftest.rs index e6dadf77..9717bc32 100644 --- a/tests/selftest.rs +++ b/tests/selftest.rs @@ -18,7 +18,7 @@ fn metadata() { let this = &metadata.packages[0]; assert_eq!(this.name, "cargo_metadata"); - assert_eq!(this.targets.len(), 3); + assert_eq!(this.targets.len(), 4); let lib = this .targets diff --git a/tests/test_samples.rs b/tests/test_samples.rs index 2f406c5b..85bd9443 100644 --- a/tests/test_samples.rs +++ b/tests/test_samples.rs @@ -6,7 +6,7 @@ extern crate serde_json; use camino::Utf8PathBuf; use cargo_metadata::{ workspace_default_members_is_missing, ArtifactDebuginfo, CargoOpt, DependencyKind, Edition, - Message, Metadata, MetadataCommand, + FeatureValue, Message, Metadata, MetadataCommand, }; /// Output from oldest version ever supported (1.24). @@ -232,7 +232,7 @@ fn all_the_fields() { ); } - assert_eq!(all.dependencies.len(), 8); + assert_eq!(all.dependencies.len(), 9); let bitflags = all .dependencies .iter() @@ -352,14 +352,41 @@ fn all_the_fields() { 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"]); + assert_eq!(all.features.len(), 7); + assert_eq!( + all.features["bitflags"], + vec![FeatureValue::Dep { + dep_name: "bitflags".to_owned() + }] + ); } else { - assert_eq!(all.features.len(), 3); + assert_eq!(all.features.len(), 6); } assert_eq!(all.features["feat1"].len(), 0); assert_eq!(all.features["feat2"].len(), 0); - assert_eq!(sorted!(all.features["default"]), vec!["bitflags", "feat1"]); + assert_eq!( + sorted!(all.features["default"]), + vec![ + FeatureValue::Feature("bitflags".to_owned()), + FeatureValue::Feature("feat1".to_owned()), + ] + ); + assert_eq!( + sorted!(all.features["opt-feat-strong"]), + vec![FeatureValue::DepFeature { + dep_name: "optdep".to_owned(), + dep_feature: "feat".to_owned(), + weak: false + },] + ); + assert_eq!( + sorted!(all.features["opt-feat-weak"]), + vec![FeatureValue::DepFeature { + dep_name: "optdep".to_owned(), + dep_feature: "feat".to_owned(), + weak: true + },] + ); assert!(all.manifest_path.ends_with("all/Cargo.toml")); assert_eq!(all.categories, vec!["command-line-utilities"]); @@ -632,7 +659,15 @@ fn advanced_feature_configuration() { }); assert_eq!( sorted!(all_features), - vec!["bitflags", "default", "feat1", "feat2"] + vec![ + "bitflags", + "default", + "feat1", + "feat2", + "opt-feat-strong", + "opt-feat-weak", + "optdep" + ] ); // The '--all-features' flag supersedes other feature flags @@ -644,6 +679,35 @@ fn advanced_feature_configuration() { assert_eq!(sorted!(all_flag_variants), sorted!(all_features)); } +#[test] +fn features() { + let mut cmd = MetadataCommand::new(); + let cmd = cmd.manifest_path("tests/all/Cargo.toml"); + let meta = cmd.exec().unwrap(); + + let package = meta.root_package().unwrap(); + let features: Vec<&String> = package.features.keys().collect(); + assert_eq!( + features, + vec![ + "bitflags", + "default", + "feat1", + "feat2", + "opt-feat-strong", + "opt-feat-weak", + "optdep", + ] + ); + + assert_eq!( + package.features["bitflags"], + vec![FeatureValue::Dep { + dep_name: "bitflags".to_owned() + }] + ); +} + #[test] fn depkind_to_string() { assert_eq!(DependencyKind::Normal.to_string(), "normal"); diff --git a/tests/visitor.rs b/tests/visitor.rs new file mode 100644 index 00000000..cb52d15a --- /dev/null +++ b/tests/visitor.rs @@ -0,0 +1,257 @@ +extern crate cargo_metadata; +extern crate semver; + +use std::collections::{BTreeMap, BTreeSet}; + +use cargo_metadata::{ + CargoOpt, FeatureValue, FeatureVisitor, FeatureWalker, MetadataCommand, Package, PackageId, +}; + +#[derive(Default, Debug)] +struct TransitiveFeatureCollector { + features: BTreeMap>, + err_on_missing: bool, +} + +impl TransitiveFeatureCollector { + fn new(err_on_missing: bool) -> Self { + Self { + features: BTreeMap::default(), + err_on_missing, + } + } + + fn collect_feature_value(&mut self, package_id: PackageId, feature_value: FeatureValue) { + self.features + .entry(package_id) + .or_default() + .insert(feature_value); + } +} + +impl FeatureVisitor for TransitiveFeatureCollector { + type Error = String; + + fn visit_missing_dependency(&mut self, dep_name: &str) -> Result<(), Self::Error> { + if self.err_on_missing { + Err(format!("missing dependency: {dep_name:?}")) + } else { + Ok(()) + } + } + + fn visit_missing_package(&mut self, pkg_name: &str) -> Result<(), Self::Error> { + if self.err_on_missing { + Err(format!("missing package: {pkg_name:?}")) + } else { + Ok(()) + } + } + + fn visit_feature( + &mut self, + package: &Package, + feature_name: &str, + ) -> Result { + self.collect_feature_value( + package.id.clone(), + FeatureValue::Feature(feature_name.to_owned()), + ); + + Ok(true) + } + + fn visit_dep(&mut self, package: &Package, dep_name: &str) -> Result { + self.collect_feature_value( + package.id.clone(), + FeatureValue::Dep { + dep_name: dep_name.to_owned(), + }, + ); + + Ok(true) + } + + fn visit_dep_feature( + &mut self, + package: &Package, + dep_name: &str, + dep_feature: &str, + weak: bool, + ) -> Result { + self.collect_feature_value( + package.id.clone(), + FeatureValue::DepFeature { + dep_name: dep_name.to_owned(), + dep_feature: dep_feature.to_owned(), + weak: weak, + }, + ); + + Ok(true) + } +} + +#[test] +fn all_features() -> Result<(), cargo_metadata::Error> { + let mut cmd = MetadataCommand::new(); + cmd.manifest_path("tests/all/Cargo.toml"); + let meta = cmd.exec().unwrap(); + + let root_package = meta.root_package().unwrap(); + + let walker = FeatureWalker::new(&meta); + + let mut collector = TransitiveFeatureCollector::new(false); + + let mut features = match walker.walk_package_features( + root_package, + root_package.features.keys(), + &mut collector, + ) { + Ok(()) => collector.features, + Err(err) => panic!("{err}"), + }; + assert_eq!(features.len(), 1); + + let package_features = features.remove(&root_package.id).unwrap(); + assert_eq!( + package_features, + BTreeSet::from_iter([ + FeatureValue::Feature("bitflags".to_owned(),), + FeatureValue::Feature("default".to_owned(),), + FeatureValue::Feature("feat1".to_owned(),), + FeatureValue::Feature("feat2".to_owned(),), + FeatureValue::Feature("opt-feat-strong".to_owned(),), + FeatureValue::Feature("opt-feat-weak".to_owned(),), + FeatureValue::Feature("optdep".to_owned(),), + FeatureValue::Dep { + dep_name: "bitflags".to_owned(), + }, + FeatureValue::Dep { + dep_name: "optdep".to_owned(), + }, + FeatureValue::DepFeature { + dep_name: "optdep".to_owned(), + dep_feature: "feat".to_owned(), + weak: false, + }, + FeatureValue::DepFeature { + dep_name: "optdep".to_owned(), + dep_feature: "feat".to_owned(), + weak: true, + }, + ]) + ); + + Ok(()) +} + +#[test] +fn default_features() -> Result<(), cargo_metadata::Error> { + let mut cmd = MetadataCommand::new(); + cmd.manifest_path("tests/all/Cargo.toml"); + let meta = cmd.exec().unwrap(); + + let root_package = meta.root_package().unwrap(); + + let walker = FeatureWalker::new(&meta); + + let mut collector = TransitiveFeatureCollector::new(false); + + let mut features = match walker.walk_package_features(root_package, ["default"], &mut collector) + { + Ok(()) => collector.features, + Err(err) => panic!("{err}"), + }; + assert_eq!(features.len(), 1); + + let package_features = features.remove(&root_package.id).unwrap(); + assert_eq!( + package_features, + BTreeSet::from_iter([ + FeatureValue::Feature("bitflags".to_owned(),), + FeatureValue::Feature("default".to_owned(),), + FeatureValue::Feature("feat1".to_owned(),), + FeatureValue::Dep { + dep_name: "bitflags".to_owned(), + }, + ]) + ); + + Ok(()) +} + +#[test] +fn strong_dep_feature() -> Result<(), cargo_metadata::Error> { + let mut cmd = MetadataCommand::new(); + cmd.manifest_path("tests/all/Cargo.toml"); + // Without this the `optdep` package will be missing on `Metadata`: + cmd.features(CargoOpt::SomeFeatures(vec!["opt-feat-strong".to_owned()])); + let meta = cmd.exec().unwrap(); + + let root_package = meta.root_package().unwrap(); + + let walker = FeatureWalker::new(&meta); + + let mut collector = TransitiveFeatureCollector::new(false); + + let mut features = + match walker.walk_package_features(root_package, ["opt-feat-strong"], &mut collector) { + Ok(()) => collector.features, + Err(err) => panic!("{err}"), + }; + assert_eq!(features.len(), 2); + + let package_features = features.remove(&root_package.id).unwrap(); + assert_eq!( + package_features, + BTreeSet::from_iter([ + FeatureValue::Feature("opt-feat-strong".to_owned(),), + FeatureValue::DepFeature { + dep_name: "optdep".to_owned(), + dep_feature: "feat".to_owned(), + weak: false, + }, + ]) + ); + + Ok(()) +} + +#[test] +fn weak_dep_feature() -> Result<(), cargo_metadata::Error> { + let mut cmd = MetadataCommand::new(); + cmd.manifest_path("tests/all/Cargo.toml"); + // Without this the `optdep` package will be missing on `Metadata`: + cmd.features(CargoOpt::SomeFeatures(vec!["opt-feat-strong".to_owned()])); + let meta = cmd.exec().unwrap(); + + let root_package = meta.root_package().unwrap(); + + let walker = FeatureWalker::new(&meta); + + let mut collector = TransitiveFeatureCollector::new(false); + + let mut features = + match walker.walk_package_features(root_package, ["opt-feat-weak"], &mut collector) { + Ok(()) => collector.features, + Err(err) => panic!("{err}"), + }; + assert_eq!(features.len(), 2); + + let package_features = features.remove(&root_package.id).unwrap(); + assert_eq!( + package_features, + BTreeSet::from_iter([ + FeatureValue::Feature("opt-feat-weak".to_owned(),), + FeatureValue::DepFeature { + dep_name: "optdep".to_owned(), + dep_feature: "feat".to_owned(), + weak: true, + }, + ]) + ); + + Ok(()) +}