mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
// Tests for mecha10_cli::services::logs

use mecha10_cli::commands::LogSource;
use mecha10_cli::services::{LogContentFilters, LogFileFilters, LogsService};
use std::collections::HashMap;
use tempfile::TempDir;
use tokio::io::AsyncWriteExt;

#[test]
fn test_logs_service_creation() {
    let temp_dir = TempDir::new().unwrap();
    let service = LogsService::new(temp_dir.path().to_path_buf());
    assert_eq!(service.logs_dir(), &temp_dir.path().to_path_buf());
}

#[tokio::test]
async fn test_collect_log_files_empty_dir() {
    let temp_dir = TempDir::new().unwrap();
    let service = LogsService::new(temp_dir.path().join("logs"));

    let filters = LogFileFilters {
        node: None,
        source: LogSource::All,
    };

    let files = service.collect_log_files(&filters).unwrap();
    assert!(files.is_empty());
}

#[tokio::test]
async fn test_collect_log_files_with_files() {
    let temp_dir = TempDir::new().unwrap();
    let logs_dir = temp_dir.path().join("logs");
    std::fs::create_dir_all(&logs_dir).unwrap();

    // Create test log files
    std::fs::write(logs_dir.join("camera_driver.log"), "log content").unwrap();
    std::fs::write(logs_dir.join("lidar_driver.log"), "log content").unwrap();
    std::fs::write(logs_dir.join("framework.log"), "log content").unwrap();

    let service = LogsService::new(logs_dir);

    // Test collecting all logs
    let filters = LogFileFilters {
        node: None,
        source: LogSource::All,
    };
    let files = service.collect_log_files(&filters).unwrap();
    assert_eq!(files.len(), 3);

    // Test filtering by node
    let filters = LogFileFilters {
        node: Some("camera".to_string()),
        source: LogSource::All,
    };
    let files = service.collect_log_files(&filters).unwrap();
    assert_eq!(files.len(), 1);
    assert!(files.contains_key("camera_driver"));

    // Test filtering by source type
    let filters = LogFileFilters {
        node: None,
        source: LogSource::Framework,
    };
    let files = service.collect_log_files(&filters).unwrap();
    assert_eq!(files.len(), 1);
    assert!(files.contains_key("framework"));
}

#[tokio::test]
async fn test_collect_log_files_ignores_non_log_files() {
    let temp_dir = TempDir::new().unwrap();
    let logs_dir = temp_dir.path().join("logs");
    std::fs::create_dir_all(&logs_dir).unwrap();

    // Create log files and non-log files
    std::fs::write(logs_dir.join("test.log"), "log content").unwrap();
    std::fs::write(logs_dir.join("test.txt"), "not a log").unwrap();
    std::fs::write(logs_dir.join("README.md"), "docs").unwrap();

    let service = LogsService::new(logs_dir);

    let filters = LogFileFilters {
        node: None,
        source: LogSource::All,
    };
    let files = service.collect_log_files(&filters).unwrap();

    // Should only find .log files
    assert_eq!(files.len(), 1);
    assert!(files.contains_key("test"));
}

#[tokio::test]
async fn test_collect_log_files_source_filter_nodes() {
    let temp_dir = TempDir::new().unwrap();
    let logs_dir = temp_dir.path().join("logs");
    std::fs::create_dir_all(&logs_dir).unwrap();

    // Create various log files
    std::fs::write(logs_dir.join("camera_driver.log"), "node log").unwrap();
    std::fs::write(logs_dir.join("lidar_driver.log"), "node log").unwrap();
    std::fs::write(logs_dir.join("framework.log"), "framework log").unwrap();
    std::fs::write(logs_dir.join("redis_service.log"), "service log").unwrap();

    let service = LogsService::new(logs_dir);

    let filters = LogFileFilters {
        node: None,
        source: LogSource::Nodes,
    };
    let files = service.collect_log_files(&filters).unwrap();

    // Should only find node logs (not framework or service)
    assert_eq!(files.len(), 2);
    assert!(files.contains_key("camera_driver"));
    assert!(files.contains_key("lidar_driver"));
}

