use std::fmt;
use std::fs;
use std::path::Path;
use anyhow::Context;
use anyhow::Result;
use serde::ser::{SerializeStruct, Serializer};
use serde::Serialize;
use similar::TextDiff;
use crate::source::SourceFile;
use crate::textedit::{replace_region, Span};
const MUTATION_MARKER_COMMENT: &str = "/* ~ changed by cargo-mutants ~ */";
#[derive(Debug, Eq, Clone, PartialEq, Serialize)]
pub enum MutationOp {
Default,
Unit,
True,
False,
EmptyString,
Xyzzy,
OkDefault,
}
impl MutationOp {
fn replacement(&self) -> &'static str {
use MutationOp::*;
match self {
Default => "Default::default()",
Unit => "()",
True => "true",
False => "false",
EmptyString => "\"\".into()",
Xyzzy => "\"xyzzy\".into()",
OkDefault => "Ok(Default::default())",
}
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct Mutation {
pub source_file: SourceFile,
function_name: String,
return_type: String,
span: Span,
pub op: MutationOp,
}
impl Mutation {
pub fn new(
source_file: SourceFile,
op: MutationOp,
function_name: String,
return_type: String,
span: Span,
) -> Mutation {
Mutation {
source_file,
op,
function_name,
return_type,
span,
}
}
pub fn mutated_code(&self) -> String {
replace_region(
&self.source_file.code,
&self.span.start,
&self.span.end,
&format!(
"{{\n{} {}\n}}\n",
self.op.replacement(),
MUTATION_MARKER_COMMENT
),
)
}
pub fn original_code(&self) -> &str {
&self.source_file.code
}
pub fn return_type(&self) -> &str {
&self.return_type
}
pub fn describe_location(&self) -> String {
format!(
"{}:{}",
self.source_file.tree_relative_slashes(),
self.span.start.line,
)
}
pub fn describe_change(&self) -> String {
format!(
"replace {} with {}",
self.function_name(),
self.op.replacement()
)
}
pub fn replacement_text(&self) -> &'static str {
self.op.replacement()
}
pub fn function_name(&self) -> &str {
&self.function_name
}
pub fn diff(&self) -> String {
let old_label = self.source_file.tree_relative_slashes();
let new_label = self.describe_change();
TextDiff::from_lines(self.original_code(), &self.mutated_code())
.unified_diff()
.context_radius(8)
.header(&old_label, &new_label)
.to_string()
}
fn apply_in_dir(&self, dir: &Path) -> Result<()> {
self.write_in_dir(dir, &self.mutated_code())
}
fn revert_in_dir(&self, dir: &Path) -> Result<()> {
self.write_in_dir(dir, self.original_code())
}
pub fn with_mutation_applied<F, T>(&self, dir: &Path, mut func: F) -> Result<T>
where
F: FnMut() -> Result<T>,
{
self.apply_in_dir(dir)?;
let r = func();
self.revert_in_dir(dir)?;
r
}
fn write_in_dir(&self, dir: &Path, code: &str) -> Result<()> {
let path = self.source_file.within_dir(dir);
assert!(path.is_file(), "{:?} is not a file", path);
fs::write(&path, code.as_bytes())
.with_context(|| format!("failed to write mutated code to {:?}", path))
}
}
impl fmt::Debug for Mutation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Mutation")
.field("op", &self.op)
.field("function_name", &self.function_name())
.field("return_type", &self.return_type)
.field("start", &(self.span.start.line, self.span.start.column))
.field("end", &(self.span.end.line, self.span.end.column))
.finish()
}
}
impl fmt::Display for Mutation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} in {}",
self.describe_change(),
self.describe_location(),
)
}
}
impl Serialize for Mutation {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut ss = serializer.serialize_struct("Mutation", 4)?;
ss.serialize_field("file", &self.source_file.tree_relative_slashes())?;
ss.serialize_field("line", &self.span.start.line)?;
ss.serialize_field("function", &self.function_name)?;
ss.serialize_field("return_type", &self.return_type)?;
ss.serialize_field("replacement", self.op.replacement())?;
ss.end()
}
}
#[cfg(test)]
mod test {
use std::path::Path;
use itertools::Itertools;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn discover_mutations() {
let source_file = SourceFile::new(
Path::new("testdata/tree/factorial"),
Path::new("src/bin/main.rs"),
)
.unwrap();
let muts = source_file.mutations().unwrap();
assert_eq!(muts.len(), 2);
assert_eq!(
format!("{:?}", muts[0]),
r#"Mutation { op: Unit, function_name: "main", return_type: "", start: (1, 11), end: (5, 2) }"#
);
assert_eq!(
format!("{:?}", muts[1]),
r#"Mutation { op: Default, function_name: "factorial", return_type: "-> u32", start: (7, 29), end: (13, 2) }"#
);
}
#[test]
fn filter_by_attributes() {
let source_file = SourceFile::new(
Path::new("testdata/tree/hang_avoided_by_attr"),
Path::new("src/lib.rs"),
)
.unwrap();
let muts = source_file.mutations().unwrap();
let descriptions = muts.iter().map(Mutation::describe_change).collect_vec();
insta::assert_snapshot!(
descriptions.join("\n"),
@"replace controlled_loop with ()"
);
}
#[test]
fn mutate_factorial() {
let source_file = SourceFile::new(
Path::new("testdata/tree/factorial"),
Path::new("src/bin/main.rs"),
)
.unwrap();
let muts = source_file.mutations().unwrap();
assert_eq!(muts.len(), 2);
let mut mutated_code = muts[0].mutated_code();
assert_eq!(muts[0].function_name(), "main");
mutated_code.retain(|c| c != '\r');
assert_eq!(
mutated_code,
r#"fn main() {
() /* ~ changed by cargo-mutants ~ */
}
fn factorial(n: u32) -> u32 {
let mut a = 1;
for i in 2..=n {
a *= i;
}
a
}
#[test]
fn test_factorial() {
println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture
assert_eq!(factorial(6), 720);
}
"#
);
let mut mutated_code = muts[1].mutated_code();
assert_eq!(muts[1].function_name(), "factorial");
mutated_code.retain(|c| c != '\r');
assert_eq!(
mutated_code,
r#"fn main() {
for i in 1..=6 {
println!("{}! = {}", i, factorial(i));
}
}
fn factorial(n: u32) -> u32 {
Default::default() /* ~ changed by cargo-mutants ~ */
}
#[test]
fn test_factorial() {
println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture
assert_eq!(factorial(6), 720);
}
"#
);
}
}