Skip to content

Commit bd3c97c

Browse files
authored
Merge pull request #365 from shepmaster/clean-report
2 parents 35dcf44 + ed78681 commit bd3c97c

File tree

4 files changed

+239
-33
lines changed

4 files changed

+239
-33
lines changed

src/lib.rs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,9 @@ mod backtrace_shim;
256256
))]
257257
pub use crate::backtrace_shim::*;
258258

259+
#[cfg(any(feature = "std", test))]
260+
mod once_bool;
261+
259262
#[cfg(feature = "backtraces-impl-backtrace-crate")]
260263
pub use backtrace::Backtrace;
261264

@@ -1216,26 +1219,17 @@ impl AsBacktrace for Option<Backtrace> {
12161219

12171220
#[cfg(any(feature = "std", test))]
12181221
fn backtrace_collection_enabled() -> bool {
1219-
use std::{
1220-
env,
1221-
sync::{
1222-
atomic::{AtomicBool, Ordering},
1223-
Once,
1224-
},
1225-
};
1222+
use crate::once_bool::OnceBool;
1223+
use std::env;
12261224

1227-
static START: Once = Once::new();
1228-
static ENABLED: AtomicBool = AtomicBool::new(false);
1225+
static ENABLED: OnceBool = OnceBool::new();
12291226

1230-
START.call_once(|| {
1227+
ENABLED.get(|| {
12311228
// TODO: What values count as "true"?
1232-
let enabled = env::var_os("RUST_LIB_BACKTRACE")
1229+
env::var_os("RUST_LIB_BACKTRACE")
12331230
.or_else(|| env::var_os("RUST_BACKTRACE"))
1234-
.map_or(false, |v| v == "1");
1235-
ENABLED.store(enabled, Ordering::SeqCst);
1236-
});
1237-
1238-
ENABLED.load(Ordering::SeqCst)
1231+
.map_or(false, |v| v == "1")
1232+
})
12391233
}
12401234

12411235
#[cfg(feature = "backtraces-impl-backtrace-crate")]

src/once_bool.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use std::sync::{
2+
atomic::{AtomicBool, Ordering},
3+
Once,
4+
};
5+
6+
pub struct OnceBool {
7+
start: Once,
8+
enabled: AtomicBool,
9+
}
10+
11+
impl OnceBool {
12+
pub const fn new() -> Self {
13+
Self {
14+
start: Once::new(),
15+
enabled: AtomicBool::new(false),
16+
}
17+
}
18+
19+
pub fn get<F: FnOnce() -> bool>(&self, f: F) -> bool {
20+
self.start.call_once(|| {
21+
let enabled = f();
22+
self.enabled.store(enabled, Ordering::SeqCst);
23+
});
24+
25+
self.enabled.load(Ordering::SeqCst)
26+
}
27+
}

src/report.rs

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,35 @@ struct ReportFormatter<'a>(&'a dyn crate::Error);
219219

