1use super::schema::Playbook;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum MutationClass {
13 StateRemoval,
15 TransitionRemoval,
17 EventSwap,
19 TargetSwap,
21 GuardNegation,
23}
24
25impl MutationClass {
26 pub fn all() -> Vec<MutationClass> {
28 vec![
29 MutationClass::StateRemoval,
30 MutationClass::TransitionRemoval,
31 MutationClass::EventSwap,
32 MutationClass::TargetSwap,
33 MutationClass::GuardNegation,
34 ]
35 }
36
37 pub fn id(&self) -> &'static str {
39 match self {
40 MutationClass::StateRemoval => "M1",
41 MutationClass::TransitionRemoval => "M2",
42 MutationClass::EventSwap => "M3",
43 MutationClass::TargetSwap => "M4",
44 MutationClass::GuardNegation => "M5",
45 }
46 }
47
48 pub fn description(&self) -> &'static str {
50 match self {
51 MutationClass::StateRemoval => "Remove a state from the machine",
52 MutationClass::TransitionRemoval => "Remove a transition from the machine",
53 MutationClass::EventSwap => "Swap event triggers between two transitions",
54 MutationClass::TargetSwap => "Change a transition's target to a different state",
55 MutationClass::GuardNegation => "Negate a transition's guard condition",
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
62pub struct Mutant {
63 pub id: String,
65 pub class: MutationClass,
67 pub description: String,
69 pub playbook: Playbook,
71}
72
73#[derive(Debug, Clone)]
75pub struct MutantResult {
76 pub mutant_id: String,
78 pub class: MutationClass,
80 pub killed: bool,
82 pub kill_reason: Option<String>,
84}
85
86#[derive(Debug, Clone)]
88pub struct MutationScore {
89 pub total_mutants: usize,
91 pub killed: usize,
93 pub survived: usize,
95 pub score: f64,
97 pub by_class: HashMap<MutationClass, ClassScore>,
99}
100
101#[derive(Debug, Clone, Default)]
103pub struct ClassScore {
104 pub total: usize,
105 pub killed: usize,
106 pub score: f64,
107}
108
109pub struct MutationGenerator<'a> {
111 playbook: &'a Playbook,
112}
113
114impl<'a> MutationGenerator<'a> {
115 pub fn new(playbook: &'a Playbook) -> Self {
117 Self { playbook }
118 }
119
120 pub fn generate_all(&self) -> Vec<Mutant> {
122 let mut mutants = Vec::new();
123 mutants.extend(self.generate_state_removals());
124 mutants.extend(self.generate_transition_removals());
125 mutants.extend(self.generate_event_swaps());
126 mutants.extend(self.generate_target_swaps());
127 mutants.extend(self.generate_guard_negations());
128 mutants
129 }
130
131 pub fn generate(&self, class: MutationClass) -> Vec<Mutant> {
133 match class {
134 MutationClass::StateRemoval => self.generate_state_removals(),
135 MutationClass::TransitionRemoval => self.generate_transition_removals(),
136 MutationClass::EventSwap => self.generate_event_swaps(),
137 MutationClass::TargetSwap => self.generate_target_swaps(),
138 MutationClass::GuardNegation => self.generate_guard_negations(),
139 }
140 }
141
142 fn generate_state_removals(&self) -> Vec<Mutant> {
144 let mut mutants = Vec::new();
145
146 for state_id in self.playbook.machine.states.keys() {
147 if *state_id == self.playbook.machine.initial {
149 continue;
150 }
151
152 let mut mutated = self.playbook.clone();
153 mutated.machine.states.remove(state_id);
154
155 mutated
157 .machine
158 .transitions
159 .retain(|t| t.from != *state_id && t.to != *state_id);
160
161 mutants.push(Mutant {
162 id: format!("M1_{}", state_id),
163 class: MutationClass::StateRemoval,
164 description: format!("Remove state '{}'", state_id),
165 playbook: mutated,
166 });
167 }
168
169 mutants
170 }
171
172 fn generate_transition_removals(&self) -> Vec<Mutant> {
174 let mut mutants = Vec::new();
175
176 for (idx, transition) in self.playbook.machine.transitions.iter().enumerate() {
177 let mut mutated = self.playbook.clone();
178 mutated.machine.transitions.remove(idx);
179
180 if !mutated.machine.transitions.is_empty() {
182 mutants.push(Mutant {
183 id: format!("M2_{}", transition.id),
184 class: MutationClass::TransitionRemoval,
185 description: format!("Remove transition '{}'", transition.id),
186 playbook: mutated,
187 });
188 }
189 }
190
191 mutants
192 }
193
194 fn generate_event_swaps(&self) -> Vec<Mutant> {
196 let mut mutants = Vec::new();
197 let transitions = &self.playbook.machine.transitions;
198
199 for i in 0..transitions.len() {
200 for j in (i + 1)..transitions.len() {
201 if transitions[i].event != transitions[j].event {
203 let mut mutated = self.playbook.clone();
204
205 let event_i = transitions[i].event.clone();
207 let event_j = transitions[j].event.clone();
208 mutated.machine.transitions[i].event = event_j;
209 mutated.machine.transitions[j].event = event_i;
210
211 mutants.push(Mutant {
212 id: format!("M3_{}_{}", transitions[i].id, transitions[j].id),
213 class: MutationClass::EventSwap,
214 description: format!(
215 "Swap events between '{}' and '{}'",
216 transitions[i].id, transitions[j].id
217 ),
218 playbook: mutated,
219 });
220 }
221 }
222 }
223
224 mutants
225 }
226
227 fn generate_target_swaps(&self) -> Vec<Mutant> {
229 let mut mutants = Vec::new();
230 let state_ids: Vec<_> = self.playbook.machine.states.keys().collect();
231
232 for (idx, transition) in self.playbook.machine.transitions.iter().enumerate() {
233 for state_id in &state_ids {
234 if **state_id == transition.to {
236 continue;
237 }
238
239 let mut mutated = self.playbook.clone();
240 mutated.machine.transitions[idx].to = (*state_id).clone();
241
242 mutants.push(Mutant {
243 id: format!("M4_{}_{}", transition.id, state_id),
244 class: MutationClass::TargetSwap,
245 description: format!(
246 "Change target of '{}' from '{}' to '{}'",
247 transition.id, transition.to, state_id
248 ),
249 playbook: mutated,
250 });
251 }
252 }
253
254 mutants
255 }
256
257 fn generate_guard_negations(&self) -> Vec<Mutant> {
259 let mut mutants = Vec::new();
260
261 for (idx, transition) in self.playbook.machine.transitions.iter().enumerate() {
262 if let Some(guard) = &transition.guard {
263 let mut mutated = self.playbook.clone();
264
265 let negated = format!("!({})", guard);
267 mutated.machine.transitions[idx].guard = Some(negated.clone());
268
269 mutants.push(Mutant {
270 id: format!("M5_{}", transition.id),
271 class: MutationClass::GuardNegation,
272 description: format!(
273 "Negate guard of '{}': '{}' → '{}'",
274 transition.id, guard, negated
275 ),
276 playbook: mutated,
277 });
278 }
279 }
280
281 mutants
282 }
283}
284
285pub fn calculate_mutation_score(results: &[MutantResult]) -> MutationScore {
287 let total_mutants = results.len();
288 let killed = results.iter().filter(|r| r.killed).count();
289 let survived = total_mutants - killed;
290 let score = if total_mutants > 0 {
291 killed as f64 / total_mutants as f64
292 } else {
293 1.0
294 };
295
296 let mut by_class: HashMap<MutationClass, ClassScore> = HashMap::new();
298
299 for class in MutationClass::all() {
300 let class_results: Vec<_> = results.iter().filter(|r| r.class == class).collect();
301 let class_total = class_results.len();
302 let class_killed = class_results.iter().filter(|r| r.killed).count();
303
304 by_class.insert(
305 class,
306 ClassScore {
307 total: class_total,
308 killed: class_killed,
309 score: if class_total > 0 {
310 class_killed as f64 / class_total as f64
311 } else {
312 1.0
313 },
314 },
315 );
316 }
317
318 MutationScore {
319 total_mutants,
320 killed,
321 survived,
322 score,
323 by_class,
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::playbook::schema::Playbook;
331
332 const TEST_PLAYBOOK: &str = r#"
333version: "1.0"
334machine:
335 id: "test"
336 initial: "start"
337 states:
338 start:
339 id: "start"
340 middle:
341 id: "middle"
342 end:
343 id: "end"
344 final_state: true
345 transitions:
346 - id: "t1"
347 from: "start"
348 to: "middle"
349 event: "next"
350 - id: "t2"
351 from: "middle"
352 to: "end"
353 event: "finish"
354 guard: "user.isLoggedIn"
355"#;
356
357 #[test]
358 fn test_generate_state_removals() {
359 let playbook = Playbook::from_yaml(TEST_PLAYBOOK).expect("parse");
360 let generator = MutationGenerator::new(&playbook);
361 let mutants = generator.generate(MutationClass::StateRemoval);
362
363 assert_eq!(mutants.len(), 2);
365 assert!(mutants
366 .iter()
367 .all(|m| m.class == MutationClass::StateRemoval));
368 }
369
370 #[test]
371 fn test_generate_transition_removals() {
372 let playbook = Playbook::from_yaml(TEST_PLAYBOOK).expect("parse");
373 let generator = MutationGenerator::new(&playbook);
374 let mutants = generator.generate(MutationClass::TransitionRemoval);
375
376 assert!(!mutants.is_empty());
379 assert!(mutants
380 .iter()
381 .all(|m| m.class == MutationClass::TransitionRemoval));
382 }
383
384 #[test]
385 fn test_generate_event_swaps() {
386 let playbook = Playbook::from_yaml(TEST_PLAYBOOK).expect("parse");
387 let generator = MutationGenerator::new(&playbook);
388 let mutants = generator.generate(MutationClass::EventSwap);
389
390 assert_eq!(mutants.len(), 1);
392 assert_eq!(mutants[0].class, MutationClass::EventSwap);
393 }
394
395 #[test]
396 fn test_generate_target_swaps() {
397 let playbook = Playbook::from_yaml(TEST_PLAYBOOK).expect("parse");
398 let generator = MutationGenerator::new(&playbook);
399 let mutants = generator.generate(MutationClass::TargetSwap);
400
401 assert_eq!(mutants.len(), 4);
403 assert!(mutants.iter().all(|m| m.class == MutationClass::TargetSwap));
404 }
405
406 #[test]
407 fn test_generate_guard_negations() {
408 let playbook = Playbook::from_yaml(TEST_PLAYBOOK).expect("parse");
409 let generator = MutationGenerator::new(&playbook);
410 let mutants = generator.generate(MutationClass::GuardNegation);
411
412 assert_eq!(mutants.len(), 1);
414 assert_eq!(mutants[0].class, MutationClass::GuardNegation);
415 assert!(mutants[0]
416 .playbook
417 .machine
418 .transitions
419 .iter()
420 .any(|t| t.guard.as_deref() == Some("!(user.isLoggedIn)")));
421 }
422
423 #[test]
424 fn test_generate_all() {
425 let playbook = Playbook::from_yaml(TEST_PLAYBOOK).expect("parse");
426 let generator = MutationGenerator::new(&playbook);
427 let mutants = generator.generate_all();
428
429 let has_m1 = mutants
431 .iter()
432 .any(|m| m.class == MutationClass::StateRemoval);
433 let has_m2 = mutants
434 .iter()
435 .any(|m| m.class == MutationClass::TransitionRemoval);
436 let has_m3 = mutants.iter().any(|m| m.class == MutationClass::EventSwap);
437 let has_m4 = mutants.iter().any(|m| m.class == MutationClass::TargetSwap);
438 let has_m5 = mutants
439 .iter()
440 .any(|m| m.class == MutationClass::GuardNegation);
441
442 assert!(has_m1);
443 assert!(has_m2);
444 assert!(has_m3);
445 assert!(has_m4);
446 assert!(has_m5);
447 }
448
449 #[test]
450 fn test_calculate_mutation_score() {
451 let results = vec![
452 MutantResult {
453 mutant_id: "M1_1".to_string(),
454 class: MutationClass::StateRemoval,
455 killed: true,
456 kill_reason: Some("Validation failed".to_string()),
457 },
458 MutantResult {
459 mutant_id: "M2_1".to_string(),
460 class: MutationClass::TransitionRemoval,
461 killed: true,
462 kill_reason: Some("Test failed".to_string()),
463 },
464 MutantResult {
465 mutant_id: "M3_1".to_string(),
466 class: MutationClass::EventSwap,
467 killed: false,
468 kill_reason: None,
469 },
470 ];
471
472 let score = calculate_mutation_score(&results);
473
474 assert_eq!(score.total_mutants, 3);
475 assert_eq!(score.killed, 2);
476 assert_eq!(score.survived, 1);
477 assert!((score.score - 0.666).abs() < 0.01);
478 }
479
480 #[test]
481 fn test_mutation_class_metadata() {
482 assert_eq!(MutationClass::StateRemoval.id(), "M1");
483 assert_eq!(MutationClass::TransitionRemoval.id(), "M2");
484 assert_eq!(MutationClass::EventSwap.id(), "M3");
485 assert_eq!(MutationClass::TargetSwap.id(), "M4");
486 assert_eq!(MutationClass::GuardNegation.id(), "M5");
487 }
488}