1use comfy_table::{Cell, Color, Row, Table, 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_style(UTF8_FULL.with_rounded_corners());
21
22 table.set_header(vec![
23 Cell::new("Status"),
24 Cell::new("# Mutants"),
25 Cell::new("% of Total"),
26 ]);
27
28 Self { table }
29 }
30
31 pub fn report(&mut self, summary: &MutationsSummary, duration: Duration) {
32 let total = summary.total_mutants();
33 if total == 0 {
34 let _ = sh_println!("No mutants were generated.");
35 return;
36 }
37
38 self.add_row("Survived", summary.total_survived(), total, Color::Red);
40 self.add_row("Killed", summary.total_dead(), total, Color::Green);
41 self.add_row("Invalid", summary.total_invalid(), total, Color::DarkGrey);
42 self.add_row("Skipped", summary.total_skipped(), total, Color::Yellow);
43 if summary.total_timed_out() > 0 {
44 self.add_row("Timed out", summary.total_timed_out(), total, Color::Magenta);
45 }
46
47 let _ = sh_println!("\n{}", "═".repeat(60));
48 let _ = sh_println!("{}", Paint::bold("MUTATION TESTING RESULTS"));
49 let _ = sh_println!("{}", "═".repeat(60));
50
51 let _ = sh_println!("\n{}\n", self.table);
52
53 let _ = sh_println!("{}", Paint::dim("Legend:"));
55 let _ = sh_println!(" {} - tests did not catch the mutation", Paint::red("Survived"));
56 let _ = sh_println!(" {} - tests caught the mutation", Paint::green("Killed"));
57 let _ = sh_println!(" {} - mutation produced a compilation error", Paint::dim("Invalid"));
58 let _ = sh_println!(
59 " {} - redundant mutation on the same expression",
60 Paint::yellow("Skipped")
61 );
62 let _ = sh_println!(
63 " {} - compile/test exceeded the configured timeout\n",
64 Paint::magenta("Timed out")
65 );
66
67 let duration_str = if duration.as_secs() >= 60 {
69 format!("{}m {:.2}s", duration.as_secs() / 60, duration.as_secs_f64() % 60.0)
70 } else {
71 format!("{:.2}s", duration.as_secs_f64())
72 };
73
74 if summary.has_reliable_score() {
75 let score = summary.mutation_score();
77 let score_display = format!("{score:.1}%");
78 let score_colored = if score >= 80.0 {
79 Paint::green(&score_display).bold()
80 } else if score >= 60.0 {
81 Paint::yellow(&score_display).bold()
82 } else {
83 Paint::red(&score_display).bold()
84 };
85
86 let _ = sh_println!(
87 "Mutation Score: {} ({}/{} mutants killed); finished in {}",
88 score_colored,
89 summary.total_dead(),
90 summary.total_evaluated(),
91 duration_str
92 );
93 } else {
94 let _ = sh_println!(
95 "Mutation Score: unavailable ({} timed out, {} evaluated); finished in {}",
96 summary.total_timed_out(),
97 summary.total_evaluated(),
98 duration_str
99 );
100 let _ = sh_println!(
101 "{}",
102 Paint::yellow(
103 "Timed-out mutants dominate this run. Increase --mutation-timeout or use a \
104 faster mutation profile, for example lower --mutation-optimizer-runs or set \
105 --mutation-via-ir false."
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}