Skip to main content

forge/mutation/
reporter.rs

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        // Summary table
39        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        // Legend: short, factual definitions of each status.
54        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        // Format duration similar to test output
68        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            // Mutation score with color
76            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        // 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}