Skip to content

Commit df6221a

Browse files
committedSep 1, 2022
Sort tests at compile time, not at startup
Recently, another Miri user was trying to run `cargo miri test` on the crate `iced-x86` with `--features=code_asm,mvex`. This configuration has a startup time of ~18 minutes. That's ~18 minutes before any tests even start to run. The fact that this crate has over 26,000 tests and Miri is slow makes a lot of code which is otherwise a bit sloppy but fine into a huge runtime issue. Sorting the tests when the test harness is created instead of at startup time knocks just under 4 minutes out of those ~18 minutes. I have ways to remove most of the rest of the startup time, but this change requires coordinating changes of both the compiler and libtest, so I'm sending it separately.
1 parent 4f9898a commit df6221a

File tree

10 files changed

+130
-57
lines changed

10 files changed

+130
-57
lines changed
 

‎compiler/rustc_builtin_macros/src/test.rs

+25-12
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,22 @@ pub fn expand_test_case(
3636
let sp = ecx.with_def_site_ctxt(attr_sp);
3737
let mut item = anno_item.expect_item();
3838
item = item.map(|mut item| {
39+
let test_path_symbol = Symbol::intern(&item_path(
40+
// skip the name of the root module
41+
&ecx.current_expansion.module.mod_path[1..],
42+
&item.ident,
43+
));
3944
item.vis = ast::Visibility {
4045
span: item.vis.span,
4146
kind: ast::VisibilityKind::Public,
4247
tokens: None,
4348
};
4449
item.ident.span = item.ident.span.with_ctxt(sp.ctxt());
45-
item.attrs.push(ecx.attribute(ecx.meta_word(sp, sym::rustc_test_marker)));
50+
item.attrs.push(ecx.attribute(attr::mk_name_value_item_str(
51+
Ident::new(sym::rustc_test_marker, sp),
52+
test_path_symbol,
53+
sp,
54+
)));
4655
item
4756
});
4857

@@ -215,6 +224,12 @@ pub fn expand_test_or_bench(
215224
)
216225
};
217226

