1use std::path::Path;
2
3use anyhow::{anyhow, Context, Result};
4
5use crate::discovery::find_unit_file;
6use crate::unit::Unit;
7use mana_core::config::Config;
8use mana_core::ops::mutate::{run_mutation_test, MutateOpts, MutationReport};
9
10pub struct MutateArgs {
12 pub id: String,
14 pub max_mutants: usize,
16 pub timeout: Option<u64>,
18 pub diff_base: String,
20 pub json: bool,
22}
23
24pub fn cmd_mutate(mana_dir: &Path, args: MutateArgs) -> Result<()> {
29 let unit_path =
30 find_unit_file(mana_dir, &args.id).map_err(|_| anyhow!("Unit not found: {}", args.id))?;
31 let unit =
32 Unit::from_file(&unit_path).with_context(|| format!("Failed to load unit: {}", args.id))?;
33
34 let verify_cmd = unit
35 .verify
36 .as_deref()
37 .filter(|v| !v.trim().is_empty())
38 .ok_or_else(|| anyhow!("Unit {} has no verify command", args.id))?;
39
40 let project_root = mana_dir
41 .parent()
42 .ok_or_else(|| anyhow!("Cannot determine project root from .mana/ dir"))?;
43
44 let config = Config::load(mana_dir).ok();
46 let timeout = args
47 .timeout
48 .or_else(|| unit.effective_verify_timeout(config.as_ref().and_then(|c| c.verify_timeout)));
49
50 eprintln!("Confirming verify passes on clean code...");
52 let baseline = mana_core::ops::verify::run_verify_command(verify_cmd, project_root, timeout)?;
53 if !baseline.passed {
54 eprintln!("✗ Verify does not pass on clean code. Fix it before mutation testing.");
55 eprintln!(" Command: {}", verify_cmd);
56 if let Some(code) = baseline.exit_code {
57 eprintln!(" Exit code: {}", code);
58 }
59 std::process::exit(1);
60 }
61 eprintln!("✓ Verify passes on clean code\n");
62
63 eprintln!("Running mutation tests against: {}", verify_cmd);
65 eprintln!("Diff base: {}", args.diff_base);
66 if args.max_mutants > 0 {
67 eprintln!("Max mutants: {}", args.max_mutants);
68 }
69 eprintln!();
70
71 let opts = MutateOpts {
72 max_mutants: args.max_mutants,
73 timeout_secs: timeout,
74 diff_base: args.diff_base,
75 };
76
77 let report = run_mutation_test(project_root, verify_cmd, &opts)?;
78
79 if args.json {
80 print_json_report(&report, &args.id);
81 } else {
82 print_human_report(&report, &args.id);
83 }
84
85 if report.survived > 0 {
87 std::process::exit(1);
88 }
89
90 Ok(())
91}
92
93fn print_human_report(report: &MutationReport, id: &str) {
94 if report.total == 0 {
95 println!("No mutants generated — no changed lines found in git diff.");
96 println!("Tip: Make sure you have uncommitted or staged changes.");
97 return;
98 }
99
100 println!("Mutation Testing Report — Unit {}", id);
101 println!("{}", "─".repeat(50));
102 println!(
103 "Total mutants: {} | Killed: {} | Survived: {} | Timed out: {}",
104 report.total, report.killed, report.survived, report.timed_out
105 );
106 println!("Mutation score: {:.1}%", report.score);
107 println!();
108
109 let survivors: Vec<_> = report.results.iter().filter(|r| !r.killed).collect();
111 if !survivors.is_empty() {
112 println!("⚠ Surviving mutants (verify still passes with these changes):");
113 println!();
114 for (i, result) in survivors.iter().enumerate() {
115 let m = &result.mutant;
116 println!(
117 " {}. {}:{} [{}]",
118 i + 1,
119 m.file.display(),
120 m.line_number,
121 m.operator,
122 );
123 println!(" original: {}", m.original.trim());
124 println!(
125 " mutated: {}",
126 if m.mutated.is_empty() {
127 "<deleted>"
128 } else {
129 m.mutated.trim()
130 }
131 );
132 println!();
133 }
134 println!("These mutations were NOT caught by the verify gate.");
135 println!("Consider strengthening the verify command to detect these changes.");
136 } else {
137 println!("✓ All mutants killed — verify gate is strong.");
138 }
139}
140
141fn print_json_report(report: &MutationReport, id: &str) {
142 let survivors: Vec<serde_json::Value> = report
143 .results
144 .iter()
145 .filter(|r| !r.killed)
146 .map(|r| {
147 serde_json::json!({
148 "file": r.mutant.file.display().to_string(),
149 "line": r.mutant.line_number,
150 "operator": r.mutant.operator.to_string(),
151 "original": r.mutant.original.trim(),
152 "mutated": if r.mutant.mutated.is_empty() { "<deleted>" } else { r.mutant.mutated.trim() },
153 })
154 })
155 .collect();
156
157 let json = serde_json::json!({
158 "id": id,
159 "total": report.total,
160 "killed": report.killed,
161 "survived": report.survived,
162 "timed_out": report.timed_out,
163 "score": report.score,
164 "survivors": survivors,
165 });
166
167 println!("{}", serde_json::to_string_pretty(&json).unwrap());
168}