Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ prelude re-exports in [crates/main/cgp-core/src/prelude.rs](crates/main/cgp-core

## Commands

This is a Cargo workspace (edition 2024, resolver 3). Toolchain is pinned to **1.96** via
This is a Cargo workspace (edition 2024, resolver 3). Toolchain is pinned to **1.98.1** via
[rust-toolchain.toml](rust-toolchain.toml). Nearly every crate is `#![no_std]` — keep new code
`no_std`-compatible (use `core`/`alloc`, gate `std`/`alloc` usage behind features as existing crates
do).
Expand Down
3 changes: 3 additions & 0 deletions crates/core/cgp-error/src/traits/can_raise_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ use crate::traits::has_error_type::HasErrorType;
#[derive_delegate(UseDelegate<SourceError>)]
#[use_type(HasErrorType.Error)]
pub trait CanRaiseError<SourceError> {
/// `#[track_caller]` here applies to every provider impl and to the generated forwarding
/// impls, so an error library that records `Location::caller()` sees the caller's line.
#[track_caller]
fn raise_error(error: SourceError) -> Error;
}
7 changes: 7 additions & 0 deletions crates/core/cgp-error/src/traits/can_wrap_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ use cgp_macro::cgp_component;

use crate::traits::HasErrorType;

/**
The `CanWrapError` trait is used to attach a detail to an abstract error
provided by [`HasErrorType`].
*/
#[cgp_component(ErrorWrapper)]
#[prefix(@cgp.core.error in DefaultNamespace)]
#[derive_delegate(UseDelegate<Detail>)]
#[use_type(HasErrorType.Error)]
pub trait CanWrapError<Detail> {
/// `#[track_caller]` here applies to every provider impl and to the generated forwarding
/// impls, so an error library that records `Location::caller()` sees the caller's line.
#[track_caller]
fn wrap_error(error: Error, detail: Detail) -> Error;
}
8 changes: 4 additions & 4 deletions crates/standalone/error/cgp-error-eyre/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ cgp = { version = "0.8.0-alpha", path = "../../../main/cgp-core", packag
# `auto-install` installs eyre's default report handler on first use. Without it, building any
# `eyre::Report` panics unless the application has called `eyre::set_hook` first.
#
# `track-caller` is left off: every report is built inside one of this crate's providers, behind
# CGP's generated forwarding impls, so the location it records is always a line in this crate
# rather than the caller's.
eyre = { version = "0.6.14", default-features = false, features = [ "auto-install" ] }
# `track-caller` records where a report was built. `CanRaiseError::raise_error` is
# `#[track_caller]`, and so is every impl of it, which is what lets that location be the caller's
# line rather than a line in this crate.
eyre = { version = "0.6.14", default-features = false, features = [ "auto-install", "track-caller" ] }
6 changes: 3 additions & 3 deletions crates/standalone/error/cgp-error-eyre/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ A `String` needs `DisplayEyreError` or `DebugEyreError`, because it is not a sta
The crate enables eyre's `auto-install` feature, so eyre's default report handler is installed the
first time a report is built. To use another handler, such as `color-eyre`, install it with
`eyre::set_hook` before the first error is raised; once a report exists, `set_hook` returns an
error. The crate leaves eyre's `track-caller` feature off, because every report is built inside one
of its providers and the recorded location would name that line rather than the caller. eyre
requires `std`, so this crate does too.
error. The crate also enables eyre's `track-caller` feature, and CGP's `raise_error` is
`#[track_caller]`, so a report's `Location:` names the line that called `raise_error`. eyre requires
`std`, so this crate does too.

The crate re-exports `eyre::Error`, eyre's alias for `Report`, as `cgp_error_eyre::Error`.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use cgp::error::{ErrorRaiser, ErrorRaiserComponent, ErrorWrapper, ErrorWrapperCo
use cgp::prelude::*;

/// Raises a standard error into [`eyre::Report`] without formatting it, so the source stays
/// available to `downcast_ref` and to the error chain, and wraps a detail with `wrap_err`.
/// available to `downcast_ref` and to the error chain, and wraps a detail with `wrap_err`. The
/// report records the location that called `raise_error`.
pub struct RaiseEyreError;

#[cgp_impl(RaiseEyreError)]
Expand Down
4 changes: 3 additions & 1 deletion crates/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ The concept targets currently cover: basic delegation, impl-side dependencies,
implicit arguments, higher-order providers, generic components, abstract types,
getters, field access, extensible records, extensible variants, checking,
dispatching, namespaces, handlers, monadic handlers, async and Send bounds,
blanket traits, and the standalone error backends (`error_backends`). The set grows and subdivides over time. `cgp-macro-tests` follows
blanket traits, and the standalone error backends (`error_backends`). The crate's build script
turns the example in each error backend's README into a test module, which the `readme_*.rs` files
in `error_backends` include, so those examples are tested without being copied. The set grows and subdivides over time. `cgp-macro-tests` follows
the same shape, with `ident_with_type_params` for parser corner cases and the
failure-case targets `parser_rejections` and `invalid_expansion`.

Expand Down
64 changes: 64 additions & 0 deletions crates/tests/cgp-tests/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! Turns the wiring example in each error backend's README into a test module, so the
//! `error_backends` target compiles and runs the README's own code instead of a copy of it.
//!
//! Each README carries one `rust,ignore` block: `use` lines and items, then top-level statements
//! after the last item's closing brace. The block is split there, and the statements become the
//! body of a `#[test]` function, written to `$OUT_DIR/readme_<backend>.rs`.

use std::path::Path;
use std::{env, fs};

const BACKENDS: [&str; 3] = ["anyhow", "eyre", "std"];

fn main() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let out_dir = env::var("OUT_DIR").unwrap();

for backend in BACKENDS {
let readme = Path::new(&manifest_dir)
.join("../../standalone/error")
.join(format!("cgp-error-{backend}"))
.join("README.md");
println!("cargo::rerun-if-changed={}", readme.display());

let text = fs::read_to_string(&readme)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", readme.display()));
let module =
readme_test(backend, &text).unwrap_or_else(|e| panic!("{}: {e}", readme.display()));

fs::write(
Path::new(&out_dir).join(format!("readme_{backend}.rs")),
module,
)
.unwrap();
}
}

fn readme_test(backend: &str, readme: &str) -> Result<String, &'static str> {
let block = readme
.split("```rust,ignore\n")
.nth(1)
.and_then(|rest| rest.split("\n```").next())
.ok_or("no `rust,ignore` block")?;

let lines: Vec<&str> = block.lines().collect();
let split = lines
.iter()
.rposition(|line| line.starts_with('}'))
.ok_or("the block has no top-level item")?;

let items = lines[..=split].join("\n");
let statements: Vec<String> = lines[split + 1..]
.iter()
.filter(|line| !line.trim().is_empty())
.map(|line| format!(" {line}"))
.collect();
if statements.is_empty() {
return Err("the block has no statements after its items");
}

Ok(format!(
"{items}\n\n#[test]\nfn test_readme_{backend}() {{\n{}\n}}\n",
statements.join("\n")
))
}
106 changes: 106 additions & 0 deletions crates/tests/cgp-tests/tests/error_backends/eyre_location.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! With eyre's `track-caller` feature on, a report records where it was built. `raise_error` and
//! `wrap_error` are `#[track_caller]` in their trait declarations, which carries through every
//! provider impl and CGP's generated forwarding impls, so the recorded location is the line that
//! called `raise_error`: through plain, `open`, and namespace-path wiring alike, for each eyre
//! provider and for the generic `RaiseFrom`, and unchanged by wrapping.
//!
//! See cgp-knowledge-base/projects/error/cgp-error-eyre/testing.md.

