use crate::domain::report::AnalysisReport;
use crate::ports::outbound::DiagnosticRenderer;
pub struct TerminalRenderer;
impl DiagnosticRenderer for TerminalRenderer {
fn render(&self, report: &AnalysisReport) -> Result<String, String> {
let mut out = String::new();
out.push_str(&format!(
"xbp audit errors — {} finding(s) in {} file(s) ({} ms)\n",
report.findings.len(),
report.files_analyzed,
report.duration_ms
));
if report.findings.is_empty() {
out.push_str("No issues found.\n");
return Ok(out);
}
for f in &report.findings {
let kind = if f.definite_bug {
"bug"
} else {
"review"
};
out.push_str(&format!(
"\n[{}] {} {} ({} / {})\n --> {}:{}:{}\n {}\n scenario: {}\n fix: {}\n",
f.severity,
f.rule_id,
kind,
f.confidence.as_str(),
f.language,
f.location.file.display(),
f.location.start_line,
f.location.start_column,
f.explanation,
f.failure_scenario,
f.remediation,
));
for e in &f.evidence {
out.push_str(&format!(" evidence[{}]: {}\n", e.kind, e.detail));
}
}
out.push_str(&format!(
"\nSummary: {} definite, {} review-required\n",
report.stats.definite_bugs, report.stats.review_required
));
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::types::{
Confidence, Finding, LanguageId, Severity, SourceLocation,
};
#[test]
fn empty_report() {
let r = AnalysisReport::default();
let s = TerminalRenderer.render(&r).unwrap();
assert!(s.contains("No issues found"));
}
#[test]
fn with_finding() {
let mut r = AnalysisReport::default();
r.findings.push(
Finding::builder("ignored-failure", LanguageId::rust())
.severity(Severity::High)
.confidence(Confidence::High)
.location(SourceLocation::single_line("a.rs", 1, 1))
.explanation("ignored")
.failure_scenario("silent")
.remediation("handle")
.build(),
);
r.recompute_stats();
let s = TerminalRenderer.render(&r).unwrap();
assert!(s.contains("ignored-failure"));
assert!(s.contains("a.rs:1:1"));
}
}