diff --git a/.github/.cspell/rust-dependencies.txt b/.github/.cspell/rust-dependencies.txt index 2edd1b7..cf8ed84 100644 --- a/.github/.cspell/rust-dependencies.txt +++ b/.github/.cspell/rust-dependencies.txt @@ -3,5 +3,6 @@ argfile easytime +fastrand lexopt termcolor diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3abe3e2..dbd58ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Cargo.toml b/Cargo.toml index bc9b8c9..12b06e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index b9d7ca2..a44f855 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,11 @@ OPTIONS: This flag can only be used together with --feature-powerset flag. + --random + 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 ... Space or comma separated list of features to group. diff --git a/src/cli.rs b/src/cli.rs index f6933e3..cf6f6c1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -82,6 +82,8 @@ pub(crate) struct Args { // options for --feature-powerset /// --depth pub(crate) depth: Option, + /// --random + pub(crate) random: Option, /// --group-features ... pub(crate) group_features: Vec, /// `--mutually-exclusive-features ` @@ -176,6 +178,7 @@ impl Args { let mut group_features: Vec = vec![]; let mut mutually_exclusive_features: Vec = vec![]; let mut depth = None; + let mut random = None; let mut verbose = 0; let mut no_default_features = false; @@ -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), @@ -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() { @@ -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::).transpose()?; + let random = random.as_deref().map(str::parse::).transpose()?; let group_features = parse_grouped_features(&group_features, "group-features")?; let mutually_exclusive_features = parse_grouped_features(&mutually_exclusive_features, "mutually-exclusive-features")?; @@ -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); @@ -630,6 +652,7 @@ impl Args { log_group, depth, + random, group_features, mutually_exclusive_features, @@ -726,6 +749,15 @@ const HELP: &[HelpText<'_>] = &[ "This flag can only be used together with --feature-powerset flag.", ], ), + ( + "", + "--random", + "", + "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", "...", "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 \ diff --git a/src/features.rs b/src/features.rs index 358ce01..8f75fd0 100644 --- a/src/features.rs +++ b/src/features.rs @@ -195,9 +195,32 @@ impl AsRef for Feature { } } +// main.rs passes Vec<&Feature> and tests in this module passes &Vec. +pub(crate) trait RefVecOrVecRef<'a, T: 'a>: IntoIterator { + 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 { + 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, + features: impl RefVecOrVecRef<'a, Feature>, depth: Option, + random: Option, at_least_one_of: &[Feature], mutually_exclusive_features: &[Feature], package_features: &BTreeMap>, @@ -205,42 +228,87 @@ pub(crate) fn feature_powerset<'a>( 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>, + mutually_exclusive_features: &[Feature], + package_features: &BTreeMap>, + deps_map: &BTreeMap<&str, BTreeSet<&str>>, + iter: impl Iterator>, +) -> Vec> { + 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>) -> BTreeMap<&str, BTreeSet<&str>> { @@ -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"], @@ -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"], @@ -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"]]); } @@ -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"]]); } diff --git a/src/main.rs b/src/main.rs index 46eded9..35dda11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, diff --git a/tests/long-help.txt b/tests/long-help.txt index 5435036..190bbe0 100644 --- a/tests/long-help.txt +++ b/tests/long-help.txt @@ -92,6 +92,11 @@ OPTIONS: This flag can only be used together with --feature-powerset flag. + --random + 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 ... Space or comma separated list of features to group. diff --git a/tests/short-help.txt b/tests/short-help.txt index a06b7c6..9188080 100644 --- a/tests/short-help.txt +++ b/tests/short-help.txt @@ -23,6 +23,8 @@ OPTIONS: --exclude-all-features Exclude run of just --all-features flag --depth Specify a max number of simultaneous feature flags of --feature-powerset + --random Performs with random feature combinations up to the number + specified per crate --group-features ... Space or comma separated list of features to group --mutually-exclusive-features ... Space or comma separated list of features to not use together