xbp_analysis/adapters/output/
terminal.rs1use 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}