Skip to content

Commit 37f17bc

Browse files
Rollup merge of #89082 - smoelius:master, r=kennytm
Implement #85440 (Random test ordering) This PR adds `--shuffle` and `--shuffle-seed` options to `libtest`. The options are similar to the [`-shuffle` option](https://github.com/golang/go/blob/c894b442d1e5e150ad33fa3ce13dbfab1c037b3a/src/testing/testing.go#L1482-L1499) that was recently added to Go. Here are the relevant parts of the help message: ``` --shuffle Run tests in random order --shuffle-seed SEED Run tests in random order; seed the random number generator with SEED ... By default, the tests are run in alphabetical order. Use --shuffle or set RUST_TEST_SHUFFLE to run the tests in random order. Pass the generated "shuffle seed" to --shuffle-seed (or set RUST_TEST_SHUFFLE_SEED) to run the tests in the same order again. Note that --shuffle and --shuffle-seed do not affect whether the tests are run in parallel. ``` Is an RFC needed for this?
2 parents 6c17601 + ecf4741 commit 37f17bc

File tree

14 files changed

+313
-40
lines changed

14 files changed

+313
-40
lines changed

library/test/src/cli.rs

+74
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub struct TestOpts {
2121
pub nocapture: bool,
2222
pub color: ColorConfig,
2323
pub format: OutputFormat,
24+
pub shuffle: bool,
25+
pub shuffle_seed: Option<u64>,
2426
pub test_threads: Option<usize>,
2527
pub skip: Vec<String>,
2628
pub time_options: Option<TestTimeOptions>,
@@ -138,6 +140,13 @@ fn optgroups() -> getopts::Options {
138140
139141
`CRITICAL_TIME` here means the limit that should not be exceeded by test.
140142
",
143+
)
144+
.optflag("", "shuffle", "Run tests in random order")
145+
.optopt(
146+
"",
147+
"shuffle-seed",
148+
"Run tests in random order; seed the random number generator with SEED",
149+
"SEED",
141150
);
142151
opts
143152
}
@@ -155,6 +164,12 @@ By default, all tests are run in parallel. This can be altered with the
155164
--test-threads flag or the RUST_TEST_THREADS environment variable when running
156165
tests (set it to 1).
157166
167+
By default, the tests are run in alphabetical order. Use --shuffle or set
168+
RUST_TEST_SHUFFLE to run the tests in random order. Pass the generated
169+
"shuffle seed" to --shuffle-seed (or set RUST_TEST_SHUFFLE_SEED) to run the
170+
tests in the same order again. Note that --shuffle and --shuffle-seed do not
171+
affect whether the tests are run in parallel.
172+
158173
All tests have their standard output and standard error captured by default.
159174
This can be overridden with the --nocapture flag or setting RUST_TEST_NOCAPTURE
160175
environment variable to a value other than "0". Logging is not captured by default.
@@ -218,6 +233,21 @@ macro_rules! unstable_optflag {
218233
}};
219234
}
220235

