Skip to content

Commit 39bf10d

Browse files
authored
Rollup merge of rust-lang#100451 - hovinen:no-panic-on-result-err-in-test, r=Mark-Simulacrum
Do not panic when a test function returns Result::Err. Rust's test library allows test functions to return a `Result`, so that the test is deemed to have failed if the function returns a `Result::Err` variant. Currently, this works by having `Result` implement the `Termination` trait and asserting in assert_test_result that `Termination::report()` indicates successful completion. This turns a `Result::Err` into a panic, which is caught and unwound in the test library. This approach is problematic in certain environments where one wishes to save on both binary size and compute resources when running tests by: * Compiling all code with `--panic=abort` to avoid having to generate unwinding tables, and * Running most tests in-process to avoid the overhead of spawning new processes. This change removes the intermediate panic step and passes a `Result::Err` directly through to the test runner. To do this, it modifies `assert_test_result` to return a `Result<(), String>` where the `Err` variant holds what was previously the panic message. It changes the types in the `TestFn` enum to return `Result<(), String>`. This tries to minimise the changes to benchmark tests, so it calls `unwrap()` on the `Result` returned by `assert_test_result`, effectively keeping the same behaviour as before. Some questions for reviewers: * Does the change to the return types in the enum `TestFn` constitute a breaking change for the library API? Namely, the enum definition is public but the test library indicates that "Currently, not much of this is meant for users" and most of the library API appears to be marked unstable. * Is there a way to test this change, i.e., to test that no panic occurs if a test returns `Result::Err`? * Is there a shorter, more idiomatic way to fold `Result<Result<T,E>,E>` into a `Result<T,E>` than the `fold_err` function I added?
2 parents 756e7be + e19a98c commit 39bf10d

File tree

7 files changed

+135
-60
lines changed

7 files changed

+135
-60
lines changed

library/test/src/bench.rs

+11-10
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ impl Bencher {
4949
self.summary = Some(iter(&mut inner));
5050
}
5151

52-
pub fn bench<F>(&mut self, mut f: F) -> Option<stats::Summary>
52+
pub fn bench<F>(&mut self, mut f: F) -> Result<Option<stats::Summary>, String>
5353
where
54-
F: FnMut(&mut Bencher),
54+
F: FnMut(&mut Bencher) -> Result<(), String>,
5555
{
56-
f(self);
57-
self.summary
56+
let result = f(self);
57+
result.map(|_| self.summary)
5858
}
5959
}
6060

