Skip to content

Commit e5bb341

Browse files
committed
Auto merge of #111992 - ferrocene:pa-panic-abort-tests-bench, r=m-ou-se
Test benchmarks with `-Z panic-abort-tests` During test execution, when a `#[bench]` benchmark is encountered it's executed once to check whether it works. Unfortunately that was not compatible with `-Z panic-abort-tests`: the feature works by spawning a subprocess for each test, which prevents the use of dynamic tests as we cannot pass closures to child processes, and before this PR the conversion from benchmark to test was done by turning benchmarks into dynamic tests whose closures execute the benchmark once. The approach this PR took was to add two new kinds of `TestFn`s: `StaticBenchAsTestFn` and `DynBenchAsTestFn` (:warning: **this is a breaking change** :warning:). With that change, a `StaticBenchFn` can be converted into a `StaticBenchAsTestFn` without creating dynamic tests, and making it possible to test `#[bench]` functions with `-Z panic-abort-tests`. The subprocess test runner also had to be updated to perform the conversion from benchmark to test when appropriate. Along with the bug fix, in the first commit I refactored how tests are executed: rather than executing the test function in multiple places across `libtest`, there is now a private `TestFn::into_runnable()` method, which returns either a `RunnableTest` or `RunnableBench`, on which you can call the `run()` method. This simplified the rest of the changes in the PR. This PR is best reviewed commit-by-commit. Fixes #73509
2 parents 6b06fdf + 58d9d8c commit e5bb341

File tree

6 files changed

+184
-122
lines changed

6 files changed

+184
-122
lines changed

