From 88cdf1c4a3a81d6ce43d9842dbb77a38b236b27b Mon Sep 17 00:00:00 2001 From: Taiki Endo Date: Sun, 8 Nov 2020 10:55:45 +0900 Subject: [PATCH] Exclude feature combinations by detecting dependencies of features --- src/features.rs | 105 +++++++++++++++++- src/main.rs | 2 +- .../powerset_deduplication/.cargo/config | 2 + .../powerset_deduplication/Cargo.lock | 5 + .../powerset_deduplication/Cargo.toml | 18 +++ .../powerset_deduplication/src/main.rs | 1 + tests/test.rs | 32 ++++++ 7 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/powerset_deduplication/.cargo/config create mode 100644 tests/fixtures/powerset_deduplication/Cargo.lock create mode 100644 tests/fixtures/powerset_deduplication/Cargo.toml create mode 100644 tests/fixtures/powerset_deduplication/src/main.rs diff --git a/src/features.rs b/src/features.rs index 3bfc3a6b..0bef4b36 100644 --- a/src/features.rs +++ b/src/features.rs @@ -1,3 +1,5 @@ +use std::collections::{BTreeMap, BTreeSet}; + use crate::{ metadata::{Dependency, Metadata}, PackageId, @@ -60,10 +62,46 @@ impl Features { } } -pub(crate) fn powerset( - iter: impl IntoIterator, +pub(crate) fn feature_powerset<'a>( + features: impl IntoIterator, depth: Option, -) -> Vec> { + map: &BTreeMap>, +) -> Vec> { + let feature_deps = feature_deps(map); + let powerset = powerset(features, depth); + powerset + .into_iter() + .filter(|a| { + !a.iter().filter_map(|b| feature_deps.get(b)).any(|c| a.iter().any(|d| c.contains(d))) + }) + .collect() +} + +fn feature_deps(map: &BTreeMap>) -> BTreeMap<&str, BTreeSet<&str>> { + let mut feat_deps = BTreeMap::new(); + for feat in map.keys() { + let mut set = BTreeSet::new(); + fn f<'a>( + map: &'a BTreeMap>, + set: &mut BTreeSet<&'a str>, + curr: &str, + root: &str, + ) { + if let Some(v) = map.get(curr) { + for x in v { + if x != root && set.insert(x) { + f(map, set, x, root); + } + } + } + } + f(map, &mut set, feat, feat); + feat_deps.insert(&**feat, set); + } + feat_deps +} + +fn powerset(iter: impl IntoIterator, depth: Option) -> Vec> { iter.into_iter().fold(vec![vec![]], |mut acc, elem| { let ext = acc.clone().into_iter().map(|mut curr| { curr.push(elem.clone()); @@ -80,7 +118,66 @@ pub(crate) fn powerset( #[cfg(test)] mod tests { - use super::powerset; + use super::{feature_deps, feature_powerset, powerset}; + use std::{ + collections::{BTreeMap, BTreeSet}, + iter::FromIterator, + }; + + macro_rules! svec { + ($($expr:expr),* $(,)?) => { + vec![$($expr.into()),*] + }; + } + + macro_rules! map { + ($(($key:expr, $value:expr)),* $(,)?) => { + BTreeMap::from_iter(vec![$(($key.into(), $value)),*]) + }; + } + + macro_rules! set { + ($($expr:expr),* $(,)?) => { + BTreeSet::from_iter(vec![$($expr),*]) + }; + } + + #[test] + fn feature_deps1() { + let map = + map![("a", svec![]), ("b", svec!["a"]), ("c", svec!["b"]), ("d", svec!["a", "b"])]; + let fd = feature_deps(&map); + assert_eq!(fd, map![ + ("a", set![]), + ("b", set!["a"]), + ("c", set!["a", "b"]), + ("d", set!["a", "b"]) + ]); + let list = vec!["a", "b", "c", "d"]; + let ps = powerset(list.clone(), None); + assert_eq!(ps, vec![ + vec![], + vec!["a"], + vec!["b"], + vec!["a", "b"], + vec!["c"], + vec!["a", "c"], + vec!["b", "c"], + vec!["a", "b", "c"], + vec!["d"], + vec!["a", "d"], + vec!["b", "d"], + vec!["a", "b", "d"], + vec!["c", "d"], + vec!["a", "c", "d"], + vec!["b", "c", "d"], + vec!["a", "b", "c", "d"], + ]); + let filtered = feature_powerset(list, None, &map); + assert_eq!(filtered, vec![vec![], vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec![ + "c", "d" + ]]); + } #[test] fn powerset_full() { diff --git a/src/main.rs b/src/main.rs index 2f3ee3c8..f9715c98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -151,7 +151,7 @@ fn determine_kind<'a>(cx: &'a Context<'_>, id: &PackageId, progress: &mut Progre Kind::Each { features } } } else if cx.feature_powerset { - let features = features::powerset(features, cx.depth); + let features = features::feature_powerset(features, cx.depth, &package.features); if (package.features.is_empty() || !cx.include_features.is_empty()) && features.is_empty() { progress.total += 1; diff --git a/tests/fixtures/powerset_deduplication/.cargo/config b/tests/fixtures/powerset_deduplication/.cargo/config new file mode 100644 index 00000000..88403f3b --- /dev/null +++ b/tests/fixtures/powerset_deduplication/.cargo/config @@ -0,0 +1,2 @@ +[build] +target-dir = "../../../target" diff --git a/tests/fixtures/powerset_deduplication/Cargo.lock b/tests/fixtures/powerset_deduplication/Cargo.lock new file mode 100644 index 00000000..6dc87d0b --- /dev/null +++ b/tests/fixtures/powerset_deduplication/Cargo.lock @@ -0,0 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "deduplication" +version = "0.1.0" diff --git a/tests/fixtures/powerset_deduplication/Cargo.toml b/tests/fixtures/powerset_deduplication/Cargo.toml new file mode 100644 index 00000000..654891d9 --- /dev/null +++ b/tests/fixtures/powerset_deduplication/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "deduplication" +version = "0.1.0" +authors = ["Taiki Endo "] +publish = false + +[workspace] + +[features] +a = [] +b = ["a"] +c = ["b"] +d = [] +e = ["b", "d"] + +[dependencies] + +[dev-dependencies] diff --git a/tests/fixtures/powerset_deduplication/src/main.rs b/tests/fixtures/powerset_deduplication/src/main.rs new file mode 100644 index 00000000..f328e4d9 --- /dev/null +++ b/tests/fixtures/powerset_deduplication/src/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/tests/test.rs b/tests/test.rs index be4c0628..dade0a8a 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -514,6 +514,38 @@ fn feature_powerset_failure() { ); } +#[test] +fn powerset_deduplication() { + cargo_hack(["check", "--feature-powerset"]) + .test_dir("tests/fixtures/powerset_deduplication") + .assert_success() + .assert_stderr_contains( + " + running `cargo check --no-default-features` on deduplication (1/11) + running `cargo check --no-default-features --features a` on deduplication (2/11) + running `cargo check --no-default-features --features b` on deduplication (3/11) + running `cargo check --no-default-features --features c` on deduplication (4/11) + running `cargo check --no-default-features --features d` on deduplication (5/11) + running `cargo check --no-default-features --features a,d` on deduplication (6/11) + running `cargo check --no-default-features --features b,d` on deduplication (7/11) + running `cargo check --no-default-features --features c,d` on deduplication (8/11) + running `cargo check --no-default-features --features e` on deduplication (9/11) + running `cargo check --no-default-features --features c,e` on deduplication (10/11) + running `cargo check --no-default-features --all-features` on deduplication (11/11) + ", + ) + .assert_stderr_not_contains( + " + a,b + b,c + a,c + a,e + b,e + d,e + ", + ); +} + #[test] fn feature_powerset_depth() { cargo_hack(["check", "--feature-powerset", "--depth", "2"])