padlock_output/
summary.rs1use padlock_core::findings::{Finding, Report, Severity, StructReport};
4
5pub fn render_report(report: &Report) -> String {
7 let mut out = String::new();
8
9 if report.analyzed_paths.len() > 1 {
11 out.push_str(&format!("Analyzed {} files, ", report.analyzed_paths.len()));
12 out.push_str(&format!(
13 "{} struct{}",
14 report.total_structs,
15 if report.total_structs == 1 { "" } else { "s" }
16 ));
17 } else {
18 out.push_str(&format!(
19 "Analyzed {} struct{}",
20 report.total_structs,
21 if report.total_structs == 1 { "" } else { "s" }
22 ));
23 }
24
25 if report.total_wasted_bytes > 0 {
26 out.push_str(&format!(
27 " — {} bytes wasted across all structs\n",
28 report.total_wasted_bytes
29 ));
30 } else {
31 out.push_str(" — no padding waste found\n");
32 }
33 out.push('\n');
34
35 for sr in &report.structs {
36 out.push_str(&render_struct(sr));
37 out.push('\n');
38 }
39
40 out
41}
42
43pub fn render_struct(sr: &StructReport) -> String {
44 let mut out = String::new();
45
46 let score_label = match sr.score as u32 {
47 90..=100 => "✓",
48 60..=89 => "~",
49 _ => "✗",
50 };
51
52 let location = match (&sr.source_file, sr.source_line) {
53 (Some(f), Some(l)) => format!(" ({}:{})", f, l),
54 (Some(f), None) => format!(" ({})", f),
55 _ => String::new(),
56 };
57
58 let holes_hint = if sr.num_holes > 0 {
59 format!(" holes={}", sr.num_holes)
60 } else {
61 String::new()
62 };
63
64 out.push_str(&format!(
65 "[{score_label}] {name}{location} {size}B fields={fields}{holes} score={score:.0}\n",
66 name = sr.struct_name,
67 size = sr.total_size,
68 fields = sr.num_fields,
69 holes = holes_hint,
70 score = sr.score,
71 ));
72
73 for finding in &sr.findings {
74 out.push_str(&format!(" {}\n", render_finding(finding)));
75 }
76
77 if sr.findings.is_empty() {
78 out.push_str(" (no issues found)\n");
79 }
80
81 out
82}
83
84fn render_finding(f: &Finding) -> String {
85 let sev = match f.severity() {
86 Severity::High => "HIGH",
87 Severity::Medium => "MEDIUM",
88 Severity::Low => "LOW",
89 };
90 match f {
91 Finding::PaddingWaste {
92 wasted_bytes,
93 waste_pct,
94 gaps,
95 ..
96 } => format!(
97 "[{sev}] Padding waste: {wasted_bytes}B ({waste_pct:.0}%) across {} gap(s)",
98 gaps.len()
99 ),
100 Finding::ReorderSuggestion {
101 savings,
102 optimized_size,
103 suggested_order,
104 ..
105 } => format!(
106 "[{sev}] Reorder fields to save {savings}B → {optimized_size}B: {}",
107 suggested_order.join(", ")
108 ),
109 Finding::FalseSharing { conflicts, .. } => format!(
110 "[{sev}] False sharing: {} cache-line conflict(s)",
111 conflicts.len()
112 ),
113 Finding::LocalityIssue {
114 hot_fields,
115 cold_fields,
116 ..
117 } => format!(
118 "[{sev}] Locality: hot [{}] interleaved with cold [{}]",
119 hot_fields.join(", "),
120 cold_fields.join(", ")
121 ),
122 }
123}
124
125#[cfg(test)]
128mod tests {
129 use super::*;
130 use padlock_core::findings::Report;
131 use padlock_core::ir::test_fixtures::{connection_layout, packed_layout};
132
133 #[test]
134 fn render_report_contains_struct_name() {
135 let report = Report::from_layouts(&[connection_layout()]);
136 let out = render_report(&report);
137 assert!(out.contains("Connection"));
138 }
139
140 #[test]
141 fn render_report_mentions_wasted_bytes() {
142 let report = Report::from_layouts(&[connection_layout()]);
143 let out = render_report(&report);
144 assert!(out.contains("waste") || out.contains("Padding"));
145 }
146
147 #[test]
148 fn render_report_shows_reorder_suggestion() {
149 let report = Report::from_layouts(&[connection_layout()]);
150 let out = render_report(&report);
151 assert!(out.contains("Reorder") || out.contains("save"));
152 }
153
154 #[test]
155 fn render_report_no_issues_on_packed() {
156 let report = Report::from_layouts(&[packed_layout()]);
157 let out = render_report(&report);
158 assert!(out.contains("no issues"));
159 }
160
161 #[test]
162 fn render_struct_shows_hole_count_when_nonzero() {
163 let report = Report::from_layouts(&[connection_layout()]);
164 let out = render_struct(&report.structs[0]);
165 assert!(out.contains("holes=2"));
166 }
167
168 #[test]
169 fn render_struct_omits_holes_when_zero() {
170 let report = Report::from_layouts(&[packed_layout()]);
171 let out = render_struct(&report.structs[0]);
172 assert!(!out.contains("holes="));
173 }
174
175 #[test]
176 fn render_struct_shows_field_count() {
177 let report = Report::from_layouts(&[connection_layout()]);
178 let out = render_struct(&report.structs[0]);
179 assert!(out.contains("fields=4"));
180 }
181
182 #[test]
183 fn render_report_multi_file_header() {
184 let mut report = Report::from_layouts(&[connection_layout()]);
185 report.analyzed_paths = vec!["a.rs".into(), "b.rs".into()];
186 let out = render_report(&report);
187 assert!(out.contains("2 files"));
188 }
189}