postfix-log-parser 0.2.0

高性能模块化Postfix日志解析器,经3.2GB生产数据验证,SMTPD事件100%准确率
Documentation
//! master_parser.rs 模块测试

use postfix_log_parser::{events::base::PostfixLogLevel, MasterParser};
use std::fs::{self, File};
use std::io::Write;

/// 从真实日志文件中获取测试数据
fn get_real_log_lines() -> Vec<String> {
    let log_content = fs::read_to_string("logs/test1.log")
        .expect("无法读取 logs/test1.log 文件,请确保日志文件存在");

    let mut lines: Vec<String> = log_content
        .lines()
        .filter(|line| !line.trim().is_empty())
        .map(|line| line.to_string())
        .collect();

    // 如果最后一行没有换行符,需要单独处理
    if !log_content.ends_with('\n') && !log_content.is_empty() {
        if let Some(last_part) = log_content.split('\n').last() {
            if !last_part.trim().is_empty() && !lines.iter().any(|line| line == last_part) {
                lines.push(last_part.to_string());
            }
        }
    }

    lines
}

/// 创建输出目录
fn ensure_output_dir() {
    if let Err(e) = fs::create_dir_all("test_outputs") {
        eprintln!("警告: 无法创建输出目录: {}", e);
    }
}

/// 输出解析结果到文件
fn write_parse_result_to_file(filename: &str, content: &str) {
    ensure_output_dir();
    let filepath = format!("test_outputs/{}", filename);

    match File::create(&filepath) {
        Ok(mut file) => {
            if let Err(e) = file.write_all(content.as_bytes()) {
                eprintln!("警告: 无法写入文件 {}: {}", filepath, e);
            } else {
                println!("✅ 解析结果已输出到: {}", filepath);
            }
        }
        Err(e) => {
            eprintln!("警告: 无法创建文件 {}: {}", filepath, e);
        }
    }
}

#[test]
fn test_parse_base_info() {
    let parser = MasterParser::new();
    let real_logs = get_real_log_lines();

    // 使用第一行真实日志进行测试
    let log_line = &real_logs[0]; // "Jun 05 15:40:52 m01 postfix/smtpd[89]: disconnect from localhost[127.0.0.1]:60090 ehlo=2 mail=1 rcpt=1 bdat=1 quit=1 commands=6"

    let base_info = parser.parse_base_info(log_line).unwrap();

    // 生成详细的解析结果报告
    let mut output = String::new();
    output.push_str("=== Master Parser - 基础信息解析测试结果 ===\n\n");
    output.push_str("测试时间: 2024-12-19\n");
    output.push_str(&format!("测试日志: {}\n\n", log_line));

    output.push_str("解析结果:\n");
    output.push_str(&format!("  主机名: {}\n", base_info.hostname));
    output.push_str(&format!("  组件: {}\n", base_info.component));
    output.push_str(&format!("  进程ID: {}\n", base_info.process_id));
    output.push_str(&format!("  日志等级: {:?}\n", base_info.log_level));
    output.push_str(&format!("  消息内容: {}\n", base_info.message));
    output.push_str(&format!(
        "  时间戳: {}\n",
        base_info.timestamp.format("%Y-%m-%d %H:%M:%S")
    ));

    output.push_str("\n断言验证:\n");
    output.push_str(&format!(
        "  ✅ 主机名 = 'm01': {}\n",
        base_info.hostname == "m01"
    ));
    output.push_str(&format!(
        "  ✅ 组件 = 'smtpd': {}\n",
        base_info.component == "smtpd"
    ));
    output.push_str(&format!(
        "  ✅ 进程ID = 89: {}\n",
        base_info.process_id == 89
    ));
    output.push_str(&format!(
        "  ✅ 日志等级 = Info: {}\n",
        base_info.log_level == PostfixLogLevel::Info
    ));

    write_parse_result_to_file("master_parser_basic_info.txt", &output);

    assert_eq!(base_info.hostname, "m01");
    assert_eq!(base_info.component, "smtpd");
    assert_eq!(base_info.process_id, 89);
    assert_eq!(base_info.log_level, PostfixLogLevel::Info);
    assert_eq!(
        base_info.message,
        "disconnect from localhost[127.0.0.1]:60090 ehlo=2 mail=1 rcpt=1 bdat=1 quit=1 commands=6"
    );
}

