mirror of
https://github.com/eyre-rs/eyre.git
synced 2025-09-27 13:01:29 +00:00
Add custom section support for reports (#18)
This commit is contained in:
parent
9f5defedb7
commit
7b2d9f4218
@ -29,6 +29,7 @@ once_cell = "1.4.0"
|
||||
tracing-subscriber = "0.2.5"
|
||||
tracing = "0.1.13"
|
||||
pretty_assertions = "0.6.1"
|
||||
thiserror = "1.0.19"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
162
README.md
162
README.md
@ -11,8 +11,14 @@
|
||||
[docs-badge]: https://img.shields.io/badge/docs-latest-blue.svg
|
||||
[docs-url]: https://docs.rs/color-eyre
|
||||
|
||||
A custom context for the [`eyre`] crate for colorful error reports, suggestions,
|
||||
and [`tracing-error`] support.
|
||||
A custom context for the [`eyre`] crate for colorful error reports with suggestions, custom
|
||||
sections, [`tracing-error`] support, and backtraces on stable.
|
||||
|
||||
## TLDR
|
||||
|
||||
`color_eyre` helps you build error reports that look like this:
|
||||
|
||||

|
||||
|
||||
## Setup
|
||||
|
||||
@ -48,17 +54,33 @@ eyre = "0.4"
|
||||
color-eyre = { version = "0.3", default-features = false }
|
||||
```
|
||||
|
||||
## Example
|
||||
## Features
|
||||
|
||||
### Multiple report format verbosity levels
|
||||
|
||||
`color-eyre` provides 3 different report formats for how it formats the captured `SpanTrace`
|
||||
and `Backtrace`, minimal, short, and full. Take the following example, taken from
|
||||
[`examples/usage.rs`]:
|
||||
|
||||
```rust
|
||||
use color_eyre::{Help, Report};
|
||||
use eyre::WrapErr;
|
||||
use tracing::{info, instrument};
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
#[instrument]
|
||||
fn main() -> Result<(), Report> {
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
install_tracing();
|
||||
|
||||
Ok(read_config()?)
|
||||
}
|
||||
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
fn install_tracing() {
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
let fmt_layer = fmt::layer().with_target(false);
|
||||
let filter_layer = EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new("info"))
|
||||
@ -69,8 +91,6 @@ fn main() -> Result<(), Report> {
|
||||
.with(fmt_layer)
|
||||
.with(ErrorLayer::default())
|
||||
.init();
|
||||
|
||||
Ok(read_config()?)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
@ -87,17 +107,112 @@ fn read_config() -> Result<(), Report> {
|
||||
}
|
||||
```
|
||||
|
||||
## Minimal Report Format
|
||||
---
|
||||
|
||||

|
||||
Running `cargo run --example usage` without `RUST_LIB_BACKTRACE` set will produce a minimal
|
||||
report like this:
|
||||
|
||||
## Short Report Format (with `RUST_LIB_BACKTRACE=1`)
|
||||

|
||||
|
||||

|
||||
<br>
|
||||
|
||||
## Full Report Format (with `RUST_LIB_BACKTRACE=full`)
|
||||
Running `RUST_LIB_BACKTRACE=1 cargo run --example usage` tells `color-eyre` to use the short
|
||||
format, which additionally capture a [`backtrace::Backtrace`]:
|
||||
|
||||

|
||||

|
||||
|
||||
<br>
|
||||
|
||||
Finally, running `RUST_LIB_BACKTRACE=full cargo run --example usage` tells `color-eyre` to use
|
||||
the full format, which in addition to the above will attempt to include source lines where the
|
||||
error originated from, assuming it can find them on the disk.
|
||||
|
||||

|
||||
|
||||
### Custom `Section`s for error reports via [`Help`] trait
|
||||
|
||||
The `section` module provides helpers for adding extra sections to error reports. Sections are
|
||||
disinct from error messages and are displayed independently from the chain of errors. Take this
|
||||
example of adding sections to contain `stderr` and `stdout` from a failed command, taken from
|
||||
[`examples/custom_section.rs`]:
|
||||
|
||||
```rust
|
||||
use color_eyre::{SectionExt, Help, Report};
|
||||
use eyre::eyre;
|
||||
use std::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
trait Output {
|
||||
fn output2(&mut self) -> Result<String, Report>;
|
||||
}
|
||||
|
||||
impl Output for Command {
|
||||
#[instrument]
|
||||
fn output2(&mut self) -> Result<String, Report> {
|
||||
let output = self.output()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Err(eyre!("cmd exited with non-zero status code"))
|
||||
.with_section(move || {
|
||||
"Stdout:"
|
||||
.skip_if(|| stdout.is_empty())
|
||||
.body(stdout.trim().to_string())
|
||||
})
|
||||
.with_section(move || {
|
||||
"Stderr:"
|
||||
.skip_if(|| stderr.is_empty())
|
||||
.body(stderr.trim().to_string())
|
||||
})
|
||||
} else {
|
||||
Ok(stdout.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Here we have an function that, if the command exits unsuccessfully, creates a report indicating
|
||||
the failure and attaches two sections, one for `stdout` and one for `stderr`. Each section
|
||||
includes a short header and a body that contains the actual output. Additionally these sections
|
||||
use `skip_if` to tell the report not to include them if there was no output, preventing empty
|
||||
sections from polluting the end report.
|
||||
|
||||
Running `cargo run --example custom_section` shows us how these sections are included in the
|
||||
output:
|
||||
|
||||

|
||||
|
||||
Only the `Stderr:` section actually gets included. The `cat` command fails, so stdout ends up
|
||||
being empty and is skipped in the final report. This gives us a short and concise error report
|
||||
indicating exactly what was attempted and how it failed.
|
||||
|
||||
### Aggregating multiple errors into one report
|
||||
|
||||
It's not uncommon for programs like batched task runners or parsers to want to
|
||||
return an error with multiple sources. The current version of the error trait
|
||||
does not support this use case very well, though there is [work being
|
||||
done](https://github.com/rust-lang/rfcs/pull/2895) to improve this.
|
||||
|
||||
For now however one way to work around this is to compose errors outside the
|
||||
error trait. `color-eyre` supports such composition in its error reports via
|
||||
the `Help` trait.
|
||||
|
||||
For an example of how to aggregate errors check out [`examples/multiple_errors.rs`].
|
||||
|
||||
### Custom configuration for `color-backtrace` for setting custom filters and more
|
||||
|
||||
The pretty printing for backtraces and span traces isn't actually provided by `color-eyre`, but
|
||||
instead comes from its dependencies [`color-backtrace`] and [`color-spantrace`].
|
||||
`color-backtrace` in particular has many more features than are exported by `color-eyre`, such
|
||||
as customized color schemes, panic hooks, and custom frame filters. The custom frame filters
|
||||
are particularly useful when combined with `color-eyre`, so to enable their usage we provide
|
||||
the `install` fn for setting up a custom `BacktracePrinter` with custom filters installed.
|
||||
|
||||
For an example of how to setup custom filters, check out [`examples/custom_filter.rs`].
|
||||
|
||||
## Explanation
|
||||
|
||||
@ -112,17 +227,7 @@ pub type Report = eyre::Report<Context>;
|
||||
pub type Result<T, E = Report> = core::result::Result<T, E>;
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- captures a [`backtrace::Backtrace`] and prints using [`color-backtrace`]
|
||||
- captures a [`tracing_error::SpanTrace`] and prints using
|
||||
[`color-spantrace`]
|
||||
- Only capture SpanTrace by default for better performance.
|
||||
- display source lines when `RUST_LIB_BACKTRACE=full` is set
|
||||
- store help text via [`Help`] trait and display after final report
|
||||
- custom `color-backtrace` configuration via `color_eyre::install`, such as
|
||||
adding custom frame filters
|
||||
|
||||
Please refer to the [`Context`] type's docs for more details about its feature set.
|
||||
|
||||
[`eyre`]: https://docs.rs/eyre
|
||||
[`tracing-error`]: https://docs.rs/tracing-error
|
||||
@ -131,9 +236,14 @@ pub type Result<T, E = Report> = core::result::Result<T, E>;
|
||||
[`backtrace::Backtrace`]: https://docs.rs/backtrace/*/backtrace/struct.Backtrace.html
|
||||
[`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html
|
||||
[`color-spantrace`]: https://github.com/yaahc/color-spantrace
|
||||
[`Help`]: trait.Help.html
|
||||
[`Help`]: https://docs.rs/color-eyre/*/color_eyre/trait.Help.html
|
||||
[`eyre::Report`]: https://docs.rs/eyre/*/eyre/struct.Report.html
|
||||
[`eyre::Result`]: https://docs.rs/eyre/*/eyre/type.Result.html
|
||||
[`Context`]: https://docs.rs/color-eyre/*/color_eyre/struct.Context.html
|
||||
[`examples/usage.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/usage.rs
|
||||
[`examples/custom_filter.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_filter.rs
|
||||
[`examples/custom_section.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_section.rs
|
||||
[`examples/multiple_errors.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/multiple_errors.rs
|
||||
|
||||
#### License
|
||||
|
||||
|
72
examples/custom_section.rs
Normal file
72
examples/custom_section.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use color_eyre::{Help, Report, SectionExt};
|
||||
use eyre::{eyre, WrapErr};
|
||||
use std::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
trait Output {
|
||||
fn output2(&mut self) -> Result<String, Report>;
|
||||
}
|
||||
|
||||
impl Output for Command {
|
||||
#[instrument]
|
||||
fn output2(&mut self) -> Result<String, Report> {
|
||||
let output = self.output()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Err(eyre!("cmd exited with non-zero status code"))
|
||||
.with_section(move || {
|
||||
"Stdout:"
|
||||
.skip_if(|| stdout.is_empty())
|
||||
.body(stdout.trim().to_string())
|
||||
})
|
||||
.with_section(move || {
|
||||
"Stderr:"
|
||||
.skip_if(|| stderr.is_empty())
|
||||
.body(stderr.trim().to_string())
|
||||
})
|
||||
} else {
|
||||
Ok(stdout.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn main() -> Result<(), Report> {
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
install_tracing();
|
||||
|
||||
Ok(read_config().map(drop)?)
|
||||
}
|
||||
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
fn install_tracing() {
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
let fmt_layer = fmt::layer().with_target(false);
|
||||
let filter_layer = EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new("info"))
|
||||
.unwrap();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter_layer)
|
||||
.with(fmt_layer)
|
||||
.with(ErrorLayer::default())
|
||||
.init();
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn read_file(path: &str) -> Result<String, Report> {
|
||||
Command::new("cat").arg("fake_file").output2()
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn read_config() -> Result<String, Report> {
|
||||
read_file("fake_file")
|
||||
.wrap_err("Unable to read config")
|
||||
.suggestion("try using a file that exists next time")
|
||||
}
|
53
examples/multiple_errors.rs
Normal file
53
examples/multiple_errors.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use color_eyre::{Help, Report};
|
||||
use eyre::eyre;
|
||||
use thiserror::Error;
|
||||
|
||||
fn main() -> Result<(), Report> {
|
||||
let errors = get_errors();
|
||||
join_errors(errors)
|
||||
}
|
||||
|
||||
fn join_errors(results: Vec<Result<(), SourceError>>) -> Result<(), Report> {
|
||||
if results.iter().all(|r| r.is_ok()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
results
|
||||
.into_iter()
|
||||
.filter(Result::is_err)
|
||||
.map(Result::unwrap_err)
|
||||
.fold(Err(eyre!("encountered multiple errors")), |report, e| {
|
||||
report.error(e)
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function to generate errors
|
||||
fn get_errors() -> Vec<Result<(), SourceError>> {
|
||||
vec![
|
||||
Err(SourceError {
|
||||
source: StrError("The task you ran encountered an error"),
|
||||
msg: "The task could not be completed",
|
||||
}),
|
||||
Err(SourceError {
|
||||
source: StrError("The machine you're connecting to is actively on fire"),
|
||||
msg: "The machine is unreachable",
|
||||
}),
|
||||
Err(SourceError {
|
||||
source: StrError("The file you're parsing is literally written in c++ instead of rust, what the hell"),
|
||||
msg: "The file could not be parsed",
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
/// Arbitrary error type for demonstration purposes
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{0}")]
|
||||
struct StrError(&'static str);
|
||||
|
||||
/// Arbitrary error type for demonstration purposes with a source error
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{msg}")]
|
||||
struct SourceError {
|
||||
msg: &'static str,
|
||||
source: StrError,
|
||||
}
|
BIN
pictures/custom_section.png
Normal file
BIN
pictures/custom_section.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 131 KiB |
218
src/help.rs
218
src/help.rs
@ -1,218 +0,0 @@
|
||||
use crate::{Report, Result};
|
||||
use ansi_term::Color::*;
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
/// A helper trait for attaching help text to errors to be displayed after the chain of errors
|
||||
pub trait Help<T>: private::Sealed {
|
||||
/// Add a note to an error, to be displayed after the chain of errors.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::{error::Error, fmt::{self, Display}};
|
||||
/// # use color_eyre::Result;
|
||||
/// # #[derive(Debug)]
|
||||
/// # struct FakeErr;
|
||||
/// # impl Display for FakeErr {
|
||||
/// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
/// # write!(f, "FakeErr")
|
||||
/// # }
|
||||
/// # }
|
||||
/// # impl std::error::Error for FakeErr {}
|
||||
/// # fn main() -> Result<()> {
|
||||
/// # fn fallible_fn() -> Result<(), FakeErr> {
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// use color_eyre::Help as _;
|
||||
///
|
||||
/// fallible_fn().note("This might have failed due to ...")?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
fn note<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static;
|
||||
|
||||
/// Add a Note to an error, to be displayed after the chain of errors, which is lazily
|
||||
/// evaluated only in the case of an error.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::{error::Error, fmt::{self, Display}};
|
||||
/// # use color_eyre::Result;
|
||||
/// # #[derive(Debug)]
|
||||
/// # struct FakeErr;
|
||||
/// # impl Display for FakeErr {
|
||||
/// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
/// # write!(f, "FakeErr")
|
||||
/// # }
|
||||
/// # }
|
||||
/// # impl std::error::Error for FakeErr {}
|
||||
/// # fn main() -> Result<()> {
|
||||
/// # fn fallible_fn() -> Result<(), FakeErr> {
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// use color_eyre::Help as _;
|
||||
///
|
||||
/// fallible_fn().with_note(|| {
|
||||
/// format!("This might have failed due to ... It has failed {} times", 100)
|
||||
/// })?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
fn with_note<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C;
|
||||
|
||||
/// Add a Warning to an error, to be displayed after the chain of errors.
|
||||
fn warning<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static;
|
||||
|
||||
/// Add a Warning to an error, to be displayed after the chain of errors, which is lazily
|
||||
/// evaluated only in the case of an error.
|
||||
fn with_warning<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C;
|
||||
|
||||
/// Add a Suggestion to an error, to be displayed after the chain of errors.
|
||||
fn suggestion<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static;
|
||||
|
||||
/// Add a Suggestion to an error, to be displayed after the chain of errors, which is lazily
|
||||
/// evaluated only in the case of an error.
|
||||
fn with_suggestion<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C;
|
||||
}
|
||||
|
||||
impl<T, E> Help<T> for std::result::Result<T, E>
|
||||
where
|
||||
E: Into<Report>,
|
||||
{
|
||||
fn note<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut().help.push(HelpInfo::Note(Box::new(context)));
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn with_note<C, F>(self, context: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut()
|
||||
.help
|
||||
.push(HelpInfo::Note(Box::new(context())));
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn warning<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut()
|
||||
.help
|
||||
.push(HelpInfo::Warning(Box::new(context)));
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn with_warning<C, F>(self, context: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut()
|
||||
.help
|
||||
.push(HelpInfo::Warning(Box::new(context())));
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn suggestion<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut()
|
||||
.help
|
||||
.push(HelpInfo::Suggestion(Box::new(context)));
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn with_suggestion<C, F>(self, context: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut()
|
||||
.help
|
||||
.push(HelpInfo::Suggestion(Box::new(context())));
|
||||
e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum HelpInfo {
|
||||
Note(Box<dyn Display + Send + Sync + 'static>),
|
||||
Warning(Box<dyn Display + Send + Sync + 'static>),
|
||||
Suggestion(Box<dyn Display + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
impl Display for HelpInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Note(context) => write!(f, "{}: {}", Cyan.paint("Note"), context),
|
||||
Self::Warning(context) => write!(f, "{}: {}", Yellow.paint("Warning"), context),
|
||||
Self::Suggestion(context) => write!(f, "{}: {}", Cyan.paint("Suggestion"), context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for HelpInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Note(context) => f
|
||||
.debug_tuple("Note")
|
||||
.field(&format_args!("{}", context))
|
||||
.finish(),
|
||||
Self::Warning(context) => f
|
||||
.debug_tuple("Warning")
|
||||
.field(&format_args!("{}", context))
|
||||
.finish(),
|
||||
Self::Suggestion(context) => f
|
||||
.debug_tuple("Suggestion")
|
||||
.field(&format_args!("{}", context))
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod private {
|
||||
use crate::Report;
|
||||
pub trait Sealed {}
|
||||
|
||||
impl<T, E> Sealed for std::result::Result<T, E> where E: Into<Report> {}
|
||||
}
|
179
src/lib.rs
179
src/lib.rs
@ -1,5 +1,11 @@
|
||||
//! A custom context for the [`eyre`] crate for colorful error reports, suggestions,
|
||||
//! and [`tracing-error`] support.
|
||||
//! A custom context for the [`eyre`] crate for colorful error reports with suggestions, custom
|
||||
//! sections, [`tracing-error`] support, and backtraces on stable.
|
||||
//!
|
||||
//! ## TLDR
|
||||
//!
|
||||
//! `color_eyre` helps you build error reports that look like this:
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! ## Setup
|
||||
//!
|
||||
@ -35,7 +41,13 @@
|
||||
//! color-eyre = { version = "0.3", default-features = false }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Example
|
||||
//! ## Features
|
||||
//!
|
||||
//! ### Multiple report format verbosity levels
|
||||
//!
|
||||
//! `color-eyre` provides 3 different report formats for how it formats the captured `SpanTrace`
|
||||
//! and `Backtrace`, minimal, short, and full. Take the following example, taken from
|
||||
//! [`examples/usage.rs`]:
|
||||
//!
|
||||
//! ```rust,should_panic
|
||||
//! use color_eyre::{Help, Report};
|
||||
@ -82,18 +94,113 @@
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Minimal Report Format
|
||||
//! ---
|
||||
//!
|
||||
//! Running `cargo run --example usage` without `RUST_LIB_BACKTRACE` set will produce a minimal
|
||||
//! report like this:
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! ## Short Report Format (with `RUST_LIB_BACKTRACE=1`)
|
||||
//! <br>
|
||||
//!
|
||||
//! Running `RUST_LIB_BACKTRACE=1 cargo run --example usage` tells `color-eyre` to use the short
|
||||
//! format, which additionally capture a [`backtrace::Backtrace`]:
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! ## Full Report Format (with `RUST_LIB_BACKTRACE=full`)
|
||||
//! <br>
|
||||
//!
|
||||
//! Finally, running `RUST_LIB_BACKTRACE=full cargo run --example usage` tells `color-eyre` to use
|
||||
//! the full format, which in addition to the above will attempt to include source lines where the
|
||||
//! error originated from, assuming it can find them on the disk.
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! ### Custom `Section`s for error reports via [`Help`] trait
|
||||
//!
|
||||
//! The `section` module provides helpers for adding extra sections to error reports. Sections are
|
||||
//! disinct from error messages and are displayed independently from the chain of errors. Take this
|
||||
//! example of adding sections to contain `stderr` and `stdout` from a failed command, taken from
|
||||
//! [`examples/custom_section.rs`]:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use color_eyre::{SectionExt, Help, Report};
|
||||
//! use eyre::eyre;
|
||||
//! use std::process::Command;
|
||||
//! use tracing::instrument;
|
||||
//!
|
||||
//! trait Output {
|
||||
//! fn output2(&mut self) -> Result<String, Report>;
|
||||
//! }
|
||||
//!
|
||||
//! impl Output for Command {
|
||||
//! #[instrument]
|
||||
//! fn output2(&mut self) -> Result<String, Report> {
|
||||
//! let output = self.output()?;
|
||||
//!
|
||||
//! let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
//!
|
||||
//! if !output.status.success() {
|
||||
//! let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
//! Err(eyre!("cmd exited with non-zero status code"))
|
||||
//! .with_section(move || {
|
||||
//! "Stdout:"
|
||||
//! .skip_if(|| stdout.is_empty())
|
||||
//! .body(stdout.trim().to_string())
|
||||
//! })
|
||||
//! .with_section(move || {
|
||||
//! "Stderr:"
|
||||
//! .skip_if(|| stderr.is_empty())
|
||||
//! .body(stderr.trim().to_string())
|
||||
//! })
|
||||
//! } else {
|
||||
//! Ok(stdout.into())
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
//! Here we have an function that, if the command exits unsuccessfully, creates a report indicating
|
||||
//! the failure and attaches two sections, one for `stdout` and one for `stderr`. Each section
|
||||
//! includes a short header and a body that contains the actual output. Additionally these sections
|
||||
//! use `skip_if` to tell the report not to include them if there was no output, preventing empty
|
||||
//! sections from polluting the end report.
|
||||
//!
|
||||
//! Running `cargo run --example custom_section` shows us how these sections are included in the
|
||||
//! output:
|
||||
//!
|
||||
//! 
|
||||
//!
|
||||
//! Only the `Stderr:` section actually gets included. The `cat` command fails, so stdout ends up
|
||||
//! being empty and is skipped in the final report. This gives us a short and concise error report
|
||||
//! indicating exactly what was attempted and how it failed.
|
||||
//!
|
||||
//! ### Aggregating multiple errors into one report
|
||||
//!
|
||||
//! It's not uncommon for programs like batched task runners or parsers to want to
|
||||
//! return an error with multiple sources. The current version of the error trait
|
||||
//! does not support this use case very well, though there is [work being
|
||||
//! done](https://github.com/rust-lang/rfcs/pull/2895) to improve this.
|
||||
//!
|
||||
//! For now however one way to work around this is to compose errors outside the
|
||||
//! error trait. `color-eyre` supports such composition in its error reports via
|
||||
//! the `Help` trait.
|
||||
//!
|
||||
//! For an example of how to aggregate errors check out [`examples/multiple_errors.rs`].
|
||||
//!
|
||||
//! ### Custom configuration for `color-backtrace` for setting custom filters and more
|
||||
//!
|
||||
//! The pretty printing for backtraces and span traces isn't actually provided by `color-eyre`, but
|
||||
//! instead comes from its dependencies [`color-backtrace`] and [`color-spantrace`].
|
||||
//! `color-backtrace` in particular has many more features than are exported by `color-eyre`, such
|
||||
//! as customized color schemes, panic hooks, and custom frame filters. The custom frame filters
|
||||
//! are particularly useful when combined with `color-eyre`, so to enable their usage we provide
|
||||
//! the `install` fn for setting up a custom `BacktracePrinter` with custom filters installed.
|
||||
//!
|
||||
//! For an example of how to setup custom filters, check out [`examples/custom_filter.rs`].
|
||||
//!
|
||||
//! ## Explanation
|
||||
//!
|
||||
//! This crate works by defining a `Context` type which implements [`eyre::EyreContext`]
|
||||
@ -109,18 +216,6 @@
|
||||
//!
|
||||
//! Please refer to the [`Context`] type's docs for more details about its feature set.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - captures a [`backtrace::Backtrace`] and prints using [`color-backtrace`]
|
||||
//! - captures a [`tracing_error::SpanTrace`] and prints using
|
||||
//! [`color-spantrace`]
|
||||
//! - Only capture SpanTrace by default for better performance.
|
||||
//! - display source lines when `RUST_LIB_BACKTRACE=full` is set
|
||||
//! - store help text via [`Help`] trait and display after final report
|
||||
//! - custom `color-backtrace` configuration via `color_eyre::install`,
|
||||
//! such as adding custom filters
|
||||
//!
|
||||
//!
|
||||
//! [`eyre`]: https://docs.rs/eyre
|
||||
//! [`tracing-error`]: https://docs.rs/tracing-error
|
||||
//! [`color-backtrace`]: https://docs.rs/color-backtrace
|
||||
@ -128,10 +223,14 @@
|
||||
//! [`backtrace::Backtrace`]: https://docs.rs/backtrace/*/backtrace/struct.Backtrace.html
|
||||
//! [`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html
|
||||
//! [`color-spantrace`]: https://github.com/yaahc/color-spantrace
|
||||
//! [`Help`]: trait.Help.html
|
||||
//! [`Help`]: https://docs.rs/color-eyre/*/color_eyre/trait.Help.html
|
||||
//! [`eyre::Report`]: https://docs.rs/eyre/*/eyre/struct.Report.html
|
||||
//! [`eyre::Result`]: https://docs.rs/eyre/*/eyre/type.Result.html
|
||||
//! [`Context`]: struct.Context.html
|
||||
//! [`Context`]: https://docs.rs/color-eyre/*/color_eyre/struct.Context.html
|
||||
//! [`examples/usage.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/usage.rs
|
||||
//! [`examples/custom_filter.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_filter.rs
|
||||
//! [`examples/custom_section.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_section.rs
|
||||
//! [`examples/multiple_errors.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/multiple_errors.rs
|
||||
#![doc(html_root_url = "https://docs.rs/color-eyre/0.3.2")]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![warn(
|
||||
@ -161,10 +260,10 @@ use ansi_term::Color::*;
|
||||
use backtrace::Backtrace;
|
||||
pub use color_backtrace::BacktracePrinter;
|
||||
use eyre::*;
|
||||
pub use help::Help;
|
||||
use help::HelpInfo;
|
||||
use indenter::{indented, Format};
|
||||
use once_cell::sync::OnceCell;
|
||||
use section::Order;
|
||||
pub use section::{help::Help, Section, SectionExt};
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
use std::error::Error;
|
||||
use std::{
|
||||
@ -175,7 +274,7 @@ use std::{
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
use tracing_error::{ExtractSpanTrace, SpanTrace, SpanTraceStatus};
|
||||
|
||||
mod help;
|
||||
pub mod section;
|
||||
|
||||
static CONFIG: OnceCell<BacktracePrinter> = OnceCell::new();
|
||||
|
||||
@ -194,7 +293,7 @@ pub struct Context {
|
||||
backtrace: Option<Backtrace>,
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
span_trace: Option<SpanTrace>,
|
||||
help: Vec<HelpInfo>,
|
||||
sections: Vec<Section>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -318,7 +417,7 @@ impl EyreContext for Context {
|
||||
backtrace,
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
span_trace,
|
||||
help: Vec::new(),
|
||||
sections: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,6 +446,22 @@ impl EyreContext for Context {
|
||||
write!(indented(f).ind(n), "{}", Red.paint(&buf))?;
|
||||
}
|
||||
|
||||
for section in self
|
||||
.sections
|
||||
.iter()
|
||||
.filter(|s| matches!(s.order, Order::AfterErrMsgs))
|
||||
{
|
||||
write!(f, "\n\n{:?}", section)?;
|
||||
}
|
||||
|
||||
for section in self
|
||||
.sections
|
||||
.iter()
|
||||
.filter(|s| matches!(s.order, Order::BeforeSpanTrace))
|
||||
{
|
||||
write!(f, "\n\n{:?}", section)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "capture-spantrace")]
|
||||
{
|
||||
let span_trace = self
|
||||
@ -378,12 +493,20 @@ impl EyreContext for Context {
|
||||
"{}",
|
||||
bt_str
|
||||
)?;
|
||||
} else if !self.help.is_empty() {
|
||||
} else if self
|
||||
.sections
|
||||
.iter()
|
||||
.any(|s| matches!(s.order, Order::AfterBackTrace))
|
||||
{
|
||||
writeln!(f)?;
|
||||
}
|
||||
|
||||
for help in &self.help {
|
||||
write!(f, "\n{}", help)?;
|
||||
for section in self
|
||||
.sections
|
||||
.iter()
|
||||
.filter(|s| matches!(s.order, Order::AfterBackTrace))
|
||||
{
|
||||
write!(f, "\n{:?}", section)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
405
src/section/help.rs
Normal file
405
src/section/help.rs
Normal file
@ -0,0 +1,405 @@
|
||||
//! Provides an extension trait for attaching `Section`s to error reports.
|
||||
use crate::{section, Report, Result, Section};
|
||||
use ansi_term::Color::*;
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
/// A helper trait for attaching help text to errors to be displayed after the chain of errors
|
||||
///
|
||||
/// `color_eyre` provides two types of help text that can be attached to error reports: custom
|
||||
/// sections and pre-configured sections. Custom sections are added via the `section` and
|
||||
/// `with_section` methods, and give maximum control over formatting. For more details check out
|
||||
/// the docs for [`Section`].
|
||||
///
|
||||
/// The pre-configured sections are provided via `suggestion`, `warning`, and `note`. These
|
||||
/// sections are displayed after all other sections with no extra newlines between subsequent Help
|
||||
/// sections. They consist only of a header portion and are prepended with a colored string
|
||||
/// indicating the kind of section, e.g. `Note: This might have failed due to ..."
|
||||
///
|
||||
/// [`Section`]: struct.Section.html
|
||||
pub trait Help<T>: private::Sealed {
|
||||
/// Add a section to an error report, to be displayed after the chain of errors.
|
||||
///
|
||||
/// Sections are displayed in the order they are added to the error report. They are displayed
|
||||
/// immediately after the `Error:` section and before the `SpanTrace` and `Backtrace` sections.
|
||||
/// They consist of a header and an optional body. The body of the section is indented by
|
||||
/// default.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,should_panic
|
||||
/// use color_eyre::{Report, Help};
|
||||
/// use eyre::eyre;
|
||||
///
|
||||
/// Err(eyre!("command failed"))
|
||||
/// .section("Please report bugs to https://real.url/bugs")?;
|
||||
/// # Ok::<_, Report>(())
|
||||
/// ```
|
||||
fn section<C>(self, section: C) -> Result<T>
|
||||
where
|
||||
C: Into<Section>;
|
||||
|
||||
/// Add a Section to an error report, to be displayed after the chain of errors. The closure to
|
||||
/// create the Section is lazily evaluated only in the case of an error.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use color_eyre::{Report, Help, SectionExt};
|
||||
/// use eyre::eyre;
|
||||
///
|
||||
/// let output = std::process::Command::new("ls")
|
||||
/// .output()?;
|
||||
///
|
||||
/// let output = if !output.status.success() {
|
||||
/// let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
/// Err(eyre!("cmd exited with non-zero status code"))
|
||||
/// .with_section(move || {
|
||||
/// "Stderr:"
|
||||
/// .skip_if(|| stderr.is_empty())
|
||||
/// .body(stderr.trim().to_string())
|
||||
/// })?
|
||||
/// } else {
|
||||
/// String::from_utf8_lossy(&output.stdout)
|
||||
/// };
|
||||
///
|
||||
/// println!("{}", output);
|
||||
/// # Ok::<_, Report>(())
|
||||
/// ```
|
||||
fn with_section<C, F>(self, section: F) -> Result<T>
|
||||
where
|
||||
C: Into<Section>,
|
||||
F: FnOnce() -> C;
|
||||
|
||||
/// Add an error section to an error report, to be displayed after the primary error message
|
||||
/// section.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,should_panic
|
||||
/// use color_eyre::{Report, Help};
|
||||
/// use eyre::eyre;
|
||||
/// use thiserror::Error;
|
||||
///
|
||||
/// #[derive(Debug, Error)]
|
||||
/// #[error("{0}")]
|
||||
/// struct StrError(&'static str);
|
||||
///
|
||||
/// Err(eyre!("command failed"))
|
||||
/// .error(StrError("got one error"))
|
||||
/// .error(StrError("got a second error"))?;
|
||||
/// # Ok::<_, Report>(())
|
||||
/// ```
|
||||
fn error<E>(self, error: E) -> Result<T>
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static;
|
||||
|
||||
/// Add an error section to an error report, to be displayed after the primary error message
|
||||
/// section. The closure to create the Section is lazily evaluated only in the case of an error.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,should_panic
|
||||
/// use color_eyre::{Report, Help};
|
||||
/// use eyre::eyre;
|
||||
/// use thiserror::Error;
|
||||
///
|
||||
/// #[derive(Debug, Error)]
|
||||
/// #[error("{0}")]
|
||||
/// struct StringError(String);
|
||||
///
|
||||
/// Err(eyre!("command failed"))
|
||||
/// .with_error(|| StringError("got one error".into()))
|
||||
/// .with_error(|| StringError("got a second error".into()))?;
|
||||
/// # Ok::<_, Report>(())
|
||||
/// ```
|
||||
fn with_error<E, F>(self, error: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce() -> E,
|
||||
E: std::error::Error + Send + Sync + 'static;
|
||||
|
||||
/// Add a Note to an error report, to be displayed after the chain of errors.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::{error::Error, fmt::{self, Display}};
|
||||
/// # use color_eyre::Result;
|
||||
/// # #[derive(Debug)]
|
||||
/// # struct FakeErr;
|
||||
/// # impl Display for FakeErr {
|
||||
/// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
/// # write!(f, "FakeErr")
|
||||
/// # }
|
||||
/// # }
|
||||
/// # impl std::error::Error for FakeErr {}
|
||||
/// # fn main() -> Result<()> {
|
||||
/// # fn fallible_fn() -> Result<(), FakeErr> {
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// use color_eyre::Help as _;
|
||||
///
|
||||
/// fallible_fn().note("This might have failed due to ...")?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
fn note<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static;
|
||||
|
||||
/// Add a Note to an error report, to be displayed after the chain of errors. The closure to
|
||||
/// create the Note is lazily evaluated only in the case of an error.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::{error::Error, fmt::{self, Display}};
|
||||
/// # use color_eyre::Result;
|
||||
/// # #[derive(Debug)]
|
||||
/// # struct FakeErr;
|
||||
/// # impl Display for FakeErr {
|
||||
/// # fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
/// # write!(f, "FakeErr")
|
||||
/// # }
|
||||
/// # }
|
||||
/// # impl std::error::Error for FakeErr {}
|
||||
/// # fn main() -> Result<()> {
|
||||
/// # fn fallible_fn() -> Result<(), FakeErr> {
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// use color_eyre::Help as _;
|
||||
///
|
||||
/// fallible_fn().with_note(|| {
|
||||
/// format!("This might have failed due to ... It has failed {} times", 100)
|
||||
/// })?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
fn with_note<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C;
|
||||
|
||||
/// Add a Warning to an error report, to be displayed after the chain of errors.
|
||||
fn warning<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static;
|
||||
|
||||
/// Add a Warning to an error report, to be displayed after the chain of errors. The closure to
|
||||
/// create the Warning is lazily evaluated only in the case of an error.
|
||||
fn with_warning<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C;
|
||||
|
||||
/// Add a Suggestion to an error report, to be displayed after the chain of errors.
|
||||
fn suggestion<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static;
|
||||
|
||||
/// Add a Suggestion to an error report, to be displayed after the chain of errors. The closure
|
||||
/// to create the Suggestion is lazily evaluated only in the case of an error.
|
||||
fn with_suggestion<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C;
|
||||
}
|
||||
|
||||
impl<T, E> Help<T> for std::result::Result<T, E>
|
||||
where
|
||||
E: Into<Report>,
|
||||
{
|
||||
fn note<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut().sections.push(
|
||||
Section::from(HelpInfo::Note(Box::new(context)))
|
||||
.order(section::Order::AfterBackTrace),
|
||||
);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn with_note<C, F>(self, context: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut().sections.push(
|
||||
Section::from(HelpInfo::Note(Box::new(context())))
|
||||
.order(section::Order::AfterBackTrace),
|
||||
);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn warning<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut().sections.push(
|
||||
Section::from(HelpInfo::Warning(Box::new(context)))
|
||||
.order(section::Order::AfterBackTrace),
|
||||
);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn with_warning<C, F>(self, context: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut().sections.push(
|
||||
Section::from(HelpInfo::Warning(Box::new(context())))
|
||||
.order(section::Order::AfterBackTrace),
|
||||
);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn suggestion<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut().sections.push(
|
||||
Section::from(HelpInfo::Suggestion(Box::new(context)))
|
||||
.order(section::Order::AfterBackTrace),
|
||||
);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn with_suggestion<C, F>(self, context: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
e.context_mut().sections.push(
|
||||
Section::from(HelpInfo::Suggestion(Box::new(context())))
|
||||
.order(section::Order::AfterBackTrace),
|
||||
);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn with_section<C, F>(self, section: F) -> Result<T>
|
||||
where
|
||||
C: Into<Section>,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
let section = section().into();
|
||||
|
||||
if !matches!(section.order, section::Order::SkipEntirely) {
|
||||
e.context_mut().sections.push(section);
|
||||
}
|
||||
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn section<C>(self, section: C) -> Result<T>
|
||||
where
|
||||
C: Into<Section>,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
let section = section.into();
|
||||
|
||||
if !matches!(section.order, section::Order::SkipEntirely) {
|
||||
e.context_mut().sections.push(section);
|
||||
}
|
||||
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn error<E2>(self, error: E2) -> Result<T>
|
||||
where
|
||||
E2: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
let section = Section {
|
||||
inner: section::SectionKind::Error(Box::new(error)),
|
||||
order: section::Order::AfterErrMsgs,
|
||||
};
|
||||
|
||||
e.context_mut().sections.push(section);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn with_error<E2, F>(self, error: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce() -> E2,
|
||||
E2: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
self.map_err(|e| {
|
||||
let mut e = e.into();
|
||||
let section = Section {
|
||||
inner: section::SectionKind::Error(Box::new(error())),
|
||||
order: section::Order::AfterErrMsgs,
|
||||
};
|
||||
|
||||
e.context_mut().sections.push(section);
|
||||
e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum HelpInfo {
|
||||
Note(Box<dyn Display + Send + Sync + 'static>),
|
||||
Warning(Box<dyn Display + Send + Sync + 'static>),
|
||||
Suggestion(Box<dyn Display + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
impl Display for HelpInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Note(context) => write!(f, "{}: {}", Cyan.paint("Note"), context),
|
||||
Self::Warning(context) => write!(f, "{}: {}", Yellow.paint("Warning"), context),
|
||||
Self::Suggestion(context) => write!(f, "{}: {}", Cyan.paint("Suggestion"), context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for HelpInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Note(context) => f
|
||||
.debug_tuple("Note")
|
||||
.field(&format_args!("{}", context))
|
||||
.finish(),
|
||||
Self::Warning(context) => f
|
||||
.debug_tuple("Warning")
|
||||
.field(&format_args!("{}", context))
|
||||
.finish(),
|
||||
Self::Suggestion(context) => f
|
||||
.debug_tuple("Suggestion")
|
||||
.field(&format_args!("{}", context))
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod private {
|
||||
use crate::Report;
|
||||
pub trait Sealed {}
|
||||
|
||||
impl<T, E> Sealed for std::result::Result<T, E> where E: Into<Report> {}
|
||||
}
|
248
src/section/mod.rs
Normal file
248
src/section/mod.rs
Normal file
@ -0,0 +1,248 @@
|
||||
//! Helpers for adding custom sections to error reports
|
||||
use ansi_term::Color::*;
|
||||
use indenter::indented;
|
||||
use std::fmt::{self, Display, Write};
|
||||
|
||||
pub mod help;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Order {
|
||||
AfterErrMsgs,
|
||||
BeforeSpanTrace,
|
||||
AfterBackTrace,
|
||||
SkipEntirely,
|
||||
}
|
||||
|
||||
/// A custom section for an error report.
|
||||
///
|
||||
/// # Details
|
||||
///
|
||||
/// Sections consist of two parts, a header: and an optional body. The header can contain any
|
||||
/// number of lines and has no indentation applied to it by default. The body can contain any
|
||||
/// number of lines and is always written after the header with indentation inserted before
|
||||
/// every line.
|
||||
///
|
||||
/// # Construction
|
||||
///
|
||||
/// Sections are meant to be constructed via `Into<Section>`, which is implemented for all types
|
||||
/// that implement `Display`. The constructed `Section` then takes ownership of the `Display` type
|
||||
/// and boxes it internally for use later when printing the report.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use color_eyre::{SectionExt, Help, Report};
|
||||
/// use eyre::eyre;
|
||||
/// use std::process::Command;
|
||||
/// use tracing::instrument;
|
||||
///
|
||||
/// trait Output {
|
||||
/// fn output2(&mut self) -> Result<String, Report>;
|
||||
/// }
|
||||
///
|
||||
/// impl Output for Command {
|
||||
/// #[instrument]
|
||||
/// fn output2(&mut self) -> Result<String, Report> {
|
||||
/// let output = self.output()?;
|
||||
///
|
||||
/// let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
///
|
||||
/// if !output.status.success() {
|
||||
/// let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
/// Err(eyre!("cmd exited with non-zero status code"))
|
||||
/// .with_section(move || {
|
||||
/// "Stdout:"
|
||||
/// .skip_if(|| stdout.is_empty())
|
||||
/// .body(stdout.trim().to_string())
|
||||
/// })
|
||||
/// .with_section(move || {
|
||||
/// "Stderr:"
|
||||
/// .skip_if(|| stderr.is_empty())
|
||||
/// .body(stderr.trim().to_string())
|
||||
/// })
|
||||
/// } else {
|
||||
/// Ok(stdout.into())
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Section {
|
||||
pub(crate) inner: SectionKind,
|
||||
pub(crate) order: Order,
|
||||
}
|
||||
|
||||
pub(crate) enum SectionKind {
|
||||
Header(Box<dyn Display + Send + Sync + 'static>),
|
||||
WithBody(
|
||||
Box<dyn Display + Send + Sync + 'static>,
|
||||
Box<dyn Display + Send + Sync + 'static>,
|
||||
),
|
||||
Error(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
/// Extension trait for customizing the content of a `Section`
|
||||
pub trait SectionExt {
|
||||
/// Add a body to a `Section`
|
||||
///
|
||||
/// Bodies are always indented to the same level as error messages and spans.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use color_eyre::{Help, SectionExt, Report};
|
||||
/// use eyre::eyre;
|
||||
///
|
||||
/// let all_in_header = "header\n body\n body";
|
||||
/// let report = Err::<(), Report>(eyre!("an error occurred"))
|
||||
/// .section(all_in_header)
|
||||
/// .unwrap_err();
|
||||
///
|
||||
/// let just_header = "header";
|
||||
/// let just_body = "body\nbody";
|
||||
/// let report2 = Err::<(), Report>(eyre!("an error occurred"))
|
||||
/// .section(just_header.body(just_body))
|
||||
/// .unwrap_err();
|
||||
///
|
||||
/// assert_eq!(format!("{:?}", report), format!("{:?}", report2))
|
||||
/// ```
|
||||
fn body<C>(self, body: C) -> Section
|
||||
where
|
||||
C: Display + Send + Sync + 'static;
|
||||
|
||||
/// Skip a section based on some condition. For example, skip a section if the body is empty.
|
||||
///
|
||||
/// The skipped section is not stored in the report and is instead immediately dropped.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use eyre::eyre;
|
||||
/// use color_eyre::{SectionExt, Report, Help};
|
||||
///
|
||||
/// fn add_body(report: Report, body: String) -> Result<(), Report> {
|
||||
/// Err(report)
|
||||
/// .with_section(|| "ExtraInfo:".skip_if(|| body.is_empty()).body(body))
|
||||
/// }
|
||||
///
|
||||
/// let report = eyre!("an error occurred");
|
||||
/// let before = format!("{:?}", report);
|
||||
/// let body = String::new();
|
||||
/// let report = add_body(report, body).unwrap_err();
|
||||
/// let after = format!("{:?}", report);
|
||||
/// assert_eq!(before, after);
|
||||
///
|
||||
/// let report = eyre!("an error occurred");
|
||||
/// let before = format!("{:?}", report);
|
||||
/// let body = String::from("Some actual text here");
|
||||
/// let report = add_body(report, body).unwrap_err();
|
||||
/// let after = format!("{:?}", report);
|
||||
/// assert_ne!(before, after);
|
||||
/// ```
|
||||
fn skip_if<F>(self, condition: F) -> Section
|
||||
where
|
||||
F: FnOnce() -> bool;
|
||||
}
|
||||
|
||||
impl Section {
|
||||
pub(crate) fn order(mut self, order: Order) -> Self {
|
||||
self.order = order;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SectionExt for T
|
||||
where
|
||||
Section: From<T>,
|
||||
{
|
||||
fn body<C>(self, body: C) -> Section
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
let section = Section::from(self);
|
||||
|
||||
let header = match section.inner {
|
||||
SectionKind::Header(header) => header,
|
||||
SectionKind::WithBody(header, _body) => header,
|
||||
SectionKind::Error(_) => unreachable!("bodies cannot be added to Error sections"),
|
||||
};
|
||||
|
||||
let inner = SectionKind::WithBody(header, Box::new(body));
|
||||
|
||||
Section {
|
||||
inner,
|
||||
order: section.order,
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_if<F>(self, condition: F) -> Section
|
||||
where
|
||||
F: FnOnce() -> bool,
|
||||
{
|
||||
let mut section = Section::from(self);
|
||||
|
||||
section.order = if condition() {
|
||||
Order::SkipEntirely
|
||||
} else {
|
||||
section.order
|
||||
};
|
||||
|
||||
section
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Section
|
||||
where
|
||||
T: Display + Send + Sync + 'static,
|
||||
{
|
||||
fn from(header: T) -> Self {
|
||||
let inner = SectionKind::Header(Box::new(header));
|
||||
|
||||
Self {
|
||||
inner,
|
||||
order: Order::BeforeSpanTrace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Section {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SectionKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SectionKind::Header(header) => write!(f, "{}", header)?,
|
||||
SectionKind::WithBody(header, body) => {
|
||||
write!(f, "{}", header)?;
|
||||
writeln!(f)?;
|
||||
write!(
|
||||
indenter::indented(f)
|
||||
.with_format(indenter::Format::Uniform { indentation: " " }),
|
||||
"{}",
|
||||
body
|
||||
)?;
|
||||
}
|
||||
SectionKind::Error(error) => {
|
||||
// a lot here
|
||||
let errors = std::iter::successors(
|
||||
Some(error.as_ref() as &(dyn std::error::Error + 'static)),
|
||||
|e| e.source(),
|
||||
);
|
||||
|
||||
write!(f, "Error:")?;
|
||||
let mut buf = String::new();
|
||||
for (n, error) in errors.enumerate() {
|
||||
writeln!(f)?;
|
||||
buf.clear();
|
||||
write!(&mut buf, "{}", error).unwrap();
|
||||
write!(indented(f).ind(n), "{}", Red.paint(&buf))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user