pymute 0.1.2

Pymute: A Mutation Testing Tool for Python/Pytest written in Rust
Documentation
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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
//! # Mutant Generation Module
//!
//! This module provides functionality to identify and mutate specific parts of Python codebases
//! based on predefined mutation criteria. It supports various mutation types including arithmetic
//! operations, boolean expressions, control flow alterations, and more. The primary purpose of this
//! module is to assist in mutation testing by generating potential code mutants, helping developers
//! and testers to identify how well a test suite can detect injected faults.
//!
//! ## Features
//!
//! - **Mutation Identification**: Scans Python files to identify possible points for mutation
//!   based on the specified mutation types.
//! - **Mutation Application**: Capable of applying mutations directly to the code, thereby
//!   generating different mutant variants which can be used for testing the effectiveness of
//!   test suites.
//! - **Support for Multiple Mutation Types**: Handles a variety of mutation types including,
//!   but not limited to, mathematical operations, boolean logic mutations, and control flow changes.
//!
//! ## Usage
//!
//! The main entry points of this module are:
//! - `find_mutants(glob_expression, mutation_types)`: Scans files matching the glob pattern and identifies
//!   potential mutants based on the provided mutation types.
//! - `Mutant::insert()`, `Mutant::insert_in_new_root()`, and `Mutant::remove()`: Methods to apply or remove
//!   mutations on the code files.
//!
//! Ensure that the `glob` crate is correctly configured and that the path specifications align with the
//! target filesystem structure.
//!
//! ## Example
//!
//! To use this module to find and apply mutations in a temporary directory (preferred way):
//!
//! ```run
//! use pymute::mutants::{MutationType, find_mutants};
//! use cp_r::CopyOptions;
//! use std::path::PathBuf;
//! use tempfile::tempdir;
//!
//! let project_root = PathBuf::from(".");
//! let glob_pattern = "**/*.py";
//! let mutation_types = &[MutationType::MathOps, MutationType::Booleans];
//! let mutants = find_mutants(glob_pattern, mutation_types).expect("Error finding mutants");
//!
//! for mutant in mutants {
//!     let dir = tempdir().expect("Failed to create temporary directory!");
//!     mutant.insert_in_new_root(&project_root, dir.path()).expect("Error inserting mutant");
//!     mutant.remove().expect("Error removing mutant");
//!     dir.close().unwrap();
//! }
//! ```
//!
//! To use this module to find and apply mutations in place (removal is not well-tested and reliable as of yet):
//!
//! ```run
//! use pymute::mutants::{find_mutants, MutationType};
//!
//! let glob_pattern = "**/*.py";
//! let mutation_types = &[MutationType::MathOps, MutationType::Booleans];
//! let mutants = find_mutants(glob_pattern, mutation_types).expect("Error finding mutants");
//!
//! for mutant in mutants {
//!     mutant.insert().expect("Error inserting mutant");
//!     mutant.remove().expect("Error removing mutant")
//! }
//! ```
//!
//! ## Dependencies
//!
//! This module depends on external crates such as `glob` for file pattern matching, `regex` for text
//! manipulation, and `colored` for enhancing output readability by coloring text.
//!

use clap::ValueEnum;
use colored::Colorize;
use glob::glob;
use regex::Regex;
use std::error::Error;
use std::fmt;
use std::fs::{self, File};
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};

/// A semantic grouping of different types of possible mutations.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum MutationType {
    /// Mutate mathematical operators (e.g. "*,+,-,/")
    MathOps,
    /// Mutate conjunctions in boolean expressions (e.g. "and/or").
    Conjunctions,
    /// Mutate booleans (e.g. "True/False").
    Booleans,
    /// Mutate control flow statements (e.g. if statements).
    ControlFlow,
    /// Mutate comparison operators (e.g. "<,>,==,!=").
    CompOps,
    /// Mutate numbers (e.g. off-by-one errors)
    Numbers,
}