220220
impl<'a> fmt::Display for ReportFormatter<'a> {
221221
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222+
#[cfg(feature = "std")]
223+
{
224+
if trace_cleaning_enabled() {
225+
self.cleaned_error_trace(f)?;
226+
} else {
227+
self.error_trace(f)?;
228+
}
229+
}
230+
231+
#[cfg(not(feature = "std"))]
232+
{
233+
self.error_trace(f)?;
234+
}
235+
236+
#[cfg(feature = "unstable-provider-api")]
237+
{
238+
use core::any;
239+
240+
if let Some(bt) = any::request_ref::<crate::Backtrace>(self.0) {
241+
writeln!(f, "\nBacktrace:\n{}", bt)?;
242+
}
243+
}
244+
245+
Ok(())
246+
}
247+
}
248+
249+
impl<'a> ReportFormatter<'a> {
250+
fn error_trace(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
222251
writeln!(f, "{}", self.0)?;
223252

224253
let sources = ChainCompat::new(self.0).skip(1);
@@ -236,19 +265,98 @@ impl<'a> fmt::Display for ReportFormatter<'a> {
236265
writeln!(f, "{:3}: {}", i, source)?;
237266
}
238267

239-
#[cfg(feature = "unstable-provider-api")]
240-
{
241-
use core::any;
268+
Ok(())
269+
}
242270

243-
if let Some(bt) = any::request_ref::<crate::Backtrace>(self.0) {
244-
writeln!(f, "\nBacktrace:\n{}", bt)?;
271+
#[cfg(feature = "std")]
272+
fn cleaned_error_trace(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
273+
const NOTE: char = '*';
274+
275+
let mut original_messages = ChainCompat::new(self.0).map(ToString::to_string);
276+
let mut prev = original_messages.next();
277+
278+
let mut cleaned_messages = vec![];
279+
let mut any_cleaned = false;
280+
let mut any_removed = false;
281+
for msg in original_messages {
282+
if let Some(mut prev) = prev {
283+
let cleaned = prev.trim_end_matches(&msg).trim_end().trim_end_matches(':');
284+
if cleaned.is_empty() {
285+
any_removed = true;
286+
// Do not add this to the output list
287+
} else if cleaned != prev {
288+
any_cleaned = true;
289+
let cleaned_len = cleaned.len();
290+
prev.truncate(cleaned_len);
291+
prev.push(' ');
292+
prev.push(NOTE);
293+
cleaned_messages.push(prev);
294+
} else {
295+
cleaned_messages.push(prev);
296+
}
245297
}
298+
299+
prev = Some(msg);
300+
}
301+
cleaned_messages.extend(prev);
302+
303+
let mut visible_messages = cleaned_messages.iter();
304+
305+
let head = match visible_messages.next() {
306+
Some(v) => v,
307+
None => return Ok(()),
308+
};
309+
310+
writeln!(f, "{}", head)?;
311+
312+
match cleaned_messages.len() {
313+
0 | 1 => {}
314+
2 => writeln!(f, "\nCaused by this error:")?,
315+
_ => writeln!(f, "\nCaused by these errors (recent errors listed first):")?,
316+
}
317+
318+
for (i, msg) in visible_messages.enumerate() {
319+
// Let's use 1-based indexing for presentation
320+
let i = i + 1;
321+
writeln!(f, "{:3}: {}", i, msg)?;
322+
}
323+
324+
if any_cleaned || any_removed {
325+
write!(f, "\nNOTE: ")?;
326+
327+
if any_cleaned {
328+
write!(
329+
f,
330+
"Some redundant information has been removed from the lines marked with {}. ",
331+
NOTE,
332+
)?;
333+
} else {
334+
write!(f, "Some redundant information has been removed. ")?;
335+
}
336+
337+
writeln!(
338+
f,
339+
"Set {}=1 to disable this behavior.",
340+
SNAFU_RAW_ERROR_MESSAGES,
341+
)?;
246342
}
247343

248344
Ok(())
249345
}
250346
}
251347

348+
#[cfg(feature = "std")]
349+
const SNAFU_RAW_ERROR_MESSAGES: &str = "SNAFU_RAW_ERROR_MESSAGES";
350+
351+
#[cfg(feature = "std")]
352+
fn trace_cleaning_enabled() -> bool {
353+
use crate::once_bool::OnceBool;
354+
use std::env;
355+
356+
static DISABLED: OnceBool = OnceBool::new();
357+
!DISABLED.get(|| env::var_os(SNAFU_RAW_ERROR_MESSAGES).map_or(false, |v| v == "1"))
358+
}
359+
252360
#[doc(hidden)]
253361
pub trait __InternalExtractErrorType {
254362
type Err;

tests/report.rs

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
use snafu::{prelude::*, IntoError, Report};
22

3+
macro_rules! assert_contains {
4+
(needle: $needle:expr, haystack: $haystack:expr) => {
5+
assert!(
6+
$haystack.contains($needle),
7+
"Expected {:?} to include {:?}",
8+
$haystack,
9+
$needle,
10+
)
11+
};
12+
}
13+
14+
macro_rules! assert_not_contains {
15+
(needle: $needle:expr, haystack: $haystack:expr) => {
16+
assert!(
17+
!$haystack.contains($needle),
18+
"Expected {:?} to not include {:?}",
19+
$haystack,
20+
$needle,
21+
)
22+
};
23+
}
24+
325
#[test]
426
fn includes_the_error_display_text() {
527
#[derive(Debug, Snafu)]
@@ -10,12 +32,7 @@ fn includes_the_error_display_text() {
1032
let msg = r.to_string();
1133

1234
let expected = "This is my Display text!";
13-
assert!(
14-
msg.contains(expected),
15-
"Expected {:?} to include {:?}",
16-
msg,
17-
expected,
18-
);
35+
assert_contains!(needle: expected, haystack: msg);
1936
}
2037

2138
#[test]
@@ -35,12 +52,72 @@ fn includes_the_source_display_text() {
3552
let msg = r.to_string();
3653

3754
let expected = "This is my inner Display";
38-
assert!(
39-
msg.contains(expected),
40-
"Expected {:?} to include {:?}",
41-
msg,
42-
expected,
43-
);
55+
assert_contains!(needle: expected, haystack: msg);
56+
}
57+
58+
#[test]
59+
fn reduces_duplication_of_the_source_display_text() {
60+
// Including the source in the Display message is discouraged but
61+
// quite common.
62+
63+
#[derive(Debug, Snafu)]
64+
#[snafu(display("Level 0"))]
65+
struct Level0Error;
66+
67+
#[derive(Debug, Snafu)]
68+
#[snafu(display("Level 1: {source}"))]
69+
struct Level1Error {
70+
source: Level0Error,
71+
}
72+
73+
#[derive(Debug, Snafu)]
74+
#[snafu(display("Level 2: {source}"))]
75+
struct Level2Error {
76+
source: Level1Error,
77+
}
78+
79+
let e = Level2Snafu.into_error(Level1Snafu.into_error(Level0Error));
80+
let raw_msg = e.to_string();
81+
82+
let expected = "Level 2: Level 1";
83+
assert_contains!(needle: expected, haystack: raw_msg);
84+
85+
let r = Report::from_error(e);
86+
let msg = r.to_string();
87+
88+
assert_not_contains!(needle: expected, haystack: msg);
89+
}
90+
91+
#[test]
92+
fn removes_complete_duplication_in_the_source_display_text() {
93+
// Including **only** the source in the Display message is also
94+
// discouraged but occurs.
95+
96+
#[derive(Debug, Snafu)]
97+
#[snafu(display("Level 0"))]
98+
struct Level0Error;
99+
100+
#[derive(Debug, Snafu)]
101+
#[snafu(display("{source}"))]
102+
struct Level1Error {
103+
source: Level0Error,
104+
}
105+
106+
#[derive(Debug, Snafu)]
107+
#[snafu(display("{source}"))]
108+
struct Level2Error {
109+
source: Level1Error,
110+
}
111+
112+
let e = Level2Snafu.into_error(Level1Snafu.into_error(Level0Error));
113+
let raw_msg = e.to_string();
114+
115+
assert_contains!(needle: "Level 0", haystack: raw_msg);
116+
117+
let r = Report::from_error(e);
118+
let msg = r.to_string();
119+
120+
assert_not_contains!(needle: "Caused by", haystack: msg);
44121
}
45122

46123
#[test]

0 commit comments

Comments
 (0)