1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#![cfg_attr(coverage_nightly, coverage(off))]
//! REFACTOR Phase Implementation for PMAT-070-001: CargoMutantsWrapper
//!
//! Wrapper for cargo-mutants mutation testing tool.
//!
//! # Functionality
//!
//! - Detect cargo-mutants in PATH (cargo subcommand)
//! - Execute `cargo mutants --version`
//! - Parse and validate version (require v24.7.0+)
//! - Graceful error handling when not installed
//!
//! # Example
//!
//! ```no_run
//! use pmat::services::mutation::cargo_mutants_wrapper::CargoMutantsWrapper;
//!
//! let wrapper = CargoMutantsWrapper::new()?;
//! if wrapper.is_installed() {
//! let version = wrapper.version()?;
//! println!("cargo-mutants version: {}", version);
//! }
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
use std::path::PathBuf;
use std::process::Command;
/// Type alias for Result with boxed error
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
/// Wrapper for cargo-mutants subprocess execution
///
/// cargo-mutants is a cargo subcommand, so we store "cargo" as the path
/// and execute via `cargo mutants <args>`.
pub struct CargoMutantsWrapper {
cargo_mutants_path: Option<PathBuf>,
}
impl CargoMutantsWrapper {
/// Initialize wrapper and detect cargo-mutants in PATH
///
/// Returns Ok even if cargo-mutants is not installed (cargo_mutants_path will be None).
/// This allows graceful degradation with helpful error messages.
///
/// # Errors
///
/// This method should not fail under normal circumstances. It returns Ok even when
/// cargo-mutants is not installed.
pub fn new() -> Result<Self> {
// cargo-mutants is a cargo subcommand, so check if `cargo` exists
let cargo_path = which::which("cargo").ok();
// Verify cargo-mutants subcommand is actually installed
let path = if cargo_path.is_some() {
// Try to run cargo mutants --version to verify it's installed
let output = Command::new("cargo")
.arg("mutants")
.arg("--version")
.output();
match output {
Ok(result) if result.status.success() => Some(PathBuf::from("cargo")),
_ => None,
}
} else {
None
};
if path.is_none() {
// Not installed, but don't error - allow graceful handling
eprintln!("⚠️ cargo-mutants not found in PATH");
eprintln!(" Install: cargo install cargo-mutants");
eprintln!();
}
Ok(Self {
cargo_mutants_path: path,
})
}
/// Get the detected cargo path (if cargo-mutants is installed)
///
/// Returns the path to the cargo binary used for executing cargo-mutants subcommand.
pub fn cargo_mutants_path(&self) -> Option<&PathBuf> {
self.cargo_mutants_path.as_ref()
}
/// Check if cargo-mutants is installed
///
/// Returns `true` if cargo-mutants subcommand is available via `cargo mutants`.
pub fn is_installed(&self) -> bool {
self.cargo_mutants_path.is_some()
}
/// Get cargo-mutants version
///
/// Executes `cargo mutants --version` and returns output.
///
/// # Errors
///
/// Returns error if cargo-mutants is not installed or execution fails.
pub fn version(&self) -> Result<String> {
self.cargo_mutants_path
.as_ref()
.ok_or("cargo-mutants not found in PATH")?;
let output = Command::new("cargo")
.arg("mutants")
.arg("--version")
.output()?;
if !output.status.success() {
return Err(format!(
"cargo mutants --version failed: {}",
String::from_utf8_lossy(&output.stderr)
)
.into());
}
let version_str = String::from_utf8(output.stdout)?.trim().to_string();
Ok(version_str)
}
/// Validate version meets minimum requirement (v24.7.0+)
///
/// # Errors
///
/// Returns error if:
/// - cargo-mutants is not installed
/// - Version cannot be retrieved
/// - Version format is invalid
/// - Version is below minimum (v24.7.0)
pub fn validate_version(&self) -> Result<()> {
let version_str = self.version()?;
let (major, minor) = Self::parse_version(&version_str)?;
// Enforce minimum v24.7.0
if major < 24 || (major == 24 && minor < 7) {
return Err(format!(
"cargo-mutants version {}.{} is too old. Minimum required: v24.7.0",
major, minor
)
.into());
}
Ok(())
}
/// Parse cargo-mutants version string
///
/// Expects format: "cargo-mutants X.Y.Z" where X is major, Y is minor, Z is patch.
///
/// # Errors
///
/// Returns error if version string format is invalid or version numbers cannot be parsed.
fn parse_version(version_str: &str) -> Result<(u32, u32)> {
// Parse version (example: "cargo-mutants 24.7.1")
let parts: Vec<&str> = version_str.split_whitespace().collect();
if parts.len() < 2 {
return Err(format!(
"Unexpected version format: '{}' (expected 'cargo-mutants X.Y.Z')",
version_str
)
.into());
}
let version_number = parts[1];
let version_parts: Vec<&str> = version_number.split('.').collect();
if version_parts.len() < 2 {
return Err(format!(
"Invalid version number: '{}' (expected X.Y.Z format)",
version_number
)
.into());
}
let major: u32 = version_parts[0].parse().map_err(|_| {
format!(
"Invalid major version: '{}' (not a number)",
version_parts[0]
)
})?;
let minor: u32 = version_parts[1].parse().map_err(|_| {
format!(
"Invalid minor version: '{}' (not a number)",
version_parts[1]
)
})?;
Ok((major, minor))
}
}
// GREEN Phase: Minimal tests to verify implementation
// More comprehensive tests are in tests/cargo_mutants_wrapper_tests.rs
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wrapper_initialization_succeeds() {
// Should not panic even if cargo-mutants is not installed
let result = CargoMutantsWrapper::new();
assert!(result.is_ok(), "Wrapper initialization should never fail");
}
#[test]
fn test_is_installed_returns_bool() {
let wrapper = CargoMutantsWrapper::new().unwrap();
// Should return true or false, not panic
let _installed = wrapper.is_installed();
}
#[test]
#[ignore] // Only run if cargo-mutants is actually installed
fn test_version_returns_string_when_installed() {
let wrapper = CargoMutantsWrapper::new().unwrap();
if wrapper.is_installed() {
let version = wrapper.version();
assert!(version.is_ok(), "version() should succeed when installed");
let version_str = version.unwrap();
assert!(
!version_str.is_empty(),
"Version string should not be empty"
);
assert!(
version_str.contains("cargo-mutants"),
"Version should mention cargo-mutants"
);
}
}
}