Skip to content

Commit 0ecf5f0

Browse files
authored
task: include panic msg when printing JoinError (#6753)
1 parent 1e798d2 commit 0ecf5f0

File tree

3 files changed

+156
-2
lines changed

3 files changed

+156
-2
lines changed

tokio/src/runtime/task/error.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,18 @@ impl fmt::Display for JoinError {
140140
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
141141
match &self.repr {
142142
Repr::Cancelled => write!(fmt, "task {} was cancelled", self.id),
143-
Repr::Panic(_) => write!(fmt, "task {} panicked", self.id),
143+
Repr::Panic(p) => match panic_payload_as_str(p) {
144+
Some(panic_str) => {
145+
write!(
146+
fmt,
147+
"task {} panicked with message {:?}",
148+
self.id, panic_str
149+
)
150+
}
151+
None => {
152+
write!(fmt, "task {} panicked", self.id)
153+
}
154+
},
144155
}
145156
}
146157
}
@@ -149,7 +160,12 @@ impl fmt::Debug for JoinError {
149160
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
150161
match &self.repr {
151162
Repr::Cancelled => write!(fmt, "JoinError::Cancelled({:?})", self.id),
152-
Repr::Panic(_) => write!(fmt, "JoinError::Panic({:?}, ...)", self.id),
163+
Repr::Panic(p) => match panic_payload_as_str(p) {
164+
Some(panic_str) => {
165+
write!(fmt, "JoinError::Panic({:?}, {:?}, ...)", self.id, panic_str)
166+
}
167+
None => write!(fmt, "JoinError::Panic({:?}, ...)", self.id),
168+
},
153169
}
154170
}
155171
}
@@ -167,3 +183,20 @@ impl From<JoinError> for io::Error {
167183
)
168184
}
169185
}
186+
187+
fn panic_payload_as_str(payload: &SyncWrapper<Box<dyn Any + Send>>) -> Option<&str> {
188+
// Panic payloads are almost always `String` (if invoked with formatting arguments)
189+
// or `&'static str` (if invoked with a string literal).
190+
//
191+
// Non-string panic payloads have niche use-cases,
192+
// so we don't really need to worry about those.
193+
if let Some(s) = payload.downcast_ref_sync::<String>() {
194+
return Some(s);
195+
}
196+
197+
if let Some(s) = payload.downcast_ref_sync::<&'static str>() {
198+
return Some(s);
199+
}
200+
201+
None
202+
}

tokio/src/util/sync_wrapper.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
//!
44
//! A similar primitive is provided in the `sync_wrapper` crate.
55
6+
use std::any::Any;
7+
68
pub(crate) struct SyncWrapper<T> {
79
value: T,
810
}
@@ -24,3 +26,12 @@ impl<T> SyncWrapper<T> {
2426
self.value
2527
}
2628
}
29+
30+
impl SyncWrapper<Box<dyn Any + Send>> {
31+
/// Attempt to downcast using `Any::downcast_ref()` to a type that is known to be `Sync`.
32+
pub(crate) fn downcast_ref_sync<T: Any + Sync>(&self) -> Option<&T> {
33+
// SAFETY: if the downcast fails, the inner value is not touched,
34+
// so no thread-safety violation can occur.
35+
self.value.downcast_ref()
36+
}
37+
}

tokio/tests/task_abort.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,113 @@ fn test_abort_task_that_panics_on_drop_returned() {
220220
assert!(handle.await.unwrap_err().is_panic());
221221
});
222222
}
223+
224+
// It's not clear where these tests belong. This was the place suggested by @Darksonn:
225+
// https://github.com/tokio-rs/tokio/pull/6753#issuecomment-2271434176
226+
/// Checks that a `JoinError` with a panic payload prints the expected text.
227+
#[test]
228+
#[cfg(panic = "unwind")]
229+
fn test_join_error_display() {
230+
let rt = Builder::new_current_thread().build().unwrap();
231+
232+
rt.block_on(async move {
233+
// `String` payload
234+
let join_err = tokio::spawn(async move {
235+
let value = 1234;
236+
panic!("Format-args payload: {}", value)
237+
})
238+
.await
239+
.unwrap_err();
240+
241+
// We can't assert the full output because the task ID can change.
242+
let join_err_str = join_err.to_string();
243+
244+
assert!(
245+
join_err_str.starts_with("task ")
246+
&& join_err_str.ends_with(" panicked with message \"Format-args payload: 1234\""),
247+
"Unexpected join_err_str {:?}",
248+
join_err_str
249+
);
250+
251+
// `&'static str` payload
252+
let join_err = tokio::spawn(async move { panic!("Const payload") })
253+
.await
254+
.unwrap_err();
255+
256+
let join_err_str = join_err.to_string();
257+
258+
assert!(
259+
join_err_str.starts_with("task ")
260+
&& join_err_str.ends_with(" panicked with message \"Const payload\""),
261+
"Unexpected join_err_str {:?}",
262+
join_err_str
263+
);
264+
265+
// Non-string payload
266+
let join_err = tokio::spawn(async move { std::panic::panic_any(1234i32) })
267+
.await
268+
.unwrap_err();
269+
270+
let join_err_str = join_err.to_string();
271+
272+
assert!(
273+
join_err_str.starts_with("task ") && join_err_str.ends_with(" panicked"),
274+
"Unexpected join_err_str {:?}",
275+
join_err_str
276+
);
277+
});
278+
}
279+
280+
/// Checks that a `JoinError` with a panic payload prints the expected text from `Debug`.
281+
#[test]
282+
#[cfg(panic = "unwind")]
283+
fn test_join_error_debug() {
284+
let rt = Builder::new_current_thread().build().unwrap();
285+
286+
rt.block_on(async move {
287+
// `String` payload
288+
let join_err = tokio::spawn(async move {
289+
let value = 1234;
290+
panic!("Format-args payload: {}", value)
291+
})
292+
.await
293+
.unwrap_err();
294+
295+
// We can't assert the full output because the task ID can change.
296+
let join_err_str = format!("{:?}", join_err);
297+
298+
assert!(
299+
join_err_str.starts_with("JoinError::Panic(Id(")
300+
&& join_err_str.ends_with("), \"Format-args payload: 1234\", ...)"),
301+
"Unexpected join_err_str {:?}",
302+
join_err_str
303+
);
304+
305+
// `&'static str` payload
306+
let join_err = tokio::spawn(async move { panic!("Const payload") })
307+
.await
308+
.unwrap_err();
309+
310+
let join_err_str = format!("{:?}", join_err);
311+
312+
assert!(
313+
join_err_str.starts_with("JoinError::Panic(Id(")
314+
&& join_err_str.ends_with("), \"Const payload\", ...)"),
315+
"Unexpected join_err_str {:?}",
316+
join_err_str
317+
);
318+
319+
// Non-string payload
320+
let join_err = tokio::spawn(async move { std::panic::panic_any(1234i32) })
321+
.await
322+
.unwrap_err();
323+
324+
let join_err_str = format!("{:?}", join_err);
325+
326+
assert!(
327+
join_err_str.starts_with("JoinError::Panic(Id(") && join_err_str.ends_with("), ...)"),
328+
"Unexpected join_err_str {:?}",
329+
join_err_str
330+
);
331+
});
332+
}

0 commit comments

Comments
 (0)