Skip to content

Commit 1483e67

Browse files
committed
Auto merge of #81367 - andersk:join-test-threads, r=dtolnay
libtest: Wait for test threads to exit after they report completion Otherwise we can miss bugs where a test reports that it succeeded but then panics within a TLS destructor. Example: ```rust use std::thread::sleep; use std::time::Duration; struct Foo; impl Drop for Foo { fn drop(&mut self) { sleep(Duration::from_secs(1)); panic!() } } thread_local!(static FOO: Foo = Foo); #[test] pub fn test() { FOO.with(|_| {}); } ``` Before this fix, `cargo test` incorrectly reports success. ```console $ cargo test Finished test [unoptimized + debuginfo] target(s) in 0.01s Running target/debug/deps/panicking_test-85130fa46b54f758 running 1 test test test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s $ echo $? 0 ``` After this fix, the failure is visible. (The entire process is aborted due to #24479.) ```console $ cargo test Finished test [unoptimized + debuginfo] target(s) in 0.01s Running target/debug/deps/panicking_test-76180625bc2ee3c9 running 1 test thread 'test' panicked at 'explicit panic', src/main.rs:9:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace fatal runtime error: failed to initiate panic, error 5 error: test failed, to rerun pass '--bin panicking-test' Caused by: process didn't exit successfully: `/tmp/panicking-test/target/debug/deps/panicking_test-76180625bc2ee3c9 --nocapture` (signal: 6, SIGABRT: process abort signal) $ echo $? 101 ```
2 parents a8f7075 + b05788e commit 1483e67

File tree

1 file changed

+64
-24
lines changed

1 file changed

+64
-24
lines changed

library/test/src/lib.rs

+64-24
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#![feature(nll)]
2626
#![feature(available_concurrency)]
2727
#![feature(internal_output_capture)]
28+
#![feature(option_unwrap_none)]
2829
#![feature(panic_unwind)]
2930
#![feature(staged_api)]
3031
#![feature(termination_trait_lib)]
@@ -61,6 +62,7 @@ pub mod test {
6162
}
6263

6364
use std::{
65+
collections::VecDeque,
6466
env, io,
6567
io::prelude::Write,
6668
panic::{self, catch_unwind, AssertUnwindSafe, PanicInfo},
@@ -208,9 +210,19 @@ where
208210
use std::collections::{self, HashMap};
209211
use std::hash::BuildHasherDefault;
210212
use std::sync::mpsc::RecvTimeoutError;
213+
214+
struct RunningTest {
215+
join_handle: Option<thread::JoinHandle<()>>,
216+
}
217+
211218
// Use a deterministic hasher
212219
type TestMap =
213-
HashMap<TestDesc, Instant, BuildHasherDefault<collections::hash_map::DefaultHasher>>;
220+
HashMap<TestDesc, RunningTest, BuildHasherDefault<collections::hash_map::DefaultHasher>>;
221+
222+
struct TimeoutEntry {
223+
desc: TestDesc,
224+
timeout: Instant,
225+
}
214226

215227
let tests_len = tests.len();
216228

@@ -255,23 +267,30 @@ where
255267
};
256268

257269
let mut running_tests: TestMap = HashMap::default();
270+
let mut timeout_queue: VecDeque<TimeoutEntry> = VecDeque::new();
258271

259-
fn get_timed_out_tests(running_tests: &mut TestMap) -> Vec<TestDesc> {
272+
fn get_timed_out_tests(
273+
running_tests: &TestMap,
274+
timeout_queue: &mut VecDeque<TimeoutEntry>,
275+
) -> Vec<TestDesc> {
260276
let now = Instant::now();
261-
let timed_out = running_tests
262-
.iter()
263-
.filter_map(|(desc, timeout)| if &now >= timeout { Some(desc.clone()) } else { None })
264-
.collect();
265-
for test in &timed_out {
266-
running_tests.remove(test);
277+
let mut timed_out = Vec::new();
278+
while let Some(timeout_entry) = timeout_queue.front() {
279+
if now < timeout_entry.timeout {
280+
break;
281+
}
282+
let timeout_entry = timeout_queue.pop_front().unwrap();
283+
if running_tests.contains_key(&timeout_entry.desc) {
284+
timed_out.push(timeout_entry.desc);
285+
}
267286
}
268287
timed_out
269288
}
270289

271-
fn calc_timeout(running_tests: &TestMap) -> Option<Duration> {
272-
running_tests.values().min().map(|next_timeout| {
290+
fn calc_timeout(timeout_queue: &VecDeque<TimeoutEntry>) -> Option<Duration> {
291+
timeout_queue.front().map(|&TimeoutEntry { timeout: next_timeout, .. }| {
273292
let now = Instant::now();
274-
if *next_timeout >= now { *next_timeout - now } else { Duration::new(0, 0) }
293+
if next_timeout >= now { next_timeout - now } else { Duration::new(0, 0) }
275294
})
276295
}
277296

@@ -280,7 +299,8 @@ where
280299
let test = remaining.pop().unwrap();
281300
let event = TestEvent::TeWait(test.desc.clone());
282301
notify_about_test_event(event)?;
283-
run_test(opts, !opts.run_tests, test, run_strategy, tx.clone(), Concurrent::No);
302+
run_test(opts, !opts.run_tests, test, run_strategy, tx.clone(), Concurrent::No)
303+
.unwrap_none();
284304
let completed_test = rx.recv().unwrap();
285305

286306
let event = TestEvent::TeResult(completed_test);
@@ -291,19 +311,28 @@ where
291311
while pending < concurrency && !remaining.is_empty() {
292312
let test = remaining.pop().unwrap();
293313
let timeout = time::get_default_test_timeout();
294-
running_tests.insert(test.desc.clone(), timeout);
314+
let desc = test.desc.clone();
295315

296-
let event = TestEvent::TeWait(test.desc.clone());
316+
let event = TestEvent::TeWait(desc.clone());
297317
notify_about_test_event(event)?; //here no pad
298-
run_test(opts, !opts.run_tests, test, run_strategy, tx.clone(), Concurrent::Yes);
318+
let join_handle = run_test(
319+
opts,
320+
!opts.run_tests,
321+
test,
322+
run_strategy,
323+
tx.clone(),
324+
Concurrent::Yes,
325+
);
326+
running_tests.insert(desc.clone(), RunningTest { join_handle });
327+
timeout_queue.push_back(TimeoutEntry { desc, timeout });
299328
pending += 1;
300329
}
301330

302331
let mut res;
303332
loop {
304-
if let Some(timeout) = calc_timeout(&running_tests) {
333+
if let Some(timeout) = calc_timeout(&timeout_queue) {
305334
res = rx.recv_timeout(timeout);
306-
for test in get_timed_out_tests(&mut running_tests) {
335+
for test in get_timed_out_tests(&running_tests, &mut timeout_queue) {
307336
let event = TestEvent::TeTimeout(test);
308337
notify_about_test_event(event)?;
309338
}
@@ -323,8 +352,16 @@ where
323352
}
324353
}
325354

326-
let completed_test = res.unwrap();
327-
running_tests.remove(&completed_test.desc);
355+
let mut completed_test = res.unwrap();
356+
let running_test = running_tests.remove(&completed_test.desc).unwrap();
357+
if let Some(join_handle) = running_test.join_handle {
358+
if let Err(_) = join_handle.join() {
359+
if let TrOk = completed_test.result {
360+
completed_test.result =
361+
TrFailedMsg("panicked after reporting success".to_string());
362+
}
363+
}
364+
}
328365

329366
let event = TestEvent::TeResult(completed_test);
330367
notify_about_test_event(event)?;
@@ -415,7 +452,7 @@ pub fn run_test(
415452
strategy: RunStrategy,
416453
monitor_ch: Sender<CompletedTest>,
417454
concurrency: Concurrent,
418-
) {
455+
) -> Option<thread::JoinHandle<()>> {
419456
let TestDescAndFn { desc, testfn } = test;
420457

421458
// Emscripten can catch panics but other wasm targets cannot
@@ -426,7 +463,7 @@ pub fn run_test(
426463
if force_ignore || desc.ignore || ignore_because_no_process_support {
427464
let message = CompletedTest::new(desc, TrIgnored, None, Vec::new());
428465
monitor_ch.send(message).unwrap();
429-
return;
466+
return None;
430467
}
431468

432469
struct TestRunOpts {
@@ -441,7 +478,7 @@ pub fn run_test(
441478
monitor_ch: Sender<CompletedTest>,
442479
testfn: Box<dyn FnOnce() + Send>,
443480
opts: TestRunOpts,
444-
) {
481+
) -> Option<thread::JoinHandle<()>> {
445482
let concurrency = opts.concurrency;
446483
let name = desc.name.clone();
447484

@@ -469,9 +506,10 @@ pub fn run_test(
469506
let supports_threads = !cfg!(target_os = "emscripten") && !cfg!(target_arch = "wasm32");
470507
if concurrency == Concurrent::Yes && supports_threads {
471508
let cfg = thread::Builder::new().name(name.as_slice().to_owned());
472-
cfg.spawn(runtest).unwrap();
509+
Some(cfg.spawn(runtest).unwrap())
473510
} else {
474511
runtest();
512+
None
475513
}
476514
}
477515

@@ -484,10 +522,12 @@ pub fn run_test(
484522
crate::bench::benchmark(desc, monitor_ch, opts.nocapture, |harness| {
485523
bencher.run(harness)
486524
});
525+
None
487526
}
488527
StaticBenchFn(benchfn) => {
489528
// Benchmarks aren't expected to panic, so we run them all in-process.
490529
crate::bench::benchmark(desc, monitor_ch, opts.nocapture, benchfn);
530+
None
491531
}
492532
DynTestFn(f) => {
493533
match strategy {
@@ -499,7 +539,7 @@ pub fn run_test(
499539
monitor_ch,
500540
Box::new(move || __rust_begin_short_backtrace(f)),
501541
test_run_opts,
502-
);
542+
)
503543
}
504544
StaticTestFn(f) => run_test_inner(
505545
desc,

0 commit comments

Comments
 (0)