use std::io;

use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent};
use cgp::extra::error::RaiseFrom;
use cgp::prelude::*;
use cgp_error_eyre::{DebugEyreError, DisplayEyreError, Error, RaiseEyreError, UseEyreError};

#[derive(Debug)]
pub struct Rejected;

pub struct PlainApp;

delegate_components! {
PlainApp {
ErrorTypeProviderComponent: UseEyreError,
[ErrorRaiserComponent, ErrorWrapperComponent]: RaiseEyreError,
}
}

pub struct OpenApp;

delegate_components! {
OpenApp {
open ErrorRaiserComponent;

ErrorTypeProviderComponent: UseEyreError,
@ErrorRaiserComponent.io::Error: RaiseEyreError,
@ErrorRaiserComponent.String: DisplayEyreError,
@ErrorRaiserComponent.Rejected: DebugEyreError,
}
}

pub struct NamespaceApp;

delegate_components! {
NamespaceApp {
namespace DefaultNamespace;

@cgp.core.error.ErrorTypeProviderComponent: UseEyreError,
@cgp.core.error.ErrorRaiserComponent.io::Error: RaiseEyreError,
}
}

pub struct GenericApp;

delegate_components! {
GenericApp {
ErrorTypeProviderComponent: UseType<Error>,
ErrorRaiserComponent: RaiseFrom,
}
}