/// Find potential python mutants from files that match the glob expression.
///
/// It will ignore any files that start with test_* and that end with *_test.py
/// to avoid mutating pytest tests.
///
/// Parameters
/// ----------
/// glob_expression: &str compatible with the `glob` crate.
/// mutation_types: Collection of MutationType. Each of the mutation types specified
/// here will be used.
pub fn find_mutants(
    glob_expression: &str,
    mutation_types: &[MutationType],
) -> Result<Vec<Mutant>, Box<dyn Error>> {
    let mut possible_mutants = Vec::<Mutant>::new();

    let replacements = build_replacements(mutation_types);

    for entry in glob(glob_expression).expect("Failed to read glob pattern") {
        match entry {
            Ok(path) => {
                let file_name = match path.file_name() {
                    Some(f) => f,
                    None => continue,
                };
                let file_name = match file_name.to_str() {
                    Some(f) => f,
                    None => continue,
                };
                if file_name.starts_with("test_") {
                    continue;
                }
                if file_name.ends_with("_test.py") {
                    continue;
                }
                let _ = add_mutants_from_file(&mut possible_mutants, &path, &replacements);
            }
            Err(_e) => {}
        }
    }

    Ok(possible_mutants)
}

/// Define parameters of a potential mutant for a python program.
#[derive(Debug)]
pub struct Mutant {
    /// Path to python file that can be mutated.
    pub file_path: PathBuf,
    /// Line number on which to insert the mutant.
    pub line_number: usize,
    /// The original string.
    pub before: String,
    /// The replacement string.
    pub after: String,
    /// The line before inserting the mutant.
    old_line: String,
}

impl Mutant {
    /// Actually insert the mutant into a file.
    ///
    /// This will take the mutant and insert it in a copy of the python project.
    ///
    /// Parameters
    /// ----------
    /// root: This is the path to the root of the original directory. The root
    /// path will be stripped from the mutants file path.
    /// new_root: This is the path to the root of the copied python project.
    /// The mutant file path will be joined into this one after stripping the original
    /// root. The mutant is then inserted into the copied version of the file
    /// where the potential mutant was found (i.e. it will be inserted into
    /// new_root / mutant_file_path_stripped_of_root)
    pub fn insert_in_new_root(&self, root: &Path, new_root: &Path) -> Result<(), Box<dyn Error>> {
        let abs_path_file = self
            .file_path
            .canonicalize()
            .expect("Failed to resolve path to file.");
        let abs_path_file = abs_path_file.as_path();

        let abs_path_root = root
            .canonicalize()
            .expect("Failed to resolve path to root.");

        let abs_path_root = abs_path_root.as_path();

        let file_from_root = abs_path_file.strip_prefix(abs_path_root)?;
        let path_to_mutant = new_root.join(file_from_root);

        let file = File::open(&path_to_mutant)?;
        let reader = BufReader::new(file);

        // read all lines into a vector
        let mut lines: Vec<String> = reader.lines().collect::<Result<_, _>>()?;
        lines[self.line_number - 1] =
            lines[self.line_number - 1].replace(&self.before, &self.after);

        let last = lines.pop().unwrap();
        lines.push(format!("{last}\n"));
        fs::write(&path_to_mutant, lines.join("\n"))
            .expect("Failed to write to file upon mutant insertion!");

        Ok(())
    }

    /// Insert the mutant in place.
    ///
    /// This will attempt to insert the mutant in the related file in the original
    /// python project (i.e. in place/where the mutant was found).
    pub fn insert(&self) -> Result<(), Box<dyn Error>> {
        let file_path = self.file_path.as_path();
        let file = File::open(file_path)?;
        let reader = BufReader::new(file);

        // read all lines into a vector
        let mut lines: Vec<String> = reader.lines().collect::<Result<_, _>>()?;
        lines[self.line_number - 1] =
            lines[self.line_number - 1].replace(&self.before, &self.after);

        let last = lines.pop().unwrap();
        lines.push(format!("{last}\n"));
        fs::write(file_path, lines.join("\n"))
            .expect("Failed to write to file upon mutant insertion!");

        Ok(())
    }

    /// Remove the mutant.
    ///
    /// Remove a mutant from the original file after it has been inserted in place.
    /// This method is not well tested and in general the temporary directory
    /// workflow should be preferred over in place operations at the moment.
    pub fn remove(&self) -> Result<(), Box<dyn Error>> {
        let file_path = self.file_path.as_path();
        let file = File::open(file_path)?;
        let reader = BufReader::new(file);

        // read all lines into a vector
        let mut lines: Vec<String> = reader.lines().collect::<Result<_, _>>()?;
        // revert the insert
        lines[self.line_number - 1] = self.old_line.clone();

        let last = lines.pop().unwrap();
        lines.push(format!("{last}\n"));
        fs::write(file_path, lines.join("\n"))
            .expect("Failed to write to file upon mutant removal!");

        Ok(())
    }
}