library/test/src/console.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ pub fn list_tests_console(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> io::Res
199199
let TestDescAndFn { desc, testfn } = test;
200200

201201
let fntype = match testfn {
202-
StaticTestFn(..) | DynTestFn(..) => {
202+
StaticTestFn(..) | DynTestFn(..) | StaticBenchAsTestFn(..) | DynBenchAsTestFn(..) => {
203203
st.tests += 1;
204204
"test"
205205
}

library/test/src/lib.rs

+89-117
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ use time::TestExecTime;
9292
const ERROR_EXIT_CODE: i32 = 101;
9393

9494
const SECONDARY_TEST_INVOKER_VAR: &str = "__RUST_TEST_INVOKE";
95+
const SECONDARY_TEST_BENCH_BENCHMARKS_VAR: &str = "__RUST_TEST_BENCH_BENCHMARKS";
9596

9697
// The default console test runner. It accepts the command line
9798
// arguments and a vector of test_descs.
@@ -171,18 +172,32 @@ pub fn test_main_static_abort(tests: &[&TestDescAndFn]) {
171172
// will then exit the process.
172173
if let Ok(name) = env::var(SECONDARY_TEST_INVOKER_VAR) {
173174
env::remove_var(SECONDARY_TEST_INVOKER_VAR);
175+
176+
// Convert benchmarks to tests if we're not benchmarking.
177+
let mut tests = tests.iter().map(make_owned_test).collect::<Vec<_>>();
178+
if env::var(SECONDARY_TEST_BENCH_BENCHMARKS_VAR).is_ok() {
179+
env::remove_var(SECONDARY_TEST_BENCH_BENCHMARKS_VAR);
180+
} else {
181+
tests = convert_benchmarks_to_tests(tests);
182+
};
183+
174184
let test = tests
175-
.iter()
185+
.into_iter()
176186
.filter(|test| test.desc.name.as_slice() == name)
177-
.map(make_owned_test)
178187
.next()
179188
.unwrap_or_else(|| panic!("couldn't find a test with the provided name '{name}'"));
180189
let TestDescAndFn { desc, testfn } = test;
181-
let testfn = match testfn {
182-
StaticTestFn(f) => f,
183-
_ => panic!("only static tests are supported"),
184-
};
185-
run_test_in_spawned_subprocess(desc, Box::new(testfn));
190+
match testfn.into_runnable() {
191+
Runnable::Test(runnable_test) => {
192+
if runnable_test.is_dynamic() {
193+
panic!("only static tests are supported");
194+
}
195+
run_test_in_spawned_subprocess(desc, runnable_test);
196+
}
197+
Runnable::Bench(_) => {
198+
panic!("benchmarks should not be executed into child processes")
199+
}
200+
}
186201
}
187202

188203
let args = env::args().collect::<Vec<_>>();
@@ -234,16 +249,6 @@ impl FilteredTests {
234249
self.tests.push((TestId(self.next_id), test));
235250
self.next_id += 1;
236251
}
237-
fn add_bench_as_test(
238-
&mut self,
239-
desc: TestDesc,
240-
benchfn: impl Fn(&mut Bencher) -> Result<(), String> + Send + 'static,
241-
) {
242-
let testfn = DynTestFn(Box::new(move || {
243-
bench::run_once(|b| __rust_begin_short_backtrace(|| benchfn(b)))
244-
}));
245-
self.add_test(desc, testfn);
246-
}
247252
fn total_len(&self) -> usize {
248253
self.tests.len() + self.benches.len()
249254
}
@@ -301,14 +306,14 @@ where
301306
if opts.bench_benchmarks {
302307
filtered.add_bench(desc, DynBenchFn(benchfn));
303308
} else {
304-
filtered.add_bench_as_test(desc, benchfn);
309+
filtered.add_test(desc, DynBenchAsTestFn(benchfn));
305310
}
306311
}
307312
StaticBenchFn(benchfn) => {
308313
if opts.bench_benchmarks {
309314
filtered.add_bench(desc, StaticBenchFn(benchfn));
310315
} else {
311-
filtered.add_bench_as_test(desc, benchfn);
316+
filtered.add_test(desc, StaticBenchAsTestFn(benchfn));
312317
}
313318
}
314319
testfn => {
@@ -519,12 +524,8 @@ pub fn convert_benchmarks_to_tests(tests: Vec<TestDescAndFn>) -> Vec<TestDescAnd
519524
.into_iter()
520525
.map(|x| {
521526
let testfn = match x.testfn {
522-
DynBenchFn(benchfn) => DynTestFn(Box::new(move || {
523-
bench::run_once(|b| __rust_begin_short_backtrace(|| benchfn(b)))
524-
})),
525-
StaticBenchFn(benchfn) => DynTestFn(Box::new(move || {
526-
bench::run_once(|b| __rust_begin_short_backtrace(|| benchfn(b)))
527-
})),
527+
DynBenchFn(benchfn) => DynBenchAsTestFn(benchfn),
528+
StaticBenchFn(benchfn) => StaticBenchAsTestFn(benchfn),
528529
f => f,
529530
};
530531
TestDescAndFn { desc: x.desc, testfn }
@@ -553,99 +554,69 @@ pub fn run_test(
553554
return None;
554555
}
555556

556-
struct TestRunOpts {
557-
pub strategy: RunStrategy,
558-
pub nocapture: bool,
559-
pub time: Option<time::TestTimeOptions>,
560-
}
557+
match testfn.into_runnable() {
558+
Runnable::Test(runnable_test) => {
559+
if runnable_test.is_dynamic() {
560+
match strategy {
561+
RunStrategy::InProcess => (),
562+
_ => panic!("Cannot run dynamic test fn out-of-process"),
563+
};
564+
}
561565

562-
fn run_test_inner(
563-
id: TestId,
564-
desc: TestDesc,
565-
monitor_ch: Sender<CompletedTest>,
566-
testfn: Box<dyn FnOnce() -> Result<(), String> + Send>,
567-
opts: TestRunOpts,
568-
) -> Option<thread::JoinHandle<()>> {
569-
let name = desc.name.clone();
570-
571-
let runtest = move || match opts.strategy {
572-
RunStrategy::InProcess => run_test_in_process(
573-
id,
574-
desc,
575-
opts.nocapture,
576-
opts.time.is_some(),
577-
testfn,
578-
monitor_ch,
579-
opts.time,
580-
),
581-
RunStrategy::SpawnPrimary => spawn_test_subprocess(
582-
id,
583-
desc,
584-
opts.nocapture,
585-
opts.time.is_some(),
586-
monitor_ch,
587-
opts.time,
588-
),
589-
};
566+
let name = desc.name.clone();
567+
let nocapture = opts.nocapture;
568+
let time_options = opts.time_options;
569+
let bench_benchmarks = opts.bench_benchmarks;
570+
571+
let runtest = move || match strategy {
572+
RunStrategy::InProcess => run_test_in_process(
573+
id,
574+
desc,
575+
nocapture,
576+
time_options.is_some(),
577+
runnable_test,
578+
monitor_ch,
579+
time_options,
580+
),
581+
RunStrategy::SpawnPrimary => spawn_test_subprocess(
582+
id,
583+
desc,
584+
nocapture,
585+
time_options.is_some(),
586+
monitor_ch,
587+
time_options,
588+
bench_benchmarks,
589+
),
590+
};
590591

591-
// If the platform is single-threaded we're just going to run
592-
// the test synchronously, regardless of the concurrency
593-
// level.
594-
let supports_threads = !cfg!(target_os = "emscripten") && !cfg!(target_family = "wasm");
595-
if supports_threads {
596-
let cfg = thread::Builder::new().name(name.as_slice().to_owned());
597-
let mut runtest = Arc::new(Mutex::new(Some(runtest)));
598-
let runtest2 = runtest.clone();
599-
match cfg.spawn(move || runtest2.lock().unwrap().take().unwrap()()) {
600-
Ok(handle) => Some(handle),
601-
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
602-
// `ErrorKind::WouldBlock` means hitting the thread limit on some
603-
// platforms, so run the test synchronously here instead.
604-
Arc::get_mut(&mut runtest).unwrap().get_mut().unwrap().take().unwrap()();
605-
None
592+
// If the platform is single-threaded we're just going to run
593+
// the test synchronously, regardless of the concurrency
594+
// level.
595+
let supports_threads = !cfg!(target_os = "emscripten") && !cfg!(target_family = "wasm");
596+
if supports_threads {
597+
let cfg = thread::Builder::new().name(name.as_slice().to_owned());
598+
let mut runtest = Arc::new(Mutex::new(Some(runtest)));
599+
let runtest2 = runtest.clone();
600+
match cfg.spawn(move || runtest2.lock().unwrap().take().unwrap()()) {
601+
Ok(handle) => Some(handle),
602+
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
603+
// `ErrorKind::WouldBlock` means hitting the thread limit on some
604+
// platforms, so run the test synchronously here instead.
605+
Arc::get_mut(&mut runtest).unwrap().get_mut().unwrap().take().unwrap()();
606+
None
607+
}
608+
Err(e) => panic!("failed to spawn thread to run test: {e}"),
606609
}
607-
Err(e) => panic!("failed to spawn thread to run test: {e}"),
610+
} else {
611+
runtest();
612+
None
608613
}
609-
} else {
610-
runtest();
611-
None
612614
}
613-
}
614-
615-
let test_run_opts =
616-
TestRunOpts { strategy, nocapture: opts.nocapture, time: opts.time_options };
617-
618-
match testfn {
619-
DynBenchFn(benchfn) => {
615+
Runnable::Bench(runnable_bench) => {
620616
// Benchmarks aren't expected to panic, so we run them all in-process.
621-
crate::bench::benchmark(id, desc, monitor_ch, opts.nocapture, benchfn);
617+
runnable_bench.run(id, &desc, &monitor_ch, opts.nocapture);
622618
None
623619
}
624-
StaticBenchFn(benchfn) => {
625-
// Benchmarks aren't expected to panic, so we run them all in-process.
626-
crate::bench::benchmark(id, desc, monitor_ch, opts.nocapture, benchfn);
627-
None
628-
}
629-
DynTestFn(f) => {
630-
match strategy {
631-
RunStrategy::InProcess => (),
632-
_ => panic!("Cannot run dynamic test fn out-of-process"),
633-
};
634-
run_test_inner(
635-
id,
636-
desc,
637-
monitor_ch,
638-
Box::new(move || __rust_begin_short_backtrace(f)),
639-
test_run_opts,
640-
)
641-
}
642-
StaticTestFn(f) => run_test_inner(
643-
id,
644-
desc,
645-
monitor_ch,
646-
Box::new(move || __rust_begin_short_backtrace(f)),
647-
test_run_opts,
648-
),
649620
}
650621
}
651622

@@ -663,7 +634,7 @@ fn run_test_in_process(
663634
desc: TestDesc,
664635
nocapture: bool,
665636
report_time: bool,
666-
testfn: Box<dyn FnOnce() -> Result<(), String> + Send>,
637+
runnable_test: RunnableTest,
667638
monitor_ch: Sender<CompletedTest>,
668639
time_opts: Option<time::TestTimeOptions>,
669640
) {
@@ -675,7 +646,7 @@ fn run_test_in_process(
675646
}
676647

677648
let start = report_time.then(Instant::now);
678-
let result = fold_err(catch_unwind(AssertUnwindSafe(testfn)));
649+
let result = fold_err(catch_unwind(AssertUnwindSafe(|| runnable_test.run())));
679650
let exec_time = start.map(|start| {
680651
let duration = start.elapsed();
681652
TestExecTime(duration)
@@ -712,13 +683,17 @@ fn spawn_test_subprocess(
712683
report_time: bool,
713684
monitor_ch: Sender<CompletedTest>,
714685
time_opts: Option<time::TestTimeOptions>,
686+
bench_benchmarks: bool,
715687
) {
716688
let (result, test_output, exec_time) = (|| {
717689
let args = env::args().collect::<Vec<_>>();
718690
let current_exe = &args[0];
719691

720692
let mut command = Command::new(current_exe);
721693
command.env(SECONDARY_TEST_INVOKER_VAR, desc.name.as_slice());
694+
if bench_benchmarks {
695+
command.env(SECONDARY_TEST_BENCH_BENCHMARKS_VAR, "1");
696+
}
722697
if nocapture {
723698
command.stdout(process::Stdio::inherit());
724699
command.stderr(process::Stdio::inherit());
@@ -760,10 +735,7 @@ fn spawn_test_subprocess(
760735
monitor_ch.send(message).unwrap();
761736
}
762737

763-
fn run_test_in_spawned_subprocess(
764-
desc: TestDesc,
765-
testfn: Box<dyn FnOnce() -> Result<(), String> + Send>,
766-
) -> ! {
738+
fn run_test_in_spawned_subprocess(desc: TestDesc, runnable_test: RunnableTest) -> ! {
767739
let builtin_panic_hook = panic::take_hook();
768740
let record_result = Arc::new(move |panic_info: Option<&'_ PanicInfo<'_>>| {
769741
let test_result = match panic_info {
@@ -789,7 +761,7 @@ fn run_test_in_spawned_subprocess(
789761
});
790762
let record_result2 = record_result.clone();
791763
panic::set_hook(Box::new(move |info| record_result2(Some(info))));
792-
if let Err(message) = testfn() {
764+
if let Err(message) = runnable_test.run() {
793765
panic!("{}", message);
794766
}
795767
record_result(None);

0 commit comments

Comments
 (0)