/// The `file:line` the default handler prints under `Location:`, without the column.
fn location(error: &Error) -> String {
let debug = format!("{error:?}");
let located = debug
.split("Location:\n")
.nth(1)
.unwrap_or_else(|| panic!("no location in report: {debug}"));
let location = located.lines().next().unwrap().trim();
location.rsplit_once(':').unwrap().0.to_owned()
}

fn here(line: u32) -> String {
format!("{}:{line}", file!())
}

#[test]
fn test_eyre_location() {
let line = line!() + 1;
let error = PlainApp::raise_error(io::Error::other("disk full"));
assert_eq!(location(&error), here(line));

// Wrapping keeps the location where the report was first built.
let error = PlainApp::wrap_error(error, "while saving");
assert_eq!(location(&error), here(line));

let line = line!() + 1;
let error = OpenApp::raise_error(io::Error::other("disk full"));
assert_eq!(location(&error), here(line));

let line = line!() + 1;
let error = OpenApp::raise_error(String::from("bad input"));
assert_eq!(location(&error), here(line));

let line = line!() + 1;
let error = OpenApp::raise_error(Rejected);
assert_eq!(location(&error), here(line));

let line = line!() + 1;
let error = NamespaceApp::raise_error(io::Error::other("disk full"));
assert_eq!(location(&error), here(line));

// The generic `RaiseFrom` converts with `Into::into`, which is `#[track_caller]` too.
let line = line!() + 1;
let error = GenericApp::raise_error(io::Error::other("disk full"));
assert_eq!(location(&error), here(line));
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,12 @@ fn test_eyre_raise_and_wrap() {
);
assert!(error.downcast_ref::<io::Error>().is_some());

// The default handler prints the chain, then a backtrace section when `RUST_BACKTRACE` or
// `RUST_LIB_BACKTRACE` is set, so only the chain is matched exactly. The crate leaves eyre's
// `track-caller` feature off, since the recorded location would be a line inside the backend,
// so no `Location:` section appears.
// The default handler prints the chain, then a `Location:` section, then a backtrace section
// when `RUST_BACKTRACE` or `RUST_LIB_BACKTRACE` is set, so only the chain is matched exactly.
// `eyre_location.rs` checks the location itself.
let debug = format!("{error:?}");
assert!(
debug.starts_with("while starting\n\nCaused by:\n 0: while loading\n 1: no file"),
"unexpected report: {debug}"
);
assert!(!debug.contains("Location:"), "unexpected report: {debug}");
}
1 change: 1 addition & 0 deletions crates/tests/cgp-tests/tests/error_backends/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
pub mod anyhow_formatting;
pub mod anyhow_raise_and_wrap;
pub mod eyre_formatting;
pub mod eyre_location;
pub mod eyre_raise_and_wrap;
pub mod generic_equivalents;
pub mod namespace_wiring;
Expand Down
30 changes: 4 additions & 26 deletions crates/tests/cgp-tests/tests/error_backends/readme_anyhow.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,7 @@
//! The wiring example in `cgp-error-anyhow`'s README, copied verbatim so that CI compiles and runs it.
//! The README marks the block `ignore` because in the crate's own doctests `cgp` names `cgp-core`;
//! keep the two in sync when either changes.
//! The wiring example in `cgp-error-anyhow`'s README, compiled and run as a test. The README marks
//! the block `ignore` because in the crate's own doctests `cgp` names `cgp-core`; the build
//! script turns the block into this module, so the README stays the only copy.
//!
//! See cgp-knowledge-base/projects/error/cgp-error-anyhow/testing.md.

