Skip to content

Commit

Permalink
Add --random option
Browse files Browse the repository at this point in the history
  • Loading branch information
taiki-e committed Jul 18, 2024
1 parent d3332d2 commit 0b81789
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 41 deletions.
1 change: 1 addition & 0 deletions .github/.cspell/rust-dependencies.txt

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

2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ jobs:
cd tests/fixtures/real
cargo hack check --feature-powerset --workspace
cargo hack check --feature-powerset --workspace --message-format=json
# TODO: move to tests/test.rs
cargo hack check --feature-powerset --workspace --optional-deps --random 20
cd ../rust-version
rustup toolchain remove 1.63 1.64 1.65
cargo hack check --rust-version --workspace --locked
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pkg-fmt = "tgz"
anyhow = "1.0.47"
cargo-config2 = "0.1.13"
ctrlc = { version = "3.4.4", features = ["termination"] }
fastrand = "2"
lexopt = "0.3"
same-file = "1.0.1"
serde_json = "1"
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ OPTIONS:

This flag can only be used together with --feature-powerset flag.

--random <NUM_SAMPLES>
Performs with random feature combinations up to the number specified per crate.

This flag can only be used together with --feature-powerset flag.

--group-features <FEATURES>...
Space or comma separated list of features to group.

Expand Down
36 changes: 34 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ pub(crate) struct Args {
// options for --feature-powerset
/// --depth <NUM>
pub(crate) depth: Option<usize>,
/// --random <NUM_SAMPLES>
pub(crate) random: Option<usize>,
/// --group-features <FEATURES>...
pub(crate) group_features: Vec<Feature>,
/// `--mutually-exclusive-features <FEATURES>`
Expand Down Expand Up @@ -176,6 +178,7 @@ impl Args {
let mut group_features: Vec<String> = vec![];
let mut mutually_exclusive_features: Vec<String> = vec![];
let mut depth = None;
let mut random = None;

let mut verbose = 0;
let mut no_default_features = false;
Expand Down Expand Up @@ -249,6 +252,7 @@ impl Args {

Long("manifest-path") => parse_opt!(manifest_path, false),
Long("depth") => parse_opt!(depth, false),
Long("random") => parse_opt!(random, false),
Long("rust-version") => parse_flag!(rust_version),
Long("version-range") => parse_opt!(version_range, false),
Long("version-step") => parse_opt!(version_step, false),
Expand Down Expand Up @@ -423,6 +427,8 @@ impl Args {
if !feature_powerset {
if depth.is_some() {
requires("--depth", &["--feature-powerset"])?;
} else if random.is_some() {
requires("--random", &["--feature-powerset"])?;
} else if !group_features.is_empty() {
requires("--group-features", &["--feature-powerset"])?;
} else if !mutually_exclusive_features.is_empty() {
Expand All @@ -431,8 +437,22 @@ impl Args {
requires("--at-least-one-of", &["--feature-powerset"])?;
}
}
if random.is_some() {
if depth.is_some() {
conflicts("--random", "--depth")?;
}
// TODO: unimplemented
if exclude_all_features {
conflicts("--random", "--exclude-all-features")?;
}
// TODO: unimplemented
if exclude_no_default_features {
conflicts("--random", "--exclude-no-default-features")?;
}
}

let depth = depth.as_deref().map(str::parse::<usize>).transpose()?;
let random = random.as_deref().map(str::parse::<usize>).transpose()?;
let group_features = parse_grouped_features(&group_features, "group-features")?;
let mutually_exclusive_features =
parse_grouped_features(&mutually_exclusive_features, "mutually-exclusive-features")?;
Expand Down Expand Up @@ -586,10 +606,12 @@ impl Args {

// https://github.com/taiki-e/cargo-hack/issues/42
// https://github.com/rust-lang/cargo/pull/8799
exclude_no_default_features |= !include_features.is_empty();
// TODO: random
exclude_no_default_features |= !include_features.is_empty() || random.is_some();
exclude_all_features |= !include_features.is_empty()
|| !exclude_features.is_empty()
|| !mutually_exclusive_features.is_empty();
|| !mutually_exclusive_features.is_empty()
|| random.is_some();
exclude_features.extend_from_slice(&features);

term::verbose::set(verbose != 0);
Expand Down Expand Up @@ -630,6 +652,7 @@ impl Args {
log_group,

depth,
random,
group_features,
mutually_exclusive_features,

Expand Down Expand Up @@ -726,6 +749,15 @@ const HELP: &[HelpText<'_>] = &[
"This flag can only be used together with --feature-powerset flag.",
],
),
(
"",
"--random",
"<NUM_SAMPLES>",
"Performs with random feature combinations up to the number specified per crate",
&[
"This flag can only be used together with --feature-powerset flag.",
],
),
("", "--group-features", "<FEATURES>...", "Space or comma separated list of features to group", &[
"This treats the specified features as if it were a single feature.",
"To specify multiple groups, use this option multiple times: `--group-features a,b \
Expand Down
146 changes: 107 additions & 39 deletions src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,52 +195,120 @@ impl AsRef<str> for Feature {
}
}

// main.rs passes Vec<&Feature> and tests in this module passes &Vec<Feature>.
pub(crate) trait RefVecOrVecRef<'a, T: 'a>: IntoIterator<Item = &'a T> {
fn get_(&self, i: usize) -> Option<&'a T>;
fn len_(&self) -> usize;
}
impl<'a, T> RefVecOrVecRef<'a, T> for Vec<&'a T> {
fn get_(&self, i: usize) -> Option<&'a T> {
self.get(i).copied()
}
fn len_(&self) -> usize {
self.len()
}
}
impl<'a, T> RefVecOrVecRef<'a, T> for &'a Vec<T> {
fn get_(&self, i: usize) -> Option<&'a T> {
self.get(i)
}
fn len_(&self) -> usize {
self.len()
}
}

pub(crate) fn feature_powerset<'a>(
features: impl IntoIterator<Item = &'a Feature>,
features: impl RefVecOrVecRef<'a, Feature>,
depth: Option<usize>,
random: Option<usize>,
at_least_one_of: &[Feature],
mutually_exclusive_features: &[Feature],
package_features: &BTreeMap<String, Vec<String>>,
) -> Vec<Vec<&'a Feature>> {
let deps_map = feature_deps(package_features);
let at_least_one_of = at_least_one_of_for_package(at_least_one_of, &deps_map);

powerset(features, depth)
.into_iter()
.skip(1) // The first element of a powerset is `[]` so it should be skipped.
.filter(|fs| {
!fs.iter().any(|f| {
f.as_group().iter().filter_map(|f| deps_map.get(&&**f)).any(|deps| {
fs.iter().any(|f| f.as_group().iter().all(|f| deps.contains(&&**f)))
if let Some(num_samples) = random {
// TODO:
// - If duplicates are found, they should be de-duplicated and regenerated.
// - Same for filtered case.
// - If the total number of possible combinations is less than num_samples,
// then we should use normal powerset().
filter_powerset(
at_least_one_of,
mutually_exclusive_features,
package_features,
&deps_map,
(0..)
.map(|_| {
let mut n = fastrand::u64(..);
let mut v = vec![];
let mut i = 0;
while i < features.len_() {
if n & 0b1 == 1 {
v.push(features.get_(i).unwrap());
}
i += 1;
if i % 64 == 0 {
n = fastrand::u64(..);
} else {
n >>= 1;
}
}
v
})
})
.take(num_samples),
)
} else {
filter_powerset(
at_least_one_of,
mutually_exclusive_features,
package_features,
&deps_map,
// The first element of a powerset is `[]` so it should be skipped.
powerset(features, depth).into_iter().skip(1),
)
}
}

fn filter_powerset<'a>(
at_least_one_of: Vec<BTreeSet<&str>>,
mutually_exclusive_features: &[Feature],
package_features: &BTreeMap<String, Vec<String>>,
deps_map: &BTreeMap<&str, BTreeSet<&str>>,
iter: impl Iterator<Item = Vec<&'a Feature>>,
) -> Vec<Vec<&'a Feature>> {
iter.filter(|fs| {
!fs.iter().any(|&f| {
f.as_group()
.iter()
.filter_map(|f| deps_map.get(&&**f))
.any(|deps| fs.iter().any(|f| f.as_group().iter().all(|f| deps.contains(&&**f))))
})
.filter(move |fs| {
// all() returns true if at_least_one_of is empty
at_least_one_of.iter().all(|required_set| {
fs
.iter()
.flat_map(|f| f.as_group())
.any(|f| required_set.contains(f.as_str()))
})
})
.filter(move |fs| {
// all() returns true if at_least_one_of is empty
at_least_one_of.iter().all(|required_set| {
fs.iter().flat_map(|&f| f.as_group()).any(|f| required_set.contains(f.as_str()))
})
.filter(move |fs| {
// Filter any feature set containing more than one feature from the same mutually
// exclusive group.
for group in mutually_exclusive_features {
let mut count = 0;
for f in fs.iter().flat_map(|f| f.as_group()) {
if group.matches_recursive(f, package_features) {
count += 1;
if count > 1 {
return false;
}
})
.filter(move |fs| {
// Filter any feature set containing more than one feature from the same mutually
// exclusive group.
for group in mutually_exclusive_features {
let mut count = 0;
for f in fs.iter().flat_map(|f| f.as_group()) {
if group.matches_recursive(f, package_features) {
count += 1;
if count > 1 {
return false;
}
}
}
true
})
.collect()
}
true
})
.collect()
}

fn feature_deps(map: &BTreeMap<String, Vec<String>>) -> BTreeMap<&str, BTreeSet<&str>> {
Expand Down Expand Up @@ -357,22 +425,22 @@ mod tests {
let map = map![("a", v![]), ("b", v!["a"]), ("c", v!["b"]), ("d", v!["a", "b"])];

let list = v!["a", "b", "c", "d"];
let filtered = feature_powerset(&list, None, &[], &[], &map);
let filtered = feature_powerset(&list, None, None, &[], &[], &map);
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);

let filtered = feature_powerset(&list, None, &["a".into()], &[], &map);
let filtered = feature_powerset(&list, None, None, &["a".into()], &[], &map);
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);

let filtered = feature_powerset(&list, None, &["c".into()], &[], &map);
let filtered = feature_powerset(&list, None, None, &["c".into()], &[], &map);
assert_eq!(filtered, vec![vec!["c"], vec!["c", "d"]]);

let filtered = feature_powerset(&list, None, &["a".into(), "c".into()], &[], &map);
let filtered = feature_powerset(&list, None, None, &["a".into(), "c".into()], &[], &map);
assert_eq!(filtered, vec![vec!["c"], vec!["c", "d"]]);

let map = map![("tokio", v![]), ("async-std", v![]), ("a", v![]), ("b", v!["a"])];
let list = v!["a", "b", "tokio", "async-std"];
let mutually_exclusive_features = [Feature::group(["tokio", "async-std"])];
let filtered = feature_powerset(&list, None, &[], &mutually_exclusive_features, &map);
let filtered = feature_powerset(&list, None, None, &[], &mutually_exclusive_features, &map);
assert_eq!(filtered, vec![
vec!["a"],
vec!["b"],
Expand All @@ -386,7 +454,7 @@ mod tests {

let mutually_exclusive_features =
[Feature::group(["tokio", "a"]), Feature::group(["tokio", "async-std"])];
let filtered = feature_powerset(&list, None, &[], &mutually_exclusive_features, &map);
let filtered = feature_powerset(&list, None, None, &[], &mutually_exclusive_features, &map);
assert_eq!(filtered, vec![
vec!["a"],
vec!["b"],
Expand All @@ -399,7 +467,7 @@ mod tests {
let map = map![("a", v![]), ("b", v!["a"]), ("c", v![]), ("d", v!["b"])];
let list = v!["a", "b", "c", "d"];
let mutually_exclusive_features = [Feature::group(["a", "c"])];
let filtered = feature_powerset(&list, None, &[], &mutually_exclusive_features, &map);
let filtered = feature_powerset(&list, None, None, &[], &mutually_exclusive_features, &map);
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"]]);
}

Expand Down Expand Up @@ -433,7 +501,7 @@ mod tests {
vec!["b", "c", "d"],
vec!["a", "b", "c", "d"],
]);
let filtered = feature_powerset(&list, None, &[], &[], &map);
let filtered = feature_powerset(&list, None, None, &[], &[], &map);
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);
}

Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ fn determine_kind<'a>(
let features = features::feature_powerset(
features,
cx.depth,
cx.random,
&cx.at_least_one_of,
&cx.mutually_exclusive_features,
&package.features,
Expand Down
5 changes: 5 additions & 0 deletions tests/long-help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ OPTIONS:

This flag can only be used together with --feature-powerset flag.

--random <NUM_SAMPLES>
Performs with random feature combinations up to the number specified per crate.

This flag can only be used together with --feature-powerset flag.

--group-features <FEATURES>...
Space or comma separated list of features to group.

Expand Down
2 changes: 2 additions & 0 deletions tests/short-help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ OPTIONS:
--exclude-all-features Exclude run of just --all-features flag
--depth <NUM> Specify a max number of simultaneous feature flags of
--feature-powerset
--random <NUM_SAMPLES> Performs with random feature combinations up to the number
specified per crate
--group-features <FEATURES>... Space or comma separated list of features to group
--mutually-exclusive-features <FEATURES>... Space or comma separated list of features to not use
together
Expand Down

0 comments on commit 0b81789

Please sign in to comment.