kerf 0.1.2

Simple tokio-based trace event collector
Documentation
use anyhow::Result;
use kerf::{ArcEvent, Config, Event, Kerf, Level, Match};
use std::sync::{Arc, Mutex};
use tokio::time::{Duration, sleep};

// Helper function to create a test event
fn create_test_event(level: Level, message: &str) -> ArcEvent {
    create_test_event_with_module(level, message, "test::module")
}

// Helper function to create a test event with specific module path
fn create_test_event_with_module(level: Level, message: &str, module_path: &str) -> ArcEvent {
    Arc::new(Event {
        id: 1,
        timestamp: chrono::Local::now(),
        level,
        target: "test".to_string(),
        name: "test_event".to_string(),
        module_path: Some(module_path.to_string()),
        file: Some("test.rs".to_string()),
        line: Some(42),
        message: message.to_string(),
        fields: std::collections::HashMap::new(),
        span_name: None,
        span_hierarchy: None,
    })
}

#[tokio::test]
async fn test_per_tab_callbacks() -> Result<()> {
    // Create shared storage for captured events
    let console_events = Arc::new(Mutex::new(Vec::new()));
    let error_events = Arc::new(Mutex::new(Vec::new()));
    let debug_events = Arc::new(Mutex::new(Vec::new()));

    // Create config with multiple tabs using different module patterns to avoid overlap
    let config = Config::empty()
        .with_tab("console", Match::info().module_pattern("console::*"))
        .with_tab("errors", Match::error().module_pattern("error::*"))
        .with_tab("debug", Match::debug().module_pattern("debug::*"));

    // Initialize kerf and set per-tab callbacks
    let console_events_clone = Arc::clone(&console_events);
    let error_events_clone = Arc::clone(&error_events);
    let debug_events_clone = Arc::clone(&debug_events);

    let kerf = Kerf::new_with_config(config)
        .with_tab_callback("console", move |event| {
            console_events_clone
                .lock()
                .unwrap()
                .push(event.message.clone());
        })
        .with_tab_callback("errors", move |event| {
            error_events_clone
                .lock()
                .unwrap()
                .push(event.message.clone());
        })
        .with_tab_callback("debug", move |event| {
            debug_events_clone
                .lock()
                .unwrap()
                .push(event.message.clone());
        })
        .build()
        .await?;

    // Send test events directly to kerf with different module paths
    let sender = kerf._get_sender_for_testing();
    sender.send(create_test_event_with_module(
        Level::INFO,
        "This is an info message",
        "console::main",
    ))?;
    sender.send(create_test_event_with_module(
        Level::ERROR,
        "This is an error message",
        "error::handler",
    ))?;
    sender.send(create_test_event_with_module(
        Level::DEBUG,
        "This is a debug message",
        "debug::trace",
    ))?;
    sender.send(create_test_event_with_module(
        Level::WARN,
        "This is a warning message",
        "other::module",
    ))?; // Should not be captured

    // Wait for events to be processed
    sleep(Duration::from_millis(100)).await;

    // Verify events were routed correctly
    let console_msgs = console_events.lock().unwrap();
    let error_msgs = error_events.lock().unwrap();
    let debug_msgs = debug_events.lock().unwrap();

    assert_eq!(console_msgs.len(), 1);
    assert!(console_msgs[0].contains("This is an info message"));

    assert_eq!(error_msgs.len(), 1);
    assert!(error_msgs[0].contains("This is an error message"));

    assert_eq!(debug_msgs.len(), 1);
    assert!(debug_msgs[0].contains("This is a debug message"));

    Ok(())
}

#[tokio::test]
async fn test_mixed_callbacks() -> Result<()> {
    // Test that per-tab callbacks and global callbacks can coexist
    let tab_events = Arc::new(Mutex::new(Vec::new()));
    #[allow(clippy::type_complexity)]
    let global_events: Arc<Mutex<Vec<(String, Vec<String>)>>> = Arc::new(Mutex::new(Vec::new()));

    let config = Config::empty()
        .with_tab("with_callback", Match::info().module_pattern("info::*"))
        .with_tab(
            "without_callback",
            Match::error().module_pattern("error::*"),
        );

    let tab_events_clone = Arc::clone(&tab_events);
    let global_events_clone = Arc::clone(&global_events);

    let kerf = Kerf::new_with_config(config)
        .with_tab_callback("with_callback", move |event| {
            tab_events_clone.lock().unwrap().push(event.message.clone());
        })
        .build()
        .await?;

    // Set global callback for tabs without per-tab callbacks
    kerf.set_callback(move |event, tab_names| {
        let tab_names_owned: Vec<String> = tab_names.iter().map(|s| s.to_string()).collect();
        global_events_clone
            .lock()
            .unwrap()
            .push((event.message.clone(), tab_names_owned));
    })?
    .await??;

    // Send test events directly
    let sender = kerf._get_sender_for_testing();
    sender.send(create_test_event_with_module(
        Level::INFO,
        "Info message",
        "info::test",
    ))?; // Should go to tab callback
    sender.send(create_test_event_with_module(
        Level::ERROR,
        "Error message",
        "error::test",
    ))?; // Should go to global callback

    // Wait for events to be processed
    sleep(Duration::from_millis(100)).await;

    // Verify routing
    let tab_msgs = tab_events.lock().unwrap();
    let global_msgs = global_events.lock().unwrap();

    assert_eq!(tab_msgs.len(), 1);
    assert!(tab_msgs[0].contains("Info message"));

    assert_eq!(global_msgs.len(), 1);
    assert!(global_msgs[0].0.contains("Error message"));
    assert_eq!(global_msgs[0].1, vec!["without_callback".to_string()]);

    Ok(())
}

