feat(errors): add stable error propagation
This commit is contained in:
1 parent
97acb2a2d7
commit
ba9c97ed3b
34 files changed
+1007
No files matched your search
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "kaptan-err-propagate-macros"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1"
|
||||||
|
quote = "1"
|
||||||
|
syn = { version = "2", features = ["full"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
trybuild = "1"
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
//! Attribute expansion for `kaptan-err-propagate`.
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use syn::{
|
||||||
|
Data, DeriveInput, Error, Fields, ItemFn, LitInt, ReturnType, Variant, parse_macro_input,
|
||||||
|
parse_quote, spanned::Spanned,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Implements `kaptan_err_propagate::ErrorReport`, [`core::fmt::Display`], and
|
||||||
|
/// [`core::error::Error`] for an explicitly numbered error enum.
|
||||||
|
///
|
||||||
|
/// Every variant must declare a stable nonzero
|
||||||
|
/// `#[kaptan_error(id = ...)]`. Duplicate IDs fail compilation within an enum
|
||||||
|
/// or crate and fail final linking when they originate in different crates.
|
||||||
|
#[proc_macro_derive(KaptanError, attributes(kaptan_error))]
|
||||||
|
pub fn derive_kaptan_error(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
expand_error(input)
|
||||||
|
.unwrap_or_else(Error::into_compile_error)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reports an [`Err`] only when it leaves the annotated function.
|
||||||
|
///
|
||||||
|
/// The function must have an explicit return type compatible with
|
||||||
|
/// [`core::result::Result`]; aliases are accepted. Const, unsafe, variadic,
|
||||||
|
/// and foreign-ABI functions are rejected because an immediately invoked
|
||||||
|
/// closure or async block cannot preserve those execution contracts without
|
||||||
|
/// additional unsafe or non-const machinery.
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn propagate(arguments: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
|
if !arguments.is_empty() {
|
||||||
|
return Error::new(
|
||||||
|
proc_macro2::Span::call_site(),
|
||||||
|
"kaptan_err_propagate::propagate does not accept arguments",
|
||||||
|
)
|
||||||
|
.into_compile_error()
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
let function = parse_macro_input!(input as ItemFn);
|
||||||
|
expand(function)
|
||||||
|
.unwrap_or_else(Error::into_compile_error)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand(mut function: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
|
||||||
|
validate_signature(&function)?;
|
||||||
|
|
||||||
|
let body = function.block;
|
||||||
|
function.block = if function.sig.asyncness.is_some() {
|
||||||
|
Box::new(syn::parse_quote!({
|
||||||
|
const __KAPTAN_RAISED_AT: &str = concat!(file!(), ":", line!());
|
||||||
|
let __kaptan_err_propagate_result = (async #body).await;
|
||||||
|
::kaptan_err_propagate::__private::report_result(
|
||||||
|
&__kaptan_err_propagate_result,
|
||||||
|
__KAPTAN_RAISED_AT,
|
||||||
|
);
|
||||||
|
__kaptan_err_propagate_result
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Box::new(syn::parse_quote!({
|
||||||
|
const __KAPTAN_RAISED_AT: &str = concat!(file!(), ":", line!());
|
||||||
|
let __kaptan_err_propagate_result = (|| #body)();
|
||||||
|
::kaptan_err_propagate::__private::report_result(
|
||||||
|
&__kaptan_err_propagate_result,
|
||||||
|
__KAPTAN_RAISED_AT,
|
||||||
|
);
|
||||||
|
__kaptan_err_propagate_result
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(quote!(#function))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_signature(function: &ItemFn) -> syn::Result<()> {
|
||||||
|
let signature = &function.sig;
|
||||||
|
if let Some(token) = signature.constness {
|
||||||
|
return Err(Error::new(
|
||||||
|
token.span(),
|
||||||
|
"kaptan_err_propagate::propagate does not support const functions",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(token) = signature.unsafety {
|
||||||
|
return Err(Error::new(
|
||||||
|
token.span(),
|
||||||
|
"kaptan_err_propagate::propagate does not support unsafe functions",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(abi) = &signature.abi {
|
||||||
|
return Err(Error::new(
|
||||||
|
abi.span(),
|
||||||
|
"kaptan_err_propagate::propagate does not support foreign-ABI functions",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(variadic) = &signature.variadic {
|
||||||
|
return Err(Error::new(
|
||||||
|
variadic.span(),
|
||||||
|
"kaptan_err_propagate::propagate does not support variadic functions",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(signature.output, ReturnType::Default) {
|
||||||
|
return Err(Error::new(
|
||||||
|
signature.output.span(),
|
||||||
|
"kaptan_err_propagate::propagate requires an explicit Result<T, E> return type",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_error(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
||||||
|
let DeriveInput {
|
||||||
|
ident,
|
||||||
|
generics,
|
||||||
|
data,
|
||||||
|
..
|
||||||
|
} = input;
|
||||||
|
let Data::Enum(data) = data else {
|
||||||
|
let kind = match data {
|
||||||
|
Data::Struct(_) => "structs",
|
||||||
|
Data::Union(_) => "unions",
|
||||||
|
Data::Enum(_) => unreachable!("enum handled above"),
|
||||||
|
};
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
ident,
|
||||||
|
format!("KaptanError can only be derived for enums, not {kind}"),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ids = BTreeMap::new();
|
||||||
|
let variants = data
|
||||||
|
.variants
|
||||||
|
.iter()
|
||||||
|
.map(|variant| {
|
||||||
|
let id = variant_id(variant)?;
|
||||||
|
if let Some(previous) = ids.insert(id, &variant.ident) {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
&variant.ident,
|
||||||
|
format!("Kaptan error ID {id} is already assigned to variant {previous}",),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok((variant, id))
|
||||||
|
})
|
||||||
|
.collect::<syn::Result<Vec<_>>>()?;
|
||||||
|
let identities = variants.iter().map(|(variant, id)| {
|
||||||
|
let variant_ident = &variant.ident;
|
||||||
|
let static_ident = format_ident!(
|
||||||
|
"__KAPTAN_ERROR_ID_{}_{}",
|
||||||
|
ident.to_string().to_uppercase(),
|
||||||
|
variant_ident.to_string().to_uppercase(),
|
||||||
|
);
|
||||||
|
let symbol = syn::LitStr::new(&format!("__kaptan_error_id_{id}"), variant_ident.span());
|
||||||
|
quote! {
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[used]
|
||||||
|
#[unsafe(export_name = #symbol)]
|
||||||
|
static #static_ident: u8 = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let arms = variants.iter().map(|(variant, id)| {
|
||||||
|
let variant_ident = &variant.ident;
|
||||||
|
let pattern = match &variant.fields {
|
||||||
|
Fields::Unit => quote!(Self::#variant_ident),
|
||||||
|
Fields::Unnamed(_) => quote!(Self::#variant_ident(..)),
|
||||||
|
Fields::Named(_) => quote!(Self::#variant_ident { .. }),
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
#pattern => ::kaptan_err_propagate::ErrorId::new(#id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
|
||||||
|
let mut error_generics = generics.clone();
|
||||||
|
error_generics
|
||||||
|
.make_where_clause()
|
||||||
|
.predicates
|
||||||
|
.push(parse_quote!(#ident #type_generics: ::core::fmt::Debug));
|
||||||
|
let (error_impl_generics, error_type_generics, error_where_clause) =
|
||||||
|
error_generics.split_for_impl();
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#(#identities)*
|
||||||
|
|
||||||
|
impl #impl_generics ::kaptan_err_propagate::ErrorReport for #ident #type_generics #where_clause {
|
||||||
|
fn error_id(&self) -> ::kaptan_err_propagate::ErrorId {
|
||||||
|
match self {
|
||||||
|
#(#arms),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #impl_generics ::core::fmt::Display for #ident #type_generics #where_clause {
|
||||||
|
fn fmt(
|
||||||
|
&self,
|
||||||
|
formatter: &mut ::core::fmt::Formatter<'_>,
|
||||||
|
) -> ::core::fmt::Result {
|
||||||
|
::core::fmt::Display::fmt(
|
||||||
|
&<Self as ::kaptan_err_propagate::ErrorReport>::error_id(self).get(),
|
||||||
|
formatter,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #error_impl_generics ::core::error::Error
|
||||||
|
for #ident #error_type_generics #error_where_clause {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn variant_id(variant: &Variant) -> syn::Result<u16> {
|
||||||
|
let mut id = None;
|
||||||
|
for attribute in &variant.attrs {
|
||||||
|
if !attribute.path().is_ident("kaptan_error") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
attribute.parse_nested_meta(|meta| {
|
||||||
|
if !meta.path.is_ident("id") {
|
||||||
|
return Err(meta.error("unsupported Kaptan error property; expected `id`"));
|
||||||
|
}
|
||||||
|
if id.is_some() {
|
||||||
|
return Err(meta.error("duplicate Kaptan error `id`"));
|
||||||
|
}
|
||||||
|
let literal: LitInt = meta.value()?.parse()?;
|
||||||
|
let value = literal.base10_parse::<u16>()?;
|
||||||
|
if value == 0 {
|
||||||
|
return Err(Error::new(
|
||||||
|
literal.span(),
|
||||||
|
"Kaptan error ID zero is reserved",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
id = Some(value);
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
id.ok_or_else(|| {
|
||||||
|
Error::new_spanned(
|
||||||
|
&variant.ident,
|
||||||
|
"missing `#[kaptan_error(id = ...)]` on Kaptan error variant",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#[test]
|
||||||
|
fn propagate_rejects_unsupported_signatures() {
|
||||||
|
let tests = trybuild::TestCases::new();
|
||||||
|
tests.pass("tests/ui/enum.rs");
|
||||||
|
tests.compile_fail("tests/ui/arguments.rs");
|
||||||
|
tests.compile_fail("tests/ui/const_fn.rs");
|
||||||
|
tests.compile_fail("tests/ui/duplicate_id.rs");
|
||||||
|
tests.compile_fail("tests/ui/duplicate_id_across_enums.rs");
|
||||||
|
tests.compile_fail("tests/ui/missing_id.rs");
|
||||||
|
tests.compile_fail("tests/ui/missing_return.rs");
|
||||||
|
tests.compile_fail("tests/ui/non_result.rs");
|
||||||
|
tests.compile_fail("tests/ui/no_error_report.rs");
|
||||||
|
tests.compile_fail("tests/ui/struct.rs");
|
||||||
|
tests.compile_fail("tests/ui/union.rs");
|
||||||
|
tests.compile_fail("tests/ui/unsafe_fn.rs");
|
||||||
|
tests.compile_fail("tests/ui/zero_id.rs");
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use kaptan_err_propagate_macros::propagate;
|
||||||
|
|
||||||
|
#[propagate(report)]
|
||||||
|
fn invalid() -> Result<(), ()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
error: kaptan_err_propagate::propagate does not accept arguments
|
||||||
|
--> tests/ui/arguments.rs:3:1
|
||||||
|
|
|
||||||
|
3 | #[propagate(report)]
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: this error originates in the attribute macro `propagate` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use kaptan_err_propagate_macros::propagate;
|
||||||
|
|
||||||
|
#[propagate]
|
||||||
|
const fn invalid() -> Result<(), ()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: kaptan_err_propagate::propagate does not support const functions
|
||||||
|
--> tests/ui/const_fn.rs:4:1
|
||||||
|
|
|
||||||
|
4 | const fn invalid() -> Result<(), ()> {
|
||||||
|
| ^^^^^
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
extern crate self as kaptan_err_propagate;
|
||||||
|
|
||||||
|
use kaptan_err_propagate_macros::KaptanError;
|
||||||
|
|
||||||
|
#[derive(Debug, KaptanError)]
|
||||||
|
enum DuplicateId {
|
||||||
|
#[kaptan_error(id = 42)]
|
||||||
|
First,
|
||||||
|
#[kaptan_error(id = 42)]
|
||||||
|
Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: Kaptan error ID 42 is already assigned to variant First
|
||||||
|
--> tests/ui/duplicate_id.rs:10:5
|
||||||
|
|
|
||||||
|
10 | Second,
|
||||||
|
| ^^^^^^
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
extern crate self as kaptan_err_propagate;
|
||||||
|
|
||||||
|
use kaptan_err_propagate_macros::KaptanError;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ErrorId(u16);
|
||||||
|
|
||||||
|
impl ErrorId {
|
||||||
|
pub const fn new(id: u16) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn get(self) -> u16 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ErrorReport {
|
||||||
|
fn error_id(&self) -> ErrorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, KaptanError)]
|
||||||
|
enum FirstError {
|
||||||
|
#[kaptan_error(id = 77)]
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, KaptanError)]
|
||||||
|
enum SecondError {
|
||||||
|
#[kaptan_error(id = 77)]
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let _ = FirstError::Failed.error_id();
|
||||||
|
let _ = SecondError::Failed.error_id();
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
error: symbol `__kaptan_error_id_77` is already defined
|
||||||
|
--> tests/ui/duplicate_id_across_enums.rs:28:17
|
||||||
|
|
|
||||||
|
28 | #[derive(Debug, KaptanError)]
|
||||||
|
| ^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: this error originates in the derive macro `KaptanError` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
extern crate self as kaptan_err_propagate;
|
||||||
|
|
||||||
|
use kaptan_err_propagate_macros::KaptanError;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ErrorId(u16);
|
||||||
|
|
||||||
|
impl ErrorId {
|
||||||
|
pub const fn new(id: u16) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn get(self) -> u16 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ErrorReport {
|
||||||
|
fn error_id(&self) -> ErrorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, KaptanError)]
|
||||||
|
enum Generic<'a, T>
|
||||||
|
where
|
||||||
|
T: Copy,
|
||||||
|
{
|
||||||
|
#[kaptan_error(id = 65201)]
|
||||||
|
Unit,
|
||||||
|
#[kaptan_error(id = 65202)]
|
||||||
|
Tuple(T, u8),
|
||||||
|
#[kaptan_error(id = 65203)]
|
||||||
|
Named { label: &'a str },
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
fn assert_error<T: core::error::Error>() {}
|
||||||
|
assert_error::<Generic<'_, u8>>();
|
||||||
|
assert_eq!(Generic::<u8>::Unit.error_id().get(), 65201);
|
||||||
|
assert_eq!(Generic::Tuple(1_u8, 2).error_id().get(), 65202);
|
||||||
|
let named: Generic<'_, u8> = Generic::Named { label: "test" };
|
||||||
|
assert_eq!(named.error_id().get(), 65203);
|
||||||
|
assert_eq!(named.to_string(), "65203");
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
extern crate self as kaptan_err_propagate;
|
||||||
|
|
||||||
|
use kaptan_err_propagate_macros::KaptanError;
|
||||||
|
|
||||||
|
#[derive(Debug, KaptanError)]
|
||||||
|
enum MissingId {
|
||||||
|
Variant,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: missing `#[kaptan_error(id = ...)]` on Kaptan error variant
|
||||||
|
--> tests/ui/missing_id.rs:7:5
|
||||||
|
|
|
||||||
|
7 | Variant,
|
||||||
|
| ^^^^^^^
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
use kaptan_err_propagate_macros::propagate;
|
||||||
|
|
||||||
|
#[propagate]
|
||||||
|
fn invalid() {}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
error: kaptan_err_propagate::propagate requires an explicit Result<T, E> return type
|
||||||
|
--> tests/ui/missing_return.rs:3:1
|
||||||
|
|
|
||||||
|
3 | #[propagate]
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: this error originates in the attribute macro `propagate` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use kaptan_err_propagate_macros::propagate;
|
||||||
|
|
||||||
|
extern crate self as kaptan_err_propagate;
|
||||||
|
|
||||||
|
pub trait ErrorReport {}
|
||||||
|
|
||||||
|
pub mod __private {
|
||||||
|
pub fn report_result<T, E: crate::ErrorReport>(_: &Result<T, E>, _: &'static str) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PlainError;
|
||||||
|
|
||||||
|
#[propagate]
|
||||||
|
fn invalid() -> Result<(), PlainError> {
|
||||||
|
Err(PlainError)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
error[E0277]: the trait bound `PlainError: ErrorReport` is not satisfied
|
||||||
|
--> tests/ui/no_error_report.rs:13:1
|
||||||
|
|
|
||||||
|
13 | #[propagate]
|
||||||
|
| ^^^^^^^^^^^^ the trait `ErrorReport` is not implemented for `PlainError`
|
||||||
|
|
|
||||||
|
help: this trait has no implementations, consider adding one
|
||||||
|
--> tests/ui/no_error_report.rs:5:1
|
||||||
|
|
|
||||||
|
5 | pub trait ErrorReport {}
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
note: required by a bound in `report_result`
|
||||||
|
--> tests/ui/no_error_report.rs:8:32
|
||||||
|
|
|
||||||
|
8 | pub fn report_result<T, E: crate::ErrorReport>(_: &Result<T, E>, _: &'static str) {}
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ required by this bound in `report_result`
|
||||||
|
= note: this error originates in the attribute macro `propagate` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use kaptan_err_propagate_macros::propagate;
|
||||||
|
|
||||||
|
extern crate self as kaptan_err_propagate;
|
||||||
|
|
||||||
|
pub mod __private {
|
||||||
|
pub fn report_result<T, E>(_: &Result<T, E>, _: &'static str) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[propagate]
|
||||||
|
fn invalid() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
error[E0308]: mismatched types
|
||||||
|
--> tests/ui/non_result.rs:9:1
|
||||||
|
|
|
||||||
|
9 | #[propagate]
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
| |
|
||||||
|
| expected `&Result<_, _>`, found `&bool`
|
||||||
|
| arguments to this function are incorrect
|
||||||
|
|
|
||||||
|
= note: expected reference `&Result<_, _>`
|
||||||
|
found reference `&bool`
|
||||||
|
note: function defined here
|
||||||
|
--> tests/ui/non_result.rs:6:12
|
||||||
|
|
|
||||||
|
6 | pub fn report_result<T, E>(_: &Result<T, E>, _: &'static str) {}
|
||||||
|
| ^^^^^^^^^^^^^ ----------------
|
||||||
|
= note: this error originates in the attribute macro `propagate` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use kaptan_err_propagate_macros::KaptanError;
|
||||||
|
|
||||||
|
#[derive(KaptanError)]
|
||||||
|
struct NotAnError {
|
||||||
|
code: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: KaptanError can only be derived for enums, not structs
|
||||||
|
--> tests/ui/struct.rs:4:8
|
||||||
|
|
|
||||||
|
4 | struct NotAnError {
|
||||||
|
| ^^^^^^^^^^
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use kaptan_err_propagate_macros::KaptanError;
|
||||||
|
|
||||||
|
#[derive(KaptanError)]
|
||||||
|
union NotAnError {
|
||||||
|
code: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: KaptanError can only be derived for enums, not unions
|
||||||
|
--> tests/ui/union.rs:4:7
|
||||||
|
|
|
||||||
|
4 | union NotAnError {
|
||||||
|
| ^^^^^^^^^^
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use kaptan_err_propagate_macros::propagate;
|
||||||
|
|
||||||
|
#[propagate]
|
||||||
|
unsafe fn invalid() -> Result<(), ()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: kaptan_err_propagate::propagate does not support unsafe functions
|
||||||
|
--> tests/ui/unsafe_fn.rs:4:1
|
||||||
|
|
|
||||||
|
4 | unsafe fn invalid() -> Result<(), ()> {
|
||||||
|
| ^^^^^^
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
extern crate self as kaptan_err_propagate;
|
||||||
|
|
||||||
|
use kaptan_err_propagate_macros::KaptanError;
|
||||||
|
|
||||||
|
#[derive(Debug, KaptanError)]
|
||||||
|
enum ZeroId {
|
||||||
|
#[kaptan_error(id = 0)]
|
||||||
|
Variant,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
error: Kaptan error ID zero is reserved
|
||||||
|
--> tests/ui/zero_id.rs:7:25
|
||||||
|
|
|
||||||
|
7 | #[kaptan_error(id = 0)]
|
||||||
|
| ^
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "kaptan-err-propagate"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
kaptan-err-propagate-macros = { path = "../kaptan-err-propagate-macros" }
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
//! Fixed, allocation-free typed-error ingress.
|
||||||
|
|
||||||
|
use core::{
|
||||||
|
cell::UnsafeCell,
|
||||||
|
mem::MaybeUninit,
|
||||||
|
sync::atomic::{AtomicU8, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{ErrorId, ErrorRecord};
|
||||||
|
|
||||||
|
static GLOBAL_INGRESS: ErrorIngress = ErrorIngress::new();
|
||||||
|
|
||||||
|
const EMPTY: u8 = 0;
|
||||||
|
const WRITING: u8 = 1;
|
||||||
|
const READY: u8 = 2;
|
||||||
|
const READING: u8 = 3;
|
||||||
|
|
||||||
|
struct ErrorIngress {
|
||||||
|
state: AtomicU8,
|
||||||
|
pending: UnsafeCell<MaybeUninit<ErrorRecord>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: `state` gives exactly one producer or consumer exclusive access to
|
||||||
|
// `pending`; neither side waits for a preempted owner.
|
||||||
|
unsafe impl Sync for ErrorIngress {}
|
||||||
|
|
||||||
|
impl ErrorIngress {
|
||||||
|
const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: AtomicU8::new(EMPTY),
|
||||||
|
pending: UnsafeCell::new(MaybeUninit::uninit()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish(&self, id: ErrorId, raised_at: &'static str) {
|
||||||
|
if self
|
||||||
|
.state
|
||||||
|
.compare_exchange(EMPTY, WRITING, Ordering::Acquire, Ordering::Relaxed)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// SAFETY: the successful state transition grants this producer sole
|
||||||
|
// access until it publishes READY with release ordering.
|
||||||
|
unsafe {
|
||||||
|
(*self.pending.get()).write(ErrorRecord::new(id, raised_at));
|
||||||
|
}
|
||||||
|
self.state.store(READY, Ordering::Release);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take(&self) -> Option<ErrorRecord> {
|
||||||
|
if self
|
||||||
|
.state
|
||||||
|
.compare_exchange(READY, READING, Ordering::Acquire, Ordering::Relaxed)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// SAFETY: READY proves that the sole producer initialized the record;
|
||||||
|
// READING grants this consumer exclusive access until EMPTY.
|
||||||
|
let record = unsafe { (*self.pending.get()).assume_init_read() };
|
||||||
|
self.state.store(EMPTY, Ordering::Release);
|
||||||
|
Some(record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn publish(id: ErrorId, raised_at: &'static str) {
|
||||||
|
GLOBAL_INGRESS.publish(id, raised_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the pending typed firmware error, if one exists.
|
||||||
|
pub fn take() -> Option<ErrorRecord> {
|
||||||
|
GLOBAL_INGRESS.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::ErrorIngress;
|
||||||
|
use crate::ErrorId;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retains_the_first_id_and_location_until_consumed() {
|
||||||
|
let ingress = ErrorIngress::new();
|
||||||
|
ingress.publish(ErrorId::new(65402), "first.rs:10");
|
||||||
|
ingress.publish(ErrorId::new(65403), "second.rs:20");
|
||||||
|
|
||||||
|
let first = ingress.take().expect("first error retained");
|
||||||
|
assert_eq!(first.id(), 65402);
|
||||||
|
assert_eq!(first.raised_at(), "first.rs:10");
|
||||||
|
assert!(ingress.take().is_none());
|
||||||
|
|
||||||
|
ingress.publish(ErrorId::new(65403), "second.rs:20");
|
||||||
|
let second = ingress.take().expect("slot reusable after consumption");
|
||||||
|
assert_eq!(second.id(), 65403);
|
||||||
|
assert_eq!(second.raised_at(), "second.rs:20");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
//! Bounded typed firmware errors and function-boundary propagation.
|
||||||
|
//!
|
||||||
|
//! This crate owns error identity and ingress. It contains no logging levels,
|
||||||
|
//! filtering, MAVLink, board, filesystem, or clock dependency.
|
||||||
|
//!
|
||||||
|
//! `#[propagate]` reports at each annotated boundary. Annotate the terminal
|
||||||
|
//! boundary where an error enters diagnostics; nested annotated callers would
|
||||||
|
//! attempt to report the same propagation again. The bounded ingress retaining
|
||||||
|
//! only one pending error is backpressure, not semantic duplicate suppression.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
extern crate std;
|
||||||
|
|
||||||
|
mod ingress;
|
||||||
|
mod record;
|
||||||
|
mod sink;
|
||||||
|
|
||||||
|
pub use ingress::take;
|
||||||
|
pub use kaptan_err_propagate_macros::{KaptanError, propagate};
|
||||||
|
pub use record::{ErrorId, ErrorRecord, ErrorReport};
|
||||||
|
pub use sink::ErrorSink;
|
||||||
|
|
||||||
|
/// Implementation details used only by macro expansions.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod __private {
|
||||||
|
use crate::ErrorReport;
|
||||||
|
|
||||||
|
/// Reports an error only when it is the final result leaving an annotated
|
||||||
|
/// function. Borrowing preserves the exact result returned to the caller.
|
||||||
|
#[inline]
|
||||||
|
pub fn report_result<T, E: ErrorReport>(
|
||||||
|
result: &core::result::Result<T, E>,
|
||||||
|
raised_at: &'static str,
|
||||||
|
) {
|
||||||
|
if let Err(error) = result {
|
||||||
|
crate::ingress::publish(error.error_id(), raised_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
//! Stable error IDs and bounded pending records.
|
||||||
|
|
||||||
|
/// Stable nonzero identity for one typed firmware error variant.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct ErrorId(u16);
|
||||||
|
|
||||||
|
impl ErrorId {
|
||||||
|
/// Creates one explicitly assigned error identity.
|
||||||
|
pub const fn new(id: u16) -> Self {
|
||||||
|
assert!(id != 0, "error ID zero is reserved");
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the compact wire representation.
|
||||||
|
pub const fn get(self) -> u16 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implemented by [`crate::KaptanError`] derives.
|
||||||
|
///
|
||||||
|
/// Obtaining an ID never publishes, allocates, or changes safety state.
|
||||||
|
pub trait ErrorReport {
|
||||||
|
/// Returns the stable ID assigned to this error variant.
|
||||||
|
fn error_id(&self) -> ErrorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorReport for ErrorId {
|
||||||
|
fn error_id(&self) -> ErrorId {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One reported typed firmware error and its reporting boundary.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ErrorRecord {
|
||||||
|
id: ErrorId,
|
||||||
|
raised_at: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorRecord {
|
||||||
|
pub(crate) const fn new(id: ErrorId, raised_at: &'static str) -> Self {
|
||||||
|
Self { id, raised_at }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stable machine-readable identity for the error type and variant.
|
||||||
|
pub const fn id(self) -> u16 {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compile-time `file:line` of the propagation boundary that reported it.
|
||||||
|
pub const fn raised_at(self) -> &'static str {
|
||||||
|
self.raised_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{ErrorId, ErrorRecord};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_keeps_only_the_explicit_id_and_reporting_boundary() {
|
||||||
|
let record = ErrorRecord::new(ErrorId::new(65004), "module.rs:42");
|
||||||
|
assert_eq!(record.id(), 65004);
|
||||||
|
assert_eq!(record.raised_at(), "module.rs:42");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
//! One-slot retry ownership for a typed-error sink.
|
||||||
|
|
||||||
|
use crate::ErrorRecord;
|
||||||
|
|
||||||
|
/// Retains one typed error until a lower sink accepts it.
|
||||||
|
///
|
||||||
|
/// Typed errors are never filtered. A composition may give them priority over
|
||||||
|
/// best-effort logs without coupling the producer to its transport.
|
||||||
|
pub struct ErrorSink {
|
||||||
|
pending: Option<ErrorRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorSink {
|
||||||
|
/// Creates an empty sink.
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self { pending: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this sink can accept a record from composition-level fanout.
|
||||||
|
pub const fn is_empty(&self) -> bool {
|
||||||
|
self.pending.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retains one record, returning it unchanged when this sink is occupied.
|
||||||
|
pub fn publish_record(&mut self, record: ErrorRecord) -> Result<(), ErrorRecord> {
|
||||||
|
if self.pending.is_some() {
|
||||||
|
return Err(record);
|
||||||
|
}
|
||||||
|
self.pending = Some(record);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts the pending record and clears it only when `submit` accepts it.
|
||||||
|
///
|
||||||
|
/// Returns whether a record was attempted, independently of admission.
|
||||||
|
pub fn submit_next(&mut self, submit: impl FnOnce(&ErrorRecord) -> bool) -> bool {
|
||||||
|
let Some(record) = self.pending else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if submit(&record) {
|
||||||
|
self.pending = None;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ErrorSink {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::ErrorSink;
|
||||||
|
use crate::{ErrorId, ErrorRecord};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retains_one_error_until_the_sink_accepts_it() {
|
||||||
|
while crate::take().is_some() {}
|
||||||
|
crate::ingress::publish(ErrorId::new(65401), "test.rs:10");
|
||||||
|
|
||||||
|
let mut sink = ErrorSink::new();
|
||||||
|
let record = crate::take().expect("published error");
|
||||||
|
sink.publish_record(record).unwrap();
|
||||||
|
assert!(!sink.is_empty());
|
||||||
|
assert!(sink.submit_next(|record| {
|
||||||
|
assert_eq!(*record, ErrorRecord::new(ErrorId::new(65401), "test.rs:10"));
|
||||||
|
false
|
||||||
|
}));
|
||||||
|
assert!(sink.submit_next(|record| {
|
||||||
|
assert_eq!(record.id(), 65401);
|
||||||
|
assert_eq!(record.raised_at(), "test.rs:10");
|
||||||
|
true
|
||||||
|
}));
|
||||||
|
assert!(sink.is_empty());
|
||||||
|
assert!(!sink.submit_next(|_| true));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
use kaptan_err_propagate::{ErrorId, ErrorReport, KaptanError};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||||
|
enum TestError {
|
||||||
|
#[kaptan_error(id = 65001)]
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||||
|
enum OuterError {
|
||||||
|
#[kaptan_error(id = 65002)]
|
||||||
|
Converted(TestError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TestError> for OuterError {
|
||||||
|
fn from(error: TestError) -> Self {
|
||||||
|
Self::Converted(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fail() -> Result<(), TestError> {
|
||||||
|
Err(TestError::Failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn middle() -> Result<(), TestError> {
|
||||||
|
fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[kaptan_err_propagate::propagate]
|
||||||
|
fn succeeds() -> Result<u8, TestError> {
|
||||||
|
Ok(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[kaptan_err_propagate::propagate]
|
||||||
|
fn annotated_outer() -> Result<(), TestError> {
|
||||||
|
middle()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[kaptan_err_propagate::propagate]
|
||||||
|
fn annotated_inner() -> Result<(), TestError> {
|
||||||
|
fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unannotated_outer() -> Result<(), TestError> {
|
||||||
|
annotated_inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[kaptan_err_propagate::propagate]
|
||||||
|
fn handles_inner_error() -> Result<(), TestError> {
|
||||||
|
let _handled = fail().is_err();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[kaptan_err_propagate::propagate]
|
||||||
|
fn returns_explicit_error(fail_at_boundary: bool) -> Result<(), TestError> {
|
||||||
|
if fail_at_boundary {
|
||||||
|
return Err(TestError::Failed);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliasedResult<T> = core::result::Result<T, TestError>;
|
||||||
|
|
||||||
|
#[kaptan_err_propagate::propagate]
|
||||||
|
fn returns_alias() -> AliasedResult<()> {
|
||||||
|
fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[kaptan_err_propagate::propagate]
|
||||||
|
fn reports_final_converted_error() -> Result<(), OuterError> {
|
||||||
|
fail()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[kaptan_err_propagate::propagate]
|
||||||
|
fn reports_error_id() -> Result<(), ErrorId> {
|
||||||
|
Err(ErrorId::new(65003))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[kaptan_err_propagate::propagate]
|
||||||
|
async fn async_boundary() -> Result<(), TestError> {
|
||||||
|
fail()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Service;
|
||||||
|
|
||||||
|
impl Service {
|
||||||
|
#[kaptan_err_propagate::propagate]
|
||||||
|
fn method(&self, fail_at_boundary: bool) -> Result<(), TestError> {
|
||||||
|
if fail_at_boundary {
|
||||||
|
fail()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_record() -> kaptan_err_propagate::ErrorRecord {
|
||||||
|
kaptan_err_propagate::take().expect("escaping error must be reported")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_only_final_errors_leaving_annotated_boundaries() {
|
||||||
|
while kaptan_err_propagate::take().is_some() {}
|
||||||
|
|
||||||
|
assert_eq!(succeeds(), Ok(7));
|
||||||
|
assert!(kaptan_err_propagate::take().is_none());
|
||||||
|
|
||||||
|
assert_eq!(handles_inner_error(), Ok(()));
|
||||||
|
assert!(kaptan_err_propagate::take().is_none());
|
||||||
|
|
||||||
|
assert_eq!(annotated_outer(), Err(TestError::Failed));
|
||||||
|
assert_eq!(take_record().id(), TestError::Failed.error_id().get());
|
||||||
|
|
||||||
|
assert_eq!(unannotated_outer(), Err(TestError::Failed));
|
||||||
|
assert_eq!(take_record().id(), TestError::Failed.error_id().get());
|
||||||
|
|
||||||
|
assert_eq!(returns_explicit_error(true), Err(TestError::Failed));
|
||||||
|
assert_eq!(take_record().id(), TestError::Failed.error_id().get());
|
||||||
|
|
||||||
|
assert_eq!(returns_alias(), Err(TestError::Failed));
|
||||||
|
assert_eq!(take_record().id(), TestError::Failed.error_id().get());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reports_final_converted_error(),
|
||||||
|
Err(OuterError::Converted(TestError::Failed))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
take_record().id(),
|
||||||
|
OuterError::Converted(TestError::Failed).error_id().get()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(reports_error_id(), Err(ErrorId::new(65003)));
|
||||||
|
assert_eq!(take_record().id(), 65003);
|
||||||
|
|
||||||
|
assert_eq!(Service.method(false), Ok(()));
|
||||||
|
assert!(kaptan_err_propagate::take().is_none());
|
||||||
|
|
||||||
|
let mut future = core::pin::pin!(async_boundary());
|
||||||
|
let mut context = core::task::Context::from_waker(core::task::Waker::noop());
|
||||||
|
assert_eq!(
|
||||||
|
core::future::Future::poll(future.as_mut(), &mut context),
|
||||||
|
core::task::Poll::Ready(Err(TestError::Failed))
|
||||||
|
);
|
||||||
|
let record = take_record();
|
||||||
|
assert_eq!(record.id(), TestError::Failed.error_id().get());
|
||||||
|
assert!(record.raised_at().contains("propagate.rs:"));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user