1use crate::audit::{AuditReport, Severity};
11use std::fmt::Write;
12
13pub fn format(report: &AuditReport, out: &mut String) {
26 for gate in &report.gates {
27 if gate.skipped {
28 let _ = writeln!(
29 out,
30 "[{}] skipped — {}",
31 gate.name,
32 gate.skip_reason.as_deref().unwrap_or("no reason given")
33 );
34 continue;
35 }
36
37 let counts = &gate.severity_counts;
38 if counts.total() == 0 {
39 let _ = writeln!(out, "[{}] OK", gate.name);
40 continue;
41 }
42 let _ = writeln!(
43 out,
44 "[{}] {} finding(s) — {} error / {} warn / {} info",
45 gate.name,
46 counts.total(),
47 counts.error,
48 counts.warn,
49 counts.info
50 );
51 for f in &gate.findings {
52 let sigil = severity_sigil(f.severity);
53 let path = f.path.as_deref().unwrap_or("");
54 let code = f.code.as_deref().unwrap_or("");
55 let _ = writeln!(
56 out,
57 " {sigil} {} {} {}",
58 code,
59 if path.is_empty() {
60 String::new()
61 } else {
62 format!("({path})")
63 },
64 f.message
65 );
66 }
67 }
68}
69
70const fn severity_sigil(s: Severity) -> &'static str {
71 match s {
72 Severity::Info => "i",
73 Severity::Warn => "!",
74 Severity::Error => "x",
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::audit::{Finding, GateResult, Severity, SeverityCounts};
82
83 #[test]
84 fn skipped_gate_renders_skipped_line() {
85 let report = AuditReport {
86 gates: vec![GateResult {
87 name: "g".to_string(),
88 skipped: true,
89 skip_reason: Some("test".to_string()),
90 severity_counts: SeverityCounts::default(),
91 findings: vec![],
92 }],
93 };
94 let mut s = String::new();
95 format(&report, &mut s);
96 assert!(s.contains("[g] skipped"));
97 assert!(s.contains("test"));
98 }
99
100 #[test]
101 fn ok_gate_renders_ok_line() {
102 let report = AuditReport {
103 gates: vec![GateResult {
104 name: "g".to_string(),
105 skipped: false,
106 skip_reason: None,
107 severity_counts: SeverityCounts::default(),
108 findings: vec![],
109 }],
110 };
111 let mut s = String::new();
112 format(&report, &mut s);
113 assert!(s.contains("[g] OK"));
114 }
115
116 #[test]
117 fn findings_are_rendered_with_severity_sigil() {
118 let report = AuditReport {
119 gates: vec![GateResult {
120 name: "g".to_string(),
121 skipped: false,
122 skip_reason: None,
123 severity_counts: SeverityCounts {
124 info: 0,
125 warn: 0,
126 error: 1,
127 },
128 findings: vec![Finding::new("g", Severity::Error, "boom")
129 .with_code("C")
130 .with_path("x.html")],
131 }],
132 };
133 let mut s = String::new();
134 format(&report, &mut s);
135 assert!(s.contains("x C"));
136 assert!(s.contains("(x.html)"));
137 assert!(s.contains("boom"));
138 }
139}