#[test]
fn test_log_level_extraction() {
    let parser = MasterParser::new();
    let real_logs = get_real_log_lines();

    let mut output = String::new();
    output.push_str("=== Master Parser - 日志等级提取测试结果 ===\n\n");
    output.push_str("测试时间: 2024-12-19\n");

    // 找到真实的warning日志进行测试
    let warning_log = real_logs
        .iter()
        .find(|line| line.contains("warning:"))
        .expect("应该存在warning级别的日志");

    output.push_str("\n警告日志测试:\n");
    output.push_str(&format!("  日志内容: {}\n", warning_log));

    let base_info = parser.parse_base_info(warning_log).unwrap();
    output.push_str(&format!("  解析等级: {:?}\n", base_info.log_level));
    output.push_str(&format!("  消息内容: {}\n", base_info.message));
    output.push_str(&format!(
        "  ✅ 等级为Warning: {}\n",
        base_info.log_level == PostfixLogLevel::Warning
    ));
    output.push_str(&format!(
        "  ✅ 包含dict_nis_init: {}\n",
        base_info.message.contains("dict_nis_init")
    ));

    // 使用真实的Info级别日志进行测试(不包含warning的第一行)
    let info_log = real_logs
        .iter()
        .find(|line| !line.contains("warning:"))
        .expect("应该存在Info级别的日志");

    output.push_str("\nInfo日志测试:\n");
    output.push_str(&format!("  日志内容: {}\n", info_log));

    let base_info_info = parser.parse_base_info(info_log).unwrap();
    output.push_str(&format!("  解析等级: {:?}\n", base_info_info.log_level));
    output.push_str(&format!(
        "  ✅ 等级为Info: {}\n",
        base_info_info.log_level == PostfixLogLevel::Info
    ));

    write_parse_result_to_file("master_parser_log_levels.txt", &output);

    assert_eq!(base_info.log_level, PostfixLogLevel::Warning);
    assert!(base_info.message.contains("dict_nis_init"));
    assert_eq!(base_info_info.log_level, PostfixLogLevel::Info);
}

#[test]
fn test_component_recognition() {
    let parser = MasterParser::new();
    let real_logs = get_real_log_lines();

    let mut output = String::new();
    output.push_str("=== Master Parser - 组件识别测试结果 ===\n\n");
    output.push_str("测试时间: 2024-12-19\n");

    // 从真实日志中找到的各种组件进行测试
    let test_cases = vec![
        ("smtpd", "postfix/smtpd["),
        ("qmgr", "postfix/qmgr["),
        ("smtp", "postfix/smtp["),
        ("cleanup", "postfix/cleanup["),
    ];

    for (expected_component, search_pattern) in test_cases {
        // 从真实日志中找到对应组件的日志
        let component_log = real_logs
            .iter()
            .find(|line| line.contains(search_pattern))
            .expect(&format!("应该存在{}组件的日志", expected_component));

        output.push_str(&format!("\n{} 组件测试:\n", expected_component));
        output.push_str(&format!("  搜索模式: {}\n", search_pattern));
        output.push_str(&format!("  找到日志: {}\n", component_log));

        let base_info = parser.parse_base_info(component_log).unwrap();
        output.push_str(&format!("  解析组件: {}\n", base_info.component));
        output.push_str(&format!("  进程ID: {}\n", base_info.process_id));
        output.push_str(&format!(
            "  ✅ 组件匹配: {}\n",
            base_info.component == expected_component
        ));

        assert_eq!(base_info.component, expected_component);
    }

    write_parse_result_to_file("master_parser_components.txt", &output);
}

