#![warn(clippy::pedantic)]
#![allow(clippy::needless_raw_string_hashes)]
use std::collections::VecDeque;
use std::sync::Arc;
use std::vec;
use camino::{Utf8Path, Utf8PathBuf};
use proc_macro2::{Ident, TokenStream};
use quote::{ToTokens, quote};
use syn::ext::IdentExt;
use syn::spanned::Spanned;
use syn::visit::Visit;
use syn::{Attribute, BinOp, Block, Expr, ExprPath, File, ItemFn, ReturnType, Signature, UnOp};
use tracing::{debug_span, error, info, trace, trace_span, warn};
use crate::console::WalkProgress;
use crate::fnvalue::return_type_replacements;
use crate::mutant::{Function, MutationTarget};
use crate::package::Package;
use crate::pretty::ToPrettyString;
use crate::source::SourceFile;
use crate::span::Span;
use crate::{Console, Context, Genre, Mutant, Options, Result, check_interrupted};
pub struct Discovered {
pub mutants: Vec<Mutant>,
pub files: Vec<SourceFile>,
}
impl Discovered {
pub(crate) fn remove_previously_caught(&mut self, previously_caught: &[String]) {
self.mutants.retain(|m| {
let c = previously_caught.contains(&m.name);
if c {
trace!(name = %m.name, "skip previously caught mutant");
}
!c
});
}
}
pub fn walk_tree(
workspace_dir: &Utf8Path,
packages: &[Arc<Package>],
options: &Options,
console: &Console,
) -> Result<Discovered> {
let mut mutants = Vec::new();
let mut files = Vec::new();
let error_exprs = options.parsed_error_exprs()?;
let progress = console.start_walk_tree();
for package in packages {
let (mut package_mutants, mut package_files) =
walk_package(workspace_dir, package, &error_exprs, &progress, options)?;
mutants.append(&mut package_mutants);
files.append(&mut package_files);
}
progress.finish();
Ok(Discovered { mutants, files })
}
#[allow(clippy::from_iter_instead_of_collect)]
fn walk_package(
workspace_dir: &Utf8Path,
package: &Package,
error_exprs: &[Expr],
progress: &WalkProgress,
options: &Options,
) -> Result<(Vec<Mutant>, Vec<SourceFile>)> {
let mut mutants = Vec::new();
let mut files = Vec::new();
let mut filename_queue =
VecDeque::from_iter(package.top_sources.iter().map(|p| (p.to_owned(), true)));
while let Some((path, package_top)) = filename_queue.pop_front() {
let Some(source_file) = SourceFile::load(workspace_dir, &path, package, package_top)?
else {
info!("Skipping source file outside of tree: {path:?}");
continue;
};
progress.increment_files(1);
check_interrupted()?;
let (mut file_mutants, external_mods) = walk_file(&source_file, error_exprs, options)?;
progress.increment_mutants(file_mutants.len());
for mod_namespace in &external_mods {
if let Some(mod_path) = find_mod_source(workspace_dir, &source_file, mod_namespace) {
filename_queue.push_back((mod_path, false));
}
}
if !options.allows_source_file_path(&source_file.tree_relative_path) {
continue;
}
mutants.append(&mut file_mutants);
files.push(source_file);
}
Ok((mutants, files))
}
pub fn walk_file(
source_file: &SourceFile,
error_exprs: &[Expr],
options: &Options,
) -> Result<(Vec<Mutant>, Vec<ExternalModRef>)> {
let _span = debug_span!("source_file", path = source_file.tree_relative_slashes()).entered();
trace!("visit source file");
let syn_file = syn::parse_str::<syn::File>(source_file.code())
.with_context(|| format!("failed to parse {}", source_file.tree_relative_slashes()))?;
let mut visitor = DiscoveryVisitor {
error_exprs,
external_mods: Vec::new(),
mutants: Vec::new(),
mod_namespace_stack: Vec::new(),
namespace_stack: Vec::new(),
fn_stack: Vec::new(),
source_file: source_file.clone(),
options,
};
visitor.visit_file(&syn_file);
Ok((visitor.mutants, visitor.external_mods))
}
#[cfg(test)]
pub fn mutate_source_str(code: &str, options: &Options) -> Result<Vec<Mutant>> {
let source_file = SourceFile::for_tests(
Utf8Path::new("src/main.rs"),
code,
"cargo-mutants-testdata-internal",
true,
);
let (mutants, _) = walk_file(&source_file, &options.parsed_error_exprs()?, options)?;
Ok(mutants)
}
#[cfg(test)]
pub fn mutate_expr(code: &str) -> Vec<String> {
let full_code = format!("fn test_harness() {{\n{code}\n}}\n");
let options = Options::default();
let source_file = SourceFile::for_tests(
Utf8Path::new("src/main.rs"),
&full_code,
"cargo-mutants-testdata-internal",
true,
);
let error_exprs = options
.parsed_error_exprs()
.expect("failed to parse error exprs");
match walk_file(&source_file, &error_exprs, &options) {
Ok((mutants, _)) => mutants
.iter()
.filter(|m| !m.name(false).ends_with("replace test_harness with ()"))
.map(|m| m.name(false))
.map(|n| {
n.strip_prefix("src/main.rs: ")
.expect("expected to stripe file name")
.strip_suffix(" in test_harness")
.expect("expected function name at end of mutant name")
.to_owned()
})
.collect(),
Err(err) => panic!("failed to mutate {code:?}: {err}"),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExternalModRef {
parts: Vec<ModNamespace>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ModNamespace {
name: String,
path_attribute: Option<Utf8PathBuf>,
source_location: Span,
}
impl ModNamespace {
fn get_filesystem_name(&self) -> &Utf8Path {
self.path_attribute
.as_ref()
.map_or(Utf8Path::new(&self.name), Utf8PathBuf::as_path)
}
}
struct DiscoveryVisitor<'o> {
mutants: Vec<Mutant>,
source_file: SourceFile,
mod_namespace_stack: Vec<ModNamespace>,
namespace_stack: Vec<String>,
fn_stack: Vec<Arc<Function>>,
external_mods: Vec<ExternalModRef>,
error_exprs: &'o [Expr],
options: &'o Options,
}
impl DiscoveryVisitor<'_> {
fn enter_function(
&mut self,
function_name: &Ident,
return_type: &ReturnType,
span: proc_macro2::Span,
) -> Arc<Function> {
self.namespace_stack.push(function_name.to_string());
let full_function_name = self.namespace_stack.join("::");
let function = Arc::new(Function {
function_name: full_function_name,
return_type: return_type.to_pretty_string(),
span: span.into(),
});
self.fn_stack.push(Arc::clone(&function));
function
}
fn leave_function(&mut self, function: Arc<Function>) {
self.namespace_stack
.pop()
.expect("Namespace stack should not be empty");
assert_eq!(
self.fn_stack.pop(),
Some(function),
"Function stack mismatch"
);
}
fn collect_mutant(
&mut self,
span: Span,
short_replaced: Option<String>,
replacement: &TokenStream,
genre: Genre,
) {
let mutant = Mutant::new_discovered(
self.source_file.clone(),
self.fn_stack.last().cloned(),
span,
short_replaced,
replacement.to_pretty_string(),
genre,
None,
);
if self.options.allows_mutant(&mutant) {
self.mutants.push(mutant);
} else {
trace!(name = mutant.name(false), "skip mutant by options");
}
}
fn collect_fn_mutants(&mut self, sig: &Signature, block: &Block) {
if let Some(function) = self.fn_stack.last().cloned() {
let body_span = function_body_span(block).expect("Empty function body");
let repls = return_type_replacements(&sig.output, self.error_exprs);
if repls.is_empty() {
trace!(
function_name = function.function_name,
return_type = function.return_type,
"No mutants generated for this return type"
);
} else {
let orig_block = block.to_token_stream().to_pretty_string();
for rep in repls {
let new_block = quote!( { #rep } ).to_token_stream().to_pretty_string();
if orig_block == new_block {
trace!("Replacement is the same as the function body; skipping");
} else {
self.collect_mutant(body_span, None, &rep, Genre::FnValue);
}
}
}
} else {
warn!("collect_fn_mutants called while not in a function?");
}
}
fn in_namespace<F, T>(&mut self, name: &str, f: F) -> T
where
F: FnOnce(&mut Self) -> T,
{
self.namespace_stack.push(name.to_owned());
let r = f(self);
assert_eq!(self.namespace_stack.pop().unwrap(), name);
r
}
}
impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> {
fn visit_expr_call(&mut self, i: &'ast syn::ExprCall) {
let _span = trace_span!("expr_call", line = i.span().start().line).entered();
if attrs_excluded(&i.attrs) {
return;
}
if let Expr::Path(ExprPath { path, .. }) = &*i.func {
trace!(path = path.to_pretty_string(), "visit call");
if let Some(hit) = self
.options
.skip_calls
.iter()
.find(|s| path_ends_with(path, s))
{
trace!("skip call to {hit}");
return;
}
}
syn::visit::visit_expr_call(self, i);
}
fn visit_expr_method_call(&mut self, i: &'ast syn::ExprMethodCall) {
let _span = trace_span!("expr_method_call", line = i.span().start().line).entered();
if attrs_excluded(&i.attrs) {
return;
}
if let Some(hit) = self.options.skip_calls.iter().find(|s| i.method == s) {
trace!("skip method call to {hit}");
return;
}
syn::visit::visit_expr_method_call(self, i);
}
fn visit_file(&mut self, i: &'ast File) {
if attrs_excluded(&i.attrs) {
trace!("file excluded by attrs");
return;
}
syn::visit::visit_file(self, i);
}
fn visit_item_fn(&mut self, i: &'ast ItemFn) {
let function_name = i.sig.ident.to_pretty_string();
let _span = trace_span!(
"fn",
line = i.sig.fn_token.span.start().line,
name = function_name
)
.entered();
trace!("visit fn");
if fn_sig_excluded(&i.sig) || attrs_excluded(&i.attrs) || block_is_empty(&i.block) {
return;
}
let function = self.enter_function(&i.sig.ident, &i.sig.output, i.span());
self.collect_fn_mutants(&i.sig, &i.block);
syn::visit::visit_item_fn(self, i);
self.leave_function(function);
}
fn visit_impl_item_fn(&mut self, i: &'ast syn::ImplItemFn) {
let function_name = i.sig.ident.to_pretty_string();
let _span = trace_span!(
"fn",
line = i.sig.fn_token.span.start().line,
name = function_name
)
.entered();
if fn_sig_excluded(&i.sig)
|| attrs_excluded(&i.attrs)
|| i.sig.ident == "new"
|| block_is_empty(&i.block)
{
return;
}
let function = self.enter_function(&i.sig.ident, &i.sig.output, i.span());
self.collect_fn_mutants(&i.sig, &i.block);
syn::visit::visit_impl_item_fn(self, i);
self.leave_function(function);
}
fn visit_trait_item_fn(&mut self, i: &'ast syn::TraitItemFn) {
let function_name = i.sig.ident.to_pretty_string();
let _span = trace_span!(
"fn",
line = i.sig.fn_token.span.start().line,
name = function_name
)
.entered();
if fn_sig_excluded(&i.sig) || attrs_excluded(&i.attrs) || i.sig.ident == "new" {
return;
}
if let Some(block) = &i.default {
if block_is_empty(block) {
return;
}
let function = self.enter_function(&i.sig.ident, &i.sig.output, i.span());
self.collect_fn_mutants(&i.sig, block);
syn::visit::visit_trait_item_fn(self, i);
self.leave_function(function);
}
}
fn visit_item_impl(&mut self, i: &'ast syn::ItemImpl) {
if attrs_excluded(&i.attrs) {
return;
}
let type_name = i.self_ty.to_pretty_string();
let name = if let Some((_, trait_path, _)) = &i.trait_ {
if path_ends_with(trait_path, "Default") {
return;
}
format!("<impl {trait} for {type_name}>", trait = trait_path.to_pretty_string())
} else {
type_name
};
self.in_namespace(&name, |v| syn::visit::visit_item_impl(v, i));
}
fn visit_item_trait(&mut self, i: &'ast syn::ItemTrait) {
let name = i.ident.to_pretty_string();
let _span = trace_span!("trait", line = i.span().start().line, name).entered();
if attrs_excluded(&i.attrs) {
return;
}
self.in_namespace(&name, |v| syn::visit::visit_item_trait(v, i));
}
fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
let mod_name = node.ident.unraw().to_string();
let _span = trace_span!("mod", line = node.mod_token.span.start().line, mod_name).entered();
if attrs_excluded(&node.attrs) {
trace!("mod excluded by attrs");
return;
}
let source_location = Span::from(node.span());
let path_attribute = match find_path_attribute(&node.attrs) {
Ok(path) => path,
Err(path_attribute) => {
let definition_site = self
.source_file
.format_source_location(source_location.start);
error!(?path_attribute, ?definition_site, %mod_name, "invalid filesystem traversal in mod path attribute");
return;
}
};
let mod_namespace = ModNamespace {
name: mod_name,
path_attribute,
source_location,
};
self.mod_namespace_stack.push(mod_namespace.clone());
if node.content.is_none() {
self.external_mods.push(ExternalModRef {
parts: self.mod_namespace_stack.clone(),
});
}
self.in_namespace(&mod_namespace.name, |v| syn::visit::visit_item_mod(v, node));
assert_eq!(self.mod_namespace_stack.pop(), Some(mod_namespace));
}
fn visit_expr_binary(&mut self, i: &'ast syn::ExprBinary) {
let _span = trace_span!("binary", line = i.op.span().start().line).entered();
trace!("visit binary operator");
if attrs_excluded(&i.attrs) {
return;
}
let replacements = match i.op {
BinOp::Eq(_) => vec![quote! { != }],
BinOp::Ne(_) => vec![quote! { == }],
BinOp::And(_) => vec![quote! { || }],
BinOp::Or(_) => vec![quote! { && }],
BinOp::Lt(_) => vec![quote! { == }, quote! {>}, quote! { <= }],
BinOp::Gt(_) => vec![quote! { == }, quote! {<}, quote! { >= }],
BinOp::Le(_) => vec![quote! {>}],
BinOp::Ge(_) => vec![quote! {<}],
BinOp::Add(_) => vec![quote! {-}, quote! {*}],
BinOp::AddAssign(_) => vec![quote! {-=}, quote! {*=}],
BinOp::Sub(_) | BinOp::Mul(_) => vec![quote! {+}, quote! {/}],
BinOp::SubAssign(_) | BinOp::MulAssign(_) => vec![quote! {+=}, quote! {/=}],
BinOp::Div(_) => vec![quote! {%}, quote! {*}],
BinOp::DivAssign(_) => vec![quote! {%=}, quote! {*=}],
BinOp::Rem(_) => vec![quote! {/}, quote! {+}],
BinOp::RemAssign(_) => vec![quote! {/=}, quote! {+=}],
BinOp::Shl(_) => vec![quote! {>>}],
BinOp::ShlAssign(_) => vec![quote! {>>=}],
BinOp::Shr(_) => vec![quote! {<<}],
BinOp::ShrAssign(_) => vec![quote! {<<=}],
BinOp::BitAnd(_) => vec![quote! {|}, quote! {^}],
BinOp::BitAndAssign(_) => vec![quote! {|=}],
BinOp::BitOr(_) => vec![quote! {&}, quote! {^}],
BinOp::BitOrAssign(_) => vec![quote! {&=}],
BinOp::BitXor(_) => vec![quote! {|}, quote! {&}],
BinOp::BitXorAssign(_) => vec![quote! {|=}, quote! {&=}],
_ => {
trace!(
op = i.op.to_pretty_string(),
"No mutants generated for this binary operator"
);
Vec::new()
}
};
for rep in replacements {
self.collect_mutant(i.op.span().into(), None, &rep, Genre::BinaryOperator);
}
syn::visit::visit_expr_binary(self, i);
}
fn visit_expr_unary(&mut self, i: &'ast syn::ExprUnary) {
let _span = trace_span!("unary", line = i.op.span().start().line).entered();
trace!("visit unary operator");
if attrs_excluded(&i.attrs) {
return;
}
match i.op {
UnOp::Not(_) | UnOp::Neg(_) => {
self.collect_mutant(i.op.span().into(), None, "e! {}, Genre::UnaryOperator);
}
_ => {
trace!(
op = i.op.to_pretty_string(),
"No mutants generated for this unary operator"
);
}
}
syn::visit::visit_expr_unary(self, i);
}
fn visit_expr_match(&mut self, i: &'ast syn::ExprMatch) {
let _span = trace_span!("match", line = i.span().start().line).entered();
if attrs_excluded(&i.attrs) {
trace!("match excluded by attrs");
return;
}
let has_catchall = i
.arms
.iter()
.any(|arm| matches!(arm.pat, syn::Pat::Wild(_)));
if has_catchall {
for arm in &i.arms {
if matches!(arm.pat, syn::Pat::Wild(_)) || arm.guard.is_some() {
continue;
}
let replacement = quote! {};
self.collect_mutant(
arm.span().into(),
Some(arm.pat.to_pretty_string()),
&replacement,
Genre::MatchArm,
);
}
} else {
trace!("match has no `_` pattern");
}
i.arms
.iter()
.flat_map(|arm| &arm.guard)
.for_each(|(_if, guard_expr)| {
self.collect_mutant(
guard_expr.span().into(),
None,
"e! { true },
Genre::MatchArmGuard,
);
self.collect_mutant(
guard_expr.span().into(),
None,
"e! { false },
Genre::MatchArmGuard,
);
});
syn::visit::visit_expr_match(self, i);
}
fn visit_expr_struct(&mut self, i: &'ast syn::ExprStruct) {
let _span = trace_span!("struct", line = i.span().start().line).entered();
trace!("visit struct expression");
if attrs_excluded(&i.attrs) {
return;
}
if let Some(_rest) = &i.rest {
let struct_name = i.path.to_pretty_string();
for pair in i.fields.pairs() {
let field = pair.value();
if let syn::Member::Named(field_name) = &field.member {
let field_name_str = field_name.to_string();
let span = if let Some(comma) = pair.punct() {
let field_span = field.span();
let comma_span = comma.span();
Span {
start: field_span.start().into(),
end: comma_span.end().into(),
}
} else {
field.span().into()
};
let mutant = Mutant::new_discovered(
self.source_file.clone(),
self.fn_stack.last().cloned(),
span,
None,
String::new(),
Genre::StructField,
Some(MutationTarget::StructLiteralField {
field_name: field_name_str,
struct_name: struct_name.clone(),
}),
);
self.mutants.push(mutant);
}
}
}
syn::visit::visit_expr_struct(self, i);
}
}
fn function_body_span(block: &Block) -> Option<Span> {
Some(Span {
start: block.stmts.first()?.span().start().into(),
end: block.stmts.last()?.span().end().into(),
})
}
fn find_mod_source(
tree_root: &Utf8Path,
parent: &SourceFile,
mod_namespace: &ExternalModRef,
) -> Option<Utf8PathBuf> {
let (mod_child, mod_parents) = mod_namespace
.parts
.split_last()
.expect("mod namespace is empty");
let parent_path = &parent.tree_relative_path;
let mut search_dir = if parent.is_top
|| parent_path.ends_with("mod.rs")
|| (mod_child.path_attribute.is_some() && mod_parents.is_empty())
{
parent_path
.parent()
.expect("mod path has no parent")
.to_owned() } else {
parent_path.with_extension("") };
search_dir.extend(mod_parents.iter().map(ModNamespace::get_filesystem_name));
let mod_child_candidates = if let Some(filesystem_name) = &mod_child.path_attribute {
vec![search_dir.join(filesystem_name)]
} else {
[".rs", "/mod.rs"]
.iter()
.map(|tail| search_dir.join(mod_child.name.clone() + tail))
.collect()
};
let mut tried_paths = Vec::new();
for relative_path in mod_child_candidates {
let full_path = tree_root.join(&relative_path);
if full_path.is_file() {
trace!("found submodule in {full_path}");
return Some(relative_path);
}
tried_paths.push(full_path);
}
let mod_name = &mod_child.name;
let definition_site = parent.format_source_location(mod_child.source_location.start);
warn!(?definition_site, %mod_name, ?tried_paths, "referent of mod not found");
None
}
fn fn_sig_excluded(sig: &syn::Signature) -> bool {
if sig.unsafety.is_some() {
trace!("Skip unsafe fn");
true
} else {
false
}
}
fn attrs_excluded(attrs: &[Attribute]) -> bool {
attrs
.iter()
.any(|attr| attr_is_cfg_test(attr) || attr_is_test(attr) || attr_is_mutants_skip(attr))
}
fn block_is_empty(block: &syn::Block) -> bool {
block.stmts.is_empty()
}
fn attr_is_cfg_test(attr: &Attribute) -> bool {
if !path_is(attr.path(), &["cfg"]) {
return false;
}
let mut contains_test = false;
if let Err(err) = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("test") {
contains_test = true;
}
Ok(())
}) {
trace!(
?err,
attr = attr.to_pretty_string(),
"Attribute is in an unrecognized form so skipped",
);
return false;
}
contains_test
}
fn attr_is_test(attr: &Attribute) -> bool {
path_ends_with(attr.path(), "test")
}
fn path_is(path: &syn::Path, idents: &[&str]) -> bool {
path.segments.iter().map(|ps| &ps.ident).eq(idents.iter())
}
fn path_ends_with(path: &syn::Path, ident: &str) -> bool {
path.segments.last().is_some_and(|s| s.ident == ident)
}
fn attr_is_mutants_skip(attr: &Attribute) -> bool {
if path_is(attr.path(), &["mutants", "skip"]) {
return true;
}
if !path_is(attr.path(), &["cfg_attr"]) {
return false;
}
let mut skip = false;
if let Err(err) = attr.parse_nested_meta(|meta| {
if path_is(&meta.path, &["mutants", "skip"]) {
skip = true;
}
Ok(())
}) {
trace!(
?attr,
?err,
"Attribute is not a path with attributes; skipping"
);
return false;
}
skip
}
fn find_path_attribute(attrs: &[Attribute]) -> std::result::Result<Option<Utf8PathBuf>, String> {
attrs
.iter()
.find_map(|attr| match &attr.meta {
syn::Meta::NameValue(meta) if meta.path.is_ident("path") => {
let syn::Expr::Lit(expr_lit) = &meta.value else {
return None;
};
let syn::Lit::Str(lit_str) = &expr_lit.lit else {
return None;
};
let path = lit_str.value();
if path.starts_with('/') {
Some(Err(path))
} else {
Some(Ok(Utf8PathBuf::from(path)))
}
}
_ => None,
})
.transpose()
}
#[cfg(test)]
mod test {
use indoc::indoc;
use itertools::Itertools;
use pretty_assertions::assert_eq;
use test_log::test;
use crate::test_util::copy_of_testdata;
use crate::workspace::{PackageFilter, Workspace};
use super::*;
#[test]
fn path_ends_with() {
use super::path_ends_with;
use syn::parse_quote;
let path = parse_quote! { foo::bar::baz };
assert!(path_ends_with(&path, "baz"));
assert!(!path_ends_with(&path, "bar"));
assert!(!path_ends_with(&path, "foo"));
let path = parse_quote! { baz };
assert!(path_ends_with(&path, "baz"));
assert!(!path_ends_with(&path, "bar"));
let path = parse_quote! { BTreeMap<K, V> };
assert!(path_ends_with(&path, "BTreeMap"));
assert!(!path_ends_with(&path, "V"));
assert!(!path_ends_with(&path, "K"));
}
#[test]
fn no_mutants_equivalent_to_source() {
let code = indoc! { "
fn always_true() -> bool { true }
"};
let source_file = SourceFile::for_tests("src/lib.rs", code, "unimportant", true);
let (mutants, _files) =
walk_file(&source_file, &[], &Options::default()).expect("walk_file");
let mutant_names = mutants.iter().map(|m| m.name(false)).collect_vec();
assert_eq!(
mutant_names,
["src/lib.rs: replace always_true -> bool with false"]
);
}
#[test]
fn no_mutants_in_files_with_inner_cfg_test_attribute() {
let options = Options::default();
let console = Console::new();
let tmp = copy_of_testdata("cfg_test_inner");
let workspace = Workspace::open(tmp.path()).unwrap();
let discovered = workspace
.discover(&PackageFilter::All, &options, &console)
.unwrap();
assert_eq!(discovered.mutants.as_slice(), &[]);
}
fn run_find_path_attribute(
token_stream: &TokenStream,
) -> std::result::Result<Option<Utf8PathBuf>, String> {
let token_string = token_stream.to_string();
let item_mod = syn::parse_str::<syn::ItemMod>(&token_string).unwrap_or_else(|err| {
panic!("Failed to parse test case token stream: {token_string}\n{err}")
});
find_path_attribute(&item_mod.attrs)
}
#[test]
fn find_path_attribute_on_module_item() {
let outer = run_find_path_attribute("e! {
#[path = "foo_file.rs"]
mod foo;
});
assert_eq!(outer, Ok(Some(Utf8PathBuf::from("foo_file.rs"))));
let inner = run_find_path_attribute("e! {
mod foo {
#![path = "foo_folder"]
#[path = "file_for_bar.rs"]
mod bar;
}
});
assert_eq!(inner, Ok(Some(Utf8PathBuf::from("foo_folder"))));
}
#[test]
fn find_path_attribute_matches_the_right_attribute() {
let mismatched = run_find_path_attribute("e! {
#[not_the_path = "something"]
mod mismatched;
});
assert_eq!(mismatched, Ok(None));
}
#[test]
fn reject_module_path_absolute() {
let dots = run_find_path_attribute("e! {
#[path = "contains/../dots.rs"]
mod dots;
});
assert_eq!(dots, Ok(Some(Utf8PathBuf::from("contains/../dots.rs"))));
let dots_inner = run_find_path_attribute("e! {
mod dots_in_path {
#![path = "contains/../dots"]
}
});
assert_eq!(dots_inner, Ok(Some(Utf8PathBuf::from("contains/../dots"))));
let leading_slash = run_find_path_attribute("e! {
#[path = "/leading_slash.rs"]
mod dots;
});
assert_eq!(leading_slash, Err("/leading_slash.rs".to_owned()));
let allow_other_slashes = run_find_path_attribute("e! {
#[path = "foo/other/slashes/are/allowed.rs"]
mod dots;
});
assert_eq!(
allow_other_slashes,
Ok(Some(Utf8PathBuf::from("foo/other/slashes/are/allowed.rs")))
);
let leading_slash2 = run_find_path_attribute("e! {
#[path = "/leading_slash/../and_dots.rs"]
mod dots;
});
assert_eq!(
leading_slash2,
Err("/leading_slash/../and_dots.rs".to_owned())
);
}
#[test]
fn mutants_from_test_str() {
let options = Options::default();
let mutants = mutate_source_str(
indoc! {"
fn always_true() -> bool { true }
"},
&options,
)
.expect("walk_file_string");
assert_eq!(
mutants.iter().map(|m| m.name(false)).collect_vec(),
["src/main.rs: replace always_true -> bool with false"]
);
}
#[test]
fn skip_unsafe_fn() {
let mutants = mutate_source_str(
indoc! {"
unsafe fn foo() -> usize {
2 + 3
}
"},
&Options::default(),
)
.unwrap();
assert_eq!(mutants, []);
}
#[test]
fn skip_functions_with_test_attribute() {
let mutants = mutate_source_str(
indoc! {"
#[test]
fn test_function() {
println!(\"test\");
}
"},
&Options::default(),
)
.unwrap();
assert_eq!(mutants, []);
}
#[test]
fn skip_functions_with_tokio_test_attribute() {
let mutants = mutate_source_str(
indoc! {"
#[tokio::test]
async fn tokio_test() {
println!(\"test\");
}
"},
&Options::default(),
)
.unwrap();
assert_eq!(mutants, []);
}
#[test]
fn skip_functions_with_sqlx_test_attribute() {
let mutants = mutate_source_str(
indoc! {"
#[sqlx::test]
async fn sqlx_test() {
println!(\"test\");
}
"},
&Options::default(),
)
.unwrap();
assert_eq!(mutants, []);
}
#[test]
fn skip_functions_with_arbitrary_test_attribute() {
let mutants = mutate_source_str(
indoc! {"
#[my_framework::test]
fn custom_test() {
println!(\"test\");
}
"},
&Options::default(),
)
.unwrap();
assert_eq!(mutants, []);
}
#[test]
fn do_not_skip_functions_with_non_test_attributes() {
let mutants = mutate_source_str(
indoc! {"
#[derive(Debug)]
pub fn testing_something() -> i32 {
42
}
#[some_crate::test]
fn some_test() {
println!(\"test\");
}
#[some_attr]
pub fn regular_function() -> i32 {
100
}
"},
&Options::default(),
)
.unwrap();
let mutant_names = mutants.iter().map(|m| m.name(false)).collect_vec();
assert_eq!(mutant_names.len(), 6); assert!(mutant_names.iter().any(|n| n.contains("testing_something")));
assert!(mutant_names.iter().any(|n| n.contains("regular_function")));
assert!(!mutant_names.iter().any(|n| n.contains("some_test")));
}
#[test]
fn skip_named_fn() {
let options = Options {
skip_calls: vec!["dont_touch_this".to_owned()],
..Default::default()
};
let mut mutants = mutate_source_str(
indoc! {"
fn main() {
dont_touch_this(2 + 3);
}
"},
&options,
)
.expect("walk_file_string");
mutants.retain(|m| m.genre != Genre::FnValue);
assert_eq!(mutants, []);
}
#[test]
fn skip_with_capacity_by_default() {
let options = Options::from_arg_strs(["mutants"]);
let mut mutants = mutate_source_str(
indoc! {"
fn main() {
let mut v = Vec::with_capacity(2 * 100);
}
"},
&options,
)
.expect("walk_file_string");
mutants.retain(|m| m.genre != Genre::FnValue);
assert_eq!(mutants, []);
}
#[test]
fn mutate_vec_with_capacity_when_default_skips_are_turned_off() {
let options = Options::from_arg_strs(["mutants", "--skip-calls-defaults", "false"]);
let mutants = mutate_source_str(
indoc! {"
fn main() {
let mut _v = std::vec::Vec::<String>::with_capacity(2 * 100);
}
"},
&options,
)
.expect("walk_file_string");
dbg!(&mutants);
assert_eq!(mutants.len(), 3);
}
#[test]
fn skip_method_calls_by_name() {
let options = Options::from_arg_strs(["mutants", "--skip-calls", "dont_touch_this"]);
let mutants = mutate_source_str(
indoc! {"
fn main() {
let mut v = v::new();
v.dont_touch_this(2 + 3);
}
"},
&options,
)
.unwrap();
dbg!(&mutants);
assert_eq!(
mutants
.iter()
.filter(|mutant| mutant.genre != Genre::FnValue)
.count(),
0
);
}
#[test]
fn mutant_name_includes_type_parameters() {
let options = Options::from_arg_strs(["mutants"]);
let mutants = mutate_source_str(
indoc! {r#"
impl AsRef<str> for Apath {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<Apath> for String {
fn from(a: Apath) -> String {
a.0
}
}
impl<'a> From<&'a str> for Apath {
fn from(s: &'a str) -> Apath {
assert!(Apath::is_valid(s), "invalid apath: {s:?}");
Apath(s.to_string())
}
}
"#},
&options,
)
.unwrap();
dbg!(&mutants);
let mutant_names = mutants.iter().map(|m| m.name(false) + "\n").join("");
assert_eq!(
mutant_names,
indoc! {r#"
src/main.rs: replace <impl AsRef<str> for Apath>::as_ref -> &str with ""
src/main.rs: replace <impl AsRef<str> for Apath>::as_ref -> &str with "xyzzy"
src/main.rs: replace <impl From<Apath> for String>::from -> String with String::new()
src/main.rs: replace <impl From<Apath> for String>::from -> String with "xyzzy".into()
src/main.rs: replace <impl From<&'a str> for Apath>::from -> Apath with Default::default()
"#}
);
}
#[test]
fn mutate_match_arms_with_fallback() {
let options = Options::default();
let mutants = mutate_source_str(
indoc! {"
fn main() {
match x {
X::A => {},
X::B => {},
_ => {},
}
}
"},
&options,
)
.unwrap();
assert_eq!(
mutants
.iter()
.filter(|m| m.genre == Genre::MatchArm)
.map(|m| m.name(true))
.collect_vec(),
[
"src/main.rs:3:9: delete match arm X::A in main",
"src/main.rs:4:9: delete match arm X::B in main",
]
);
}
#[test]
fn skip_match_arms_without_fallback() {
let options = Options::default();
let mutants = mutate_source_str(
indoc! {"
fn main() {
match x {
X::A => {},
X::B => {},
}
}
"},
&options,
)
.unwrap();
let empty: &[&str] = &[];
assert_eq!(
mutants
.iter()
.filter(|m| m.genre == Genre::MatchArm)
.map(|m| m.name(true))
.collect_vec(),
empty
);
}
#[test]
fn mutate_match_guard() {
let options = Options::default();
let mutants = mutate_source_str(
indoc! {"
fn main() {
match x {
X::A if foo() => {},
X::A => {},
X::B => {},
X::C if bar() => {},
}
}
"},
&options,
)
.unwrap();
assert_eq!(
mutants
.iter()
.filter(|m| m.genre == Genre::MatchArmGuard)
.map(|m| m.name(true))
.collect_vec(),
[
"src/main.rs:3:17: replace match guard foo() with true in main",
"src/main.rs:3:17: replace match guard foo() with false in main",
"src/main.rs:6:17: replace match guard bar() with true in main",
"src/main.rs:6:17: replace match guard bar() with false in main",
]
);
}
#[test]
fn skip_removing_match_arm_with_guard() {
let options = Options::default();
let mutants = mutate_source_str(
indoc! {"
fn main() {
match x {
X::A if foo() => {},
X::A => {},
_ => {},
}
}
"},
&options,
)
.unwrap();
assert_eq!(
mutants
.iter()
.filter(|m| matches!(m.genre, Genre::MatchArmGuard | Genre::MatchArm))
.map(|m| m.name(true))
.collect_vec(),
[
"src/main.rs:4:9: delete match arm X::A in main",
"src/main.rs:3:17: replace match guard foo() with true in main",
"src/main.rs:3:17: replace match guard foo() with false in main",
]
);
}
#[test]
fn mutate_comparisons() {
assert_eq!(
mutate_expr("a > b"),
&["replace > with ==", "replace > with <", "replace > with >="]
);
assert_eq!(
mutate_expr("a < b "),
&["replace < with ==", "replace < with >", "replace < with <="]
);
assert_eq!(mutate_expr("a == b"), &["replace == with !="]);
assert_eq!(mutate_expr("a != b"), &["replace != with =="]);
assert_eq!(mutate_expr("a >= b"), &["replace >= with <"]);
assert_eq!(mutate_expr("a <= b"), &["replace <= with >"]);
}
#[test]
fn mutate_binops() {
assert_eq!(mutate_expr(" a >> 2; "), &["replace >> with <<"]);
assert_eq!(mutate_expr("a << 2; "), &["replace << with >>"]);
assert_eq!(mutate_expr("a && b"), &["replace && with ||"]);
assert_eq!(mutate_expr("a || b"), &["replace || with &&"]);
assert_eq!(
mutate_expr("a + b"),
&["replace + with -", "replace + with *"]
);
assert_eq!(
mutate_expr("a - b"),
&["replace - with +", "replace - with /"]
);
assert_eq!(
mutate_expr("a * b"),
&["replace * with +", "replace * with /"]
);
assert_eq!(
mutate_expr("a / b"),
&["replace / with %", "replace / with *"]
);
assert_eq!(
mutate_expr("a & b"),
&["replace & with |", "replace & with ^"]
);
assert_eq!(
mutate_expr("a | b"),
&["replace | with &", "replace | with ^"]
);
assert_eq!(
mutate_expr("a ^ b"),
&["replace ^ with |", "replace ^ with &"]
);
}
#[test]
fn mutate_assign_ops() {
assert_eq!(
mutate_expr("a += b"),
&["replace += with -=", "replace += with *="]
);
assert_eq!(
mutate_expr("a -= b"),
&["replace -= with +=", "replace -= with /="]
);
assert_eq!(
mutate_expr("a *= b"),
&["replace *= with +=", "replace *= with /="]
);
assert_eq!(
mutate_expr("a /= b"),
&["replace /= with %=", "replace /= with *="]
);
assert_eq!(mutate_expr("a &= b"), &["replace &= with |="]);
assert_eq!(mutate_expr("a |= b"), &["replace |= with &="]);
assert_eq!(
mutate_expr("a ^= b"),
&["replace ^= with |=", "replace ^= with &="]
);
assert_eq!(
mutate_expr("a %= b"),
&["replace %= with /=", "replace %= with +="]
);
assert_eq!(mutate_expr("a >>= b"), &["replace >>= with <<="]);
assert_eq!(mutate_expr("a <<= b"), &["replace <<= with >>="]);
}
#[test]
fn delete_struct_field_with_default() {
let mutants = mutate_expr(
r#"
let cat = Cat {
name: "Felix",
coat: Coat::Tuxedo,
..Default::default()
};
"#,
);
assert_eq!(
mutants,
&[
"delete field name from struct Cat expression",
"delete field coat from struct Cat expression"
]
);
}
#[test]
fn skip_struct_without_default() {
let mutants = mutate_expr(
r#"
let cat = Cat {
name: "Felix",
coat: Coat::Tuxedo,
};
"#,
);
assert!(mutants.is_empty());
}
#[test]
fn delete_struct_field_with_custom_default() {
let mutants = mutate_expr(
r#"
let config = Config {
timeout: 30,
retries: 5,
..base_config
};
"#,
);
assert_eq!(
mutants,
&[
"delete field timeout from struct Config expression",
"delete field retries from struct Config expression"
]
);
}
#[test]
fn delete_struct_field_single_field_with_default() {
let mutants = mutate_expr(
r#"
let point = Point {
x: 10,
..Default::default()
};
"#,
);
assert_eq!(mutants, &["delete field x from struct Point expression"]);
}
#[test]
fn delete_struct_field_complex_values() {
let mutants = mutate_expr(
r#"
let settings = Settings {
enabled: get_enabled(),
count: compute_count() + 10,
..Settings::default()
};
"#,
);
assert_eq!(
mutants,
&[
"delete field enabled from struct Settings expression",
"delete field count from struct Settings expression",
"replace + with -",
"replace + with *"
]
);
}
}