Skip to content

Commit 351623a

Browse files
committed
Add --random option
1 parent d3332d2 commit 351623a

9 files changed

Lines changed: 158 additions & 41 deletions

File tree

.github/.cspell/rust-dependencies.txt

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ jobs:
7272
cd tests/fixtures/real
7373
cargo hack check --feature-powerset --workspace
7474
cargo hack check --feature-powerset --workspace --message-format=json
75+
# TODO: move to tests/test.rs
76+
cargo hack check --feature-powerset --workspace --random 20
7577
cd ../rust-version
7678
rustup toolchain remove 1.63 1.64 1.65
7779
cargo hack check --rust-version --workspace --locked

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pkg-fmt = "tgz"
2727
anyhow = "1.0.47"
2828
cargo-config2 = "0.1.13"
2929
ctrlc = { version = "3.4.4", features = ["termination"] }
30+
fastrand = "2"
3031
lexopt = "0.3"
3132
same-file = "1.0.1"
3233
serde_json = "1"

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ OPTIONS:
122122

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

125+
--random <NUM_SAMPLES>
126+
Performs with random feature combinations up to the number specified per crate.
127+
128+
This flag can only be used together with --feature-powerset flag.
129+
125130
--group-features <FEATURES>...
126131
Space or comma separated list of features to group.
127132

src/cli.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ pub(crate) struct Args {
8282
// options for --feature-powerset
8383
/// --depth <NUM>
8484
pub(crate) depth: Option<usize>,
85+
/// --random <NUM_SAMPLES>
86+
pub(crate) random: Option<usize>,
8587
/// --group-features <FEATURES>...
8688
pub(crate) group_features: Vec<Feature>,
8789
/// `--mutually-exclusive-features <FEATURES>`
@@ -176,6 +178,7 @@ impl Args {
176178
let mut group_features: Vec<String> = vec![];
177179
let mut mutually_exclusive_features: Vec<String> = vec![];
178180
let mut depth = None;
181+
let mut random = None;
179182

180183
let mut verbose = 0;
181184
let mut no_default_features = false;
@@ -249,6 +252,7 @@ impl Args {
249252

250253
Long("manifest-path") => parse_opt!(manifest_path, false),
251254
Long("depth") => parse_opt!(depth, false),
255+
Long("random") => parse_opt!(random, false),
252256
Long("rust-version") => parse_flag!(rust_version),
253257
Long("version-range") => parse_opt!(version_range, false),
254258
Long("version-step") => parse_opt!(version_step, false),
@@ -423,6 +427,8 @@ impl Args {
423427
if !feature_powerset {
424428
if depth.is_some() {
425429
requires("--depth", &["--feature-powerset"])?;
430+
} else if random.is_some() {
431+
requires("--random", &["--feature-powerset"])?;
426432
} else if !group_features.is_empty() {
427433
requires("--group-features", &["--feature-powerset"])?;
428434
} else if !mutually_exclusive_features.is_empty() {
@@ -431,8 +437,22 @@ impl Args {
431437
requires("--at-least-one-of", &["--feature-powerset"])?;
432438
}
433439
}
440+
if random.is_some() {
441+
if depth.is_some() {
442+
conflicts("--random", "--depth")?;
443+
}
444+
// TODO: unimplemented
445+
if exclude_all_features {
446+
conflicts("--random", "--exclude-all-features")?;
447+
}
448+
// TODO: unimplemented
449+
if exclude_no_default_features {
450+
conflicts("--random", "--exclude-no-default-features")?;
451+
}
452+
}
434453

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

587607
// https://github.com/taiki-e/cargo-hack/issues/42
588608
// https://github.com/rust-lang/cargo/pull/8799
589-
exclude_no_default_features |= !include_features.is_empty();
609+
// TODO: random
610+
exclude_no_default_features |= !include_features.is_empty() || random.is_some();
590611
exclude_all_features |= !include_features.is_empty()
591612
|| !exclude_features.is_empty()
592-
|| !mutually_exclusive_features.is_empty();
613+
|| !mutually_exclusive_features.is_empty()
614+
|| random.is_some();
593615
exclude_features.extend_from_slice(&features);
594616

595617
term::verbose::set(verbose != 0);
@@ -630,6 +652,7 @@ impl Args {
630652
log_group,
631653

632654
depth,
655+
random,
633656
group_features,
634657
mutually_exclusive_features,
635658

@@ -726,6 +749,15 @@ const HELP: &[HelpText<'_>] = &[
726749
"This flag can only be used together with --feature-powerset flag.",
727750
],
728751
),
752+
(
753+
"",
754+
"--random",
755+
"<NUM_SAMPLES>",
756+
"Performs with random feature combinations up to the number specified per crate",
757+
&[
758+
"This flag can only be used together with --feature-powerset flag.",
759+
],
760+
),
729761
("", "--group-features", "<FEATURES>...", "Space or comma separated list of features to group", &[
730762
"This treats the specified features as if it were a single feature.",
731763
"To specify multiple groups, use this option multiple times: `--group-features a,b \

src/features.rs

Lines changed: 107 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -195,52 +195,120 @@ impl AsRef<str> for Feature {
195195
}
196196
}
197197

198+
// main.rs passes Vec<&Feature> and tests in this module passes &Vec<Feature>.
199+
pub(crate) trait RefVecOrVecRef<'a, T: 'a>: IntoIterator<Item = &'a T> {
200+
fn get_(&self, i: usize) -> Option<&'a T>;
201+
fn len_(&self) -> usize;
202+
}
203+
impl<'a, T> RefVecOrVecRef<'a, T> for Vec<&'a T> {
204+
fn get_(&self, i: usize) -> Option<&'a T> {
205+
self.get(i).copied()
206+
}
207+
fn len_(&self) -> usize {
208+
self.len()
209+
}
210+
}
211+
impl<'a, T> RefVecOrVecRef<'a, T> for &'a Vec<T> {
212+
fn get_(&self, i: usize) -> Option<&'a T> {
213+
self.get(i)
214+
}
215+
fn len_(&self) -> usize {
216+
self.len()
217+
}
218+
}
219+
198220
pub(crate) fn feature_powerset<'a>(
199-
features: impl IntoIterator<Item = &'a Feature>,
221+
features: impl RefVecOrVecRef<'a, Feature>,
200222
depth: Option<usize>,
223+
random: Option<usize>,
201224
at_least_one_of: &[Feature],
202225
mutually_exclusive_features: &[Feature],
203226
package_features: &BTreeMap<String, Vec<String>>,
204227
) -> Vec<Vec<&'a Feature>> {
205228
let deps_map = feature_deps(package_features);
206229
let at_least_one_of = at_least_one_of_for_package(at_least_one_of, &deps_map);
207230

