1use comfy_table::{Cell, Color, Row, Table, modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL};
2use std::time::Duration;
3use yansi::Paint;
4
5use crate::mutation::{MutationsSummary, mutant::Mutant};
6
7pub struct MutationReporter {
8 table: Table,
9}
10
11impl Default for MutationReporter {
12 fn default() -> Self {
13 Self::new()
14 }
15}
16
17impl MutationReporter {
18 pub fn new() -> Self {
19 let mut table = Table::new();
20 table.load_preset(UTF8_FULL);
21 table.apply_modifier(UTF8_ROUND_CORNERS);
22
23 table.set_header(vec![
24 Cell::new("Status"),
25 Cell::new("# Mutants"),
26 Cell::new("% of Total"),
27 ]);
28
29 Self { table }
30 }
31
32 pub fn report(&mut self, summary: &MutationsSummary, duration: Duration) {
33 let total = summary.total_mutants();
34 if total == 0 {
35 let _ = sh_println!("No mutants were generated.");
36 return;
37 }
38
39 self.add_row("Survived", summary.total_survived(), total, Color::Red);
41 self.add_row("Killed", summary.total_dead(), total, Color::Green);
42 self.add_row("Invalid", summary.total_invalid(), total, Color::DarkGrey);
43 self.add_row("Skipped", summary.total_skipped(), total, Color::Yellow);
44 if summary.total_timed_out() > 0 {
45 self.add_row("Timed out", summary.total_timed_out(), total, Color::Magenta);
46 }
47
48 let _ = sh_println!("\n{}", "═".repeat(60));
49 let _ = sh_println!("{}", Paint::bold("MUTATION TESTING RESULTS"));
50 let _ = sh_println!("{}", "═".repeat(60));
51
52 let _ = sh_println!("\n{}\n", self.table);
53
54 let _ = sh_println!("{}", Paint::dim("Legend:"));
56 let _ = sh_println!(" {} - tests did not catch the mutation", Paint::red("Survived"));
57 let _ = sh_println!(" {} - tests caught the mutation", Paint::green("Killed"));
58 let _ = sh_println!(" {} - mutation produced a compilation error", Paint::dim("Invalid"));
59 let _ = sh_println!(
60 " {} - redundant mutation on the same expression",
61 Paint::yellow("Skipped")
62 );
63 let _ = sh_println!(
64 " {} - compile/test exceeded the configured timeout\n",
65 Paint::magenta("Timed out")
66 );
67
68 let duration_str = if duration.as_secs() >= 60 {
70 format!("{}m {:.2}s", duration.as_secs() / 60, duration.as_secs_f64() % 60.0)
71 } else {
72 format!("{:.2}s", duration.as_secs_f64())
73 };
74
75 if summary.has_reliable_score() {
76 let score = summary.mutation_score();
78 let score_display = format!("{score:.1}%");
79 let score_colored = if score >= 80.0 {
80 Paint::green(&score_display).bold()
81 } else if score >= 60.0 {
82 Paint::yellow(&score_display).bold()
83 } else {
84 Paint::red(&score_display).bold()
85 };
86
87 let _ = sh_println!(
88 "Mutation Score: {} ({}/{} mutants killed); finished in {}",
89 score_colored,
90 summary.total_dead(),
91 summary.total_evaluated(),
92 duration_str
93 );
94 } else {
95 let _ = sh_println!(
96 "Mutation Score: unavailable ({} timed out, {} evaluated); finished in {}",
97 summary.total_timed_out(),
98 summary.total_evaluated(),
99 duration_str
100 );
101 let _ = sh_println!(
102 "{}",
103 Paint::yellow(
104 "Timed-out mutants dominate this run. Increase --mutation-timeout or use a \
105 faster mutation profile, for example lower optimizer_runs or disable via_ir."
106 )
107 .bold()
108 );
109 }
110
111 if !summary.get_survived().is_empty() {
113 let _ = sh_println!("\n{}", "─".repeat(60));
114 let _ = sh_println!("{}", Paint::red("Survived mutants").bold());
115 let _ = sh_println!("{}", "─".repeat(60));
116
117 let mut survived: Vec<&Mutant> = summary.get_survived().iter().collect();
122 survived.sort_by(|a, b| {
123 (
124 a.relative_path(),
125 a.line_number,
126 a.column_number,
127 a.span.lo().0,
128 a.span.hi().0,
129 a.mutation.to_string(),
130 )
131 .cmp(&(
132 b.relative_path(),
133 b.line_number,
134 b.column_number,
135 b.span.lo().0,
136 b.span.hi().0,
137 b.mutation.to_string(),
138 ))
139 });
140 for (i, mutant) in survived.iter().enumerate() {
141 self.print_survived_mutant(i + 1, mutant);
142 }
143 }
144
145 if !summary.get_dead().is_empty() {
147 let _ = sh_println!("\n{}", "─".repeat(60));
148 let _ = sh_println!("{} mutants {}", summary.total_dead(), Paint::green("killed"));
149 }
150
151 if !summary.get_invalid().is_empty() {
153 let _ = sh_println!("\n{}", "─".repeat(60));
154 let _ = sh_println!("{} mutants {}", summary.total_invalid(), Paint::dim("invalid"));
155 }
156
157 if !summary.get_timed_out().is_empty() {
159 let _ = sh_println!("\n{}", "─".repeat(60));
160 let _ = sh_println!(
161 "{} mutants {}",
162 summary.total_timed_out(),
163 Paint::magenta("timed out")
164 );
165 }
166
167 let _ = sh_println!("\n{}", "═".repeat(60));
168 }
169
170 fn add_row(&mut self, status: &str, count: usize, total: usize, color: Color) {
171 let pct = if total > 0 { count as f64 / total as f64 * 100.0 } else { 0.0 };
172
173 let mut row = Row::new();
174 row.add_cell(Cell::new(status).fg(color))
175 .add_cell(Cell::new(count.to_string()))
176 .add_cell(Cell::new(format!("{pct:.1}%")));
177 self.table.add_row(row);
178 }
179
180 fn print_survived_mutant(&self, index: usize, mutant: &Mutant) {
181 let location = if mutant.line_number > 0 {
183 format!("{}:{}", mutant.relative_path(), mutant.line_number)
184 } else {
185 mutant.relative_path()
186 };
187
188 let _ = sh_println!("\n {}. {}", Paint::red(&index).bold(), Paint::bold(&location));
189
190 if !mutant.source_line.is_empty() {
192 let _ = sh_println!(" {}", Paint::dim(&mutant.source_line));
193 }
194
195 let _ = sh_println!(" {}", Paint::dim("Mutation:"));
197 let original = if mutant.original.is_empty() {
198 "<unknown>".to_string()
199 } else {
200 mutant.original.clone()
201 };
202 let mutated = mutant.mutation.to_string();
203
204 let _ = sh_println!(" {} {}", Paint::red("-"), Paint::red(original.trim()));
205 let _ = sh_println!(" {} {}", Paint::green("+"), Paint::green(&mutated));
206 }
207}