227+
let test_path_symbol = Symbol::intern(&item_path(
228+
// skip the name of the root module
229+
&cx.current_expansion.module.mod_path[1..],
230+
&item.ident,
231+
));
232+
218233
let mut test_const = cx.item(
219234
sp,
220235
Ident::new(item.ident.name, sp),
@@ -224,9 +239,14 @@ pub fn expand_test_or_bench(
224239
Ident::new(sym::cfg, attr_sp),
225240
vec![attr::mk_nested_word_item(Ident::new(sym::test, attr_sp))],
226241
)),
227-
// #[rustc_test_marker]
228-
cx.attribute(cx.meta_word(attr_sp, sym::rustc_test_marker)),
229-
],
242+
// #[rustc_test_marker = "test_case_sort_key"]
243+
cx.attribute(attr::mk_name_value_item_str(
244+
Ident::new(sym::rustc_test_marker, attr_sp),
245+
test_path_symbol,
246+
attr_sp,
247+
)),
248+
]
249+
.into(),
230250
// const $ident: test::TestDescAndFn =
231251
ast::ItemKind::Const(
232252
ast::Defaultness::Final,
@@ -250,14 +270,7 @@ pub fn expand_test_or_bench(
250270
cx.expr_call(
251271
sp,
252272
cx.expr_path(test_path("StaticTestName")),
253-
vec![cx.expr_str(
254-
sp,
255-
Symbol::intern(&item_path(
256-
// skip the name of the root module
257-
&cx.current_expansion.module.mod_path[1..],
258-
&item.ident,
259-
)),
260-
)],
273+
vec![cx.expr_str(sp, test_path_symbol)],
261274
),
262275
),
263276
// ignore: true | false

‎compiler/rustc_builtin_macros/src/test_harness.rs

+10-5
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ use tracing::debug;
1919

2020
use std::{iter, mem};
2121

22+
#[derive(Clone)]
2223
struct Test {
2324
span: Span,
2425
ident: Ident,
26+
name: Symbol,
2527
}
2628

2729
struct TestCtxt<'a> {
@@ -121,10 +123,10 @@ impl<'a> MutVisitor for TestHarnessGenerator<'a> {
121123

122124
fn flat_map_item(&mut self, i: P<ast::Item>) -> SmallVec<[P<ast::Item>; 1]> {
123125
let mut item = i.into_inner();
124-
if is_test_case(&self.cx.ext_cx.sess, &item) {
126+
if let Some(name) = get_test_name(&self.cx.ext_cx.sess, &item) {
125127
debug!("this is a test item");
126128

127-
let test = Test { span: item.span, ident: item.ident };
129+
let test = Test { span: item.span, ident: item.ident, name };
128130
self.tests.push(test);
129131
}
130132

@@ -355,9 +357,12 @@ fn mk_tests_slice(cx: &TestCtxt<'_>, sp: Span) -> P<ast::Expr> {
355357
debug!("building test vector from {} tests", cx.test_cases.len());
356358
let ecx = &cx.ext_cx;
357359

360+
let mut tests = cx.test_cases.clone();
361+
tests.sort_by(|a, b| a.name.as_str().cmp(&b.name.as_str()));
362+
358363
ecx.expr_array_ref(
359364
sp,
360-
cx.test_cases
365+
tests
361366
.iter()
362367
.map(|test| {
363368
ecx.expr_addr_of(test.span, ecx.expr_path(ecx.path(test.span, vec![test.ident])))
@@ -366,8 +371,8 @@ fn mk_tests_slice(cx: &TestCtxt<'_>, sp: Span) -> P<ast::Expr> {
366371
)
367372
}
368373

369-
fn is_test_case(sess: &Session, i: &ast::Item) -> bool {
370-
sess.contains_name(&i.attrs, sym::rustc_test_marker)
374+
fn get_test_name(sess: &Session, i: &ast::Item) -> Option<Symbol> {
375+
sess.first_attr_value_str_by_name(&i.attrs, sym::rustc_test_marker)
371376
}
372377

373378
fn get_test_runner(

‎compiler/rustc_feature/src/builtin_attrs.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ pub const BUILTIN_ATTRIBUTES: &[BuiltinAttribute] = &[
727727
for reserving for `for<T> From<!> for T` impl"
728728
),
729729
rustc_attr!(
730-
rustc_test_marker, Normal, template!(Word), WarnFollowing,
730+
rustc_test_marker, Normal, template!(NameValueStr: "name"), WarnFollowing,
731731
"the `#[rustc_test_marker]` attribute is used internally to track tests",
732732
),
733733
rustc_attr!(

‎library/test/src/console.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ pub fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Res
147147
let mut ntest = 0;
148148
let mut nbench = 0;
149149

150-
for test in filter_tests(&opts, tests) {
150+
for test in filter_tests(&opts, tests).into_iter() {
151151
use crate::TestFn::*;
152152

153153
let TestDescAndFn { desc: TestDesc { name, .. }, testfn } = test;

‎library/test/src/lib.rs

+7-10
Original file line numberDiff line numberDiff line change
@@ -242,20 +242,20 @@ where
242242
let event = TestEvent::TeFiltered(filtered_descs, shuffle_seed);
243243
notify_about_test_event(event)?;
244244

245-
let (filtered_tests, filtered_benchs): (Vec<_>, _) = filtered_tests
245+
let (mut filtered_tests, filtered_benchs): (Vec<_>, _) = filtered_tests
246246
.into_iter()
247247
.enumerate()
248248
.map(|(i, e)| (TestId(i), e))
249249
.partition(|(_, e)| matches!(e.testfn, StaticTestFn(_) | DynTestFn(_)));
250250

251251
let concurrency = opts.test_threads.unwrap_or_else(get_concurrency);
252252

253-
let mut remaining = filtered_tests;
254253
if let Some(shuffle_seed) = shuffle_seed {
255-
shuffle_tests(shuffle_seed, &mut remaining);
256-
} else {
257-
remaining.reverse();
254+
shuffle_tests(shuffle_seed, &mut filtered_tests);
258255
}
256+
// Store the tests in a VecDeque so we can efficiently remove the first element to run the
257+
// tests in the order they were passed (unless shuffled).
258+
let mut remaining = VecDeque::from(filtered_tests);
259259
let mut pending = 0;
260260

261261
let (tx, rx) = channel::<CompletedTest>();
@@ -295,7 +295,7 @@ where
295295

296296
if concurrency == 1 {
297297
while !remaining.is_empty() {
298-
let (id, test) = remaining.pop().unwrap();
298+
let (id, test) = remaining.pop_front().unwrap();
299299
let event = TestEvent::TeWait(test.desc.clone());
300300
notify_about_test_event(event)?;
301301
let join_handle =
@@ -309,7 +309,7 @@ where
309309
} else {
310310
while pending > 0 || !remaining.is_empty() {
311311
while pending < concurrency && !remaining.is_empty() {
312-
let (id, test) = remaining.pop().unwrap();
312+
let (id, test) = remaining.pop_front().unwrap();
313313
let timeout = time::get_default_test_timeout();
314314
let desc = test.desc.clone();
315315

@@ -421,9 +421,6 @@ pub fn filter_tests(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> Vec<TestDescA
421421
RunIgnored::No => {}
422422
}
423423

424-
// Sort the tests alphabetically
425-
filtered.sort_by(|t1, t2| t1.desc.name.as_slice().cmp(t2.desc.name.as_slice()));
426-
427424
filtered
428425
}
429426

‎library/test/src/tests.rs

-27
Original file line numberDiff line numberDiff line change
@@ -600,33 +600,6 @@ fn sample_tests() -> Vec<TestDescAndFn> {
600600
tests
601601
}
602602

603-
#[test]
604-
pub fn sort_tests() {
605-
let mut opts = TestOpts::new();
606-
opts.run_tests = true;
607-
608-
let tests = sample_tests();
609-
let filtered = filter_tests(&opts, tests);
610-
611-
let expected = vec![
612-
"isize::test_pow".to_string(),
613-
"isize::test_to_str".to_string(),
614-
"sha1::test".to_string(),
615-
"test::do_not_run_ignored_tests".to_string(),
616-
"test::filter_for_ignored_option".to_string(),
617-
"test::first_free_arg_should_be_a_filter".to_string(),
618-
"test::ignored_tests_result_in_ignored".to_string(),
619-
"test::parse_ignored_flag".to_string(),
620-
"test::parse_include_ignored_flag".to_string(),
621-
"test::run_include_ignored_option".to_string(),
622-
"test::sort_tests".to_string(),
623-
];
624-
625-
for (a, b) in expected.iter().zip(filtered) {
626-
assert_eq!(*a, b.desc.name.to_string());
627-
}
628-
}
629-
630603
#[test]
631604
pub fn shuffle_tests() {
632605
let mut opts = TestOpts::new();

‎src/librustdoc/doctest.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,13 @@ pub(crate) fn run(options: RustdocOptions) -> Result<(), ErrorGuaranteed> {
210210
pub(crate) fn run_tests(
211211
mut test_args: Vec<String>,
212212
nocapture: bool,
213-
tests: Vec<test::TestDescAndFn>,
213+
mut tests: Vec<test::TestDescAndFn>,
214214
) {
215215
test_args.insert(0, "rustdoctest".to_string());
216216
if nocapture {
217217
test_args.push("--nocapture".to_string());
218218
}
219+
tests.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice()));
219220
test::test_main(&test_args, tests, None);
220221
}
221222

‎src/test/pretty/tests-are-sorted.pp

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#![feature(prelude_import)]
2+
#![no_std]
3+
#[prelude_import]
4+
use ::std::prelude::rust_2015::*;
5+
#[macro_use]
6+
extern crate std;
7+
// compile-flags: --crate-type=lib --test
8+
// pretty-compare-only
9+
// pretty-mode:expanded
10+
// pp-exact:tests-are-sorted.pp
11+
12+
extern crate test;
13+
#[cfg(test)]
14+
#[rustc_test_marker = "m_test"]
15+
pub const m_test: test::TestDescAndFn =
16+
test::TestDescAndFn {
17+
desc: test::TestDesc {
18+
name: test::StaticTestName("m_test"),
19+
ignore: false,
20+
ignore_message: ::core::option::Option::None,
21+
compile_fail: false,
22+
no_run: false,
23+
should_panic: test::ShouldPanic::No,
24+
test_type: test::TestType::Unknown,
25+
},
26+
testfn: test::StaticTestFn(|| test::assert_test_result(m_test())),
27+
};
28+
fn m_test() {}
29+
30+
extern crate test;
31+
#[cfg(test)]
32+
#[rustc_test_marker = "z_test"]
33+
pub const z_test: test::TestDescAndFn =
34+
test::TestDescAndFn {
35+
desc: test::TestDesc {
36+
name: test::StaticTestName("z_test"),
37+
ignore: false,
38+
ignore_message: ::core::option::Option::None,
39+
compile_fail: false,
40+
no_run: false,
41+
should_panic: test::ShouldPanic::No,
42+
test_type: test::TestType::Unknown,
43+
},
44+
testfn: test::StaticTestFn(|| test::assert_test_result(z_test())),
45+
};
46+
fn z_test() {}
47+
48+
extern crate test;
49+
#[cfg(test)]
50+
#[rustc_test_marker = "a_test"]
51+
pub const a_test: test::TestDescAndFn =
52+
test::TestDescAndFn {
53+
desc: test::TestDesc {
54+
name: test::StaticTestName("a_test"),
55+
ignore: false,
56+
ignore_message: ::core::option::Option::None,
57+
compile_fail: false,
58+
no_run: false,
59+
should_panic: test::ShouldPanic::No,
60+
test_type: test::TestType::Unknown,
61+
},
62+
testfn: test::StaticTestFn(|| test::assert_test_result(a_test())),
63+
};
64+
fn a_test() {}
65+
#[rustc_main]
66+
pub fn main() -> () {
67+
extern crate test;
68+
test::test_main_static(&[&a_test, &m_test, &z_test])
69+
}

‎src/test/pretty/tests-are-sorted.rs

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// compile-flags: --crate-type=lib --test
2+
// pretty-compare-only
3+
// pretty-mode:expanded
4+
// pp-exact:tests-are-sorted.pp
5+
6+
#[test]
7+
fn m_test() {}
8+
9+
#[test]
10+
fn z_test() {}
11+
12+
#[test]
13+
fn a_test() {}

‎src/tools/compiletest/src/main.rs

+2
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,8 @@ pub fn run_tests(config: Config) {
401401
make_tests(c, &mut tests);
402402
}
403403

404+
tests.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice()));
405+
404406
let res = test::run_tests_console(&opts, tests);
405407
match res {
406408
Ok(true) => {}

0 commit comments

Comments
 (0)
Please sign in to comment.