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};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum MutationType {
MathOps,
Conjunctions,
Booleans,
ControlFlow,
CompOps,
Numbers,
}
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)
}
#[derive(Debug)]
pub struct Mutant {
pub file_path: PathBuf,
pub line_number: usize,
pub before: String,
pub after: String,
old_line: String,
}
impl Mutant {
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);
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(())
}
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);
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(())
}
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);
let mut lines: Vec<String> = reader.lines().collect::<Result<_, _>>()?;
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 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
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(),
)
}
}
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() {
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;
}
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(())
}
fn remove_quotes(input: &str) -> String {
let re = Regex::new(r#"'[^']*'|"[^"]*""#).unwrap();
re.replace_all(input, "").to_string()
}
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()))
}
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
";
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");
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");
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}");
}
}