Skip to main content

verificar/mutator/
ast_mutator.rs

1//! AST-based mutation operators
2//!
3//! Provides proper AST-level mutations using the `PythonNode` representation.
4//! Unlike string-based mutations, these guarantee syntactic validity.
5
6// Allow match arms with same bodies - intentional for different AST node types
7// Allow self_only_used_in_recursion - the methods need &self for trait consistency
8#![allow(clippy::match_same_arms)]
9#![allow(clippy::self_only_used_in_recursion)]
10
11use crate::generator::{BinaryOp, CompareOp, PythonNode, UnaryOp};
12
13use super::MutationOperator;
14
15/// A mutation applied to an AST node
16#[derive(Debug, Clone)]
17pub struct AstMutation {
18    /// The operator used
19    pub operator: MutationOperator,
20    /// Path to the mutated node (indices in children)
21    pub path: Vec<usize>,
22    /// Description of the mutation
23    pub description: String,
24    /// The mutated AST
25    pub mutated_ast: PythonNode,
26}
27
28/// AST-based mutator for Python code
29#[derive(Debug, Default)]
30pub struct AstMutator {
31    /// Enabled mutation operators
32    enabled_operators: Vec<MutationOperator>,
33}
34
35impl AstMutator {
36    /// Create a new AST mutator with all operators enabled
37    #[must_use]
38    pub fn new() -> Self {
39        Self {
40            enabled_operators: MutationOperator::all(),
41        }
42    }
43
44    /// Create a mutator with specific operators enabled
45    #[must_use]
46    pub fn with_operators(operators: Vec<MutationOperator>) -> Self {
47        Self {
48            enabled_operators: operators,
49        }
50    }
51
52    /// Generate all possible mutations for the given AST
53    #[must_use]
54    pub fn mutate(&self, ast: &PythonNode) -> Vec<AstMutation> {
55        let mut mutations = Vec::new();
56
57        for operator in &self.enabled_operators {
58            let op_mutations = match operator {
59                MutationOperator::Aor => self.apply_aor(ast, vec![]),
60                MutationOperator::Ror => self.apply_ror(ast, vec![]),
61                MutationOperator::Lor => self.apply_lor(ast, vec![]),
62                MutationOperator::Uoi => self.apply_uoi(ast, vec![]),
63                MutationOperator::Abs => self.apply_abs(ast, vec![]),
64                MutationOperator::Sdl => self.apply_sdl(ast, vec![]),
65                MutationOperator::Svr => self.apply_svr(ast, vec![]),
66                MutationOperator::Bsr => self.apply_bsr(ast, vec![]),
67            };
68            mutations.extend(op_mutations);
69        }
70
71        mutations
72    }
73
74    /// AOR: Arithmetic Operator Replacement
75    /// `a + b` → `a - b`, `a * b`, `a / b`, etc.
76    fn apply_aor(&self, ast: &PythonNode, path: Vec<usize>) -> Vec<AstMutation> {
77        let mut mutations = Vec::new();
78
79        match ast {
80            PythonNode::BinOp { left, op, right } => {
81                // Generate mutations for this BinOp
82                let replacements = arithmetic_replacements(*op);
83                for new_op in replacements {
84                    let mutated = PythonNode::BinOp {
85                        left: left.clone(),
86                        op: new_op,
87                        right: right.clone(),
88                    };
89                    mutations.push(AstMutation {
90                        operator: MutationOperator::Aor,
91                        path: path.clone(),
92                        description: format!("Replace {} with {}", op.to_str(), new_op.to_str()),
93                        mutated_ast: mutated,
94                    });
95                }
96
97                // Recurse into children
98                let mut left_path = path.clone();
99                left_path.push(0);
100                mutations.extend(self.apply_aor(left, left_path));
101
102                let mut right_path = path;
103                right_path.push(1);
104                mutations.extend(self.apply_aor(right, right_path));
105            }
106            PythonNode::Module(stmts) => {
107                for (i, stmt) in stmts.iter().enumerate() {
108                    let mut child_path = path.clone();
109                    child_path.push(i);
110                    mutations.extend(self.apply_aor(stmt, child_path));
111                }
112            }
113            PythonNode::Assign { value, .. } => {
114                let mut child_path = path;
115                child_path.push(0);
116                mutations.extend(self.apply_aor(value, child_path));
117            }
118            PythonNode::If { test, body, orelse } => {
119                let mut test_path = path.clone();
120                test_path.push(0);
121                mutations.extend(self.apply_aor(test, test_path));
122
123                for (i, stmt) in body.iter().enumerate() {
124                    let mut child_path = path.clone();
125                    child_path.push(1 + i);
126                    mutations.extend(self.apply_aor(stmt, child_path));
127                }
128
129                for (i, stmt) in orelse.iter().enumerate() {
130                    let mut child_path = path.clone();
131                    child_path.push(1 + body.len() + i);
132                    mutations.extend(self.apply_aor(stmt, child_path));
133                }
134            }
135            PythonNode::Return(Some(value)) => {
136                let mut child_path = path;
137                child_path.push(0);
138                mutations.extend(self.apply_aor(value, child_path));
139            }
140            PythonNode::UnaryOp { operand, .. } => {
141                let mut child_path = path;
142                child_path.push(0);
143                mutations.extend(self.apply_aor(operand, child_path));
144            }
145            PythonNode::Compare { left, right, .. } => {
146                let mut left_path = path.clone();
147                left_path.push(0);
148                mutations.extend(self.apply_aor(left, left_path));
149
150                let mut right_path = path;
151                right_path.push(1);
152                mutations.extend(self.apply_aor(right, right_path));
153            }
154            _ => {}
155        }
156
157        mutations
158    }
159
160    /// ROR: Relational Operator Replacement
161    /// `a < b` → `a <= b`, `a > b`, `a >= b`, `a == b`, `a != b`
162    fn apply_ror(&self, ast: &PythonNode, path: Vec<usize>) -> Vec<AstMutation> {
163        let mut mutations = Vec::new();
164
165        match ast {
166            PythonNode::Compare { left, op, right } => {
167                // Generate mutations for this Compare
168                let replacements = relational_replacements(*op);
169                for new_op in replacements {
170                    let mutated = PythonNode::Compare {
171                        left: left.clone(),
172                        op: new_op,
173                        right: right.clone(),
174                    };
175                    mutations.push(AstMutation {
176                        operator: MutationOperator::Ror,
177                        path: path.clone(),
178                        description: format!("Replace {} with {}", op.to_str(), new_op.to_str()),
179                        mutated_ast: mutated,
180                    });
181                }
182
183                // Recurse
184                let mut left_path = path.clone();
185                left_path.push(0);
186                mutations.extend(self.apply_ror(left, left_path));
187
188                let mut right_path = path;
189                right_path.push(1);
190                mutations.extend(self.apply_ror(right, right_path));
191            }
192            PythonNode::Module(stmts) => {
193                for (i, stmt) in stmts.iter().enumerate() {
194                    let mut child_path = path.clone();
195                    child_path.push(i);
196                    mutations.extend(self.apply_ror(stmt, child_path));
197                }
198            }
199            PythonNode::If { test, body, orelse } => {
200                let mut test_path = path.clone();
201                test_path.push(0);
202                mutations.extend(self.apply_ror(test, test_path));
203
204                for (i, stmt) in body.iter().enumerate() {
205                    let mut child_path = path.clone();
206                    child_path.push(1 + i);
207                    mutations.extend(self.apply_ror(stmt, child_path));
208                }
209
210                for (i, stmt) in orelse.iter().enumerate() {
211                    let mut child_path = path.clone();
212                    child_path.push(1 + body.len() + i);
213                    mutations.extend(self.apply_ror(stmt, child_path));
214                }
215            }
216            PythonNode::While { test, body } => {
217                let mut test_path = path.clone();
218                test_path.push(0);
219                mutations.extend(self.apply_ror(test, test_path));
220
221                for (i, stmt) in body.iter().enumerate() {
222                    let mut child_path = path.clone();
223                    child_path.push(1 + i);
224                    mutations.extend(self.apply_ror(stmt, child_path));
225                }
226            }
227            _ => {}
228        }
229
230        mutations
231    }
232
233    /// LOR: Logical Operator Replacement
234    /// `a and b` → `a or b`
235    fn apply_lor(&self, ast: &PythonNode, path: Vec<usize>) -> Vec<AstMutation> {
236        let mut mutations = Vec::new();
237
238        match ast {
239            PythonNode::BinOp { left, op, right } => {
240                // Only mutate logical operators
241                let replacements = logical_replacements(*op);
242                for new_op in replacements {
243                    let mutated = PythonNode::BinOp {
244                        left: left.clone(),
245                        op: new_op,
246                        right: right.clone(),
247                    };
248                    mutations.push(AstMutation {
249                        operator: MutationOperator::Lor,
250                        path: path.clone(),
251                        description: format!("Replace {} with {}", op.to_str(), new_op.to_str()),
252                        mutated_ast: mutated,
253                    });
254                }
255
256                // Recurse
257                let mut left_path = path.clone();
258                left_path.push(0);
259                mutations.extend(self.apply_lor(left, left_path));
260
261                let mut right_path = path;
262                right_path.push(1);
263                mutations.extend(self.apply_lor(right, right_path));
264            }
265            PythonNode::Module(stmts) => {
266                for (i, stmt) in stmts.iter().enumerate() {
267                    let mut child_path = path.clone();
268                    child_path.push(i);
269                    mutations.extend(self.apply_lor(stmt, child_path));
270                }
271            }
272            PythonNode::If { test, body, orelse } => {
273                let mut test_path = path.clone();
274                test_path.push(0);
275                mutations.extend(self.apply_lor(test, test_path));
276
277                for (i, stmt) in body.iter().enumerate() {
278                    let mut child_path = path.clone();
279                    child_path.push(1 + i);
280                    mutations.extend(self.apply_lor(stmt, child_path));
281                }
282
283                for (i, stmt) in orelse.iter().enumerate() {
284                    let mut child_path = path.clone();
285                    child_path.push(1 + body.len() + i);
286                    mutations.extend(self.apply_lor(stmt, child_path));
287                }
288            }
289            _ => {}
290        }
291
292        mutations
293    }
294
295    /// UOI: Unary Operator Insertion
296    /// `x` → `-x`, `not x`
297    fn apply_uoi(&self, ast: &PythonNode, path: Vec<usize>) -> Vec<AstMutation> {
298        let mut mutations = Vec::new();
299
300        match ast {
301            PythonNode::Name(name) => {
302                // Insert negation
303                let neg_mutated = PythonNode::UnaryOp {
304                    op: UnaryOp::Neg,
305                    operand: Box::new(ast.clone()),
306                };
307                mutations.push(AstMutation {
308                    operator: MutationOperator::Uoi,
309                    path: path.clone(),
310                    description: format!("Insert negation: {name} → -{name}"),
311                    mutated_ast: neg_mutated,
312                });
313
314                // Insert not
315                let not_mutated = PythonNode::UnaryOp {
316                    op: UnaryOp::Not,
317                    operand: Box::new(ast.clone()),
318                };
319                mutations.push(AstMutation {
320                    operator: MutationOperator::Uoi,
321                    path,
322                    description: format!("Insert not: {name} → not {name}"),
323                    mutated_ast: not_mutated,
324                });
325            }
326            PythonNode::IntLit(n) => {
327                // Insert negation for integers
328                let neg_mutated = PythonNode::UnaryOp {
329                    op: UnaryOp::Neg,
330                    operand: Box::new(ast.clone()),
331                };
332                mutations.push(AstMutation {
333                    operator: MutationOperator::Uoi,
334                    path,
335                    description: format!("Insert negation: {n} → -{n}"),
336                    mutated_ast: neg_mutated,
337                });
338            }
339            PythonNode::Module(stmts) => {
340                for (i, stmt) in stmts.iter().enumerate() {
341                    let mut child_path = path.clone();
342                    child_path.push(i);
343                    mutations.extend(self.apply_uoi(stmt, child_path));
344                }
345            }
346            PythonNode::Assign { value, .. } => {
347                let mut child_path = path;
348                child_path.push(0);
349                mutations.extend(self.apply_uoi(value, child_path));
350            }
351            PythonNode::BinOp { left, right, .. } => {
352                let mut left_path = path.clone();
353                left_path.push(0);
354                mutations.extend(self.apply_uoi(left, left_path));
355
356                let mut right_path = path;
357                right_path.push(1);
358                mutations.extend(self.apply_uoi(right, right_path));
359            }
360            _ => {}
361        }
362
363        mutations
364    }
365
366    /// ABS: Absolute Value Insertion
367    /// `x` → `abs(x)`
368    fn apply_abs(&self, ast: &PythonNode, path: Vec<usize>) -> Vec<AstMutation> {
369        let mut mutations = Vec::new();
370
371        match ast {
372            PythonNode::Name(name) => {
373                let abs_mutated = PythonNode::Call {
374                    func: "abs".to_string(),
375                    args: vec![ast.clone()],
376                };
377                mutations.push(AstMutation {
378                    operator: MutationOperator::Abs,
379                    path,
380                    description: format!("Insert abs: {name} → abs({name})"),
381                    mutated_ast: abs_mutated,
382                });
383            }
384            PythonNode::IntLit(n) => {
385                let abs_mutated = PythonNode::Call {
386                    func: "abs".to_string(),
387                    args: vec![ast.clone()],
388                };
389                mutations.push(AstMutation {
390                    operator: MutationOperator::Abs,
391                    path,
392                    description: format!("Insert abs: {n} → abs({n})"),
393                    mutated_ast: abs_mutated,
394                });
395            }
396            PythonNode::Module(stmts) => {
397                for (i, stmt) in stmts.iter().enumerate() {
398                    let mut child_path = path.clone();
399                    child_path.push(i);
400                    mutations.extend(self.apply_abs(stmt, child_path));
401                }
402            }
403            PythonNode::Assign { value, .. } => {
404                let mut child_path = path;
405                child_path.push(0);
406                mutations.extend(self.apply_abs(value, child_path));
407            }
408            _ => {}
409        }
410
411        mutations
412    }
413
414    /// SDL: Statement Deletion
415    /// Delete a statement from the program
416    fn apply_sdl(&self, ast: &PythonNode, path: Vec<usize>) -> Vec<AstMutation> {
417        let mut mutations = Vec::new();
418
419        if let PythonNode::Module(stmts) = ast {
420            // For each statement, create a mutation that deletes it
421            for i in 0..stmts.len() {
422                if stmts.len() > 1 {
423                    // Can only delete if more than one statement
424                    let mut new_stmts = stmts.clone();
425                    new_stmts.remove(i);
426
427                    let mutated = PythonNode::Module(new_stmts);
428                    let mut del_path = path.clone();
429                    del_path.push(i);
430
431                    mutations.push(AstMutation {
432                        operator: MutationOperator::Sdl,
433                        path: del_path,
434                        description: format!("Delete statement {}", i + 1),
435                        mutated_ast: mutated,
436                    });
437                }
438            }
439
440            // Also check for deletable statements within compound statements
441            for (i, stmt) in stmts.iter().enumerate() {
442                let mut child_path = path.clone();
443                child_path.push(i);
444                mutations.extend(self.apply_sdl_compound(stmt, child_path));
445            }
446        }
447
448        mutations
449    }
450
451    /// Helper for SDL in compound statements
452    fn apply_sdl_compound(&self, ast: &PythonNode, path: Vec<usize>) -> Vec<AstMutation> {
453        let mut mutations = Vec::new();
454
455        match ast {
456            PythonNode::If { test, body, orelse } => {
457                // Delete statements from body
458                if body.len() > 1 {
459                    for i in 0..body.len() {
460                        let mut new_body = body.clone();
461                        new_body.remove(i);
462
463                        let mutated = PythonNode::If {
464                            test: test.clone(),
465                            body: new_body,
466                            orelse: orelse.clone(),
467                        };
468
469                        let mut del_path = path.clone();
470                        del_path.push(i);
471
472                        mutations.push(AstMutation {
473                            operator: MutationOperator::Sdl,
474                            path: del_path,
475                            description: format!("Delete if-body statement {}", i + 1),
476                            mutated_ast: mutated,
477                        });
478                    }
479                }
480
481                // Delete statements from orelse
482                if orelse.len() > 1 {
483                    for i in 0..orelse.len() {
484                        let mut new_orelse = orelse.clone();
485                        new_orelse.remove(i);
486
487                        let mutated = PythonNode::If {
488                            test: test.clone(),
489                            body: body.clone(),
490                            orelse: new_orelse,
491                        };
492
493                        let mut del_path = path.clone();
494                        del_path.push(body.len() + i);
495
496                        mutations.push(AstMutation {
497                            operator: MutationOperator::Sdl,
498                            path: del_path,
499                            description: format!("Delete else-body statement {}", i + 1),
500                            mutated_ast: mutated,
501                        });
502                    }
503                }
504            }
505            PythonNode::FuncDef { name, args, body } => {
506                if body.len() > 1 {
507                    for i in 0..body.len() {
508                        let mut new_body = body.clone();
509                        new_body.remove(i);
510
511                        let mutated = PythonNode::FuncDef {
512                            name: name.clone(),
513                            args: args.clone(),
514                            body: new_body,
515                        };
516
517                        let mut del_path = path.clone();
518                        del_path.push(i);
519
520                        mutations.push(AstMutation {
521                            operator: MutationOperator::Sdl,
522                            path: del_path,
523                            description: format!("Delete function body statement {}", i + 1),
524                            mutated_ast: mutated,
525                        });
526                    }
527                }
528            }
529            _ => {}
530        }
531
532        mutations
533    }
534
535    /// SVR: Scalar Variable Replacement
536    /// `x` → `y` (replace one variable with another in scope)
537    fn apply_svr(&self, ast: &PythonNode, path: Vec<usize>) -> Vec<AstMutation> {
538        let mut mutations = Vec::new();
539
540        // First, collect all variable names in the AST
541        let var_names = collect_variable_names(ast);
542
543        if var_names.len() < 2 {
544            return mutations; // Need at least 2 variables for SVR
545        }
546
547        self.apply_svr_recursive(ast, path, &var_names, &mut mutations);
548        mutations
549    }
550
551    fn apply_svr_recursive(
552        &self,
553        ast: &PythonNode,
554        path: Vec<usize>,
555        var_names: &[String],
556        mutations: &mut Vec<AstMutation>,
557    ) {
558        match ast {
559            PythonNode::Name(name) => {
560                // Replace with each other variable
561                for other_var in var_names {
562                    if other_var != name {
563                        let mutated = PythonNode::Name(other_var.clone());
564                        mutations.push(AstMutation {
565                            operator: MutationOperator::Svr,
566                            path: path.clone(),
567                            description: format!("Replace {name} with {other_var}"),
568                            mutated_ast: mutated,
569                        });
570                    }
571                }
572            }
573            PythonNode::Module(stmts) => {
574                for (i, stmt) in stmts.iter().enumerate() {
575                    let mut child_path = path.clone();
576                    child_path.push(i);
577                    self.apply_svr_recursive(stmt, child_path, var_names, mutations);
578                }
579            }
580            PythonNode::Assign { value, .. } => {
581                let mut child_path = path;
582                child_path.push(0);
583                self.apply_svr_recursive(value, child_path, var_names, mutations);
584            }
585            PythonNode::BinOp { left, right, .. } => {
586                let mut left_path = path.clone();
587                left_path.push(0);
588                self.apply_svr_recursive(left, left_path, var_names, mutations);
589
590                let mut right_path = path;
591                right_path.push(1);
592                self.apply_svr_recursive(right, right_path, var_names, mutations);
593            }
594            _ => {}
595        }
596    }
597
598    /// BSR: Boundary Substitution Replacement
599    /// `0` → `-1`, `1` → `0`, `""` → `" "`, etc.
600    fn apply_bsr(&self, ast: &PythonNode, path: Vec<usize>) -> Vec<AstMutation> {
601        let mut mutations = Vec::new();
602
603        match ast {
604            PythonNode::IntLit(n) => {
605                let boundaries = boundary_int_values(*n);
606                for new_val in boundaries {
607                    let mutated = PythonNode::IntLit(new_val);
608                    mutations.push(AstMutation {
609                        operator: MutationOperator::Bsr,
610                        path: path.clone(),
611                        description: format!("Boundary: {n} → {new_val}"),
612                        mutated_ast: mutated,
613                    });
614                }
615            }
616            PythonNode::StrLit(s) => {
617                let boundaries = boundary_str_values(s);
618                for new_val in boundaries {
619                    let mutated = PythonNode::StrLit(new_val.clone());
620                    mutations.push(AstMutation {
621                        operator: MutationOperator::Bsr,
622                        path: path.clone(),
623                        description: format!("Boundary: \"{s}\" → \"{new_val}\""),
624                        mutated_ast: mutated,
625                    });
626                }
627            }
628            PythonNode::BoolLit(b) => {
629                let mutated = PythonNode::BoolLit(!b);
630                mutations.push(AstMutation {
631                    operator: MutationOperator::Bsr,
632                    path,
633                    description: format!("Boundary: {b} → {}", !b),
634                    mutated_ast: mutated,
635                });
636            }
637            PythonNode::Module(stmts) => {
638                for (i, stmt) in stmts.iter().enumerate() {
639                    let mut child_path = path.clone();
640                    child_path.push(i);
641                    mutations.extend(self.apply_bsr(stmt, child_path));
642                }
643            }
644            PythonNode::Assign { value, .. } => {
645                let mut child_path = path;
646                child_path.push(0);
647                mutations.extend(self.apply_bsr(value, child_path));
648            }
649            PythonNode::BinOp { left, right, .. } => {
650                let mut left_path = path.clone();
651                left_path.push(0);
652                mutations.extend(self.apply_bsr(left, left_path));
653
654                let mut right_path = path;
655                right_path.push(1);
656                mutations.extend(self.apply_bsr(right, right_path));
657            }
658            _ => {}
659        }
660
661        mutations
662    }
663}
664
665/// Get arithmetic operator replacements
666fn arithmetic_replacements(op: BinaryOp) -> Vec<BinaryOp> {
667    match op {
668        BinaryOp::Add => vec![BinaryOp::Sub, BinaryOp::Mult],
669        BinaryOp::Sub => vec![BinaryOp::Add, BinaryOp::Mult],
670        BinaryOp::Mult => vec![BinaryOp::Add, BinaryOp::Div],
671        BinaryOp::Div => vec![BinaryOp::Mult, BinaryOp::FloorDiv],
672        BinaryOp::Mod => vec![BinaryOp::Div, BinaryOp::FloorDiv],
673        BinaryOp::FloorDiv => vec![BinaryOp::Div, BinaryOp::Mod],
674        BinaryOp::Pow => vec![BinaryOp::Mult],
675        BinaryOp::And | BinaryOp::Or => vec![], // Not arithmetic
676    }
677}
678
679/// Get relational operator replacements
680fn relational_replacements(op: CompareOp) -> Vec<CompareOp> {
681    match op {
682        CompareOp::Eq => vec![CompareOp::NotEq],
683        CompareOp::NotEq => vec![CompareOp::Eq],
684        CompareOp::Lt => vec![CompareOp::LtE, CompareOp::Gt],
685        CompareOp::LtE => vec![CompareOp::Lt, CompareOp::GtE],
686        CompareOp::Gt => vec![CompareOp::GtE, CompareOp::Lt],
687        CompareOp::GtE => vec![CompareOp::Gt, CompareOp::LtE],
688    }
689}
690
691/// Get logical operator replacements
692fn logical_replacements(op: BinaryOp) -> Vec<BinaryOp> {
693    match op {
694        BinaryOp::And => vec![BinaryOp::Or],
695        BinaryOp::Or => vec![BinaryOp::And],
696        _ => vec![], // Not logical
697    }
698}
699
700/// Collect all variable names from an AST
701fn collect_variable_names(ast: &PythonNode) -> Vec<String> {
702    let mut names = Vec::new();
703    collect_names_recursive(ast, &mut names);
704    names.sort();
705    names.dedup();
706    names
707}
708
709fn collect_names_recursive(ast: &PythonNode, names: &mut Vec<String>) {
710    match ast {
711        PythonNode::Name(name) => names.push(name.clone()),
712        PythonNode::Assign { target, value } => {
713            names.push(target.clone());
714            collect_names_recursive(value, names);
715        }
716        PythonNode::Module(stmts) => {
717            for stmt in stmts {
718                collect_names_recursive(stmt, names);
719            }
720        }
721        PythonNode::BinOp { left, right, .. } | PythonNode::Compare { left, right, .. } => {
722            collect_names_recursive(left, names);
723            collect_names_recursive(right, names);
724        }
725        PythonNode::UnaryOp { operand, .. } => {
726            collect_names_recursive(operand, names);
727        }
728        PythonNode::If { test, body, orelse } => {
729            collect_names_recursive(test, names);
730            for stmt in body {
731                collect_names_recursive(stmt, names);
732            }
733            for stmt in orelse {
734                collect_names_recursive(stmt, names);
735            }
736        }
737        PythonNode::FuncDef { args, body, .. } => {
738            for arg in args {
739                names.push(arg.clone());
740            }
741            for stmt in body {
742                collect_names_recursive(stmt, names);
743            }
744        }
745        PythonNode::Return(Some(value)) => collect_names_recursive(value, names),
746        _ => {}
747    }
748}
749
750/// Get boundary integer values
751fn boundary_int_values(n: i64) -> Vec<i64> {
752    match n {
753        0 => vec![-1, 1],
754        1 => vec![0, 2],
755        -1 => vec![0, -2],
756        _ if n > 0 => vec![n - 1, n + 1, 0],
757        _ => vec![n - 1, n + 1, 0],
758    }
759}
760
761/// Get boundary string values
762fn boundary_str_values(s: &str) -> Vec<String> {
763    if s.is_empty() {
764        vec![" ".to_string(), "a".to_string()]
765    } else {
766        vec![String::new(), format!("{s} ")]
767    }
768}
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773
774    #[test]
775    fn test_aor_basic() {
776        let ast = PythonNode::BinOp {
777            left: Box::new(PythonNode::Name("a".to_string())),
778            op: BinaryOp::Add,
779            right: Box::new(PythonNode::Name("b".to_string())),
780        };
781
782        let mutator = AstMutator::with_operators(vec![MutationOperator::Aor]);
783        let mutations = mutator.mutate(&ast);
784
785        assert!(!mutations.is_empty());
786        // Should have mutations replacing + with - and *
787        assert!(mutations
788            .iter()
789            .any(|m| m.description.contains("Replace + with -")));
790    }
791
792    #[test]
793    fn test_ror_basic() {
794        let ast = PythonNode::Compare {
795            left: Box::new(PythonNode::Name("x".to_string())),
796            op: CompareOp::Lt,
797            right: Box::new(PythonNode::IntLit(10)),
798        };
799
800        let mutator = AstMutator::with_operators(vec![MutationOperator::Ror]);
801        let mutations = mutator.mutate(&ast);
802
803        assert!(!mutations.is_empty());
804        assert!(mutations
805            .iter()
806            .any(|m| m.description.contains("Replace < with <=")));
807    }
808
809    #[test]
810    fn test_lor_basic() {
811        let ast = PythonNode::BinOp {
812            left: Box::new(PythonNode::Name("a".to_string())),
813            op: BinaryOp::And,
814            right: Box::new(PythonNode::Name("b".to_string())),
815        };
816
817        let mutator = AstMutator::with_operators(vec![MutationOperator::Lor]);
818        let mutations = mutator.mutate(&ast);
819
820        assert!(!mutations.is_empty());
821        assert!(mutations
822            .iter()
823            .any(|m| m.description.contains("Replace and with or")));
824    }
825
826    #[test]
827    fn test_uoi_basic() {
828        let ast = PythonNode::Assign {
829            target: "x".to_string(),
830            value: Box::new(PythonNode::Name("y".to_string())),
831        };
832
833        let mutator = AstMutator::with_operators(vec![MutationOperator::Uoi]);
834        let mutations = mutator.mutate(&ast);
835
836        assert!(!mutations.is_empty());
837        assert!(mutations
838            .iter()
839            .any(|m| m.description.contains("Insert negation")));
840    }
841
842    #[test]
843    fn test_abs_basic() {
844        let ast = PythonNode::Assign {
845            target: "x".to_string(),
846            value: Box::new(PythonNode::Name("y".to_string())),
847        };
848
849        let mutator = AstMutator::with_operators(vec![MutationOperator::Abs]);
850        let mutations = mutator.mutate(&ast);
851
852        assert!(!mutations.is_empty());
853        assert!(mutations
854            .iter()
855            .any(|m| m.description.contains("Insert abs")));
856    }
857
858    #[test]
859    fn test_sdl_basic() {
860        let ast = PythonNode::Module(vec![
861            PythonNode::Assign {
862                target: "x".to_string(),
863                value: Box::new(PythonNode::IntLit(1)),
864            },
865            PythonNode::Assign {
866                target: "y".to_string(),
867                value: Box::new(PythonNode::IntLit(2)),
868            },
869        ]);
870
871        let mutator = AstMutator::with_operators(vec![MutationOperator::Sdl]);
872        let mutations = mutator.mutate(&ast);
873
874        assert_eq!(mutations.len(), 2); // Can delete either statement
875    }
876
877    #[test]
878    fn test_svr_basic() {
879        let ast = PythonNode::Module(vec![
880            PythonNode::Assign {
881                target: "x".to_string(),
882                value: Box::new(PythonNode::IntLit(1)),
883            },
884            PythonNode::Assign {
885                target: "y".to_string(),
886                value: Box::new(PythonNode::Name("x".to_string())),
887            },
888        ]);
889
890        let mutator = AstMutator::with_operators(vec![MutationOperator::Svr]);
891        let mutations = mutator.mutate(&ast);
892
893        assert!(!mutations.is_empty());
894        // Should have mutation replacing x with y
895        assert!(mutations
896            .iter()
897            .any(|m| m.description.contains("Replace x with y")));
898    }
899
900    #[test]
901    fn test_bsr_basic() {
902        let ast = PythonNode::Assign {
903            target: "x".to_string(),
904            value: Box::new(PythonNode::IntLit(0)),
905        };
906
907        let mutator = AstMutator::with_operators(vec![MutationOperator::Bsr]);
908        let mutations = mutator.mutate(&ast);
909
910        assert!(!mutations.is_empty());
911        // Should have mutations for 0 → -1 and 0 → 1
912        assert!(mutations.iter().any(|m| m.description.contains("0 → -1")));
913        assert!(mutations.iter().any(|m| m.description.contains("0 → 1")));
914    }
915
916    #[test]
917    fn test_all_operators() {
918        let ast = PythonNode::Module(vec![
919            PythonNode::Assign {
920                target: "x".to_string(),
921                value: Box::new(PythonNode::BinOp {
922                    left: Box::new(PythonNode::IntLit(1)),
923                    op: BinaryOp::Add,
924                    right: Box::new(PythonNode::IntLit(2)),
925                }),
926            },
927            PythonNode::If {
928                test: Box::new(PythonNode::Compare {
929                    left: Box::new(PythonNode::Name("x".to_string())),
930                    op: CompareOp::Lt,
931                    right: Box::new(PythonNode::IntLit(10)),
932                }),
933                body: vec![PythonNode::Pass],
934                orelse: vec![],
935            },
936        ]);
937
938        let mutator = AstMutator::new();
939        let mutations = mutator.mutate(&ast);
940
941        // Should have mutations from multiple operators
942        assert!(mutations.len() > 5);
943
944        // Check we have mutations from different operators
945        let operators: std::collections::HashSet<_> =
946            mutations.iter().map(|m| m.operator).collect();
947        assert!(operators.len() >= 3);
948    }
949
950    #[test]
951    fn test_mutation_produces_valid_ast() {
952        let ast = PythonNode::BinOp {
953            left: Box::new(PythonNode::IntLit(1)),
954            op: BinaryOp::Add,
955            right: Box::new(PythonNode::IntLit(2)),
956        };
957
958        let mutator = AstMutator::new();
959        let mutations = mutator.mutate(&ast);
960
961        // All mutations should produce valid Python code
962        for mutation in &mutations {
963            let code = mutation.mutated_ast.to_code(0);
964            assert!(!code.is_empty());
965        }
966    }
967
968    #[test]
969    fn test_ast_mutator_default() {
970        let mutator = AstMutator::default();
971        assert!(mutator.enabled_operators.is_empty());
972    }
973
974    #[test]
975    fn test_ast_mutator_debug() {
976        let mutator = AstMutator::new();
977        let debug = format!("{:?}", mutator);
978        assert!(debug.contains("AstMutator"));
979    }
980
981    #[test]
982    fn test_ast_mutation_debug() {
983        let mutation = AstMutation {
984            operator: MutationOperator::Aor,
985            path: vec![0, 1],
986            description: "Test mutation".to_string(),
987            mutated_ast: PythonNode::IntLit(1),
988        };
989        let debug = format!("{:?}", mutation);
990        assert!(debug.contains("AstMutation"));
991    }
992
993    #[test]
994    fn test_ast_mutation_clone() {
995        let mutation = AstMutation {
996            operator: MutationOperator::Aor,
997            path: vec![0, 1],
998            description: "Test mutation".to_string(),
999            mutated_ast: PythonNode::IntLit(1),
1000        };
1001        let cloned = mutation.clone();
1002        assert_eq!(cloned.operator, mutation.operator);
1003        assert_eq!(cloned.path, mutation.path);
1004    }
1005
1006    #[test]
1007    fn test_aor_in_if_body() {
1008        let ast = PythonNode::If {
1009            test: Box::new(PythonNode::BoolLit(true)),
1010            body: vec![PythonNode::Assign {
1011                target: "x".to_string(),
1012                value: Box::new(PythonNode::BinOp {
1013                    left: Box::new(PythonNode::IntLit(1)),
1014                    op: BinaryOp::Add,
1015                    right: Box::new(PythonNode::IntLit(2)),
1016                }),
1017            }],
1018            orelse: vec![PythonNode::Assign {
1019                target: "y".to_string(),
1020                value: Box::new(PythonNode::BinOp {
1021                    left: Box::new(PythonNode::IntLit(3)),
1022                    op: BinaryOp::Sub,
1023                    right: Box::new(PythonNode::IntLit(4)),
1024                }),
1025            }],
1026        };
1027
1028        let mutator = AstMutator::with_operators(vec![MutationOperator::Aor]);
1029        let mutations = mutator.mutate(&ast);
1030        assert!(!mutations.is_empty());
1031    }
1032
1033    #[test]
1034    fn test_aor_in_return() {
1035        let ast = PythonNode::Return(Some(Box::new(PythonNode::BinOp {
1036            left: Box::new(PythonNode::IntLit(1)),
1037            op: BinaryOp::Mult,
1038            right: Box::new(PythonNode::IntLit(2)),
1039        })));
1040
1041        let mutator = AstMutator::with_operators(vec![MutationOperator::Aor]);
1042        let mutations = mutator.mutate(&ast);
1043        assert!(!mutations.is_empty());
1044    }
1045
1046    #[test]
1047    fn test_aor_in_unary_op() {
1048        let ast = PythonNode::UnaryOp {
1049            op: UnaryOp::Neg,
1050            operand: Box::new(PythonNode::BinOp {
1051                left: Box::new(PythonNode::IntLit(1)),
1052                op: BinaryOp::Add,
1053                right: Box::new(PythonNode::IntLit(2)),
1054            }),
1055        };
1056
1057        let mutator = AstMutator::with_operators(vec![MutationOperator::Aor]);
1058        let mutations = mutator.mutate(&ast);
1059        assert!(!mutations.is_empty());
1060    }
1061
1062    #[test]
1063    fn test_aor_in_compare() {
1064        let ast = PythonNode::Compare {
1065            left: Box::new(PythonNode::BinOp {
1066                left: Box::new(PythonNode::IntLit(1)),
1067                op: BinaryOp::Add,
1068                right: Box::new(PythonNode::IntLit(2)),
1069            }),
1070            op: CompareOp::Lt,
1071            right: Box::new(PythonNode::IntLit(10)),
1072        };
1073
1074        let mutator = AstMutator::with_operators(vec![MutationOperator::Aor]);
1075        let mutations = mutator.mutate(&ast);
1076        assert!(!mutations.is_empty());
1077    }
1078
1079    #[test]
1080    fn test_ror_in_while() {
1081        let ast = PythonNode::While {
1082            test: Box::new(PythonNode::Compare {
1083                left: Box::new(PythonNode::Name("x".to_string())),
1084                op: CompareOp::Lt,
1085                right: Box::new(PythonNode::IntLit(10)),
1086            }),
1087            body: vec![PythonNode::Assign {
1088                target: "x".to_string(),
1089                value: Box::new(PythonNode::BinOp {
1090                    left: Box::new(PythonNode::Name("x".to_string())),
1091                    op: BinaryOp::Add,
1092                    right: Box::new(PythonNode::IntLit(1)),
1093                }),
1094            }],
1095        };
1096
1097        let mutator = AstMutator::with_operators(vec![MutationOperator::Ror]);
1098        let mutations = mutator.mutate(&ast);
1099        assert!(!mutations.is_empty());
1100    }
1101
1102    #[test]
1103    fn test_ror_in_if_orelse() {
1104        let ast = PythonNode::If {
1105            test: Box::new(PythonNode::Compare {
1106                left: Box::new(PythonNode::Name("x".to_string())),
1107                op: CompareOp::Gt,
1108                right: Box::new(PythonNode::IntLit(0)),
1109            }),
1110            body: vec![PythonNode::Pass],
1111            orelse: vec![PythonNode::If {
1112                test: Box::new(PythonNode::Compare {
1113                    left: Box::new(PythonNode::Name("x".to_string())),
1114                    op: CompareOp::Eq,
1115                    right: Box::new(PythonNode::IntLit(0)),
1116                }),
1117                body: vec![PythonNode::Pass],
1118                orelse: vec![],
1119            }],
1120        };
1121
1122        let mutator = AstMutator::with_operators(vec![MutationOperator::Ror]);
1123        let mutations = mutator.mutate(&ast);
1124        assert!(!mutations.is_empty());
1125    }
1126
1127    #[test]
1128    fn test_lor_in_if() {
1129        let ast = PythonNode::If {
1130            test: Box::new(PythonNode::BinOp {
1131                left: Box::new(PythonNode::Name("a".to_string())),
1132                op: BinaryOp::And,
1133                right: Box::new(PythonNode::Name("b".to_string())),
1134            }),
1135            body: vec![PythonNode::Pass],
1136            orelse: vec![PythonNode::Pass],
1137        };
1138
1139        let mutator = AstMutator::with_operators(vec![MutationOperator::Lor]);
1140        let mutations = mutator.mutate(&ast);
1141        assert!(!mutations.is_empty());
1142    }
1143
1144    #[test]
1145    fn test_bsr_in_binop() {
1146        let ast = PythonNode::BinOp {
1147            left: Box::new(PythonNode::IntLit(0)),
1148            op: BinaryOp::Add,
1149            right: Box::new(PythonNode::IntLit(1)),
1150        };
1151
1152        let mutator = AstMutator::with_operators(vec![MutationOperator::Bsr]);
1153        let mutations = mutator.mutate(&ast);
1154        assert!(!mutations.is_empty());
1155    }
1156
1157    #[test]
1158    fn test_bsr_string() {
1159        let ast = PythonNode::Assign {
1160            target: "s".to_string(),
1161            value: Box::new(PythonNode::StrLit("hello".to_string())),
1162        };
1163
1164        let mutator = AstMutator::with_operators(vec![MutationOperator::Bsr]);
1165        let mutations = mutator.mutate(&ast);
1166        assert!(!mutations.is_empty());
1167    }
1168
1169    #[test]
1170    fn test_bsr_empty_string() {
1171        let ast = PythonNode::Assign {
1172            target: "s".to_string(),
1173            value: Box::new(PythonNode::StrLit(String::new())),
1174        };
1175
1176        let mutator = AstMutator::with_operators(vec![MutationOperator::Bsr]);
1177        let mutations = mutator.mutate(&ast);
1178        assert!(!mutations.is_empty());
1179    }
1180
1181    #[test]
1182    fn test_arithmetic_replacements() {
1183        assert!(!arithmetic_replacements(BinaryOp::Add).is_empty());
1184        assert!(!arithmetic_replacements(BinaryOp::Sub).is_empty());
1185        assert!(!arithmetic_replacements(BinaryOp::Mult).is_empty());
1186        assert!(!arithmetic_replacements(BinaryOp::Div).is_empty());
1187        assert!(!arithmetic_replacements(BinaryOp::Mod).is_empty());
1188        assert!(!arithmetic_replacements(BinaryOp::FloorDiv).is_empty());
1189        assert!(!arithmetic_replacements(BinaryOp::Pow).is_empty());
1190        assert!(arithmetic_replacements(BinaryOp::And).is_empty()); // Not arithmetic
1191    }
1192
1193    #[test]
1194    fn test_relational_replacements() {
1195        assert!(!relational_replacements(CompareOp::Eq).is_empty());
1196        assert!(!relational_replacements(CompareOp::NotEq).is_empty());
1197        assert!(!relational_replacements(CompareOp::Lt).is_empty());
1198        assert!(!relational_replacements(CompareOp::LtE).is_empty());
1199        assert!(!relational_replacements(CompareOp::Gt).is_empty());
1200        assert!(!relational_replacements(CompareOp::GtE).is_empty());
1201    }
1202
1203    #[test]
1204    fn test_logical_replacements() {
1205        assert!(!logical_replacements(BinaryOp::And).is_empty());
1206        assert!(!logical_replacements(BinaryOp::Or).is_empty());
1207        assert!(logical_replacements(BinaryOp::Add).is_empty()); // Not logical
1208    }
1209
1210    #[test]
1211    fn test_collect_variable_names() {
1212        let ast = PythonNode::Module(vec![
1213            PythonNode::Assign {
1214                target: "x".to_string(),
1215                value: Box::new(PythonNode::IntLit(1)),
1216            },
1217            PythonNode::Assign {
1218                target: "y".to_string(),
1219                value: Box::new(PythonNode::Name("x".to_string())),
1220            },
1221        ]);
1222
1223        let names = collect_variable_names(&ast);
1224        assert!(names.contains(&"x".to_string()));
1225        assert!(names.contains(&"y".to_string()));
1226    }
1227
1228    #[test]
1229    fn test_collect_variable_names_in_binop() {
1230        let ast = PythonNode::BinOp {
1231            left: Box::new(PythonNode::Name("a".to_string())),
1232            op: BinaryOp::Add,
1233            right: Box::new(PythonNode::Name("b".to_string())),
1234        };
1235
1236        let names = collect_variable_names(&ast);
1237        assert!(names.contains(&"a".to_string()));
1238        assert!(names.contains(&"b".to_string()));
1239    }
1240
1241    #[test]
1242    fn test_collect_variable_names_in_unaryop() {
1243        let ast = PythonNode::UnaryOp {
1244            op: UnaryOp::Neg,
1245            operand: Box::new(PythonNode::Name("x".to_string())),
1246        };
1247
1248        let names = collect_variable_names(&ast);
1249        assert!(names.contains(&"x".to_string()));
1250    }
1251
1252    #[test]
1253    fn test_collect_variable_names_in_if() {
1254        let ast = PythonNode::If {
1255            test: Box::new(PythonNode::Name("cond".to_string())),
1256            body: vec![PythonNode::Assign {
1257                target: "a".to_string(),
1258                value: Box::new(PythonNode::IntLit(1)),
1259            }],
1260            orelse: vec![PythonNode::Assign {
1261                target: "b".to_string(),
1262                value: Box::new(PythonNode::IntLit(2)),
1263            }],
1264        };
1265
1266        let names = collect_variable_names(&ast);
1267        assert!(names.contains(&"cond".to_string()));
1268        assert!(names.contains(&"a".to_string()));
1269        assert!(names.contains(&"b".to_string()));
1270    }
1271
1272    #[test]
1273    fn test_collect_variable_names_in_funcdef() {
1274        let ast = PythonNode::FuncDef {
1275            name: "foo".to_string(),
1276            args: vec!["x".to_string(), "y".to_string()],
1277            body: vec![PythonNode::Return(Some(Box::new(PythonNode::Name(
1278                "x".to_string(),
1279            ))))],
1280        };
1281
1282        let names = collect_variable_names(&ast);
1283        assert!(names.contains(&"x".to_string()));
1284        assert!(names.contains(&"y".to_string()));
1285    }
1286
1287    #[test]
1288    fn test_boundary_int_values() {
1289        let vals_0 = boundary_int_values(0);
1290        assert!(vals_0.contains(&-1));
1291        assert!(vals_0.contains(&1));
1292
1293        let vals_1 = boundary_int_values(1);
1294        assert!(vals_1.contains(&0));
1295        assert!(vals_1.contains(&2));
1296
1297        let vals_neg1 = boundary_int_values(-1);
1298        assert!(vals_neg1.contains(&0));
1299        assert!(vals_neg1.contains(&-2));
1300
1301        let vals_5 = boundary_int_values(5);
1302        assert!(vals_5.contains(&4));
1303        assert!(vals_5.contains(&6));
1304        assert!(vals_5.contains(&0));
1305
1306        let vals_neg5 = boundary_int_values(-5);
1307        assert!(vals_neg5.contains(&-6));
1308        assert!(vals_neg5.contains(&-4));
1309    }
1310
1311    #[test]
1312    fn test_boundary_str_values() {
1313        let vals_empty = boundary_str_values("");
1314        assert!(vals_empty.contains(&" ".to_string()));
1315        assert!(vals_empty.contains(&"a".to_string()));
1316
1317        let vals_nonempty = boundary_str_values("hello");
1318        assert!(vals_nonempty.contains(&String::new()));
1319        assert!(vals_nonempty.contains(&"hello ".to_string()));
1320    }
1321
1322    #[test]
1323    fn test_abs_with_intlit() {
1324        // Test ABS mutation on IntLit directly
1325        let ast = PythonNode::Assign {
1326            target: "x".to_string(),
1327            value: Box::new(PythonNode::IntLit(42)),
1328        };
1329
1330        let mutator = AstMutator::with_operators(vec![MutationOperator::Abs]);
1331        let mutations = mutator.mutate(&ast);
1332
1333        assert!(!mutations.is_empty());
1334        assert!(mutations.iter().any(|m| m.description.contains("abs(42)")));
1335    }
1336
1337    #[test]
1338    fn test_sdl_if_with_multiple_body_statements() {
1339        // If with >1 statements in body - tests SDL compound for If body
1340        let ast = PythonNode::Module(vec![PythonNode::If {
1341            test: Box::new(PythonNode::BoolLit(true)),
1342            body: vec![
1343                PythonNode::Assign {
1344                    target: "x".to_string(),
1345                    value: Box::new(PythonNode::IntLit(1)),
1346                },
1347                PythonNode::Assign {
1348                    target: "y".to_string(),
1349                    value: Box::new(PythonNode::IntLit(2)),
1350                },
1351            ],
1352            orelse: vec![],
1353        }]);
1354
1355        let mutator = AstMutator::with_operators(vec![MutationOperator::Sdl]);
1356        let mutations = mutator.mutate(&ast);
1357
1358        assert!(!mutations.is_empty());
1359        assert!(mutations.iter().any(|m| m.description.contains("if-body")));
1360    }
1361
1362    #[test]
1363    fn test_sdl_if_with_multiple_orelse_statements() {
1364        // If with >1 statements in orelse - tests SDL compound for If orelse
1365        let ast = PythonNode::Module(vec![PythonNode::If {
1366            test: Box::new(PythonNode::BoolLit(true)),
1367            body: vec![PythonNode::Pass],
1368            orelse: vec![
1369                PythonNode::Assign {
1370                    target: "x".to_string(),
1371                    value: Box::new(PythonNode::IntLit(1)),
1372                },
1373                PythonNode::Assign {
1374                    target: "y".to_string(),
1375                    value: Box::new(PythonNode::IntLit(2)),
1376                },
1377            ],
1378        }]);
1379
1380        let mutator = AstMutator::with_operators(vec![MutationOperator::Sdl]);
1381        let mutations = mutator.mutate(&ast);
1382
1383        assert!(!mutations.is_empty());
1384        assert!(mutations
1385            .iter()
1386            .any(|m| m.description.contains("else-body")));
1387    }
1388
1389    #[test]
1390    fn test_sdl_funcdef_with_multiple_body_statements() {
1391        // FuncDef with >1 statements in body - tests SDL compound for FuncDef
1392        let ast = PythonNode::Module(vec![PythonNode::FuncDef {
1393            name: "foo".to_string(),
1394            args: vec![],
1395            body: vec![
1396                PythonNode::Assign {
1397                    target: "x".to_string(),
1398                    value: Box::new(PythonNode::IntLit(1)),
1399                },
1400                PythonNode::Return(Some(Box::new(PythonNode::Name("x".to_string())))),
1401            ],
1402        }]);
1403
1404        let mutator = AstMutator::with_operators(vec![MutationOperator::Sdl]);
1405        let mutations = mutator.mutate(&ast);
1406
1407        assert!(!mutations.is_empty());
1408        assert!(mutations
1409            .iter()
1410            .any(|m| m.description.contains("function body")));
1411    }
1412
1413    #[test]
1414    fn test_svr_in_binop() {
1415        // SVR with variable replacement inside BinOp - tests apply_svr_recursive for BinOp
1416        let ast = PythonNode::Module(vec![
1417            PythonNode::Assign {
1418                target: "x".to_string(),
1419                value: Box::new(PythonNode::IntLit(1)),
1420            },
1421            PythonNode::Assign {
1422                target: "y".to_string(),
1423                value: Box::new(PythonNode::IntLit(2)),
1424            },
1425            PythonNode::Assign {
1426                target: "z".to_string(),
1427                value: Box::new(PythonNode::BinOp {
1428                    left: Box::new(PythonNode::Name("x".to_string())),
1429                    op: BinaryOp::Add,
1430                    right: Box::new(PythonNode::Name("y".to_string())),
1431                }),
1432            },
1433        ]);
1434
1435        let mutator = AstMutator::with_operators(vec![MutationOperator::Svr]);
1436        let mutations = mutator.mutate(&ast);
1437
1438        assert!(!mutations.is_empty());
1439        // Should have replacements for x and y in the binop
1440        assert!(mutations.iter().any(|m| m.description.contains("Replace")));
1441    }
1442
1443    #[test]
1444    fn test_bsr_boolean() {
1445        // BSR mutation on BoolLit - tests apply_bsr for BoolLit
1446        let ast = PythonNode::Assign {
1447            target: "flag".to_string(),
1448            value: Box::new(PythonNode::BoolLit(true)),
1449        };
1450
1451        let mutator = AstMutator::with_operators(vec![MutationOperator::Bsr]);
1452        let mutations = mutator.mutate(&ast);
1453
1454        assert!(!mutations.is_empty());
1455        assert!(mutations
1456            .iter()
1457            .any(|m| m.description.contains("true → false")));
1458    }
1459
1460    #[test]
1461    fn test_bsr_boolean_false() {
1462        // BSR mutation on BoolLit false
1463        let ast = PythonNode::Assign {
1464            target: "flag".to_string(),
1465            value: Box::new(PythonNode::BoolLit(false)),
1466        };
1467
1468        let mutator = AstMutator::with_operators(vec![MutationOperator::Bsr]);
1469        let mutations = mutator.mutate(&ast);
1470
1471        assert!(!mutations.is_empty());
1472        assert!(mutations
1473            .iter()
1474            .any(|m| m.description.contains("false → true")));
1475    }
1476}