Skip to main content

forge/mutation/
reporter.rs

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        // Summary table
40        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        // Legend: short, factual definitions of each status.
55        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        // Format duration similar to test output
69        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            // Mutation score with color
77            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        // Survived mutants section - the most important for developers.
112        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            // Sort by (file, line, column, span, mutation text) so the
118            // reported order is deterministic across runs / worker counts.
119            // Workers complete in arbitrary order, so without this every run
120            // can permute the report.
121            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        // Killed mutants (collapsed: just count).
146        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        // Invalid mutants (collapsed: just count).
152        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        // Timed-out mutants (collapsed: just count).
158        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        // Show file:line
182        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        // Show the source line context if available
191        if !mutant.source_line.is_empty() {
192            let _ = sh_println!("     {}", Paint::dim(&mutant.source_line));
193        }
194
195        // Show the diff
196        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}