secreport 0.3.0

Output formatters for security findings — JSON, JSONL, SARIF, Markdown, Text
Documentation
//! Concurrent access and thread-safety tests.

use secreport::{Format, render, render_any};
use secfinding::{Finding, Reportable, Severity};
use std::sync::Arc;
use std::thread;

#[test]
fn concurrent_render_all_formats_20_threads() {
    let finding = Arc::new(Finding::new("s", "t", Severity::Critical, "T", "D").unwrap());
    let mut handles = vec![];
    for _ in 0..20 {
        let f = finding.clone();
        handles.push(thread::spawn(move || {
            for format in [
                Format::Text,
                Format::Json,
                Format::Jsonl,
                Format::Sarif,
                Format::Markdown,
            ] {
                let out = render(std::slice::from_ref(&*f), format, "tool").unwrap();
                assert!(!out.is_empty());
            }
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
}

#[test]
fn concurrent_render_any_with_custom_reportable() {
    #[derive(Clone)]
    struct R;
    impl Reportable for R {
        fn scanner(&self) -> &str { "s" }
        fn target(&self) -> &str { "t" }
        fn severity(&self) -> Severity { Severity::High }
        fn title(&self) -> &str { "T" }
    }
    let items: Vec<R> = (0..50).map(|_| R).collect();
    let mut handles = vec![];
    for _ in 0..10 {
        let items = items.clone();
        handles.push(thread::spawn(move || {
            let out = render_any(&items, Format::Json, "tool").unwrap();
            let parsed: Vec<serde_json::Value> = serde_json::from_str(&out).unwrap();
            assert_eq!(parsed.len(), 50);
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
}

#[test]
fn concurrent_mixed_formats_same_findings() {
    let findings: Vec<Finding> = (0..100)
        .map(|i| Finding::new("s", "t", Severity::Medium, &format!("T{}", i), "D").unwrap())
        .collect();
    let findings = Arc::new(findings);
    let mut handles = vec![];
    for format in [
        Format::Text,
        Format::Json,
        Format::Jsonl,
        Format::Sarif,
        Format::Markdown,
    ] {
        let f = findings.clone();
        handles.push(thread::spawn(move || {
            let out = render(&*f, format, "tool").unwrap();
            assert!(!out.is_empty());
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
}

#[test]
fn concurrent_scale_jsonl() {
    let findings: Vec<Finding> = (0..10_000)
        .map(|i| Finding::new("s", "t", Severity::Info, &format!("T{}", i), "D").unwrap())
        .collect();
    let findings = Arc::new(findings);
    let mut handles = vec![];
    for _ in 0..8 {
        let f = findings.clone();
        handles.push(thread::spawn(move || {
            let out = render(&*f, Format::Jsonl, "tool").unwrap();
            assert_eq!(out.lines().count(), 10_000);
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
}

#[test]
fn concurrent_scale_sarif() {
    let findings: Vec<Finding> = (0..1_000)
        .map(|i| Finding::new("s", "t", Severity::High, &format!("T{}", i), "D").unwrap())
        .collect();
    let findings = Arc::new(findings);
    let mut handles = vec![];
    for _ in 0..8 {
        let f = findings.clone();
        handles.push(thread::spawn(move || {
            let out = render(&*f, Format::Sarif, "tool").unwrap();
            let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
            let results = parsed["runs"][0]["results"].as_array().unwrap();
            assert_eq!(results.len(), 1_000);
        }));
    }
    for h in handles {
        h.join().unwrap();
    }
}