Skip to content

Commit 779418d

Browse files
author
Yuki Okushi
authored
Rollup merge of #99939 - saethlin:pre-sort-tests, r=thomcc,jackh726
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. (except for doctests, because there is no compile-time harness)
2 parents fbb3650 + df6221a commit 779418d

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
@@ -18,9 +18,11 @@ use thin_vec::thin_vec;
1818

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

21+
#[derive(Clone)]
2122
struct Test {
2223
span: Span,
2324
ident: Ident,
25+
name: Symbol,
2426
}
2527

2628
struct TestCtxt<'a> {
@@ -120,10 +122,10 @@ impl<'a> MutVisitor for TestHarnessGenerator<'a> {
120122

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

126-
let test = Test { span: item.span, ident: item.ident };
128+
let test = Test { span: item.span, ident: item.ident, name };
127129
self.tests.push(test);
128130
}
129131

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

362+
let mut tests = cx.test_cases.clone();
363+
tests.sort_by(|a, b| a.name.as_str().cmp(&b.name.as_str()));
364+
360365
ecx.expr_array_ref(
361366
sp,
362-
cx.test_cases
367+
tests
363368
.iter()
364369
.map(|test| {
365370
ecx.expr_addr_of(test.span, ecx.expr_path(ecx.path(test.span, vec![test.ident])))
@@ -368,8 +373,8 @@ fn mk_tests_slice(cx: &TestCtxt<'_>, sp: Span) -> P<ast::Expr> {
368373
)
369374
}
370375

371-
fn is_test_case(sess: &Session, i: &ast::Item) -> bool {
372-
sess.contains_name(&i.attrs, sym::rustc_test_marker)
376+
fn get_test_name(sess: &Session, i: &ast::Item) -> Option<Symbol> {
377+
sess.first_attr_value_str_by_name(&i.attrs, sym::rustc_test_marker)
373378
}
374379

375380
fn get_test_runner(

compiler/rustc_feature/src/builtin_attrs.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ pub const BUILTIN_ATTRIBUTES: &[BuiltinAttribute] = &[
746746
for reserving for `for<T> From<!> for T` impl"
747747
),
748748
rustc_attr!(
749-
rustc_test_marker, Normal, template!(Word), WarnFollowing,
749+
rustc_test_marker, Normal, template!(NameValueStr: "name"), WarnFollowing,
750750
"the `#[rustc_test_marker]` attribute is used internally to track tests",
751751
),
752752
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
@@ -247,20 +247,20 @@ where
247247
let event = TestEvent::TeFiltered(filtered_descs, shuffle_seed);
248248
notify_about_test_event(event)?;
249249

250-
let (filtered_tests, filtered_benchs): (Vec<_>, _) = filtered_tests
250+
let (mut filtered_tests, filtered_benchs): (Vec<_>, _) = filtered_tests
251251
.into_iter()
252252
.enumerate()
253253
.map(|(i, e)| (TestId(i), e))
254254
.partition(|(_, e)| matches!(e.testfn, StaticTestFn(_) | DynTestFn(_)));
255255

256256
let concurrency = opts.test_threads.unwrap_or_else(get_concurrency);
257257

258-
let mut remaining = filtered_tests;
259258
if let Some(shuffle_seed) = shuffle_seed {
260-
shuffle_tests(shuffle_seed, &mut remaining);
261-
} else {
262-
remaining.reverse();
259+
shuffle_tests(shuffle_seed, &mut filtered_tests);
263260
}
261+
// Store the tests in a VecDeque so we can efficiently remove the first element to run the
262+
// tests in the order they were passed (unless shuffled).
263+
let mut remaining = VecDeque::from(filtered_tests);
264264
let mut pending = 0;
265265

266266
let (tx, rx) = channel::<CompletedTest>();
@@ -300,7 +300,7 @@ where
300300

301301
if concurrency == 1 {
302302
while !remaining.is_empty() {
303-
let (id, test) = remaining.pop().unwrap();
303+
let (id, test) = remaining.pop_front().unwrap();
304304
let event = TestEvent::TeWait(test.desc.clone());
305305
notify_about_test_event(event)?;
306306
let join_handle =
@@ -314,7 +314,7 @@ where
314314
} else {
315315
while pending > 0 || !remaining.is_empty() {
316316
while pending < concurrency && !remaining.is_empty() {
317-
let (id, test) = remaining.pop().unwrap();
317+
let (id, test) = remaining.pop_front().unwrap();
318318
let timeout = time::get_default_test_timeout();
319319
let desc = test.desc.clone();
320320

@@ -426,9 +426,6 @@ pub fn filter_tests(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> Vec<TestDescA
426426
RunIgnored::No => {}
427427
}
428428

429-
// Sort the tests alphabetically
430-
filtered.sort_by(|t1, t2| t1.desc.name.as_slice().cmp(t2.desc.name.as_slice()));
431-
432429
filtered
433430
}
434431

library/test/src/tests.rs

-27
Original file line numberDiff line numberDiff line change
@@ -610,33 +610,6 @@ fn sample_tests() -> Vec<TestDescAndFn> {
610610
tests
611611
}
612612

613-
#[test]
614-
pub fn sort_tests() {
615-
let mut opts = TestOpts::new();
616-
opts.run_tests = true;
617-
618-
let tests = sample_tests();
619-
let filtered = filter_tests(&opts, tests);
620-
621-
let expected = vec![
622-
"isize::test_pow".to_string(),
623-
"isize::test_to_str".to_string(),
624-
"sha1::test".to_string(),
625-
"test::do_not_run_ignored_tests".to_string(),
626-
"test::filter_for_ignored_option".to_string(),
627-
"test::first_free_arg_should_be_a_filter".to_string(),
628-
"test::ignored_tests_result_in_ignored".to_string(),
629-
"test::parse_ignored_flag".to_string(),
630-
"test::parse_include_ignored_flag".to_string(),
631-
"test::run_include_ignored_option".to_string(),
632-
"test::sort_tests".to_string(),
633-
];
634-
635-
for (a, b) in expected.iter().zip(filtered) {
636-
assert_eq!(*a, b.desc.name.to_string());
637-
}
638-
}
639-
640613
#[test]
641614
pub fn shuffle_tests() {
642615
let mut opts = TestOpts::new();

src/librustdoc/doctest.rs

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

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
@@ -397,6 +397,8 @@ pub fn run_tests(config: Config) {
397397
make_tests(c, &mut tests);
398398
}
399399

400+
tests.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice()));
401+
400402
let res = test::run_tests_console(&opts, tests);
401403
match res {
402404
Ok(true) => {}

0 commit comments

Comments
 (0)