236+
// Gets the option value and checks if unstable features are enabled.
237+
macro_rules! unstable_optopt {
238+
($matches:ident, $allow_unstable:ident, $option_name:literal) => {{
239+
let opt = $matches.opt_str($option_name);
240+
if !$allow_unstable && opt.is_some() {
241+
return Err(format!(
242+
"The \"{}\" option is only accepted on the nightly compiler with -Z unstable-options",
243+
$option_name
244+
));
245+
}
246+
247+
opt
248+
}};
249+
}
250+
221251
// Implementation of `parse_opts` that doesn't care about help message
222252
// and returns a `Result`.
223253
fn parse_opts_impl(matches: getopts::Matches) -> OptRes {
@@ -227,6 +257,8 @@ fn parse_opts_impl(matches: getopts::Matches) -> OptRes {
227257
let force_run_in_process = unstable_optflag!(matches, allow_unstable, "force-run-in-process");
228258
let exclude_should_panic = unstable_optflag!(matches, allow_unstable, "exclude-should-panic");
229259
let time_options = get_time_options(&matches, allow_unstable)?;
260+
let shuffle = get_shuffle(&matches, allow_unstable)?;
261+
let shuffle_seed = get_shuffle_seed(&matches, allow_unstable)?;
230262

231263
let include_ignored = matches.opt_present("include-ignored");
232264
let quiet = matches.opt_present("quiet");
@@ -260,6 +292,8 @@ fn parse_opts_impl(matches: getopts::Matches) -> OptRes {
260292
nocapture,
261293
color,
262294
format,
295+
shuffle,
296+
shuffle_seed,
263297
test_threads,
264298
skip,
265299
time_options,
@@ -303,6 +337,46 @@ fn get_time_options(
303337
Ok(options)
304338
}
305339

340+
fn get_shuffle(matches: &getopts::Matches, allow_unstable: bool) -> OptPartRes<bool> {
341+
let mut shuffle = unstable_optflag!(matches, allow_unstable, "shuffle");
342+
if !shuffle && allow_unstable {
343+
shuffle = match env::var("RUST_TEST_SHUFFLE") {
344+
Ok(val) => &val != "0",
345+
Err(_) => false,
346+
};
347+
}
348+
349+
Ok(shuffle)
350+
}
351+
352+
fn get_shuffle_seed(matches: &getopts::Matches, allow_unstable: bool) -> OptPartRes<Option<u64>> {
353+
let mut shuffle_seed = match unstable_optopt!(matches, allow_unstable, "shuffle-seed") {
354+
Some(n_str) => match n_str.parse::<u64>() {
355+
Ok(n) => Some(n),
356+
Err(e) => {
357+
return Err(format!(
358+
"argument for --shuffle-seed must be a number \
359+
(error: {})",
360+
e
361+
));
362+
}
363+
},
364+
None => None,
365+
};
366+
367+
if shuffle_seed.is_none() && allow_unstable {
368+
shuffle_seed = match env::var("RUST_TEST_SHUFFLE_SEED") {
369+
Ok(val) => match val.parse::<u64>() {
370+
Ok(n) => Some(n),
371+
Err(_) => panic!("RUST_TEST_SHUFFLE_SEED is `{}`, should be a number.", val),
372+
},
373+
Err(_) => None,
374+
};
375+
}
376+
377+
Ok(shuffle_seed)
378+
}
379+
306380
fn get_test_threads(matches: &getopts::Matches) -> OptPartRes<Option<usize>> {
307381
let test_threads = match matches.opt_str("test-threads") {
308382
Some(n_str) => match n_str.parse::<usize>() {

library/test/src/console.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,9 @@ fn on_test_event(
225225
out: &mut dyn OutputFormatter,
226226
) -> io::Result<()> {
227227
match (*event).clone() {
228-
TestEvent::TeFiltered(ref filtered_tests) => {
228+
TestEvent::TeFiltered(ref filtered_tests, shuffle_seed) => {
229229
st.total = filtered_tests.len();
230-
out.write_run_start(filtered_tests.len())?;
230+
out.write_run_start(filtered_tests.len(), shuffle_seed)?;
231231
}
232232
TestEvent::TeFilteredOut(filtered_out) => {
233233
st.filtered_out = filtered_out;

library/test/src/event.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ impl CompletedTest {
2828

2929
#[derive(Debug, Clone)]
3030
pub enum TestEvent {
31-
TeFiltered(Vec<TestDesc>),
31+
TeFiltered(Vec<TestDesc>, Option<u64>),
3232
TeWait(TestDesc),
3333
TeResult(CompletedTest),
3434
TeTimeout(TestDesc),

library/test/src/formatters/json.rs

+8-3
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,15 @@ impl<T: Write> JsonFormatter<T> {
6060
}
6161

6262
impl<T: Write> OutputFormatter for JsonFormatter<T> {
63-
fn write_run_start(&mut self, test_count: usize) -> io::Result<()> {
63+
fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option<u64>) -> io::Result<()> {
64+
let shuffle_seed_json = if let Some(shuffle_seed) = shuffle_seed {
65+
format!(r#", "shuffle_seed": {}"#, shuffle_seed)
66+
} else {
67+
String::new()
68+
};
6469
self.writeln_message(&*format!(
65-
r#"{{ "type": "suite", "event": "started", "test_count": {} }}"#,
66-
test_count
70+
r#"{{ "type": "suite", "event": "started", "test_count": {}{} }}"#,
71+
test_count, shuffle_seed_json
6772
))
6873
}
6974

library/test/src/formatters/junit.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ impl<T: Write> JunitFormatter<T> {
2727
}
2828

2929
impl<T: Write> OutputFormatter for JunitFormatter<T> {
30-
fn write_run_start(&mut self, _test_count: usize) -> io::Result<()> {
30+
fn write_run_start(
31+
&mut self,
32+
_test_count: usize,
33+
_shuffle_seed: Option<u64>,
34+
) -> io::Result<()> {
3135
// We write xml header on run start
3236
self.out.write_all(b"\n")?;
3337
self.write_message("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")

library/test/src/formatters/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub(crate) use self::pretty::PrettyFormatter;
1818
pub(crate) use self::terse::TerseFormatter;
1919

2020
pub(crate) trait OutputFormatter {
21-
fn write_run_start(&mut self, test_count: usize) -> io::Result<()>;
21+
fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option<u64>) -> io::Result<()>;
2222
fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()>;
2323
fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()>;
2424
fn write_result(

library/test/src/formatters/pretty.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,14 @@ impl<T: Write> PrettyFormatter<T> {
181181
}
182182

183183
impl<T: Write> OutputFormatter for PrettyFormatter<T> {
184-
fn write_run_start(&mut self, test_count: usize) -> io::Result<()> {
184+
fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option<u64>) -> io::Result<()> {
185185
let noun = if test_count != 1 { "tests" } else { "test" };
186-
self.write_plain(&format!("\nrunning {} {}\n", test_count, noun))
186+
let shuffle_seed_msg = if let Some(shuffle_seed) = shuffle_seed {
187+
format!(" (shuffle seed: {})", shuffle_seed)
188+
} else {
189+
String::new()
190+
};
191+
self.write_plain(&format!("\nrunning {} {}{}\n", test_count, noun, shuffle_seed_msg))
187192
}
188193

189194
fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> {

library/test/src/formatters/terse.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,15 @@ impl<T: Write> TerseFormatter<T> {
170170
}
171171

172172
impl<T: Write> OutputFormatter for TerseFormatter<T> {
173-
fn write_run_start(&mut self, test_count: usize) -> io::Result<()> {
173+
fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option<u64>) -> io::Result<()> {
174174
self.total_test_count = test_count;
175175
let noun = if test_count != 1 { "tests" } else { "test" };
176-
self.write_plain(&format!("\nrunning {} {}\n", test_count, noun))
176+
let shuffle_seed_msg = if let Some(shuffle_seed) = shuffle_seed {
177+
format!(" (shuffle seed: {})", shuffle_seed)
178+
} else {
179+
String::new()
180+
};
181+
self.write_plain(&format!("\nrunning {} {}{}\n", test_count, noun, shuffle_seed_msg))
177182
}
178183

179184
fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> {

library/test/src/helpers/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ pub mod concurrency;
55
pub mod exit_code;
66
pub mod isatty;
77
pub mod metrics;
8+
pub mod shuffle;

library/test/src/helpers/shuffle.rs

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use crate::cli::TestOpts;
2+
use crate::types::{TestDescAndFn, TestId, TestName};
3+
use std::collections::hash_map::DefaultHasher;
4+
use std::hash::Hasher;
5+
use std::time::{SystemTime, UNIX_EPOCH};
6+
7+
pub fn get_shuffle_seed(opts: &TestOpts) -> Option<u64> {
8+
opts.shuffle_seed.or_else(|| {
9+
if opts.shuffle {
10+
Some(
11+
SystemTime::now()
12+
.duration_since(UNIX_EPOCH)
13+
.expect("Failed to get system time")
14+
.as_nanos() as u64,
15+
)
16+
} else {
17+
None
18+
}
19+
})
20+
}
21+
22+
pub fn shuffle_tests(shuffle_seed: u64, tests: &mut [(TestId, TestDescAndFn)]) {
23+
let test_names: Vec<&TestName> = tests.iter().map(|test| &test.1.desc.name).collect();
24+
let test_names_hash = calculate_hash(&test_names);
25+
let mut rng = Rng::new(shuffle_seed, test_names_hash);
26+
shuffle(&mut rng, tests);
27+
}
28+
29+
// `shuffle` is from `rust-analyzer/src/cli/analysis_stats.rs`.
30+
fn shuffle<T>(rng: &mut Rng, slice: &mut [T]) {
31+
for i in 0..slice.len() {
32+
randomize_first(rng, &mut slice[i..]);
33+
}
34+
35+
fn randomize_first<T>(rng: &mut Rng, slice: &mut [T]) {
36+
assert!(!slice.is_empty());
37+
let idx = rng.rand_range(0..slice.len() as u64) as usize;
38+
slice.swap(0, idx);
39+
}
40+
}
41+
42+
struct Rng {
43+
state: u64,
44+
extra: u64,
45+
}
46+
47+
impl Rng {
48+
fn new(seed: u64, extra: u64) -> Self {
49+
Self { state: seed, extra }
50+
}
51+
52+
fn rand_range(&mut self, range: core::ops::Range<u64>) -> u64 {
53+
self.rand_u64() % (range.end - range.start) + range.start
54+
}
55+
56+
fn rand_u64(&mut self) -> u64 {
57+
self.state = calculate_hash(&(self.state, self.extra));
58+
self.state
59+
}
60+
}
61+
62+
// `calculate_hash` is from `core/src/hash/mod.rs`.
63+
fn calculate_hash<T: core::hash::Hash>(t: &T) -> u64 {
64+
let mut s = DefaultHasher::new();
65+
t.hash(&mut s);
66+
s.finish()
67+
}

library/test/src/lib.rs

+9-2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ mod tests;
9191
use event::{CompletedTest, TestEvent};
9292
use helpers::concurrency::get_concurrency;
9393
use helpers::exit_code::get_exit_code;
94+
use helpers::shuffle::{get_shuffle_seed, shuffle_tests};
9495
use options::{Concurrent, RunStrategy};
9596
use test_result::*;
9697
use time::TestExecTime;
@@ -247,7 +248,9 @@ where
247248

248249
let filtered_descs = filtered_tests.iter().map(|t| t.desc.clone()).collect();
249250

250-
let event = TestEvent::TeFiltered(filtered_descs);
251+
let shuffle_seed = get_shuffle_seed(opts);
252+
253+
let event = TestEvent::TeFiltered(filtered_descs, shuffle_seed);
251254
notify_about_test_event(event)?;
252255

253256
let (filtered_tests, filtered_benchs): (Vec<_>, _) = filtered_tests
@@ -259,7 +262,11 @@ where
259262
let concurrency = opts.test_threads.unwrap_or_else(get_concurrency);
260263

261264
let mut remaining = filtered_tests;
262-
remaining.reverse();
265+
if let Some(shuffle_seed) = shuffle_seed {
266+
shuffle_tests(shuffle_seed, &mut remaining);
267+
} else {
268+
remaining.reverse();
269+
}
263270
let mut pending = 0;
264271

265272
let (tx, rx) = channel::<CompletedTest>();

0 commit comments

Comments
 (0)