mirror of
https://github.com/eyre-rs/eyre.git
synced 2025-09-26 20:40:49 +00:00
Fix compile probe and nightly backtraces (#160)
This addresses the invalid compile probe testing for a non-longer existing feature by updating it and eyre to use the `error_generic_member_access` features instead. The report and errors have all been updated to accomodate this and the new backtrace provide API Fixes: #84 and #97
This commit is contained in:
commit
df42dc4338
@ -17,6 +17,7 @@ rust-version = "1.65.0"
|
||||
indenter = "0.3.0"
|
||||
once_cell = "1.18.0"
|
||||
owo-colors = "4.0"
|
||||
autocfg = "1.0"
|
||||
|
||||
[profile.dev.package.backtrace]
|
||||
opt-level = 3
|
||||
|
22
README.md
22
README.md
@ -122,9 +122,12 @@ avoid using `eyre::Report` as your public error type.
|
||||
}
|
||||
```
|
||||
|
||||
- If using the nightly channel, a backtrace is captured and printed with the
|
||||
error if the underlying error type does not already provide its own. In order
|
||||
to see backtraces, they must be enabled through the environment variables
|
||||
- If using rust >1.65, a backtrace is captured and printed with the
|
||||
error.
|
||||
|
||||
On nightly eyre will use the underlying error's backtrace if it has one.
|
||||
|
||||
In order to see backtraces, they must be enabled through the environment variables
|
||||
described in [`std::backtrace`]:
|
||||
|
||||
- If you want panics and errors to both have backtraces, set
|
||||
@ -141,7 +144,7 @@ avoid using `eyre::Report` as your public error type.
|
||||
- Eyre works with any error type that has an impl of `std::error::Error`,
|
||||
including ones defined in your crate. We do not bundle a `derive(Error)` macro
|
||||
but you can write the impls yourself or use a standalone macro like
|
||||
[thiserror].
|
||||
[thiserror](https://github.com/dtolnay/thiserror).
|
||||
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
@ -178,6 +181,15 @@ No-std support was removed in 2020 in [commit 608a16a] due to unaddressed upstre
|
||||
[commit 608a16a]:
|
||||
https://github.com/eyre-rs/eyre/pull/29/commits/608a16aa2c2c27eca6c88001cc94c6973c18f1d5
|
||||
|
||||
|
||||
## Backtrace support
|
||||
|
||||
The built in default handler has support for capturing backtrace using `rustc-1.65` or later.
|
||||
|
||||
Backtraces are captured when an error is converted to an `eyre::Report` (such as using `?` or `eyre!`).
|
||||
|
||||
If using the nightly toolchain, backtraces will also be captured and accessed from other errors using [error_generic_member_access](https://github.com/rust-lang/rfcs/pull/2895) if available.
|
||||
|
||||
## Comparison to failure
|
||||
|
||||
The `eyre::Report` type works something like `failure::Error`, but unlike
|
||||
@ -195,8 +207,6 @@ you need an error type that can be handled via match or reported. This is
|
||||
common in library crates where you don't know how your users will handle
|
||||
your errors.
|
||||
|
||||
[thiserror]: https://github.com/dtolnay/thiserror
|
||||
|
||||
## Compatibility with `anyhow`
|
||||
|
||||
This crate does its best to be usable as a drop in replacement of `anyhow` and
|
||||
|
@ -23,6 +23,9 @@ indenter = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
pyo3 = { version = "0.20", optional = true, default-features = false }
|
||||
|
||||
[build-dependencies]
|
||||
autocfg = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
futures = { version = "0.3", default-features = false }
|
||||
rustversion = "1.0"
|
||||
|
173
eyre/build.rs
173
eyre/build.rs
@ -1,23 +1,53 @@
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, ExitStatus};
|
||||
use std::str;
|
||||
use std::{
|
||||
env, fs,
|
||||
path::Path,
|
||||
process::{Command, ExitStatus},
|
||||
};
|
||||
|
||||
// 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.
|
||||
const BACKTRACE_PROBE: &str = r#"
|
||||
#![feature(backtrace)]
|
||||
fn main() {
|
||||
let ac = autocfg::new();
|
||||
|
||||
// https://github.com/rust-lang/rust/issues/99301 [nightly]
|
||||
//
|
||||
// Autocfg does currently not support custom probes, or `nightly` only features
|
||||
match compile_probe(GENERIC_MEMBER_ACCESS_PROBE) {
|
||||
Some(status) if status.success() => autocfg::emit("generic_member_access"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// https://github.com/rust-lang/rust/issues/47809 [rustc-1.46]
|
||||
ac.emit_expression_cfg("std::panic::Location::caller", "track_caller");
|
||||
|
||||
if ac.probe_rustc_version(1, 52) {
|
||||
autocfg::emit("eyre_no_fmt_arguments_as_str");
|
||||
}
|
||||
|
||||
if ac.probe_rustc_version(1, 58) {
|
||||
autocfg::emit("eyre_no_fmt_args_capture");
|
||||
}
|
||||
|
||||
if ac.probe_rustc_version(1, 65) {
|
||||
autocfg::emit("backtrace")
|
||||
}
|
||||
}
|
||||
|
||||
// This code exercises the surface area or the generic member access feature for the `std::error::Error` trait.
|
||||
//
|
||||
// This is use to detect and supply backtrace information through different errors types.
|
||||
const GENERIC_MEMBER_ACCESS_PROBE: &str = r#"
|
||||
#![feature(error_generic_member_access)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::backtrace::{Backtrace, BacktraceStatus};
|
||||
use std::error::Error;
|
||||
use std::error::{Error, Request};
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct E;
|
||||
struct E {
|
||||
backtrace: MyBacktrace,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MyBacktrace;
|
||||
|
||||
impl Display for E {
|
||||
fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
@ -26,59 +56,44 @@ const BACKTRACE_PROBE: &str = r#"
|
||||
}
|
||||
|
||||
impl Error for E {
|
||||
fn backtrace(&self) -> Option<&Backtrace> {
|
||||
let backtrace = Backtrace::capture();
|
||||
match backtrace.status() {
|
||||
BacktraceStatus::Captured | BacktraceStatus::Disabled | _ => {}
|
||||
}
|
||||
unimplemented!()
|
||||
fn provide<'a>(&'a self, request: &mut Request<'a>) {
|
||||
request
|
||||
.provide_ref::<MyBacktrace>(&self.backtrace);
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
const TRACK_CALLER_PROBE: &str = r#"
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[track_caller]
|
||||
fn foo() {
|
||||
let _location = std::panic::Location::caller();
|
||||
}
|
||||
"#;
|
||||
|
||||
fn main() {
|
||||
match compile_probe(BACKTRACE_PROBE) {
|
||||
Some(status) if status.success() => println!("cargo:rustc-cfg=backtrace"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match compile_probe(TRACK_CALLER_PROBE) {
|
||||
Some(status) if status.success() => println!("cargo:rustc-cfg=track_caller"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let version = match rustc_version_info() {
|
||||
Some(version) => version,
|
||||
None => return,
|
||||
};
|
||||
|
||||
version.toolchain.set_feature();
|
||||
|
||||
if version.minor < 52 {
|
||||
println!("cargo:rustc-cfg=eyre_no_fmt_arguments_as_str");
|
||||
}
|
||||
|
||||
if version.minor < 58 {
|
||||
println!("cargo:rustc-cfg=eyre_no_fmt_args_capture");
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_probe(probe: &str) -> Option<ExitStatus> {
|
||||
let rustc = env::var_os("RUSTC")?;
|
||||
let out_dir = env::var_os("OUT_DIR")?;
|
||||
let probefile = Path::new(&out_dir).join("probe.rs");
|
||||
fs::write(&probefile, probe).ok()?;
|
||||
Command::new(rustc)
|
||||
.arg("--edition=2018")
|
||||
|
||||
let rustc_wrapper = env::var_os("RUSTC_WRAPPER").filter(|wrapper| !wrapper.is_empty());
|
||||
let rustc_workspace_wrapper =
|
||||
env::var_os("RUSTC_WORKSPACE_WRAPPER").filter(|wrapper| !wrapper.is_empty());
|
||||
let mut rustc = rustc_wrapper
|
||||
.into_iter()
|
||||
.chain(rustc_workspace_wrapper)
|
||||
.chain(std::iter::once(rustc));
|
||||
|
||||
let mut cmd = Command::new(rustc.next().unwrap());
|
||||
cmd.args(rustc);
|
||||
|
||||
if let Some(target) = env::var_os("TARGET") {
|
||||
cmd.arg("--target").arg(target);
|
||||
}
|
||||
|
||||
// If Cargo wants to set RUSTFLAGS, use that.
|
||||
if let Ok(rustflags) = env::var("CARGO_ENCODED_RUSTFLAGS") {
|
||||
if !rustflags.is_empty() {
|
||||
for arg in rustflags.split('\x1f') {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd.arg("--edition=2018")
|
||||
.arg("--crate-name=eyre_build")
|
||||
.arg("--crate-type=lib")
|
||||
.arg("--emit=metadata")
|
||||
@ -88,45 +103,3 @@ fn compile_probe(probe: &str) -> Option<ExitStatus> {
|
||||
.status()
|
||||
.ok()
|
||||
}
|
||||
|
||||
// TODO factor this toolchain parsing and related tests into its own file
|
||||
#[derive(PartialEq)]
|
||||
enum Toolchain {
|
||||
Stable,
|
||||
Beta,
|
||||
Nightly,
|
||||
}
|
||||
impl Toolchain {
|
||||
fn set_feature(self) {
|
||||
match self {
|
||||
Toolchain::Nightly => println!("cargo:rustc-cfg=nightly"),
|
||||
Toolchain::Beta => println!("cargo:rustc-cfg=beta"),
|
||||
Toolchain::Stable => println!("cargo:rustc-cfg=stable"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct VersionInfo {
|
||||
minor: u32,
|
||||
toolchain: Toolchain,
|
||||
}
|
||||
|
||||
fn rustc_version_info() -> Option<VersionInfo> {
|
||||
let rustc = env::var_os("RUSTC").unwrap_or_else(|| OsString::from("rustc"));
|
||||
let output = Command::new(rustc).arg("--version").output().ok()?;
|
||||
let version = str::from_utf8(&output.stdout).ok()?;
|
||||
let mut pieces = version.split(['.', ' ', '-']);
|
||||
if pieces.next() != Some("rustc") {
|
||||
return None;
|
||||
}
|
||||
let _major: u32 = pieces.next()?.parse().ok()?;
|
||||
let minor = pieces.next()?.parse().ok()?;
|
||||
let _patch: u32 = pieces.next()?.parse().ok()?;
|
||||
let toolchain = match pieces.next() {
|
||||
Some("beta") => Toolchain::Beta,
|
||||
Some("nightly") => Toolchain::Nightly,
|
||||
_ => Toolchain::Stable,
|
||||
};
|
||||
let version = VersionInfo { minor, toolchain };
|
||||
Some(version)
|
||||
}
|
||||
|
@ -5,18 +5,32 @@ pub(crate) use std::backtrace::Backtrace;
|
||||
pub(crate) enum Backtrace {}
|
||||
|
||||
#[cfg(backtrace)]
|
||||
macro_rules! backtrace_if_absent {
|
||||
($err:expr) => {
|
||||
match $err.backtrace() {
|
||||
Some(_) => None,
|
||||
None => Some(Backtrace::capture()),
|
||||
}
|
||||
macro_rules! capture_backtrace {
|
||||
() => {
|
||||
Some(Backtrace::capture())
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(backtrace))]
|
||||
macro_rules! backtrace_if_absent {
|
||||
($err:expr) => {
|
||||
macro_rules! capture_backtrace {
|
||||
() => {
|
||||
None
|
||||
};
|
||||
}
|
||||
/// Capture a backtrace iff there is not already a backtrace in the error chain
|
||||
#[cfg(generic_member_access)]
|
||||
macro_rules! backtrace_if_absent {
|
||||
($err:expr) => {
|
||||
match std::error::request_ref::<std::backtrace::Backtrace>($err as &dyn std::error::Error) {
|
||||
Some(_) => None,
|
||||
None => capture_backtrace!(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(generic_member_access))]
|
||||
macro_rules! backtrace_if_absent {
|
||||
($err:expr) => {
|
||||
capture_backtrace!()
|
||||
};
|
||||
}
|
||||
|
@ -2,9 +2,6 @@ use crate::error::{ContextError, ErrorImpl};
|
||||
use crate::{Report, StdError, WrapErr};
|
||||
use core::fmt::{self, Debug, Display, Write};
|
||||
|
||||
#[cfg(backtrace)]
|
||||
use std::backtrace::Backtrace;
|
||||
|
||||
mod ext {
|
||||
use super::*;
|
||||
|
||||
@ -139,9 +136,9 @@ where
|
||||
D: Display,
|
||||
E: StdError + 'static,
|
||||
{
|
||||
#[cfg(backtrace)]
|
||||
fn backtrace(&self) -> Option<&Backtrace> {
|
||||
self.error.backtrace()
|
||||
#[cfg(generic_member_access)]
|
||||
fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) {
|
||||
self.error.provide(request);
|
||||
}
|
||||
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
|
@ -855,6 +855,11 @@ impl<E> StdError for ErrorImpl<E>
|
||||
where
|
||||
E: StdError,
|
||||
{
|
||||
#[cfg(generic_member_access)]
|
||||
fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) {
|
||||
self._object.provide(request)
|
||||
}
|
||||
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
ErrorImpl::<()>::error(self.erase()).source()
|
||||
}
|
||||
|
@ -355,7 +355,7 @@
|
||||
unused_parens,
|
||||
while_true
|
||||
)]
|
||||
#![cfg_attr(backtrace, feature(backtrace))]
|
||||
#![cfg_attr(generic_member_access, feature(error_generic_member_access))]
|
||||
#![cfg_attr(doc_cfg, feature(doc_cfg))]
|
||||
#![allow(
|
||||
clippy::needless_doctest_main,
|
||||
@ -636,7 +636,7 @@ impl dyn EyreHandler {
|
||||
t == concrete
|
||||
}
|
||||
|
||||
/// Downcast the handler to a concrete type `T`
|
||||
/// Downcast the handler to a concrete type
|
||||
pub fn downcast_ref<T: EyreHandler>(&self) -> Option<&T> {
|
||||
if self.is::<T>() {
|
||||
unsafe { Some(&*(self as *const dyn EyreHandler as *const T)) }
|
||||
@ -645,7 +645,7 @@ impl dyn EyreHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Downcast the handler to a concrete type `T`
|
||||
/// Downcast the handler to a concrete type
|
||||
pub fn downcast_mut<T: EyreHandler>(&mut self) -> Option<&mut T> {
|
||||
if self.is::<T>() {
|
||||
unsafe { Some(&mut *(self as *mut dyn EyreHandler as *mut T)) }
|
||||
@ -778,6 +778,7 @@ impl DefaultHandler {
|
||||
#[allow(unused_variables)]
|
||||
#[cfg_attr(not(feature = "auto-install"), allow(dead_code))]
|
||||
pub fn default_with(error: &(dyn StdError + 'static)) -> Box<dyn EyreHandler> {
|
||||
// Capture the backtrace if the source error did not already capture one
|
||||
let backtrace = backtrace_if_absent!(error);
|
||||
|
||||
Box::new(Self {
|
||||
@ -837,15 +838,19 @@ impl EyreHandler for DefaultHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(backtrace)]
|
||||
#[cfg(generic_member_access)]
|
||||
{
|
||||
use std::backtrace::BacktraceStatus;
|
||||
|
||||
// The backtrace can be stored either in the handler instance, or the error itself.
|
||||
//
|
||||
// If the source error has a backtrace, the handler should not capture one
|
||||
let backtrace = self
|
||||
.backtrace
|
||||
.as_ref()
|
||||
.or_else(|| error.backtrace())
|
||||
.or_else(|| std::error::request_ref::<Backtrace>(error))
|
||||
.expect("backtrace capture failed");
|
||||
|
||||
if let BacktraceStatus::Captured = backtrace.status() {
|
||||
write!(f, "\n\nStack backtrace:\n{}", backtrace)?;
|
||||
}
|
||||
|
@ -82,9 +82,9 @@ impl Display for BoxedError {
|
||||
}
|
||||
|
||||
impl StdError for BoxedError {
|
||||
#[cfg(backtrace)]
|
||||
fn backtrace(&self) -> Option<&crate::backtrace::Backtrace> {
|
||||
self.0.backtrace()
|
||||
#[cfg(generic_member_access)]
|
||||
fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) {
|
||||
self.0.provide(request);
|
||||
}
|
||||
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
|
73
eyre/tests/generic_member_access.rs
Normal file
73
eyre/tests/generic_member_access.rs
Normal file
@ -0,0 +1,73 @@
|
||||
#![cfg_attr(generic_member_access, feature(error_generic_member_access))]
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(all(generic_member_access, not(miri)))]
|
||||
#[test]
|
||||
/// Tests that generic member access works through an `eyre::Report`
|
||||
fn generic_member_access() {
|
||||
use crate::common::maybe_install_handler;
|
||||
|
||||
use eyre::WrapErr;
|
||||
use std::backtrace::Backtrace;
|
||||
use std::fmt::Display;
|
||||
|
||||
fn fail() -> Result<(), MyError> {
|
||||
Err(MyError {
|
||||
cupcake: MyCupcake("Blueberry".into()),
|
||||
backtrace: std::backtrace::Backtrace::capture(),
|
||||
})
|
||||
}
|
||||
|
||||
maybe_install_handler().unwrap();
|
||||
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct MyCupcake(String);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MyError {
|
||||
cupcake: MyCupcake,
|
||||
backtrace: std::backtrace::Backtrace,
|
||||
}
|
||||
|
||||
impl Display for MyError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Error: {}", self.cupcake.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MyError {
|
||||
fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) {
|
||||
request
|
||||
.provide_ref(&self.cupcake)
|
||||
.provide_ref(&self.backtrace);
|
||||
}
|
||||
}
|
||||
|
||||
let err = fail()
|
||||
.wrap_err("Failed to bake my favorite cupcake")
|
||||
.unwrap_err();
|
||||
|
||||
let err: Box<dyn std::error::Error> = err.into();
|
||||
|
||||
assert!(
|
||||
format!("{:?}", err).contains("generic_member_access::generic_member_access::fail"),
|
||||
"should contain the source error backtrace"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
std::error::request_ref::<MyCupcake>(&*err),
|
||||
Some(&MyCupcake("Blueberry".into()))
|
||||
);
|
||||
|
||||
let bt = std::error::request_ref::<Backtrace>(&*err).unwrap();
|
||||
|
||||
assert!(
|
||||
bt.to_string()
|
||||
.contains("generic_member_access::generic_member_access::fail"),
|
||||
"should contain the fail method as it was captured by the original error\n\n{}",
|
||||
bt
|
||||
);
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
// These tests check our build script against rustversion.
|
||||
|
||||
#[rustversion::attr(not(nightly), ignore)]
|
||||
#[test]
|
||||
fn nightlytest() {
|
||||
if !cfg!(nightly) {
|
||||
panic!("nightly feature isn't set when the toolchain is nightly.");
|
||||
}
|
||||
if cfg!(any(beta, stable)) {
|
||||
panic!("beta, stable, and nightly are mutually exclusive features.")
|
||||
}
|
||||
}
|
||||
|
||||
#[rustversion::attr(not(beta), ignore)]
|
||||
#[test]
|
||||
fn betatest() {
|
||||
if !cfg!(beta) {
|
||||
panic!("beta feature is not set when the toolchain is beta.");
|
||||
}
|
||||
if cfg!(any(nightly, stable)) {
|
||||
panic!("beta, stable, and nightly are mutually exclusive features.")
|
||||
}
|
||||
}
|
||||
|
||||
#[rustversion::attr(not(stable), ignore)]
|
||||
#[test]
|
||||
fn stabletest() {
|
||||
if !cfg!(stable) {
|
||||
panic!("stable feature is not set when the toolchain is stable.");
|
||||
}
|
||||
if cfg!(any(nightly, beta)) {
|
||||
panic!("beta, stable, and nightly are mutually exclusive features.")
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user