Skip to main content

xbp_analysis/adapters/output/
terminal.rs

1use crate::domain::report::AnalysisReport;
2use crate::ports::outbound::DiagnosticRenderer;
3
4pub struct TerminalRenderer;
5
6impl DiagnosticRenderer for TerminalRenderer {
7    fn render(&self, report: &AnalysisReport) -> Result<String, String> {
8        let mut out = String::new();
9        out.push_str(&format!(
10            "xbp audit errors — {} finding(s) in {} file(s) ({} ms)\n",
11            report.findings.len(),
12            report.files_analyzed,
13            report.duration_ms
14        ));
15        if report.findings.is_empty() {
16            out.push_str("No issues found.\n");
17            return Ok(out);
18        }
19        for f in &report.findings {
20            let kind = if f.definite_bug {
21                "bug"
22            } else {
23                "review"
24            };
25            out.push_str(&format!(
26                "\n[{}] {} {} ({} / {})\n  --> {}:{}:{}\n  {}\n  scenario: {}\n  fix: {}\n",
27                f.severity,
28                f.rule_id,
29                kind,
30                f.confidence.as_str(),
31                f.language,
32                f.location.file.display(),
33                f.location.start_line,
34                f.location.start_column,
35                f.explanation,
36                f.failure_scenario,
37                f.remediation,
38            ));
39            for e in &f.evidence {
40                out.push_str(&format!("  evidence[{}]: {}\n", e.kind, e.detail));
41            }
42        }
43        out.push_str(&format!(
44            "\nSummary: {} definite, {} review-required\n",
45            report.stats.definite_bugs, report.stats.review_required
46        ));
47        Ok(out)
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use crate::domain::types::{
55        Confidence, Finding, LanguageId, Severity, SourceLocation,
56    };
57
58    #[test]
59    fn empty_report() {
60        let r = AnalysisReport::default();
61        let s = TerminalRenderer.render(&r).unwrap();
62        assert!(s.contains("No issues found"));
63    }
64
65    #[test]
66    fn with_finding() {
67        let mut r = AnalysisReport::default();
68        r.findings.push(
69            Finding::builder("ignored-failure", LanguageId::rust())
70                .severity(Severity::High)
71                .confidence(Confidence::High)
72                .location(SourceLocation::single_line("a.rs", 1, 1))
73                .explanation("ignored")
74                .failure_scenario("silent")
75                .remediation("handle")
76                .build(),
77        );
78        r.recompute_stats();
79        let s = TerminalRenderer.render(&r).unwrap();
80        assert!(s.contains("ignored-failure"));
81        assert!(s.contains("a.rs:1:1"));
82    }
83}