@@ -195,7 +195,7 @@ pub fn benchmark<F>(
195195
nocapture: bool,
196196
f: F,
197197
) where
198-
F: FnMut(&mut Bencher),
198+
F: FnMut(&mut Bencher) -> Result<(), String>,
199199
{
200200
let mut bs = Bencher { mode: BenchMode::Auto, summary: None, bytes: 0 };
201201

@@ -211,32 +211,33 @@ pub fn benchmark<F>(
211211

212212
let test_result = match result {
213213
//bs.bench(f) {
214-
Ok(Some(ns_iter_summ)) => {
214+
Ok(Ok(Some(ns_iter_summ))) => {
215215
let ns_iter = cmp::max(ns_iter_summ.median as u64, 1);
216216
let mb_s = bs.bytes * 1000 / ns_iter;
217217

218218
let bs = BenchSamples { ns_iter_summ, mb_s: mb_s as usize };
219219
TestResult::TrBench(bs)
220220
}
221-
Ok(None) => {
221+
Ok(Ok(None)) => {
222222
// iter not called, so no data.
223223
// FIXME: error in this case?
224224
let samples: &mut [f64] = &mut [0.0_f64; 1];
225225
let bs = BenchSamples { ns_iter_summ: stats::Summary::new(samples), mb_s: 0 };
226226
TestResult::TrBench(bs)
227227
}
228228
Err(_) => TestResult::TrFailed,
229+
Ok(Err(_)) => TestResult::TrFailed,
229230
};
230231

231232
let stdout = data.lock().unwrap().to_vec();
232233
let message = CompletedTest::new(id, desc, test_result, None, stdout);
233234
monitor_ch.send(message).unwrap();
234235
}
235236

236-
pub fn run_once<F>(f: F)
237+
pub fn run_once<F>(f: F) -> Result<(), String>
237238
where
238-
F: FnMut(&mut Bencher),
239+
F: FnMut(&mut Bencher) -> Result<(), String>,
239240
{
240241
let mut bs = Bencher { mode: BenchMode::Single, summary: None, bytes: 0 };
241-
bs.bench(f);
242+
bs.bench(f).map(|_| ())
242243
}

library/test/src/lib.rs

+42-19
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
//! benchmarks themselves) should be done via the `#[test]` and
77
//! `#[bench]` attributes.
88
//!
9-
//! See the [Testing Chapter](../book/ch11-00-testing.html) of the book for more details.
9+
//! See the [Testing Chapter](../book/ch11-00-testing.html) of the book for more
10+
//! details.
1011
1112
// Currently, not much of this is meant for users. It is intended to
1213
// support the simplest interface possible for representing and
@@ -76,6 +77,7 @@ mod types;
7677
#[cfg(test)]
7778
mod tests;
7879

80+
use core::any::Any;
7981
use event::{CompletedTest, TestEvent};
8082
use helpers::concurrency::get_concurrency;
8183
use helpers::exit_code::get_exit_code;
@@ -175,17 +177,20 @@ fn make_owned_test(test: &&TestDescAndFn) -> TestDescAndFn {
175177
}
176178
}
177179

178-
/// Invoked when unit tests terminate. Should panic if the unit
179-
/// Tests is considered a failure. By default, invokes `report()`
180-
/// and checks for a `0` result.
181-
pub fn assert_test_result<T: Termination>(result: T) {
180+
/// Invoked when unit tests terminate. Returns `Result::Err` if the test is
181+
/// considered a failure. By default, invokes `report() and checks for a `0`
182+
/// result.
183+
pub fn assert_test_result<T: Termination>(result: T) -> Result<(), String> {
182184
let code = result.report().to_i32();
183-
assert_eq!(
184-
code, 0,
185-
"the test returned a termination value with a non-zero status code ({}) \
186-
which indicates a failure",
187-
code
188-
);
185+
if code == 0 {
186+
Ok(())
187+
} else {
188+
Err(format!(
189+
"the test returned a termination value with a non-zero status code \
190+
({}) which indicates a failure",
191+
code
192+
))
193+
}
189194
}
190195

191196
pub fn run_tests<F>(
@@ -478,7 +483,7 @@ pub fn run_test(
478483
id: TestId,
479484
desc: TestDesc,
480485
monitor_ch: Sender<CompletedTest>,
481-
testfn: Box<dyn FnOnce() + Send>,
486+
testfn: Box<dyn FnOnce() -> Result<(), String> + Send>,
482487
opts: TestRunOpts,
483488
) -> Option<thread::JoinHandle<()>> {
484489
let concurrency = opts.concurrency;
@@ -567,19 +572,19 @@ pub fn run_test(
567572

568573
/// Fixed frame used to clean the backtrace with `RUST_BACKTRACE=1`.
569574
#[inline(never)]
570-
fn __rust_begin_short_backtrace<F: FnOnce()>(f: F) {
571-
f();
575+
fn __rust_begin_short_backtrace<T, F: FnOnce() -> T>(f: F) -> T {
576+
let result = f();
572577

573578
// prevent this frame from being tail-call optimised away
574-
black_box(());
579+
black_box(result)
575580
}
576581

577582
fn run_test_in_process(
578583
id: TestId,
579584
desc: TestDesc,
580585
nocapture: bool,
581586
report_time: bool,
582-
testfn: Box<dyn FnOnce() + Send>,
587+
testfn: Box<dyn FnOnce() -> Result<(), String> + Send>,
583588
monitor_ch: Sender<CompletedTest>,
584589
time_opts: Option<time::TestTimeOptions>,
585590
) {
@@ -591,7 +596,7 @@ fn run_test_in_process(
591596
}
592597

593598
let start = report_time.then(Instant::now);
594-
let result = catch_unwind(AssertUnwindSafe(testfn));
599+
let result = fold_err(catch_unwind(AssertUnwindSafe(testfn)));
595600
let exec_time = start.map(|start| {
596601
let duration = start.elapsed();
597602
TestExecTime(duration)
@@ -608,6 +613,19 @@ fn run_test_in_process(
608613
monitor_ch.send(message).unwrap();
609614
}
610615

616+
fn fold_err<T, E>(
617+
result: Result<Result<T, E>, Box<dyn Any + Send>>,
618+
) -> Result<T, Box<dyn Any + Send>>
619+
where
620+
E: Send + 'static,
621+
{
622+
match result {
623+
Ok(Err(e)) => Err(Box::new(e)),
624+
Ok(Ok(v)) => Ok(v),
625+
Err(e) => Err(e),
626+
}
627+
}
628+
611629
fn spawn_test_subprocess(
612630
id: TestId,
613631
desc: TestDesc,
@@ -663,7 +681,10 @@ fn spawn_test_subprocess(
663681
monitor_ch.send(message).unwrap();
664682
}
665683

666-
fn run_test_in_spawned_subprocess(desc: TestDesc, testfn: Box<dyn FnOnce() + Send>) -> ! {
684+
fn run_test_in_spawned_subprocess(
685+
desc: TestDesc,
686+
testfn: Box<dyn FnOnce() -> Result<(), String> + Send>,
687+
) -> ! {
667688
let builtin_panic_hook = panic::take_hook();
668689
let record_result = Arc::new(move |panic_info: Option<&'_ PanicInfo<'_>>| {
669690
let test_result = match panic_info {
@@ -689,7 +710,9 @@ fn run_test_in_spawned_subprocess(desc: TestDesc, testfn: Box<dyn FnOnce() + Sen
689710
});
690711
let record_result2 = record_result.clone();
691712
panic::set_hook(Box::new(move |info| record_result2(Some(&info))));
692-
testfn();
713+
if let Err(message) = testfn() {
714+
panic!("{}", message);
715+
}
693716
record_result(None);
694717
unreachable!("panic=abort callback should have exited the process")
695718
}

0 commit comments

Comments
 (0)