208-
powerset(features, depth)
209-
.into_iter()
210-
.skip(1) // The first element of a powerset is `[]` so it should be skipped.
211-
.filter(|fs| {
212-
!fs.iter().any(|f| {
213-
f.as_group().iter().filter_map(|f| deps_map.get(&&**f)).any(|deps| {
214-
fs.iter().any(|f| f.as_group().iter().all(|f| deps.contains(&&**f)))
231+
if let Some(num_samples) = random {
232+
// TODO:
233+
// - If duplicates are found, they should be de-duplicated and regenerated.
234+
// - Same for filtered case.
235+
// - If the total number of possible combinations is less than num_samples,
236+
// then we should use normal powerset().
237+
filter_powerset(
238+
at_least_one_of,
239+
mutually_exclusive_features,
240+
package_features,
241+
&deps_map,
242+
(0..)
243+
.map(|_| {
244+
let mut n = fastrand::u64(..);
245+
let mut v = vec![];
246+
let mut i = 0;
247+
while i < features.len_() {
248+
if n & 0b1 == 1 {
249+
v.push(features.get_(i).unwrap());
250+
}
251+
i += 1;
252+
if i % 64 == 0 {
253+
n = fastrand::u64(..);
254+
} else {
255+
n >>= 1;
256+
}
257+
}
258+
v
215259
})
216-
})
260+
.take(num_samples),
261+
)
262+
} else {
263+
filter_powerset(
264+
at_least_one_of,
265+
mutually_exclusive_features,
266+
package_features,
267+
&deps_map,
268+
// The first element of a powerset is `[]` so it should be skipped.
269+
powerset(features, depth).into_iter().skip(1),
270+
)
271+
}
272+
}
273+
274+
fn filter_powerset<'a>(
275+
at_least_one_of: Vec<BTreeSet<&str>>,
276+
mutually_exclusive_features: &[Feature],
277+
package_features: &BTreeMap<String, Vec<String>>,
278+
deps_map: &BTreeMap<&str, BTreeSet<&str>>,
279+
iter: impl Iterator<Item = Vec<&'a Feature>>,
280+
) -> Vec<Vec<&'a Feature>> {
281+
iter.filter(|fs| {
282+
!fs.iter().any(|&f| {
283+
f.as_group()
284+
.iter()
285+
.filter_map(|f| deps_map.get(&&**f))
286+
.any(|deps| fs.iter().any(|f| f.as_group().iter().all(|f| deps.contains(&&**f))))
217287
})
218-
.filter(move |fs| {
219-
// all() returns true if at_least_one_of is empty
220-
at_least_one_of.iter().all(|required_set| {
221-
fs
222-
.iter()
223-
.flat_map(|f| f.as_group())
224-
.any(|f| required_set.contains(f.as_str()))
225-
})
288+
})
289+
.filter(move |fs| {
290+
// all() returns true if at_least_one_of is empty
291+
at_least_one_of.iter().all(|required_set| {
292+
fs.iter().flat_map(|&f| f.as_group()).any(|f| required_set.contains(f.as_str()))
226293
})
227-
.filter(move |fs| {
228-
// Filter any feature set containing more than one feature from the same mutually
229-
// exclusive group.
230-
for group in mutually_exclusive_features {
231-
let mut count = 0;
232-
for f in fs.iter().flat_map(|f| f.as_group()) {
233-
if group.matches_recursive(f, package_features) {
234-
count += 1;
235-
if count > 1 {
236-
return false;
237-
}
294+
})
295+
.filter(move |fs| {
296+
// Filter any feature set containing more than one feature from the same mutually
297+
// exclusive group.
298+
for group in mutually_exclusive_features {
299+
let mut count = 0;
300+
for f in fs.iter().flat_map(|f| f.as_group()) {
301+
if group.matches_recursive(f, package_features) {
302+
count += 1;
303+
if count > 1 {
304+
return false;
238305
}
239306
}
240307
}
241-
true
242-
})
243-
.collect()
308+
}
309+
true
310+
})
311+
.collect()
244312
}
245313

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

359427
let list = v!["a", "b", "c", "d"];
360-
let filtered = feature_powerset(&list, None, &[], &[], &map);
428+
let filtered = feature_powerset(&list, None, None, &[], &[], &map);
361429
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);
362430

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

366-
let filtered = feature_powerset(&list, None, &["c".into()], &[], &map);
434+
let filtered = feature_powerset(&list, None, None, &["c".into()], &[], &map);
367435
assert_eq!(filtered, vec![vec!["c"], vec!["c", "d"]]);
368436

369-
let filtered = feature_powerset(&list, None, &["a".into(), "c".into()], &[], &map);
437+
let filtered = feature_powerset(&list, None, None, &["a".into(), "c".into()], &[], &map);
370438
assert_eq!(filtered, vec![vec!["c"], vec!["c", "d"]]);
371439

372440
let map = map![("tokio", v![]), ("async-std", v![]), ("a", v![]), ("b", v!["a"])];
373441
let list = v!["a", "b", "tokio", "async-std"];
374442
let mutually_exclusive_features = [Feature::group(["tokio", "async-std"])];
375-
let filtered = feature_powerset(&list, None, &[], &mutually_exclusive_features, &map);
443+
let filtered = feature_powerset(&list, None, None, &[], &mutually_exclusive_features, &map);
376444
assert_eq!(filtered, vec![
377445
vec!["a"],
378446
vec!["b"],
@@ -386,7 +454,7 @@ mod tests {
386454

387455
let mutually_exclusive_features =
388456
[Feature::group(["tokio", "a"]), Feature::group(["tokio", "async-std"])];
389-
let filtered = feature_powerset(&list, None, &[], &mutually_exclusive_features, &map);
457+
let filtered = feature_powerset(&list, None, None, &[], &mutually_exclusive_features, &map);
390458
assert_eq!(filtered, vec![
391459
vec!["a"],
392460
vec!["b"],
@@ -399,7 +467,7 @@ mod tests {
399467
let map = map![("a", v![]), ("b", v!["a"]), ("c", v![]), ("d", v!["b"])];
400468
let list = v!["a", "b", "c", "d"];
401469
let mutually_exclusive_features = [Feature::group(["a", "c"])];
402-
let filtered = feature_powerset(&list, None, &[], &mutually_exclusive_features, &map);
470+
let filtered = feature_powerset(&list, None, None, &[], &mutually_exclusive_features, &map);
403471
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"]]);
404472
}
405473

@@ -433,7 +501,7 @@ mod tests {
433501
vec!["b", "c", "d"],
434502
vec!["a", "b", "c", "d"],
435503
]);
436-
let filtered = feature_powerset(&list, None, &[], &[], &map);
504+
let filtered = feature_powerset(&list, None, None, &[], &[], &map);
437505
assert_eq!(filtered, vec![vec!["a"], vec!["b"], vec!["c"], vec!["d"], vec!["c", "d"]]);
438506
}
439507

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ fn determine_kind<'a>(
278278
let features = features::feature_powerset(
279279
features,
280280
cx.depth,
281+
cx.random,
281282
&cx.at_least_one_of,
282283
&cx.mutually_exclusive_features,
283284
&package.features,

tests/long-help.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ OPTIONS:
9292

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

95+
--random <NUM_SAMPLES>
96+
Performs with random feature combinations up to the number specified per crate.
97+
98+
This flag can only be used together with --feature-powerset flag.
99+
95100
--group-features <FEATURES>...
96101
Space or comma separated list of features to group.
97102

tests/short-help.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ OPTIONS:
2323
--exclude-all-features Exclude run of just --all-features flag
2424
--depth <NUM> Specify a max number of simultaneous feature flags of
2525
--feature-powerset
26+
--random <NUM_SAMPLES> Performs with random feature combinations up to the number
27+
specified per crate
2628
--group-features <FEATURES>... Space or comma separated list of features to group
2729
--mutually-exclusive-features <FEATURES>... Space or comma separated list of features to not use
2830
together

0 commit comments

Comments
 (0)