Skip to content

Commit

Permalink
Exclude feature combinations by detecting dependencies of features
Browse files Browse the repository at this point in the history
  • Loading branch information
taiki-e committed Nov 14, 2020
1 parent 5288c01 commit 88cdf1c
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 5 deletions.
105 changes: 101 additions & 4 deletions src/features.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::{BTreeMap, BTreeSet};

use crate::{
metadata::{Dependency, Metadata},
PackageId,
Expand Down Expand Up @@ -60,10 +62,46 @@ impl Features {
}
}

pub(crate) fn powerset<T: Clone>(
iter: impl IntoIterator<Item = T>,
pub(crate) fn feature_powerset<'a>(
features: impl IntoIterator<Item = &'a str>,
depth: Option<usize>,
) -> Vec<Vec<T>> {
map: &BTreeMap<String, Vec<String>>,
) -> Vec<Vec<&'a str>> {
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<String, Vec<String>>) -> 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<String, Vec<String>>,
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<T: Clone>(iter: impl IntoIterator<Item = T>, depth: Option<usize>) -> Vec<Vec<T>> {
iter.into_iter().fold(vec![vec![]], |mut acc, elem| {
let ext = acc.clone().into_iter().map(|mut curr| {
curr.push(elem.clone());
Expand All @@ -80,7 +118,66 @@ pub(crate) fn powerset<T: Clone>(

#[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() {
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/powerset_deduplication/.cargo/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
target-dir = "../../../target"
5 changes: 5 additions & 0 deletions tests/fixtures/powerset_deduplication/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions tests/fixtures/powerset_deduplication/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "deduplication"
version = "0.1.0"
authors = ["Taiki Endo <te316e89@gmail.com>"]
publish = false

[workspace]

[features]
a = []
b = ["a"]
c = ["b"]
d = []
e = ["b", "d"]

[dependencies]

[dev-dependencies]
1 change: 1 addition & 0 deletions tests/fixtures/powerset_deduplication/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
32 changes: 32 additions & 0 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down

0 comments on commit 88cdf1c

Please sign in to comment.