#[tokio::test]
async fn test_collect_log_files_source_filter_services() {
    let temp_dir = TempDir::new().unwrap();
    let logs_dir = temp_dir.path().join("logs");
    std::fs::create_dir_all(&logs_dir).unwrap();

    // Create various log files
    std::fs::write(logs_dir.join("camera_driver.log"), "node log").unwrap();
    std::fs::write(logs_dir.join("redis_service.log"), "service log").unwrap();
    std::fs::write(logs_dir.join("postgres_service.log"), "service log").unwrap();

    let service = LogsService::new(logs_dir);

    let filters = LogFileFilters {
        node: None,
        source: LogSource::Services,
    };
    let files = service.collect_log_files(&filters).unwrap();

    // Should only find service logs
    assert_eq!(files.len(), 2);
    assert!(files.contains_key("redis_service"));
    assert!(files.contains_key("postgres_service"));
}

#[tokio::test]
async fn test_read_logs() {
    let temp_dir = TempDir::new().unwrap();
    let logs_dir = temp_dir.path().join("logs");
    std::fs::create_dir_all(&logs_dir).unwrap();

    // Create test log file
    let log_path = logs_dir.join("test.log");
    let mut file = tokio::fs::File::create(&log_path).await.unwrap();
    file.write_all(b"INFO line 1\nDEBUG line 2\nERROR line 3\n")
        .await
        .unwrap();
    file.flush().await.unwrap();

    let service = LogsService::new(logs_dir);

    let mut sources = HashMap::new();
    sources.insert("test".to_string(), log_path);

    // Test reading all lines
    let filters = LogContentFilters {
        pattern: None,
        level: None,
        lines: None,
    };
    let lines = service.read_logs(&sources, &filters).await.unwrap();
    assert_eq!(lines.len(), 3);
    assert_eq!(lines[0].source_name, "test");
    assert!(lines[0].content.contains("INFO line 1"));

    // Test filtering by level
    let filters = LogContentFilters {
        pattern: None,
        level: Some("ERROR".to_string()),
        lines: None,
    };
    let lines = service.read_logs(&sources, &filters).await.unwrap();
    assert_eq!(lines.len(), 1);
    assert!(lines[0].content.contains("ERROR"));

    // Test line limit
    let filters = LogContentFilters {
        pattern: None,
        level: None,
        lines: Some(2),
    };
    let lines = service.read_logs(&sources, &filters).await.unwrap();
    assert_eq!(lines.len(), 2);
}

#[tokio::test]
async fn test_read_logs_with_pattern_filter() {
    let temp_dir = TempDir::new().unwrap();
    let logs_dir = temp_dir.path().join("logs");
    std::fs::create_dir_all(&logs_dir).unwrap();

    let log_path = logs_dir.join("test.log");
    let mut file = tokio::fs::File::create(&log_path).await.unwrap();
    file.write_all(b"Starting camera\nCapturing frame\nCamera error occurred\n")
        .await
        .unwrap();
    file.flush().await.unwrap();

    let service = LogsService::new(logs_dir);

    let mut sources = HashMap::new();
    sources.insert("test".to_string(), log_path);

    let filters = LogContentFilters {
        pattern: Some("camera".to_string()),
        level: None,
        lines: None,
    };
    let lines = service.read_logs(&sources, &filters).await.unwrap();

    // Should match "camera" and "Camera" (case insensitive)
    assert_eq!(lines.len(), 2);
}

#[tokio::test]
async fn test_read_logs_multiple_sources() {
    let temp_dir = TempDir::new().unwrap();
    let logs_dir = temp_dir.path().join("logs");
    std::fs::create_dir_all(&logs_dir).unwrap();

    // Create multiple log files
    let camera_log = logs_dir.join("camera.log");
    let lidar_log = logs_dir.join("lidar.log");

    tokio::fs::write(&camera_log, "Camera line 1\nCamera line 2\n").await.unwrap();
    tokio::fs::write(&lidar_log, "Lidar line 1\n").await.unwrap();

    let service = LogsService::new(logs_dir);

    let mut sources = HashMap::new();
    sources.insert("camera".to_string(), camera_log);
    sources.insert("lidar".to_string(), lidar_log);

    let filters = LogContentFilters {
        pattern: None,
        level: None,
        lines: None,
    };
    let lines = service.read_logs(&sources, &filters).await.unwrap();

    // Should have lines from both sources
    assert_eq!(lines.len(), 3);
}

