use crate::ErrorKind;
use std::sync::atomic::{AtomicU8, Ordering};
#[doc(hidden)]
pub use static_assertions;
#[doc(hidden)]
pub use tracing::{Level, event};
use paste::paste;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u8)]
#[non_exhaustive]
pub enum ProtocolWarningMode {
Off = 0,
Warn = 1,
}
impl ProtocolWarningMode {
fn from_raw(raw: u8) -> Option<Self> {
match raw {
0 => Some(Self::Off),
1 => Some(Self::Warn),
_ => None,
}
}
}
static PROTOCOL_WARNING_MODE: AtomicU8 = AtomicU8::new(ProtocolWarningMode::Off as u8);
pub fn set_protocol_warning_mode(mode: ProtocolWarningMode) {
PROTOCOL_WARNING_MODE.store(mode as u8, Ordering::Relaxed);
}
pub fn protocol_warning_mode() -> ProtocolWarningMode {
let raw = PROTOCOL_WARNING_MODE.load(Ordering::Relaxed);
ProtocolWarningMode::from_raw(raw).unwrap_or(ProtocolWarningMode::Off)
}
#[doc(hidden)]
#[inline]
pub fn __should_promote_to_warn(kind: ErrorKind) -> bool {
kind.is_always_a_warning()
|| (protocol_warning_mode() == ProtocolWarningMode::Warn
&& kind == ErrorKind::TorProtocolViolation)
}
impl ErrorKind {
pub fn is_always_a_warning(&self) -> bool {
matches!(self, ErrorKind::Internal | ErrorKind::BadApiUsage)
}
}
#[macro_export]
macro_rules! event_report {
($level:expr, $err:expr) => {
$crate::event_report!($level, $err,)
};
($level:expr, $err:expr, $($arg:tt)*) => {
{
use $crate::{tracing as tr, HasKind as _, };
let err = $err;
let kind = err.kind();
if tr::Level::WARN < $level && tr::__should_promote_to_warn(kind) {
$crate::event_report!(@raw tr::Level::WARN, err, $($arg)*);
} else {
$crate::event_report!(@raw $level, err, $($arg)*);
}
}
};
(@raw $level:expr, $err:expr) => {
$crate::event_report!(@raw $level, $err,)
};
(@raw $level:expr, $err:expr, $($arg:tt)*) => {
{
use $crate::tracing as tr;
use ::std::ops::Deref as _;
tr::event!(
$level,
error = ((&($err)).deref() as &dyn std::error::Error),
$($arg)*
)
}
}
}
macro_rules! define_report_macros { {
# $title_1:tt
LEVEL
# $title_2:tt
$D:tt
$( [$($flag:tt)*] $level:ident )*
} => { $( paste!{
# $title_1
#[doc = concat!("`", stringify!( [< $level:upper >] ), "`")]
# $title_2
#[doc = concat!("use tor_error::", stringify!($level), "_report;")]
#[doc = concat!(stringify!($level), "_report!",
r#"(err, "Cheese exhausted (ephemeral)");"#)]
#[doc = concat!(stringify!($level), "_report!",
r#"(err, "Unable to parse message {:?}", msg);"#)]
#[macro_export]
macro_rules! [< $level _report >] {
( $D err:expr ) => {
$D crate::event_report!($($flag)*
$D crate::tracing::Level::[< $level:upper >],
$D err)
};
( $D err:expr, $D ($D rest:tt)* ) => {
$D crate::event_report!($($flag)*
$D crate::tracing::Level::[< $level:upper >],
$D err, $D ($D rest)*)
}
}
} )* } }
define_report_macros! {
LEVEL
$ [] trace
[] debug
[] info
}
define_report_macros! {
LEVEL
$ [@raw] warn
[@raw] error
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use crate::internal;
use crate::report::ErrorReport;
use std::sync::Mutex;
use thiserror::Error;
use tracing_test::traced_test;
static PROTOCOL_MODE_TEST_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn protocol_warning_mode_roundtrip_and_fallback() {
let _lock = PROTOCOL_MODE_TEST_LOCK.lock().expect("poisoned mutex");
struct RestoreMode(super::ProtocolWarningMode);
impl Drop for RestoreMode {
fn drop(&mut self) {
super::set_protocol_warning_mode(self.0);
}
}
let original = super::protocol_warning_mode();
let _restore = RestoreMode(original);
super::set_protocol_warning_mode(super::ProtocolWarningMode::Off);
assert_eq!(
super::protocol_warning_mode(),
super::ProtocolWarningMode::Off
);
super::set_protocol_warning_mode(super::ProtocolWarningMode::Warn);
assert_eq!(
super::protocol_warning_mode(),
super::ProtocolWarningMode::Warn
);
super::PROTOCOL_WARNING_MODE.store(u8::MAX, std::sync::atomic::Ordering::Relaxed);
assert_eq!(
super::protocol_warning_mode(),
super::ProtocolWarningMode::Off
);
}
#[derive(Debug)]
struct KindError(super::ErrorKind);
impl std::fmt::Display for KindError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "kind error")
}
}
impl std::error::Error for KindError {}
impl crate::HasKind for KindError {
fn kind(&self) -> super::ErrorKind {
self.0
}
}
#[test]
#[traced_test]
#[allow(clippy::cognitive_complexity)]
fn event_report_protocol_warning_policy() {
let _lock = PROTOCOL_MODE_TEST_LOCK.lock().expect("poisoned mutex");
struct RestoreMode(super::ProtocolWarningMode);
impl Drop for RestoreMode {
fn drop(&mut self) {
super::set_protocol_warning_mode(self.0);
}
}
let original = super::protocol_warning_mode();
let _restore = RestoreMode(original);
let msg_off = "torproto-off-debug";
super::set_protocol_warning_mode(super::ProtocolWarningMode::Off);
let err = KindError(super::ErrorKind::TorProtocolViolation);
debug_report!(err, "{msg_off}");
let msg_warn = "torproto-warn-promoted";
super::set_protocol_warning_mode(super::ProtocolWarningMode::Warn);
let err = KindError(super::ErrorKind::TorProtocolViolation);
debug_report!(err, "{msg_warn}");
let msg_internal = "internal-always-warn";
super::set_protocol_warning_mode(super::ProtocolWarningMode::Off);
let err = KindError(super::ErrorKind::Internal);
debug_report!(err, "{msg_internal}");
let msg_other = "other-kind-stays-debug";
super::set_protocol_warning_mode(super::ProtocolWarningMode::Warn);
let err = KindError(super::ErrorKind::TorDirectoryUnusable);
debug_report!(err, "{msg_other}");
logs_assert(|lines: &[&str]| {
let level_for = |needle: &str| {
lines
.iter()
.find(|line| line.contains(needle))
.ok_or_else(|| format!("missing log line containing '{needle}'"))
.and_then(|line| {
line.split_whitespace()
.find(|tok| {
matches!(*tok, "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR")
})
.ok_or_else(|| format!("could not parse level from line: {line}"))
})
};
let msg_off_level = level_for(msg_off)?;
if msg_off_level != "DEBUG" {
return Err(format!(
"expected DEBUG for '{msg_off}', got {msg_off_level}"
));
}
let msg_warn_level = level_for(msg_warn)?;
if msg_warn_level != "WARN" {
return Err(format!(
"expected WARN for '{msg_warn}', got {msg_warn_level}"
));
}
let msg_internal_level = level_for(msg_internal)?;
if msg_internal_level != "WARN" {
return Err(format!(
"expected WARN for '{msg_internal}', got {msg_internal_level}"
));
}
let msg_other_level = level_for(msg_other)?;
if msg_other_level != "DEBUG" {
return Err(format!(
"expected DEBUG for '{msg_other}', got {msg_other_level}"
));
}
Ok(())
});
}
#[derive(Error, Debug)]
#[error("my error")]
struct MyError;
#[test]
#[traced_test]
#[allow(clippy::cognitive_complexity)]
fn warn_report() {
let me = MyError;
let _ = me.report();
warn_report!(me, "reporting unwrapped");
let ae = anyhow::Error::from(me).context("context");
let _ = ae.report();
warn_report!(ae, "reporting anyhow");
let ie = internal!("Foo was not initialized");
let _ = ie.report();
warn_report!(ie);
}
}