#[test]
fn test_all_real_logs_parsing() {
    let parser = MasterParser::new();
    let real_logs = get_real_log_lines();

    let mut successful_parses = 0;
    let mut failed_parses = 0;
    let mut component_stats = std::collections::HashMap::new();
    let mut output = String::new();

    output.push_str("=== Master Parser - 全量真实日志解析测试结果 ===\n\n");
    output.push_str("测试时间: 2024-12-19\n");
    output.push_str(&format!("总日志数: {}\n\n", real_logs.len()));

    output.push_str("解析详情:\n");

    for (line_num, log_line) in real_logs.iter().enumerate() {
        match parser.parse_base_info(log_line) {
            Ok(base_info) => {
                successful_parses += 1;
                *component_stats
                    .entry(base_info.component.clone())
                    .or_insert(0) += 1;

                // 验证所有解析结果都有基本字段
                assert!(!base_info.hostname.is_empty());
                assert!(!base_info.component.is_empty());
                assert!(base_info.process_id > 0);
                assert!(!base_info.message.is_empty());

                // 只记录前10行和最后10行的详细信息,避免文件过大
                if line_num < 10 || line_num >= real_logs.len() - 10 {
                    output.push_str(&format!(
                        "  [{}] ✅ {} | {}[{}] | {}\n",
                        line_num + 1,
                        base_info.timestamp.format("%H:%M:%S"),
                        base_info.component,
                        base_info.process_id,
                        if base_info.message.len() > 50 {
                            format!("{}...", &base_info.message[..50])
                        } else {
                            base_info.message.clone()
                        }
                    ));
                }
            }
            Err(e) => {
                failed_parses += 1;
                output.push_str(&format!(
                    "  [{}] ❌ 解析失败: {} - 错误: {}\n",
                    line_num + 1,
                    if log_line.len() > 50 {
                        &log_line[..50]
                    } else {
                        log_line
                    },
                    e
                ));
            }
        }
    }

    if real_logs.len() > 20 {
        output.push_str(&format!(
            "  ... (省略中间{}行详情) ...\n",
            real_logs.len() - 20
        ));
    }

    output.push_str("\n统计结果:\n");
    output.push_str(&format!("  成功解析: {}\n", successful_parses));
    output.push_str(&format!("  解析失败: {}\n", failed_parses));
    output.push_str(&format!("  总数: {}\n", real_logs.len()));
    output.push_str(&format!(
        "  成功率: {:.2}%\n",
        (successful_parses as f64 / real_logs.len() as f64) * 100.0
    ));

    output.push_str("\n组件分布:\n");
    for (component, count) in &component_stats {
        let percentage = (*count as f64 / successful_parses as f64) * 100.0;
        output.push_str(&format!(
            "  {}: {} 条 ({:.1}%)\n",
            component, count, percentage
        ));
    }

    write_parse_result_to_file("master_parser_full_analysis.txt", &output);

    println!("真实日志解析统计:");
    println!("  成功: {}", successful_parses);
    println!("  失败: {}", failed_parses);
    println!("  总数: {}", real_logs.len());

    // 验证100%解析成功率(实际行数可能因为最后一行没换行符而略有不同)
    assert!(successful_parses >= 399, "应该成功解析至少399行真实日志");
    assert_eq!(failed_parses, 0, "不应该有解析失败的日志");
}

#[test]
fn test_invalid_format() {
    let parser = MasterParser::new();

    let mut output = String::new();
    output.push_str("=== Master Parser - 无效格式测试结果 ===\n\n");
    output.push_str("测试时间: 2024-12-19\n");

    // 只测试明显无效的格式,而不是假造的日志
    let invalid_logs = vec![
        "",
        "invalid log line",
        "Jun 05 17:24:32 m01 not-postfix[123]: message",
        "Jun 05 invalid format",
    ];

    output.push_str("\n无效日志测试:\n");

    for invalid_log in invalid_logs {
        if invalid_log.is_empty() {
            output.push_str("  跳过空行测试\n");
            continue;
        }

        output.push_str(&format!("  测试日志: '{}'\n", invalid_log));

        let result = parser.parse_base_info(invalid_log);
        let is_error = result.is_err();
        match result {
            Ok(_) => {
                output.push_str("    ❌ 意外解析成功(应该失败)\n");
            }
            Err(e) => {
                output.push_str(&format!("    ✅ 正确失败: {}\n", e));
            }
        }

        assert!(is_error, "应该解析失败: {}", invalid_log);
    }

    write_parse_result_to_file("master_parser_invalid_format.txt", &output);
}