use std::fmt;
use std::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WarningSeverity {
Info,
Low,
Medium,
High,
Critical,
}
impl fmt::Display for WarningSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Info => f.write_str("INFO"),
Self::Low => f.write_str("LOW"),
Self::Medium => f.write_str("MEDIUM"),
Self::High => f.write_str("HIGH"),
Self::Critical => f.write_str("CRITICAL"),
}
}
}
#[derive(Debug, Clone)]
pub struct ExtensionWarning {
pub code: &'static str,
pub severity: WarningSeverity,
pub message: String,
pub cwe: Option<u32>,
}
impl fmt::Display for ExtensionWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}: {}", self.severity, self.code, self.message)?;
if let Some(cwe) = self.cwe {
write!(f, " (CWE-{cwe})")?;
}
Ok(())
}
}
pub struct WarningCollector {
warnings: Mutex<Vec<ExtensionWarning>>,
}
impl WarningCollector {
#[must_use]
pub const fn new() -> Self {
Self {
warnings: Mutex::new(Vec::new()),
}
}
pub fn emit(&self, warning: ExtensionWarning) {
if let Ok(mut warnings) = self.warnings.lock() {
warnings.push(warning);
}
}
#[must_use]
pub fn len(&self) -> usize {
self.warnings.lock().map(|w| w.len()).unwrap_or(0)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn snapshot(&self) -> Vec<ExtensionWarning> {
self.warnings.lock().map(|w| w.clone()).unwrap_or_default()
}
pub fn drain(&self) -> Vec<ExtensionWarning> {
self.warnings
.lock()
.map(|mut w| std::mem::take(&mut *w))
.unwrap_or_default()
}
pub fn clear(&self) {
if let Ok(mut warnings) = self.warnings.lock() {
warnings.clear();
}
}
}
impl Default for WarningCollector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn severity_display() {
assert_eq!(WarningSeverity::Info.to_string(), "INFO");
assert_eq!(WarningSeverity::Low.to_string(), "LOW");
assert_eq!(WarningSeverity::Medium.to_string(), "MEDIUM");
assert_eq!(WarningSeverity::High.to_string(), "HIGH");
assert_eq!(WarningSeverity::Critical.to_string(), "CRITICAL");
}
#[test]
fn warning_display_without_cwe() {
let w = ExtensionWarning {
code: "TEST_WARN",
severity: WarningSeverity::Medium,
message: "something happened".into(),
cwe: None,
};
assert_eq!(w.to_string(), "[MEDIUM] TEST_WARN: something happened");
}
#[test]
fn warning_display_with_cwe() {
let w = ExtensionWarning {
code: "TLS_NO_VERIFY",
severity: WarningSeverity::High,
message: "TLS verification disabled".into(),
cwe: Some(295),
};
assert_eq!(
w.to_string(),
"[HIGH] TLS_NO_VERIFY: TLS verification disabled (CWE-295)"
);
}
#[test]
fn collector_emit_and_drain() {
let c = WarningCollector::new();
assert!(c.is_empty());
assert_eq!(c.len(), 0);
c.emit(ExtensionWarning {
code: "A",
severity: WarningSeverity::Low,
message: "first".into(),
cwe: None,
});
c.emit(ExtensionWarning {
code: "B",
severity: WarningSeverity::High,
message: "second".into(),
cwe: Some(200),
});
assert_eq!(c.len(), 2);
assert!(!c.is_empty());
let warnings = c.drain();
assert_eq!(warnings.len(), 2);
assert_eq!(warnings[0].code, "A");
assert_eq!(warnings[1].code, "B");
assert!(c.is_empty());
}
#[test]
fn collector_snapshot_does_not_clear() {
let c = WarningCollector::new();
c.emit(ExtensionWarning {
code: "X",
severity: WarningSeverity::Info,
message: "test".into(),
cwe: None,
});
let snap = c.snapshot();
assert_eq!(snap.len(), 1);
assert_eq!(c.len(), 1);
}
#[test]
fn collector_clear() {
let c = WarningCollector::new();
c.emit(ExtensionWarning {
code: "Y",
severity: WarningSeverity::Critical,
message: "urgent".into(),
cwe: Some(798),
});
assert_eq!(c.len(), 1);
c.clear();
assert!(c.is_empty());
}
#[test]
fn collector_default() {
let c = WarningCollector::default();
assert!(c.is_empty());
}
#[test]
fn collector_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<WarningCollector>();
}
#[test]
fn severity_clone_eq_hash() {
use std::collections::HashSet;
let s1 = WarningSeverity::High;
let s2 = s1;
assert_eq!(s1, s2);
let mut set = HashSet::new();
set.insert(WarningSeverity::Low);
set.insert(WarningSeverity::High);
assert_eq!(set.len(), 2);
}
#[test]
fn collector_drain_returns_in_order() {
let c = WarningCollector::new();
for i in 0..10 {
c.emit(ExtensionWarning {
code: "SEQ",
severity: WarningSeverity::Info,
message: format!("msg-{i}"),
cwe: None,
});
}
let warnings = c.drain();
assert_eq!(warnings.len(), 10);
for (i, w) in warnings.iter().enumerate() {
assert_eq!(w.message, format!("msg-{i}"));
}
}
#[test]
fn collector_clear_then_emit() {
let c = WarningCollector::new();
c.emit(ExtensionWarning {
code: "A",
severity: WarningSeverity::Low,
message: "first".into(),
cwe: None,
});
c.clear();
c.emit(ExtensionWarning {
code: "B",
severity: WarningSeverity::High,
message: "second".into(),
cwe: None,
});
let warnings = c.drain();
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].code, "B");
}
#[test]
fn warning_clone() {
let w = ExtensionWarning {
code: "TEST",
severity: WarningSeverity::Critical,
message: "important".into(),
cwe: Some(798),
};
let w2 = w.clone();
assert_eq!(w.code, w2.code);
assert_eq!(w.severity, w2.severity);
assert_eq!(w.message, w2.message);
assert_eq!(w.cwe, w2.cwe);
}
#[test]
fn warning_debug_contains_fields() {
let w = ExtensionWarning {
code: "DBG",
severity: WarningSeverity::Medium,
message: "test debug".into(),
cwe: Some(100),
};
let debug = format!("{w:?}");
assert!(debug.contains("DBG"));
assert!(debug.contains("Medium"));
assert!(debug.contains("test debug"));
assert!(debug.contains("100"));
}
#[test]
fn snapshot_after_drain_is_empty() {
let c = WarningCollector::new();
c.emit(ExtensionWarning {
code: "X",
severity: WarningSeverity::Info,
message: "test".into(),
cwe: None,
});
let _ = c.drain();
let snap = c.snapshot();
assert!(snap.is_empty());
}
#[test]
fn multiple_drains_second_is_empty() {
let c = WarningCollector::new();
c.emit(ExtensionWarning {
code: "X",
severity: WarningSeverity::Info,
message: "test".into(),
cwe: None,
});
let first = c.drain();
assert_eq!(first.len(), 1);
let second = c.drain();
assert!(second.is_empty());
}
}