impl fmt::Display for Mutant {
    // This trait requires `fmt` with this exact signature.
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Write strictly the first element into the supplied output
        // stream: `f`. Returns `fmt::Result` which indicates whether the
        // operation succeeded or failed. Note that `write!` uses syntax which
        // is very similar to `println!`.
        write!(
            f,
            "{} replaced by {} in file {} on line {}",
            self.before.green(),
            self.after.red(),
            self.file_path
                .clone()
                .into_os_string()
                .to_str()
                .expect("Failed to convert file path to string!")
                .yellow(),
            self.line_number.to_string().yellow(),
        )
    }
}

/// Search for potential mutants in a file given some replacements.
/// The replacement tuples in the Vec give the (before, after) string
/// values i.e. before can be replaced by after.
fn add_mutants_from_file(
    mutant_vec: &mut Vec<Mutant>,
    path: &PathBuf,
    replacements: &[(String, String)],
) -> Result<(), Box<dyn Error>> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);

    let mut in_docstring = false;
    let docstring_markers = ["\"\"\"", "'''"];

    for (line_nr, line_result) in reader.lines().enumerate() {
        // ignore comments
        let line = line_result?;

        if docstring_markers
            .iter()
            .any(|&marker| line.matches(marker).count() == 2)
        {
            continue;
        }

        if docstring_markers
            .iter()
            .any(|&marker| line.contains(marker))
        {
            in_docstring = !in_docstring;
        }
        if line.starts_with('#') {
            continue;
        }

        if in_docstring {
            continue;
        }

        // also only consider stuff on left of comment
        let line_split = line.split('#').collect::<Vec<_>>()[0];
        let replacement = replacement_from_line(line_split, replacements);
        match replacement {
            Some((before, after)) => {
                let mutant = Mutant {
                    file_path: path.clone(),
                    line_number: line_nr + 1,
                    before,
                    after,
                    old_line: line,
                };
                mutant_vec.push(mutant);
            }

            None => continue,
        };
    }
    Ok(())
}

