Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --random option #255

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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