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 --mutation-optimizer-runs or set \
106 --mutation-via-ir false."
107 )
108 .bold()
109 );
110 }
111
112 if !summary.get_survived().is_empty() {
114 let _ = sh_println!("\n{}", "─".repeat(60));
115 let _ = sh_println!("{}", Paint::red("Survived mutants").bold());
116 let _ = sh_println!("{}", "─".repeat(60));
117
118 let mut survived: Vec<&Mutant> = summary.get_survived().iter().collect();
123 survived.sort_by(|a, b| {
124 (
125 a.relative_path(),
126 a.line_number,
127 a.column_number,
128 a.span.lo().0,
129 a.span.hi().0,
130 a.mutation.to_string(),
131 )
132 .cmp(&(
133 b.relative_path(),
134 b.line_number,
135 b.column_number,
136 b.span.lo().0,
137 b.span.hi().0,
138 b.mutation.to_string(),
139 ))
140 });
141 for (i, mutant) in survived.iter().enumerate() {
142 self.print_survived_mutant(i + 1, mutant);
143 }
144 }
145
146 if !summary.get_dead().is_empty() {
148 let _ = sh_println!("\n{}", "─".repeat(60));
149 let _ = sh_println!("{} mutants {}", summary.total_dead(), Paint::green("killed"));
150 }
151
152 if !summary.get_invalid().is_empty() {
154 let _ = sh_println!("\n{}", "─".repeat(60));
155 let _ = sh_println!("{} mutants {}", summary.total_invalid(), Paint::dim("invalid"));
156 }
157
158 if !summary.get_timed_out().is_empty() {
160 let _ = sh_println!("\n{}", "─".repeat(60));
161 let _ = sh_println!(
162 "{} mutants {}",
163 summary.total_timed_out(),
164 Paint::magenta("timed out")
165 );
166 }
167
168 let _ = sh_println!("\n{}", "═".repeat(60));
169 }
170
171 fn add_row(&mut self, status: &str, count: usize, total: usize, color: Color) {
172 let pct = if total > 0 { count as f64 / total as f64 * 100.0 } else { 0.0 };
173
174 let mut row = Row::new();
175 row.add_cell(Cell::new(status).fg(color))
176 .add_cell(Cell::new(count.to_string()))
177 .add_cell(Cell::new(format!("{pct:.1}%")));
178 self.table.add_row(row);
179 }
180
181 fn print_survived_mutant(&self, index: usize, mutant: &Mutant) {
182 let location = if mutant.line_number > 0 {
184 format!("{}:{}", mutant.relative_path(), mutant.line_number)
185 } else {
186 mutant.relative_path()
187 };
188
189 let _ = sh_println!("\n {}. {}", Paint::red(&index).bold(), Paint::bold(&location));
190
191 if !mutant.source_line.is_empty() {
193 let _ = sh_println!(" {}", Paint::dim(&mutant.source_line));
194 }
195
196 let _ = sh_println!(" {}", Paint::dim("Mutation:"));
198 let original = if mutant.original.is_empty() {
199 "<unknown>".to_string()
200 } else {
201 mutant.original.clone()
202 };
203 let mutated = mutant.mutation.to_string();
204
205 let _ = sh_println!(" {} {}", Paint::red("-"), Paint::red(original.trim()));
206 let _ = sh_println!(" {} {}", Paint::green("+"), Paint::green(&mutated));
207 }
208}