/// Remove quotes so that python strings are ignored.
fn remove_quotes(input: &str) -> String {
    let re = Regex::new(r#"'[^']*'|"[^"]*""#).unwrap();
    re.replace_all(input, "").to_string()
}

/// Find a before/after replacement tuple in `line`. Possible tuples are
/// specified in `replacements`.
///If no possible replacement is found, it returns None.
fn replacement_from_line(
    line: &str,
    replacements: &[(String, String)],
) -> Option<(String, String)> {
    let line = remove_quotes(line);

    replacements
        .iter()
        .find(|(from, _)| line.contains(from))
        .map(|(from, to)| (from.into(), to.into()))
}

/// Build a Vec of before/after replacement tuples from the specified types of
/// mutations.
fn build_replacements(mutation_types: &[MutationType]) -> Vec<(String, String)> {
    let mut replacements = Vec::new();

    let mut numbers = Vec::new();
    for n in 0..10 {
        numbers.push((n.to_string(), (n + 1).to_string()));
    }

    mutation_types
        .iter()
        .for_each(|mutation_type| match mutation_type {
            MutationType::MathOps => {
                replacements.append(&mut vec![
                    (" + ".into(), " - ".into()),
                    (" - ".into(), " + ".into()),
                    (" * ".into(), " / ".into()),
                    (" / ".into(), " * ".into()),
                ]);
            }
            MutationType::Conjunctions => {
                replacements.append(&mut vec![
                    (" and ".into(), " or ".into()),
                    (" or ".into(), " and ".into()),
                ]);
            }
            MutationType::Booleans => {
                replacements.append(&mut vec![
                    (" True ".into(), " False ".into()),
                    (" False ".into(), " True ".into()),
                ]);
            }
            MutationType::ControlFlow => {
                replacements.append(&mut vec![
                    (" else: ".into(), " elif False: ".into()),
                    (" if not ".into(), " if ".into()),
                    (" if ".into(), " if not ".into()),
                ]);
            }
            MutationType::CompOps => {
                replacements.append(&mut vec![
                    (" > ".into(), " < ".into()),
                    (" < ".into(), " > ".into()),
                    ("==".into(), "!=".into()),
                    ("!=".into(), "==".into()),
                ]);
            }
            MutationType::Numbers => replacements.append(&mut numbers),
        });

    replacements
}

#[cfg(test)]
mod tests {
    use crate::mutants::{self, build_replacements, MutationType};
    use colored::Colorize;
    use std::{
        fs::{self, read_to_string, File},
        io::Write,
    };
    use tempfile::{tempdir, NamedTempFile};

    #[test]
    fn test_find_mutants() {
        let temp_dir = tempdir().unwrap();
        let base_path = temp_dir.path();

        let multiline_string_script_1 = "def add(a, b):
    return a + b

# this is a + comment
def sub(a, b):
    return a - b

res = sub(5, 6) * add(7, 8)
print(res) # print the result *
";

        let multiline_string_script_2 = "def div(a, b):
    return a / b

# this is a + comment
def mul(a, b):
    return a * b

res = div(5, 6) - mul(7, 8)
print(res) # print the result +
";
        let multiline_string_script_3 = "def print_number(a, b):
    res = a + b
    print(\"a + b = {res}\")

# this is a + comment

";

        let multiline_string_script_test_1 = "def print_number(a, b):
    res = a + b
    print(\"a + b = {res}\")

# this is a + comment

";
        let multiline_string_script_test_2 = "def print_number(a, b):
    res = a + b
    print(\"a + b = {res}\")

# this is a + comment

";

        // creating a nested directory structure
        let sub_dir1 = base_path.join("dir1");
        let sub_dir1_1 = sub_dir1.join("dir1_1");
        let sub_dir1_1_1 = sub_dir1_1.join("dir1_1_1");

        // ensure all directories are created
        fs::create_dir_all(&sub_dir1_1_1).unwrap();

        let script1 = sub_dir1.join("script1.py");
        let mut script1 = File::create(script1).unwrap();

        write!(script1, "{}", multiline_string_script_1)
            .expect("Failed to write to temporary file");

        // create a decoy txt file that should not be matched
        let decoy = base_path.join("script1.txt");
        let mut decoy = File::create(decoy).unwrap();

        write!(decoy, "{}", multiline_string_script_1).expect("Failed to write txt file.");

        let script2 = sub_dir1_1.join("script2.py");
        let mut script2 = File::create(script2).unwrap();

        write!(script2, "{}", multiline_string_script_2)
            .expect("Failed to write to temporary file");

        let script3 = sub_dir1_1_1.join("script3.py");
        let mut script3 = File::create(script3).unwrap();

        write!(script3, "{}", multiline_string_script_3)
            .expect("Failed to write to temporary file");

        let test_script = sub_dir1_1_1.join("test_script.py");
        let mut test_script = File::create(test_script).unwrap();

        write!(test_script, "{}", multiline_string_script_test_1)
            .expect("Failed to write to temporary file");

        let script_test = sub_dir1_1_1.join("script_test.py");
        let mut script_test = File::create(script_test).unwrap();

        write!(script_test, "{}", multiline_string_script_test_2)
            .expect("Failed to write to temporary file");

        let glob_expr = base_path.to_str().unwrap();
        let glob_expr = format!("{glob_expr}/**/*.py");

        let mutation_types = vec![
            MutationType::MathOps,
            MutationType::Conjunctions,
            MutationType::Booleans,
            MutationType::ControlFlow,
            MutationType::CompOps,
            MutationType::Numbers,
        ];
        let mutants_vec = mutants::find_mutants(&glob_expr, &mutation_types).unwrap();

        assert_eq!(mutants_vec.len(), 7);

        temp_dir.close().unwrap();
    }

    #[test]
    fn test_replacement_from_line_with_single_quotes() {
        let line = r#"print('a + b')"#;
        let mutation_types = vec![
            MutationType::MathOps,
            MutationType::Conjunctions,
            MutationType::Booleans,
            MutationType::ControlFlow,
            MutationType::CompOps,
            MutationType::Numbers,
        ];

        let replacements = build_replacements(&mutation_types);

        let option = mutants::replacement_from_line(line, &replacements);
        assert!(option.is_none(), "Expected the option to be None");
    }

    #[test]
    fn test_replacement_from_line_with_double_quotes() {
        let line = r#"print("a + b")"#;
        let mutation_types = vec![
            MutationType::MathOps,
            MutationType::Conjunctions,
            MutationType::Booleans,
            MutationType::ControlFlow,
            MutationType::CompOps,
            MutationType::Numbers,
        ];

        let replacements = build_replacements(&mutation_types);

        let option = mutants::replacement_from_line(line, &replacements);
        assert!(option.is_none(), "Expected the option to be None");
    }

    #[test]
    fn test_add_mutants_from_file() {
        let multiline_string = "def add(a, b):
    return a + b";

        let mut temp_file = NamedTempFile::new().expect("Failed to create temporary file");
        write!(temp_file, "{}", multiline_string).expect("Failed to write to temporary file");

        let mutation_types = vec![
            MutationType::MathOps,
            MutationType::Conjunctions,
            MutationType::Booleans,
            MutationType::ControlFlow,
            MutationType::CompOps,
            MutationType::Numbers,
        ];

        let replacements = build_replacements(&mutation_types);

        let mut possible_mutants = Vec::<mutants::Mutant>::new();
        let _ = mutants::add_mutants_from_file(
            &mut possible_mutants,
            &temp_file.path().to_path_buf(),
            &replacements,
        );

        assert_eq!(possible_mutants.len(), 1);
        assert_eq!(possible_mutants[0].line_number, 2);
        assert_eq!(possible_mutants[0].before, String::from(" + "));
        assert_eq!(possible_mutants[0].after, String::from(" - "));
    }

    #[test]
    fn test_add_mutants_from_file_trickier() {
        let multiline_string = "def add(a, b):
    return a + b

# this is a + comment
def sub(a, b):
    return a - b

res = sub(5, 6) * add(7, 8)
print(res) # print the result *
";

        let mut temp_file = NamedTempFile::new().expect("Failed to create temporary file");
        write!(temp_file, "{}", multiline_string).expect("Failed to write to temporary file");

        let mutation_types = vec![
            MutationType::MathOps,
            MutationType::Conjunctions,
            MutationType::Booleans,
            MutationType::ControlFlow,
            MutationType::CompOps,
            MutationType::Numbers,
        ];

        let replacements = build_replacements(&mutation_types);
        let mut possible_mutants = Vec::<mutants::Mutant>::new();
        let _ = mutants::add_mutants_from_file(
            &mut possible_mutants,
            &temp_file.path().to_path_buf(),
            &replacements,
        );

        assert_eq!(possible_mutants.len(), 3);

        assert_eq!(possible_mutants[0].line_number, 2);
        assert_eq!(possible_mutants[0].before, String::from(" + "));
        assert_eq!(possible_mutants[0].after, String::from(" - "));

        assert_eq!(possible_mutants[1].line_number, 6);
        assert_eq!(possible_mutants[1].before, String::from(" - "));
        assert_eq!(possible_mutants[1].after, String::from(" + "));

        assert_eq!(possible_mutants[2].line_number, 8);
        assert_eq!(possible_mutants[2].before, String::from(" * "));
        assert_eq!(possible_mutants[2].after, String::from(" / "));
    }

    #[test]
    fn test_replacement_from_line_none() {
        let line = "print('Hello World')";
        let mutation_types = vec![
            MutationType::MathOps,
            MutationType::Conjunctions,
            MutationType::Booleans,
            MutationType::ControlFlow,
            MutationType::CompOps,
            MutationType::Numbers,
        ];

        let replacements = build_replacements(&mutation_types);
        let option = mutants::replacement_from_line(line, &replacements);
        println!("{:?}", option);
        assert!(option.is_none(), "Expected the option to be None");
    }

    #[test]
    fn test_replacement_from_line_math_operators() {
        let mutation_types = vec![
            MutationType::MathOps,
            MutationType::Conjunctions,
            MutationType::Booleans,
            MutationType::ControlFlow,
            MutationType::CompOps,
            MutationType::Numbers,
        ];

        let replacements = build_replacements(&mutation_types);

        let line = "5 + 5";
        let option = mutants::replacement_from_line(line, &replacements);
        assert_eq!(option.unwrap(), (" + ".into(), " - ".into()));

        let line = "5 - 5";
        let option = mutants::replacement_from_line(line, &replacements);
        assert_eq!(option.unwrap(), (" - ".into(), " + ".into()));

        let line = "5 * 5";
        let option = mutants::replacement_from_line(line, &replacements);
        assert_eq!(option.unwrap(), (" * ".into(), " / ".into()));

        let line = "5 / 5";
        let option = mutants::replacement_from_line(line, &replacements);
        assert_eq!(option.unwrap(), (" / ".into(), " * ".into()));
    }

    #[test]
    fn test_replacement_from_line_conjunctions() {
        let mutation_types = vec![
            MutationType::MathOps,
            MutationType::Conjunctions,
            MutationType::Booleans,
            MutationType::ControlFlow,
            MutationType::CompOps,
            MutationType::Numbers,
        ];

        let replacements = build_replacements(&mutation_types);
        let line = "True and False";
        let option = mutants::replacement_from_line(line, &replacements);
        assert_eq!(option.unwrap(), (" and ".into(), " or ".into()));

        let line = "True or False";
        let option = mutants::replacement_from_line(line, &replacements);
        assert_eq!(option.unwrap(), (" or ".into(), " and ".into()));
    }

    #[test]
    fn test_replacement_from_line_comparison_operators() {
        let mutation_types = vec![
            MutationType::MathOps,
            MutationType::Conjunctions,
            MutationType::Booleans,
            MutationType::ControlFlow,
            MutationType::CompOps,
            MutationType::Numbers,
        ];

        let replacements = build_replacements(&mutation_types);

        let line = "5 == 5";
        let option = mutants::replacement_from_line(line, &replacements);
        assert_eq!(option.unwrap(), ("==".into(), "!=".into()));

        let line = "5 != 5";
        let option = mutants::replacement_from_line(line, &replacements);
        assert_eq!(option.unwrap(), ("!=".into(), "==".into()));

        let line = "5 > 5";
        let option = mutants::replacement_from_line(line, &replacements);
        assert_eq!(option.unwrap(), (" > ".into(), " < ".into()));

        let line = "5 < 5";
        let option = mutants::replacement_from_line(line, &replacements);
        assert_eq!(option.unwrap(), (" < ".into(), " > ".into()));
    }

    #[test]
    fn test_mutant_insert() {
        let multiline_string = "def add(a, b):
    return a + b";

        let temp_dir = tempdir().unwrap();
        let base_path = temp_dir.path();
        let file_path_original = base_path.join("script.py");

        let _temp_dir_copy = tempdir().unwrap();
        let base_path_copy = temp_dir.path();
        let file_path_copy = base_path_copy.join("script.py");

        let mut file_original = File::create(&file_path_original).unwrap();
        let mut file_copy = File::create(&file_path_copy).unwrap();
        write!(file_original, "{}", multiline_string).expect("Failed to write to temporary file");
        write!(file_copy, "{}", multiline_string).expect("Failed to write to temporary file");

        let mutant = mutants::Mutant {
            file_path: file_path_original.clone(),
            line_number: 2,
            before: " + ".into(),
            after: " - ".into(),
            old_line: "    return a + b".into(),
        };

        mutant.insert().unwrap();

        let result = read_to_string(&file_path_original).unwrap();
        let desired_result = String::from("def add(a, b):\n    return a - b\n");
        assert_eq!(result, desired_result);

        mutant.remove().unwrap();

        let result = read_to_string(&file_path_original).unwrap();
        let desired_result = String::from("def add(a, b):\n    return a + b\n");
        assert_eq!(result, desired_result);

        mutant
            .insert_in_new_root(base_path, base_path_copy)
            .unwrap();
        let result = read_to_string(file_path_copy).unwrap();
        let desired_result = String::from("def add(a, b):\n    return a - b\n");
        assert_eq!(result, desired_result);

        let file_name_str = file_path_original.clone().into_os_string();
        let _file_name_str = file_name_str
            .to_str()
            .expect("Failed to convert file path to string!")
            .yellow();

        let _display = format!("{mutant}");
    }
}