#[test]
fn test_should_display_line() {
    let temp_dir = TempDir::new().unwrap();
    let service = LogsService::new(temp_dir.path().to_path_buf());

    // Test no filters
    let filters = LogContentFilters {
        pattern: None,
        level: None,
        lines: None,
    };
    assert!(service.should_display_line("any line", &filters));

    // Test pattern filter
    let filters = LogContentFilters {
        pattern: Some("ERROR".to_string()),
        level: None,
        lines: None,
    };
    assert!(service.should_display_line("ERROR: something failed", &filters));
    assert!(!service.should_display_line("INFO: all good", &filters));

    // Test level filter
    let filters = LogContentFilters {
        pattern: None,
        level: Some("WARN".to_string()),
        lines: None,
    };
    assert!(service.should_display_line("WARN: warning message", &filters));
    assert!(!service.should_display_line("INFO: info message", &filters));

    // Test case insensitive pattern
    let filters = LogContentFilters {
        pattern: Some("error".to_string()),
        level: None,
        lines: None,
    };
    assert!(service.should_display_line("ERROR: something failed", &filters));
}

#[test]
fn test_should_display_line_combined_filters() {
    let temp_dir = TempDir::new().unwrap();
    let service = LogsService::new(temp_dir.path().to_path_buf());

    // Test pattern + level filter
    let filters = LogContentFilters {
        pattern: Some("connection".to_string()),
        level: Some("ERROR".to_string()),
        lines: None,
    };

    assert!(service.should_display_line("ERROR: connection failed", &filters));
    assert!(!service.should_display_line("ERROR: database failed", &filters));
    assert!(!service.should_display_line("INFO: connection established", &filters));
}

#[test]
fn test_open_for_follow() {
    let temp_dir = TempDir::new().unwrap();
    let logs_dir = temp_dir.path().join("logs");
    std::fs::create_dir_all(&logs_dir).unwrap();

    // Create test log file
    let log_path = logs_dir.join("test.log");
    std::fs::write(&log_path, "existing content\n").unwrap();

    let service = LogsService::new(logs_dir);

    let mut sources = HashMap::new();
    sources.insert("test".to_string(), log_path);

    let readers = service.open_for_follow(&sources).unwrap();
    assert_eq!(readers.len(), 1);
    assert!(readers.contains_key("test"));
}

#[test]
fn test_read_new_lines() {
    use std::io::Write;

    let temp_dir = TempDir::new().unwrap();
    let logs_dir = temp_dir.path().join("logs");
    std::fs::create_dir_all(&logs_dir).unwrap();

    let log_path = logs_dir.join("test.log");
    let mut file = std::fs::File::create(&log_path).unwrap();
    file.write_all(b"initial line\n").unwrap();
    file.flush().unwrap();
    drop(file);

    let service = LogsService::new(logs_dir);

    let mut sources = HashMap::new();
    sources.insert("test".to_string(), log_path.clone());

    let mut readers = service.open_for_follow(&sources).unwrap();
    let reader = readers.get_mut("test").unwrap();

    // Initially at end, should return no lines
    let filters = LogContentFilters {
        pattern: None,
        level: None,
        lines: None,
    };
    let lines = service.read_new_lines(reader, &filters).unwrap();
    assert_eq!(lines.len(), 0);

    // Append new content
    let mut file = std::fs::OpenOptions::new()
        .append(true)
        .open(&log_path)
        .unwrap();
    file.write_all(b"new line 1\nnew line 2\n").unwrap();
    file.flush().unwrap();
    drop(file);

    // Should now read new lines
    let lines = service.read_new_lines(reader, &filters).unwrap();
    assert_eq!(lines.len(), 2);
    assert!(lines[0].contains("new line 1"));
}