Capture output from threads spawned in tests

Fixes #42474.
This commit is contained in:
Tyler Mandry 2020-08-04 18:42:36 -07:00 committed by Sergio Benitez
parent 6b9fbf212a
commit d0d0e78208
14 changed files with 177 additions and 12 deletions

View File

@ -113,6 +113,11 @@ fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl io::LocalOutput for Sink {
fn clone_box(&self) -> Box<dyn io::LocalOutput> {
Box::new(Self(self.0.clone()))
}
}
/// Like a `thread::Builder::spawn` followed by a `join()`, but avoids the need
/// for `'static` bounds.

View File

@ -213,13 +213,13 @@ fn read_line(&mut self, buf: &mut String) -> io::Result<usize> {
#[cfg(test)]
/// This impl is only used by printing logic, so any error returned is always
/// of kind `Other`, and should be ignored.
impl Write for Box<dyn (::realstd::io::Write) + Send> {
impl Write for dyn ::realstd::io::LocalOutput {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
(**self).write(buf).map_err(|_| ErrorKind::Other.into())
(*self).write(buf).map_err(|_| ErrorKind::Other.into())
}
fn flush(&mut self) -> io::Result<()> {
(**self).flush().map_err(|_| ErrorKind::Other.into())
(*self).flush().map_err(|_| ErrorKind::Other.into())
}
}

View File

@ -277,10 +277,12 @@
pub use self::stdio::{_eprint, _print};
#[unstable(feature = "libstd_io_internals", issue = "42788")]
#[doc(no_inline, hidden)]
pub use self::stdio::{set_panic, set_print};
pub use self::stdio::{set_panic, set_print, LocalOutput};
#[stable(feature = "rust1", since = "1.0.0")]
pub use self::util::{copy, empty, repeat, sink, Empty, Repeat, Sink};
pub(crate) use self::stdio::clone_io;
mod buffered;
mod cursor;
mod error;

View File

@ -18,14 +18,14 @@
thread_local! {
/// Used by the test crate to capture the output of the print! and println! macros.
static LOCAL_STDOUT: RefCell<Option<Box<dyn Write + Send>>> = {
static LOCAL_STDOUT: RefCell<Option<Box<dyn LocalOutput>>> = {
RefCell::new(None)
}
}
thread_local! {
/// Used by the test crate to capture the output of the eprint! and eprintln! macros, and panics.
static LOCAL_STDERR: RefCell<Option<Box<dyn Write + Send>>> = {
static LOCAL_STDERR: RefCell<Option<Box<dyn LocalOutput>>> = {
RefCell::new(None)
}
}
@ -888,6 +888,18 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
}
}
/// A writer than can be cloned to new threads.
#[unstable(
feature = "set_stdio",
reason = "this trait may disappear completely or be replaced \
with a more general mechanism",
issue = "none"
)]
#[doc(hidden)]
pub trait LocalOutput: Write + Send {
fn clone_box(&self) -> Box<dyn LocalOutput>;
}
/// Resets the thread-local stderr handle to the specified writer
///
/// This will replace the current thread's stderr handle, returning the old
@ -903,7 +915,7 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
issue = "none"
)]
#[doc(hidden)]
pub fn set_panic(sink: Option<Box<dyn Write + Send>>) -> Option<Box<dyn Write + Send>> {
pub fn set_panic(sink: Option<Box<dyn LocalOutput>>) -> Option<Box<dyn LocalOutput>> {
use crate::mem;
if sink.is_none() && !LOCAL_STREAMS.load(Ordering::Relaxed) {
// LOCAL_STDERR is definitely None since LOCAL_STREAMS is false.
@ -934,7 +946,7 @@ pub fn set_panic(sink: Option<Box<dyn Write + Send>>) -> Option<Box<dyn Write +
issue = "none"
)]
#[doc(hidden)]
pub fn set_print(sink: Option<Box<dyn Write + Send>>) -> Option<Box<dyn Write + Send>> {
pub fn set_print(sink: Option<Box<dyn LocalOutput>>) -> Option<Box<dyn LocalOutput>> {
use crate::mem;
if sink.is_none() && !LOCAL_STREAMS.load(Ordering::Relaxed) {
// LOCAL_STDOUT is definitely None since LOCAL_STREAMS is false.
@ -950,6 +962,17 @@ pub fn set_print(sink: Option<Box<dyn Write + Send>>) -> Option<Box<dyn Write +
s
}
pub(crate) fn clone_io() -> (Option<Box<dyn LocalOutput>>, Option<Box<dyn LocalOutput>>) {
LOCAL_STDOUT.with(|stdout| {
LOCAL_STDERR.with(|stderr| {
(
stdout.borrow().as_ref().map(|o| o.clone_box()),
stderr.borrow().as_ref().map(|o| o.clone_box()),
)
})
})
}
/// Write `args` to output stream `local_s` if possible, `global_s`
/// otherwise. `label` identifies the stream in a panic message.
///
@ -962,7 +985,7 @@ pub fn set_print(sink: Option<Box<dyn Write + Send>>) -> Option<Box<dyn Write +
/// However, if the actual I/O causes an error, this function does panic.
fn print_to<T>(
args: fmt::Arguments<'_>,
local_s: &'static LocalKey<RefCell<Option<Box<dyn Write + Send>>>>,
local_s: &'static LocalKey<RefCell<Option<Box<dyn LocalOutput>>>>,
global_s: fn() -> T,
label: &str,
) where

View File

@ -220,7 +220,7 @@ fn default_hook(info: &PanicInfo<'_>) {
if let Some(mut local) = set_panic(None) {
// NB. In `cfg(test)` this uses the forwarding impl
// for `Box<dyn (::realstd::io::Write) + Send>`.
// for `dyn ::realstd::io::LocalOutput`.
write(&mut local);
set_panic(Some(local));
} else if let Some(mut out) = panic_output() {

View File

@ -457,11 +457,16 @@ pub unsafe fn spawn_unchecked<'a, F, T>(self, f: F) -> io::Result<JoinHandle<T>>
let my_packet: Arc<UnsafeCell<Option<Result<T>>>> = Arc::new(UnsafeCell::new(None));
let their_packet = my_packet.clone();
let (stdout, stderr) = crate::io::clone_io();
let main = move || {
if let Some(name) = their_thread.cname() {
imp::Thread::set_name(name);
}
crate::io::set_print(stdout);
crate::io::set_panic(stderr);
// SAFETY: the stack guard passed is the one for the current thread.
// This means the current thread's stack and the new thread's stack
// are properly set and protected from each other.

View File

@ -6,6 +6,7 @@
sync::{Arc, Mutex},
};
#[derive(Clone)]
pub struct Sink(Arc<Mutex<Vec<u8>>>);
impl Sink {
@ -14,6 +15,12 @@ pub fn new_boxed(data: &Arc<Mutex<Vec<u8>>>) -> Box<Self> {
}
}
impl io::LocalOutput for Sink {
fn clone_box(&self) -> Box<dyn io::LocalOutput> {
Box::new(self.clone())
}
}
impl Write for Sink {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
Write::write(&mut *self.0.lock().unwrap(), data)

View File

@ -5,7 +5,7 @@
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io::set_panic;
use std::io::{self, set_panic, LocalOutput, Write};
pub struct A;
@ -15,8 +15,23 @@ fn fmt(&self, _f: &mut Formatter<'_>) -> fmt::Result {
}
}
struct Sink;
impl Write for Sink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl LocalOutput for Sink {
fn clone_box(&self) -> Box<dyn LocalOutput> {
Box::new(Sink)
}
}
fn main() {
set_panic(Some(Box::new(Vec::new())));
set_panic(Some(Box::new(Sink)));
assert!(std::panic::catch_unwind(|| {
eprintln!("{}", A);
})

View File

@ -0,0 +1,30 @@
// compile-flags: --test
// run-fail
// run-flags: --test-threads=1
// check-run-results
// exec-env:RUST_BACKTRACE=0
#[test]
fn thready_pass() {
println!("fee");
std::thread::spawn(|| {
println!("fie");
println!("foe");
})
.join()
.unwrap();
println!("fum");
}
#[test]
fn thready_fail() {
println!("fee");
std::thread::spawn(|| {
println!("fie");
println!("foe");
})
.join()
.unwrap();
println!("fum");
panic!();
}

View File

@ -0,0 +1,21 @@
running 2 tests
test thready_fail ... FAILED
test thready_pass ... ok
failures:
---- thready_fail stdout ----
fee
fie
foe
fum
thread 'main' panicked at 'explicit panic', $DIR/test-thread-capture.rs:29:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
thready_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

View File

@ -0,0 +1,30 @@
// compile-flags: --test
// run-fail
// run-flags: --test-threads=1 --nocapture
// check-run-results
// exec-env:RUST_BACKTRACE=0
#[test]
fn thready_pass() {
println!("fee");
std::thread::spawn(|| {
println!("fie");
println!("foe");
})
.join()
.unwrap();
println!("fum");
}
#[test]
fn thready_fail() {
println!("fee");
std::thread::spawn(|| {
println!("fie");
println!("foe");
})
.join()
.unwrap();
println!("fum");
panic!();
}

View File

@ -0,0 +1,2 @@
thread 'main' panicked at 'explicit panic', $DIR/test-thread-nocapture.rs:29:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

View File

@ -0,0 +1,20 @@
running 2 tests
test thready_fail ... fee
fie
foe
fum
FAILED
test thready_pass ... fee
fie
foe
fum
ok
failures:
failures:
thready_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

View File

@ -16,6 +16,11 @@ fn write(&mut self, data: &[u8]) -> io::Result<usize> {
}
fn flush(&mut self) -> io::Result<()> { Ok(()) }
}
impl io::LocalOutput for Sink {
fn clone_box(&self) -> Box<dyn io::LocalOutput> {
Box::new(Sink(self.0.clone()))
}
}
fn main() {
let data = Arc::new(Mutex::new(Vec::new()));