diff --git a/eyre/Cargo.toml b/eyre/Cargo.toml index 39c0578..ecfaf87 100644 --- a/eyre/Cargo.toml +++ b/eyre/Cargo.toml @@ -26,7 +26,7 @@ pyo3 = { version = "0.20", optional = true, default-features = false } futures = { version = "0.3", default-features = false } rustversion = "1.0" thiserror = "1.0" -trybuild = { version = "1.0.19", features = ["diff"] } +ui_test = "0.21.0" backtrace = "0.3.46" anyhow = "1.0.28" syn = { version = "2.0", features = ["full"] } diff --git a/eyre/build.rs b/eyre/build.rs index e0b2d29..162f1cc 100644 --- a/eyre/build.rs +++ b/eyre/build.rs @@ -5,35 +5,38 @@ use std::path::Path; use std::process::{Command, ExitStatus}; use std::str; -// This code exercises the surface area that we expect of the std Backtrace -// type. If the current toolchain is able to compile it, we go ahead and use -// backtrace in eyre. +// This code exercises the surface area that we expect of the Error generic +// member access API. If the current toolchain is able to compile it, then +// anyhow is able to provide backtrace support. const BACKTRACE_PROBE: &str = r#" - #![feature(backtrace)] - #![allow(dead_code)] + #![feature(error_generic_member_access)] - use std::backtrace::{Backtrace, BacktraceStatus}; - use std::error::Error; - use std::fmt::{self, Display}; + use std::backtrace::Backtrace; + use std::error::{self, Error, Request}; + use std::fmt::{self, Debug, Display}; - #[derive(Debug)] - struct E; + struct MyError(Thing); + struct Thing; - impl Display for E { + impl Debug for MyError { fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { unimplemented!() } } - impl Error for E { - fn backtrace(&self) -> Option<&Backtrace> { - let backtrace = Backtrace::capture(); - match backtrace.status() { - BacktraceStatus::Captured | BacktraceStatus::Disabled | _ => {} - } + impl Display for MyError { + fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { unimplemented!() } } + + impl Error for MyError { + fn provide<'a>(&'a self, request: &mut Request<'a>) { + request.provide_ref(&self.0); + } + } + + const _: fn(&dyn Error) -> Option<&Backtrace> = |err| error::request_ref::(err); "#; const TRACK_CALLER_PROBE: &str = r#" diff --git a/eyre/src/backtrace.rs b/eyre/src/backtrace.rs index 6c00d7f..c6787a9 100644 --- a/eyre/src/backtrace.rs +++ b/eyre/src/backtrace.rs @@ -7,7 +7,7 @@ pub(crate) enum Backtrace {} #[cfg(backtrace)] macro_rules! backtrace_if_absent { ($err:expr) => { - match $err.backtrace() { + match std::error::request_ref::($err as &dyn std::error::Error) { Some(_) => None, None => Some(Backtrace::capture()), } diff --git a/eyre/src/context.rs b/eyre/src/context.rs index a15b41b..5fdde59 100644 --- a/eyre/src/context.rs +++ b/eyre/src/context.rs @@ -3,7 +3,7 @@ use crate::{ContextCompat, Report, StdError, WrapErr}; use core::fmt::{self, Debug, Display, Write}; #[cfg(backtrace)] -use std::backtrace::Backtrace; +use std::error::Request; mod ext { use super::*; @@ -144,8 +144,8 @@ where E: StdError + 'static, { #[cfg(backtrace)] - fn backtrace(&self) -> Option<&Backtrace> { - self.error.backtrace() + fn provide<'a>(&'a self, request: &mut Request<'a>) { + StdError::provide(&self.error, request); } fn source(&self) -> Option<&(dyn StdError + 'static)> { diff --git a/eyre/src/lib.rs b/eyre/src/lib.rs index 3d887ab..7aabafa 100644 --- a/eyre/src/lib.rs +++ b/eyre/src/lib.rs @@ -341,7 +341,7 @@ unused_parens, while_true )] -#![cfg_attr(backtrace, feature(backtrace))] +#![cfg_attr(backtrace, feature(error_generic_member_access))] #![cfg_attr(doc_cfg, feature(doc_cfg))] #![allow( clippy::needless_doctest_main, @@ -789,6 +789,8 @@ impl EyreHandler for DefaultHandler { f: &mut core::fmt::Formatter<'_>, ) -> core::fmt::Result { use core::fmt::Write as _; + #[cfg(backtrace)] + use std::error::request_ref; if f.alternate() { return core::fmt::Debug::fmt(error, f); @@ -824,7 +826,7 @@ impl EyreHandler for DefaultHandler { let backtrace = self .backtrace .as_ref() - .or_else(|| error.backtrace()) + .or_else(|| request_ref::(error)) .expect("backtrace capture failed"); if let BacktraceStatus::Captured = backtrace.status() { write!(f, "\n\nStack backtrace:\n{}", backtrace)?; diff --git a/eyre/src/wrapper.rs b/eyre/src/wrapper.rs index b4da68a..58eb378 100644 --- a/eyre/src/wrapper.rs +++ b/eyre/src/wrapper.rs @@ -1,6 +1,9 @@ use crate::StdError; use core::fmt::{self, Debug, Display}; +#[cfg(backtrace)] +use std::error::Request; + #[repr(transparent)] pub(crate) struct DisplayError(pub(crate) M); @@ -83,8 +86,8 @@ impl Display for BoxedError { impl StdError for BoxedError { #[cfg(backtrace)] - fn backtrace(&self) -> Option<&crate::backtrace::Backtrace> { - self.0.backtrace() + fn provide<'a>(&'a self, request: &mut Request<'a>) { + self.0.provide(request); } fn source(&self) -> Option<&(dyn StdError + 'static)> { diff --git a/eyre/tests/compiletest.rs b/eyre/tests/compiletest.rs index 7974a62..1c73b59 100644 --- a/eyre/tests/compiletest.rs +++ b/eyre/tests/compiletest.rs @@ -1,7 +1,19 @@ -#[rustversion::attr(not(nightly), ignore)] +#[cfg_attr(not(backtrace), ignore)] #[cfg_attr(miri, ignore)] #[test] fn ui() { - let t = trybuild::TestCases::new(); - t.compile_fail("tests/ui/*.rs"); + let mut test_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + test_dir.push("tests"); + test_dir.push("ui"); + let rust_backtrace_val = "1"; + let mut config = ui_test::Config { + mode: ui_test::Mode::Run { exit_code: 0 }, + filter_files: vec!["ui_test".to_owned()], + + ..ui_test::Config::cargo(test_dir) + }; + config.program.args = vec!["run".into()]; + config.program.envs = vec![("RUST_BACKTRACE".into(), Some(rust_backtrace_val.into()))]; + + let _ = ui_test::run_tests(config); } diff --git a/eyre/tests/test_backtrace.rs b/eyre/tests/test_backtrace.rs new file mode 100644 index 0000000..9094f68 --- /dev/null +++ b/eyre/tests/test_backtrace.rs @@ -0,0 +1,117 @@ +mod test_backtrace { + use eyre::{eyre, Report, WrapErr}; + + #[allow(unused_variables)] + #[allow(dead_code)] + enum FailFrame { + None, + Low, + Med, + High, + } + + fn low(frame: FailFrame) -> Result<(), Report> { + let e: Report = eyre!("This program's goodness is suspect!"); + if let FailFrame::Low = frame { + Err::<(), Report>(e).wrap_err("The low-level code has failed!") + } else { + Ok(()) + } + } + + fn med(frame: FailFrame) -> Result<(), Report> { + let e: Report = eyre!("This program's goodness is suspect!"); + if let FailFrame::Med = frame { + Err(e).wrap_err("The low-level code has failed!") + } else { + low(frame) + } + } + fn high(frame: FailFrame) -> Result<(), Report> { + let e: Report = eyre!("This program's goodness is suspect!"); + if let FailFrame::High = frame { + Err(e).wrap_err("The low-level code has failed!") + } else { + med(frame) + } + } + + use std::panic; + + static BACKTRACE_SNIPPET_HIGH: &str = " +10: test_backtrace::test_backtrace::low + at .\\tests\\test_backtrace.rs:14 +11: test_backtrace::test_backtrace::med + at .\\tests\\test_backtrace.rs:27 +12: test_backtrace::test_backtrace::high + at .\\tests\\test_backtrace.rs:35 +13: test_backtrace::test_backtrace::test_backtrace + "; + + use std::backtrace::Backtrace; + use std::sync::{Arc, Mutex}; + + /* This test does produce a backtrace for panic or error with the standard panic hook, + * but I'm at a loss for how to capture the backtrace and compare it to a snippet. + */ + #[cfg_attr(not(backtrace), ignore)] + // #[test] + // #[should_panic] + fn test_backtrace_simple() { + let report = high(FailFrame::Low).expect_err("Must be error"); + let handler: &eyre::DefaultHandler = report.handler().downcast_ref().unwrap(); + eprintln!("{:?}", handler); + // let backtrace: Backtrace = handler.backtrace.unwrap(); + // let + /* + let backtrace: Option = handler.backtrace; + assert!(backtrace.is_some()); + */ + } + + #[cfg_attr(not(backtrace), ignore)] + // #[test] + fn test_backtrace() { + /* FIXME: check that the backtrace actually happens here + * It's not trivial to compare the *whole* output, + * but we could somehow grep the output for 'stack_backtrace', + * maybe check for this string... though including line numbers is problematic, + * and the frames could change if core changes. + * + */ + + let global_buffer = Arc::new(Mutex::new(String::new())); + let old_hook = panic::take_hook(); + panic::set_hook({ + /* fixme: this panic hook is not working ;( + */ + let global_buffer = global_buffer.clone(); + Box::new(move |info| { + let mut global_buffer = global_buffer.lock().unwrap(); + + if let Some(s) = info.payload().downcast_ref::<&str>() { + println!("PANIC: {}", *s); + *global_buffer = (*s).to_string() + } else { + // panic!("help!"); + } + }) + }); + + panic::catch_unwind(|| { + high(FailFrame::Low).unwrap(); //.unwrap_or(println!("test")); + }) + .expect_err("Backtrace test did not panic."); + let binding = global_buffer.lock().unwrap(); + let panic_output = binding.clone(); + panic::set_hook(old_hook); + if !panic_output.contains(BACKTRACE_SNIPPET_HIGH) { + println!("Backtrace test fail."); + println!("Expected output to contain:"); + println!("{}", BACKTRACE_SNIPPET_HIGH); + println!("Instead, outputted:"); + println!("{}", panic_output); + panic!(); + } + } +} diff --git a/eyre/tests/ui/ui_test_backtrace.rs b/eyre/tests/ui/ui_test_backtrace.rs new file mode 100644 index 0000000..72d85ff --- /dev/null +++ b/eyre/tests/ui/ui_test_backtrace.rs @@ -0,0 +1,14 @@ +use eyre::Report; + +fn fail(fail: bool) -> Result<(), Report> { + let e: Report = eyre!("Internal error message"); + if fail { + Err(e).wrap_err("External error message") + } else { + Ok(()) + } +} + +fn main() { + fail(true); +} diff --git a/eyre/tests/ui/ui_test_backtrace.stderr b/eyre/tests/ui/ui_test_backtrace.stderr new file mode 100644 index 0000000..f8d406b --- /dev/null +++ b/eyre/tests/ui/ui_test_backtrace.stderr @@ -0,0 +1 @@ +error: embedded manifest `$DIR/ui_test_backtrace.rs` requires `-Zscript`