#![forbid(unsafe_code)]
use std::collections::HashMap;
use std::sync::{Arc, Mutex, Once};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::registry::LookupSpan;
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct CapturedEvent {
level: tracing::Level,
target: String,
fields: HashMap<String, String>,
message: Option<String>,
parent_span_name: Option<String>,
}
impl CapturedEvent {
fn has_structured_fields(&self) -> bool {
self.fields.keys().any(|k| k != "message")
}
fn structured_field_names(&self) -> Vec<&str> {
self.fields
.keys()
.filter(|k| k.as_str() != "message")
.map(String::as_str)
.collect()
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct CapturedSpan {
name: String,
level: tracing::Level,
fields: HashMap<String, String>,
parent_name: Option<String>,
}
struct EventCapture {
events: Arc<Mutex<Vec<CapturedEvent>>>,
spans: Arc<Mutex<Vec<CapturedSpan>>>,
}
impl EventCapture {
fn new() -> (Self, EventCaptureHandle) {
let events = Arc::new(Mutex::new(Vec::new()));
let spans = Arc::new(Mutex::new(Vec::new()));
let handle = EventCaptureHandle {
events: events.clone(),
spans: spans.clone(),
};
let layer = Self { events, spans };
(layer, handle)
}
}
struct EventCaptureHandle {
events: Arc<Mutex<Vec<CapturedEvent>>>,
#[allow(dead_code)]
spans: Arc<Mutex<Vec<CapturedSpan>>>,
}
impl EventCaptureHandle {
fn events(&self) -> Vec<CapturedEvent> {
self.events.lock().unwrap().clone()
}
fn events_at_level(&self, level: tracing::Level) -> Vec<CapturedEvent> {
self.events()
.into_iter()
.filter(|e| e.level == level)
.collect()
}
}
struct FieldVisitor(Vec<(String, String)>);
impl tracing::field::Visit for FieldVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.0
.push((field.name().to_string(), format!("{value:?}")));
}
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
self.0.push((field.name().to_string(), value.to_string()));
}
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
self.0.push((field.name().to_string(), value.to_string()));
}
fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
self.0.push((field.name().to_string(), value.to_string()));
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
self.0.push((field.name().to_string(), value.to_string()));
}
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
self.0.push((field.name().to_string(), value.to_string()));
}
}
impl<S> tracing_subscriber::Layer<S> for EventCapture
where
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
{
fn on_new_span(
&self,
attrs: &tracing::span::Attributes<'_>,
_id: &tracing::span::Id,
ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let mut visitor = FieldVisitor(Vec::new());
attrs.record(&mut visitor);
let parent_name = ctx
.current_span()
.id()
.and_then(|id| ctx.span(id))
.map(|span_ref| span_ref.name().to_string());
let fields: HashMap<String, String> = visitor.0.into_iter().collect();
self.spans.lock().unwrap().push(CapturedSpan {
name: attrs.metadata().name().to_string(),
level: *attrs.metadata().level(),
fields,
parent_name,
});
}
fn on_event(&self, event: &tracing::Event<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) {
let mut visitor = FieldVisitor(Vec::new());
event.record(&mut visitor);
let fields: HashMap<String, String> = visitor.0.clone().into_iter().collect();
let message = fields.get("message").cloned();
let parent_span_name = ctx
.current_span()
.id()
.and_then(|id| ctx.span(id))
.map(|span_ref| span_ref.name().to_string());
self.events.lock().unwrap().push(CapturedEvent {
level: *event.metadata().level(),
target: event.metadata().target().to_string(),
fields,
message,
parent_span_name,
});
}
}
fn with_captured_events<F>(f: F) -> EventCaptureHandle
where
F: FnOnce(),
{
ensure_global_trace_level();
let (layer, handle) = EventCapture::new();
let subscriber = tracing_subscriber::registry()
.with(tracing_subscriber::filter::LevelFilter::TRACE)
.with(layer);
tracing::subscriber::with_default(subscriber, || {
tracing::callsite::rebuild_interest_cache();
f();
});
handle
}
fn ensure_global_trace_level() {
static INIT: Once = Once::new();
INIT.call_once(|| {
let subscriber =
tracing_subscriber::registry().with(tracing_subscriber::filter::LevelFilter::TRACE);
let _ = tracing::subscriber::set_global_default(subscriber);
});
}
#[test]
fn info_level_for_significant_state_changes() {
let handle = with_captured_events(|| {
tracing::info!(
old = "adaptive",
new = "active_only",
"tick strategy changed at runtime"
);
tracing::info!(count = 5u32, "loaded widget state from persistence");
tracing::info!(
inline_height = 24u16,
render_mode = "scroll_region",
"inline mode activated"
);
});
let info_events = handle.events_at_level(tracing::Level::INFO);
assert_eq!(
info_events.len(),
3,
"Should have 3 INFO events, got {}",
info_events.len()
);
for event in &info_events {
assert!(
event.has_structured_fields(),
"INFO event '{}' should have structured fields beyond message. Got fields: {:?}",
event.message.as_deref().unwrap_or("<none>"),
event.fields.keys().collect::<Vec<_>>()
);
}
}
#[test]
fn debug_level_for_operational_details() {
let handle = with_captured_events(|| {
tracing::debug!(
width = 80u16,
height = 24u16,
behavior = "coalesce",
"Resize event received"
);
tracing::debug!(
render_ms = 12u32,
budget_ms = 16u32,
"render phase exceeded budget"
);
tracing::debug!(
bucket = "mode:80x24",
upper_us = 15000.0f64,
budget_us = 16666.0f64,
risk = false,
"conformal risk gate"
);
});
let debug_events = handle.events_at_level(tracing::Level::DEBUG);
assert_eq!(
debug_events.len(),
3,
"Should have 3 DEBUG events, got {}",
debug_events.len()
);
for event in &debug_events {
assert!(
event.has_structured_fields(),
"DEBUG event '{}' should have structured fields. Got: {:?}",
event.message.as_deref().unwrap_or("<none>"),
event.fields.keys().collect::<Vec<_>>()
);
}
}
#[test]
fn warn_level_for_recoverable_errors() {
let handle = with_captured_events(|| {
tracing::warn!(error = "file not found", "failed to load widget state");
tracing::warn!(
inline_height = 24u16,
render_mode = "overlay",
"scrollback preservation failed during inline render"
);
});
let warn_events = handle.events_at_level(tracing::Level::WARN);
assert_eq!(warn_events.len(), 2, "Should have 2 WARN events");
for event in &warn_events {
assert!(
event.has_structured_fields(),
"WARN event '{}' should have structured error/context fields. Got: {:?}",
event.message.as_deref().unwrap_or("<none>"),
event.fields.keys().collect::<Vec<_>>()
);
}
}
#[test]
fn error_level_for_critical_issues() {
let handle = with_captured_events(|| {
tracing::error!(
task_id = "bg-worker-1",
panic_msg = "index out of bounds",
"spawned task panicked"
);
});
let error_events = handle.events_at_level(tracing::Level::ERROR);
assert_eq!(error_events.len(), 1, "Should have 1 ERROR event");
let error = &error_events[0];
assert!(
error.has_structured_fields(),
"ERROR events must have structured fields for actionability. Got: {:?}",
error.fields.keys().collect::<Vec<_>>()
);
}
#[test]
fn trace_level_for_fine_grained_flow() {
let handle = with_captured_events(|| {
tracing::trace!(
new_id_count = 3u32,
active_before = 2u32,
"subscription reconcile starting"
);
tracing::trace!(
strategy = "dirty",
cost_full = 1920u32,
cost_dirty = 48u32,
dirty_rows = 2u32,
total_rows = 24u32,
"diff strategy cost analysis"
);
});
let trace_events = handle.events_at_level(tracing::Level::TRACE);
assert_eq!(trace_events.len(), 2, "Should have 2 TRACE events");
for event in &trace_events {
let field_count = event.structured_field_names().len();
assert!(
field_count >= 2,
"TRACE event '{}' should have at least 2 structured fields for operational detail. Got {} fields: {:?}",
event.message.as_deref().unwrap_or("<none>"),
field_count,
event.structured_field_names()
);
}
}
#[test]
fn policy_debug_and_above_use_structured_fields() {
let handle = with_captured_events(|| {
tracing::info!(count = 5u32, "loaded widget state");
tracing::debug!(width = 80u16, height = 24u16, "resize received");
tracing::warn!(error = "timeout", "operation failed");
});
let events = handle.events();
for event in &events {
if event.level <= tracing::Level::DEBUG {
assert!(
event.has_structured_fields(),
"Events at {} level should have structured fields. \
Event message: '{}', fields: {:?}",
event.level,
event.message.as_deref().unwrap_or("<none>"),
event.fields.keys().collect::<Vec<_>>()
);
}
}
}
#[test]
fn policy_trace_events_include_component_and_operation_context() {
let handle = with_captured_events(|| {
tracing::trace!(
new_id_count = 3u32,
active_before = 2u32,
"subscription reconcile starting"
);
tracing::trace!(
strategy = "dirty",
cost_full = 1920u32,
cost_dirty = 48u32,
dirty_rows = 2u32,
total_rows = 24u32,
total_cells = 1920u32,
bayesian_enabled = true,
"diff strategy cost analysis"
);
tracing::trace!(
tick = 42u64,
active = "main_screen",
ticked = 1u32,
"tick dispatched to active screen"
);
});
let trace_events = handle.events_at_level(tracing::Level::TRACE);
for event in &trace_events {
let field_names = event.structured_field_names();
assert!(
field_names.len() >= 2,
"TRACE event must include at least 2 contextual fields (component/operation). \
Message: '{}', fields: {:?}",
event.message.as_deref().unwrap_or("<none>"),
field_names
);
}
}
#[test]
fn policy_error_events_are_actionable() {
let handle = with_captured_events(|| {
tracing::error!(
task_id = "bg-worker-1",
panic_msg = "index out of bounds: len is 0 but index is 1",
"spawned task panicked"
);
tracing::error!(
operation = "present",
error = "broken pipe",
"terminal output failed"
);
});
let error_events = handle.events_at_level(tracing::Level::ERROR);
assert!(!error_events.is_empty(), "Should have ERROR events to test");
for event in &error_events {
assert!(
event.has_structured_fields(),
"ERROR event must be actionable with structured context. \
Message: '{}', fields: {:?}",
event.message.as_deref().unwrap_or("<none>"),
event.fields.keys().collect::<Vec<_>>()
);
}
}
#[test]
fn policy_warn_events_include_error_or_reason() {
let handle = with_captured_events(|| {
tracing::warn!(error = "file not found", "failed to load widget state");
tracing::warn!(
inline_height = 24u16,
render_mode = "overlay",
"scrollback preservation failed"
);
tracing::warn!(
reason = "not yet implemented",
"SetMode received but runtime mode switching not yet implemented"
);
});
let warn_events = handle.events_at_level(tracing::Level::WARN);
for event in &warn_events {
let fields = event.structured_field_names();
let has_error_or_reason = fields.iter().any(|f| {
*f == "error" || *f == "reason" || *f == "inline_height" || *f == "render_mode"
});
assert!(
has_error_or_reason,
"WARN event should include 'error', 'reason', or contextual fields. \
Message: '{}', fields: {:?}",
event.message.as_deref().unwrap_or("<none>"),
fields
);
}
}
#[test]
fn policy_no_sensitive_data_in_events() {
let handle = with_captured_events(|| {
tracing::info!(count = 5u32, "loaded widget state");
tracing::debug!(
width = 80u16,
height = 24u16,
"terminal dimensions detected"
);
tracing::trace!(tick = 42u64, active = "main_screen", "tick processed");
});
let events = handle.events();
for event in &events {
for (key, value) in &event.fields {
assert!(
!value.starts_with('/') && !value.starts_with("C:\\"),
"Field '{key}' contains what looks like a file path: '{value}'. \
File paths should not appear in log events per redaction policy."
);
assert!(
!value.starts_with("0x"),
"Field '{key}' contains what looks like a memory address: '{value}'. \
Memory addresses should not appear in log events per redaction policy."
);
assert!(
!value.contains("HOME=") && !value.contains("PATH="),
"Field '{key}' contains what looks like env var data: '{value}'. \
Environment variables should not appear in log events per redaction policy."
);
}
}
}
#[test]
fn policy_event_types_are_enum_variants_not_content() {
let handle = with_captured_events(|| {
tracing::debug!(event_type = "Key", "input event processed");
tracing::debug!(event_type = "Mouse", "input event processed");
tracing::debug!(event_type = "Tick", "input event processed");
tracing::debug!(event_type = "Resize", "input event processed");
});
let events = handle.events();
let allowed_event_types = ["Key", "Mouse", "Tick", "Resize", "FocusGained", "FocusLost"];
for event in &events {
if let Some(event_type) = event.fields.get("event_type") {
assert!(
allowed_event_types.contains(&event_type.as_str()),
"event_type '{}' is not a recognized enum variant. \
Allowed: {:?}",
event_type,
allowed_event_types
);
}
}
}
#[test]
fn state_transitions_use_info_level() {
let handle = with_captured_events(|| {
tracing::info!(
old = "adaptive",
new = "active_only",
"tick strategy changed at runtime"
);
tracing::info!(
inline_height = 24u16,
render_mode = "scroll_region",
"inline mode activated"
);
});
let info_events = handle.events_at_level(tracing::Level::INFO);
assert!(
info_events.len() >= 2,
"State transitions should be logged at INFO level"
);
}
#[test]
fn diagnostic_details_use_debug_level() {
let handle = with_captured_events(|| {
tracing::debug!(
render_ms = 12u32,
budget_ms = 16u32,
"render phase exceeded budget"
);
tracing::debug!(
monotonic_counter_conformal_gate_triggers_total = 1u64,
bucket = "mode:80x24",
"conformal gate trigger"
);
});
let debug_events = handle.events_at_level(tracing::Level::DEBUG);
assert!(
debug_events.len() >= 2,
"Diagnostic/metric details should be logged at DEBUG level"
);
}
#[test]
fn fine_grained_flow_uses_trace_level() {
let handle = with_captured_events(|| {
tracing::trace!(
new_id_count = 3u32,
active_before = 2u32,
"subscription reconcile starting"
);
tracing::trace!(
strategy = "dirty",
cost_full = 1920u32,
cost_dirty = 48u32,
"diff strategy cost analysis"
);
});
let trace_events = handle.events_at_level(tracing::Level::TRACE);
assert!(
trace_events.len() >= 2,
"Fine-grained operational flow should be logged at TRACE level"
);
}
#[test]
fn field_names_use_snake_case() {
let handle = with_captured_events(|| {
tracing::info!(widget_count = 5u32, frame_idx = 42u64, "view complete");
tracing::debug!(
render_ms = 12u32,
budget_ms = 16u32,
dirty_rows = 2u32,
total_cells = 1920u32,
"render statistics"
);
});
let events = handle.events();
for event in &events {
for key in event.fields.keys() {
if key == "message" {
continue;
}
assert!(
key.chars()
.all(|c| c.is_ascii_lowercase() || c == '_' || c.is_ascii_digit() || c == '.'),
"Field name '{}' violates snake_case convention. \
Only lowercase, underscores, digits, and dots are allowed.",
key
);
}
}
}
#[test]
fn duration_fields_use_consistent_suffix() {
let handle = with_captured_events(|| {
tracing::debug!(
render_ms = 12u32,
budget_ms = 16u32,
"render phase budget check"
);
tracing::debug!(
present_ms = 3u32,
budget_ms = 5u32,
"present phase budget check"
);
});
let events = handle.events();
for event in &events {
for key in event.fields.keys() {
if key.contains("duration") || key.contains("latency") || key.contains("elapsed") {
assert!(
key.ends_with("_us") || key.ends_with("_ms"),
"Duration field '{}' should end with '_us' or '_ms' for unit clarity",
key
);
}
}
}
}
#[test]
fn boolean_fields_are_clear() {
let handle = with_captured_events(|| {
tracing::debug!(
bayesian_enabled = true,
bracket_supported = false,
"rendering configuration"
);
});
let events = handle.events();
let debug_event = &events[0];
assert_eq!(
debug_event
.fields
.get("bayesian_enabled")
.map(String::as_str),
Some("true")
);
assert_eq!(
debug_event
.fields
.get("bracket_supported")
.map(String::as_str),
Some("false")
);
}
#[test]
fn monotonic_counters_use_namespace() {
let handle = with_captured_events(|| {
tracing::debug!(
monotonic_counter_conformal_gate_triggers_total = 1u64,
bucket = "mode:80x24",
"conformal gate trigger"
);
tracing::debug!(
monotonic_histogram_conformal_prediction_interval_width_us = 1500.0f64,
bucket = "mode:80x24",
"conformal prediction interval width"
);
});
let events = handle.events();
let has_counter = events.iter().any(|e| {
e.fields
.keys()
.any(|k| k.starts_with("monotonic_counter_") || k.contains("monotonic.counter"))
});
assert!(
has_counter,
"Monotonic counter fields should use monotonic_counter_ or monotonic.counter namespace"
);
}
#[test]
fn events_inherit_parent_span_context() {
let handle = with_captured_events(|| {
let span = tracing::info_span!("ftui.render.frame", width = 80u16, height = 24u16);
let _guard = span.enter();
tracing::debug!(
render_ms = 12u32,
budget_ms = 16u32,
"render phase exceeded budget"
);
});
let events = handle.events();
assert_eq!(events.len(), 1);
assert_eq!(
events[0].parent_span_name.as_deref(),
Some("ftui.render.frame"),
"Event emitted inside ftui.render.frame span should have it as parent"
);
}
#[test]
fn events_without_span_have_no_parent() {
let handle = with_captured_events(|| {
tracing::info!(count = 5u32, "standalone info event");
});
let events = handle.events();
assert_eq!(events.len(), 1);
assert!(
events[0].parent_span_name.is_none(),
"Event outside any span should have no parent"
);
}
#[test]
fn test_data_follows_conventions() {
let test_field_names = [
"count",
"width",
"height",
"render_ms",
"budget_ms",
"error",
"inline_height",
"render_mode",
"task_id",
"panic_msg",
"operation",
"new_id_count",
"active_before",
"strategy",
"cost_full",
"cost_dirty",
"dirty_rows",
"total_rows",
"total_cells",
"bayesian_enabled",
"tick",
"active",
"ticked",
"event_type",
"widget_count",
"frame_idx",
"bucket",
"upper_us",
"budget_us",
"risk",
"degradation",
"sub_id",
];
for name in &test_field_names {
assert!(
name.chars()
.all(|c| c.is_ascii_lowercase() || c == '_' || c.is_ascii_digit()),
"Test field name '{}' should be snake_case",
name
);
}
}
#[test]
fn recoverable_errors_stay_at_warn() {
let handle = with_captured_events(|| {
tracing::warn!(error = "file not found", "failed to load widget state");
tracing::warn!(error = "timeout", "failed to save widget state");
});
let warn_events = handle.events_at_level(tracing::Level::WARN);
let error_events = handle.events_at_level(tracing::Level::ERROR);
assert_eq!(warn_events.len(), 2, "Recoverable errors should be WARN");
assert_eq!(
error_events.len(),
0,
"No ERROR events expected for recoverable conditions"
);
}
#[test]
fn performance_diagnostics_stay_at_debug() {
let handle = with_captured_events(|| {
tracing::debug!(
render_ms = 20u32,
budget_ms = 16u32,
"render phase exceeded budget"
);
tracing::debug!(
present_ms = 8u32,
budget_ms = 5u32,
"present phase exceeded budget"
);
tracing::debug!(
degradation = 2u32,
"frame skipped: budget exhausted before render"
);
});
let debug_events = handle.events_at_level(tracing::Level::DEBUG);
let warn_events = handle.events_at_level(tracing::Level::WARN);
assert_eq!(
debug_events.len(),
3,
"Budget diagnostics should be DEBUG level"
);
assert_eq!(
warn_events.len(),
0,
"Budget diagnostics should not escalate to WARN"
);
}
#[test]
fn event_messages_are_descriptive() {
let handle = with_captured_events(|| {
tracing::info!(count = 5u32, "loaded widget state from persistence");
tracing::debug!(width = 80u16, height = 24u16, "Resize event received");
tracing::warn!(error = "file not found", "failed to load widget state");
});
let events = handle.events();
for event in &events {
if let Some(ref msg) = event.message {
assert!(!msg.trim().is_empty(), "Event message should not be empty");
let word_count = msg.split_whitespace().count();
assert!(
word_count >= 2,
"Event message '{}' should be descriptive (at least 2 words), got {} word(s)",
msg,
word_count
);
}
}
}
#[test]
fn event_messages_follow_verb_convention() {
let handle = with_captured_events(|| {
tracing::info!(count = 5u32, "loaded widget state from persistence");
tracing::debug!(
render_ms = 12u32,
budget_ms = 16u32,
"render phase exceeded budget"
);
tracing::debug!(
degradation = 2u32,
"frame skipped: budget exhausted before render"
);
});
let events = handle.events();
for event in &events {
if let Some(ref msg) = event.message {
assert!(
!msg.starts_with("TODO") && !msg.starts_with("FIXME") && !msg.starts_with("HACK"),
"Event message '{}' should not contain TODO/FIXME/HACK markers",
msg
);
}
}
}