#[tokio::test]
async fn test_dynamic_tab_callbacks() -> Result<()> {
    // Test adding and removing tab callbacks dynamically
    let events = Arc::new(Mutex::new(Vec::new()));

    let config = Config::empty().with_tab("dynamic", Match::info().all_modules());

    let kerf = Kerf::new_with_config(config);

    // Send event before callback is set
    let sender = kerf._get_sender_for_testing();
    sender.send(create_test_event(Level::INFO, "Message 1"))?;
    sleep(Duration::from_millis(50)).await;

    // Add callback dynamically
    let events_clone = Arc::clone(&events);
    kerf.set_tab_callback("dynamic", move |event| {
        events_clone.lock().unwrap().push(event.message.clone());
    })
    .await?;

    sender.send(create_test_event(Level::INFO, "Message 2"))?;
    sleep(Duration::from_millis(50)).await;

    // Remove callback
    kerf.remove_tab_callback("dynamic").await?;

    sender.send(create_test_event(Level::INFO, "Message 3"))?;
    sleep(Duration::from_millis(50)).await;

    // Only message 2 should have been captured
    let captured_events = events.lock().unwrap();
    assert_eq!(captured_events.len(), 1);
    assert!(captured_events[0].contains("Message 2"));

    Ok(())
}

#[tokio::test]
async fn test_multiple_tabs_same_event() -> Result<()> {
    // Test that an event can be captured by multiple tabs with different callbacks
    let tab1_events = Arc::new(Mutex::new(Vec::new()));
    let tab2_events = Arc::new(Mutex::new(Vec::new()));

    let config = Config::empty()
        .with_tab("all_info", Match::info().all_modules())
        .with_tab("all_levels", Match::trace().all_modules()); // Includes info

    let tab1_events_clone = Arc::clone(&tab1_events);
    let tab2_events_clone = Arc::clone(&tab2_events);

    let kerf = Kerf::new_with_config(config)
        .with_tab_callback("all_info", move |event| {
            tab1_events_clone
                .lock()
                .unwrap()
                .push(format!("TAB1: {}", event.message));
        })
        .with_tab_callback("all_levels", move |event| {
            tab2_events_clone
                .lock()
                .unwrap()
                .push(format!("TAB2: {}", event.message));
        })
        .build()
        .await?;

    let sender = kerf._get_sender_for_testing();
    sender.send(create_test_event(Level::INFO, "Shared message"))?;
    sleep(Duration::from_millis(100)).await;

    // Both tabs should have captured the event
    let tab1_msgs = tab1_events.lock().unwrap();
    let tab2_msgs = tab2_events.lock().unwrap();

    assert_eq!(tab1_msgs.len(), 1);
    assert!(tab1_msgs[0].contains("TAB1: Shared message"));

    assert_eq!(tab2_msgs.len(), 1);
    assert!(tab2_msgs[0].contains("TAB2: Shared message"));

    Ok(())
}

#[tokio::test]
async fn test_error_handling() -> Result<()> {
    let config = Config::empty().with_tab("test", Match::info().all_modules());

    let kerf = Kerf::new_with_config(config);

    // Try to set callback for non-existent tab
    let result = kerf.set_tab_callback("nonexistent", |_event| {}).await;
    assert!(result.is_err());

    // Try to remove callback for non-existent tab
    let result = kerf.remove_tab_callback("nonexistent").await;
    assert!(result.is_err());

    Ok(())
}

#[tokio::test]
async fn test_serializable_config() -> Result<()> {
    // Verify that Config remains serializable
    let config = Config::empty()
        .with_tab("test1", Match::info().all_modules())
        .with_tab("test2", Match::error().all_modules());

    // This should compile and work
    let serialized = serde_json::to_string(&config)?;
    let deserialized: Config = serde_json::from_str(&serialized)?;

    assert_eq!(config.tabs.len(), deserialized.tabs.len());
    assert_eq!(config.tabs[0].name, deserialized.tabs[0].name);
    assert_eq!(config.tabs[1].name, deserialized.tabs[1].name);

    Ok(())
}