use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent};
use cgp::prelude::*;
use cgp_error_anyhow::{DisplayAnyhowError, RaiseAnyhowError, UseAnyhowError};

pub struct App;

delegate_components! {
App {
open ErrorRaiserComponent;

ErrorTypeProviderComponent: UseAnyhowError,
@ErrorRaiserComponent.std::io::Error: RaiseAnyhowError,
@ErrorRaiserComponent.String: DisplayAnyhowError,
ErrorWrapperComponent: RaiseAnyhowError,
}
}

#[test]
fn test_readme_anyhow() {
let error = App::raise_error(std::io::Error::other("disk full"));
let error = App::wrap_error(error, "while saving");
assert_eq!(format!("{error:#}"), "while saving: disk full");
}
include!(concat!(env!("OUT_DIR"), "/readme_anyhow.rs"));
30 changes: 4 additions & 26 deletions crates/tests/cgp-tests/tests/error_backends/readme_eyre.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,7 @@
//! The wiring example in `cgp-error-eyre`'s README, copied verbatim so that CI compiles and runs it.
//! The README marks the block `ignore` because in the crate's own doctests `cgp` names `cgp-core`;
//! keep the two in sync when either changes.
//! The wiring example in `cgp-error-eyre`'s README, compiled and run as a test. The README marks
//! the block `ignore` because in the crate's own doctests `cgp` names `cgp-core`; the build
//! script turns the block into this module, so the README stays the only copy.
//!
//! See cgp-knowledge-base/projects/error/cgp-error-eyre/testing.md.

use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent};
use cgp::prelude::*;
use cgp_error_eyre::{DisplayEyreError, RaiseEyreError, UseEyreError};

pub struct App;

delegate_components! {
App {
open ErrorRaiserComponent;

ErrorTypeProviderComponent: UseEyreError,
@ErrorRaiserComponent.std::io::Error: RaiseEyreError,
@ErrorRaiserComponent.String: DisplayEyreError,
ErrorWrapperComponent: RaiseEyreError,
}
}

#[test]
fn test_readme_eyre() {
let error = App::raise_error(std::io::Error::other("disk full"));
let error = App::wrap_error(error, "while saving");
assert_eq!(format!("{error:#}"), "while saving: disk full");
}
include!(concat!(env!("OUT_DIR"), "/readme_eyre.rs"));
31 changes: 4 additions & 27 deletions crates/tests/cgp-tests/tests/error_backends/readme_std.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,7 @@
//! The wiring example in `cgp-error-std`'s README, copied verbatim so that CI compiles and runs it.
//! The README marks the block `ignore` because in the crate's own doctests `cgp` names `cgp-core`;
//! keep the two in sync when either changes.
//! The wiring example in `cgp-error-std`'s README, compiled and run as a test. The README marks
//! the block `ignore` because in the crate's own doctests `cgp` names `cgp-core`; the build
//! script turns the block into this module, so the README stays the only copy.
//!
//! See cgp-knowledge-base/projects/error/cgp-error-std/testing.md.

use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent};
use cgp::prelude::*;
use cgp_error_std::{DisplayBoxedStdError, RaiseBoxedStdError, UseBoxedStdError};

pub struct App;

delegate_components! {
App {
open ErrorRaiserComponent;

ErrorTypeProviderComponent: UseBoxedStdError,
@ErrorRaiserComponent.std::io::Error: RaiseBoxedStdError,
@ErrorRaiserComponent.String: DisplayBoxedStdError,
ErrorWrapperComponent: RaiseBoxedStdError,
}
}

#[test]
fn test_readme_std() {
let error = App::raise_error(std::io::Error::other("disk full"));
let error = App::wrap_error(error, "while saving");
assert_eq!(format!("{error}"), "while saving");
assert_eq!(format!("{error:#}"), "while saving: disk full");
}
include!(concat!(env!("OUT_DIR"), "/readme_std.rs"));
Loading