Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion book/src/mutants.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,14 @@ too prone to generate false positives, for example when unsigned integers are co
## Unary operators

Unary operators are deleted in expressions like `-a` and `!a`.
They are not currently replaced with other unary operators because they are too prone to
They are not currently replaced with other unary operators because they are too prone to
generate unviable cases (e.g. `!1.0`, `-false`).

## Match arms

Entire match arms are deleted in match expressions when a wildcard pattern is present in one of the arms.
Match expressions without a wildcard pattern would be too prone to unviable mutations of this kind.

## Match arm guards

Match arm guard expressions are replaced with `true` and `false`.
3 changes: 0 additions & 3 deletions src/fnvalue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,6 @@ fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> impl Iterator<Item =
.collect_vec()
}
},
Type::Tuple(TypeTuple { elems, .. }) if elems.is_empty() => {
vec![quote! { () }]
}
Type::Tuple(TypeTuple { elems, .. }) => {
// Generate the cartesian product of replacements of every type within the tuple.
elems
Expand Down
62 changes: 38 additions & 24 deletions src/mutant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub enum Genre {
/// Replace `==` with `!=` and so on.
BinaryOperator,
UnaryOperator,
/// Delete match arm.
MatchArm,
/// Replace the expression of a match arm guard with a fixed value.
MatchArmGuard,
}

/// A mutation applied to source code.
Expand Down Expand Up @@ -131,33 +135,43 @@ impl Mutant {
style(s.to_string())
}
let mut v: Vec<StyledObject<String>> = Vec::new();
if self.genre == Genre::FnValue {
v.push(s("replace "));
let function = self
.function
.as_ref()
.expect("FnValue mutant should have a function");
v.push(s(&function.function_name).bright().magenta());
if !function.return_type.is_empty() {
v.push(s(" "));
v.push(s(&function.return_type).magenta());
}
v.push(s(" with "));
v.push(s(self.replacement_text()).yellow());
} else {
if self.replacement.is_empty() {
v.push(s("delete "));
} else {
match self.genre {
Genre::FnValue => {
v.push(s("replace "));
}
v.push(s(self.original_text()).yellow());
if !self.replacement.is_empty() {
let function = self
.function
.as_ref()
.expect("FnValue mutant should have a function");
v.push(s(&function.function_name).bright().magenta());
if !function.return_type.is_empty() {
v.push(s(" "));
v.push(s(&function.return_type).magenta());
}
v.push(s(" with "));
v.push(s(&self.replacement).bright().yellow());
v.push(s(self.replacement_text()).yellow());
}
if let Some(function) = &self.function {
v.push(s(" in "));
v.push(s(&function.function_name).bright().magenta());
Genre::MatchArmGuard => {
v.push(s("replace match guard with "));
v.push(s(self.replacement_text()).yellow());
}
Genre::MatchArm => {
v.push(s("delete match arm"));
}
_ => {
if self.replacement.is_empty() {
v.push(s("delete "));
} else {
v.push(s("replace "));
}
v.push(s(self.original_text()).yellow());
if !self.replacement.is_empty() {
v.push(s(" with "));
v.push(s(&self.replacement).bright().yellow());
}
if let Some(function) = &self.function {
v.push(s(" in "));
v.push(s(&function.function_name).bright().magenta());
}
}
}
v
Expand Down
165 changes: 165 additions & 0 deletions src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,53 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> {
};
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();

// While it's not currently possible to annotate expressions with custom attributes, this
// limitation could be lifted in the future.
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 {
i.arms
.iter()
// Don't mutate the wild arm, because that will likely be unviable, and also
// skip it if a guard is present, because the replacement of the guard with 'false'
// below is logically equivalent to removing the arm.
.filter(|arm| !matches!(arm.pat, syn::Pat::Wild(_)) && arm.guard.is_none())
.for_each(|arm| {
self.collect_mutant(arm.span().into(), &quote! {}, 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(),
&quote! { true },
Genre::MatchArmGuard,
);
self.collect_mutant(
guard_expr.span().into(),
&quote! { false },
Genre::MatchArmGuard,
);
});

syn::visit::visit_expr_match(self, i);
}
}

// Get the span of the block excluding the braces, or None if it is empty.
Expand Down Expand Up @@ -1084,4 +1131,122 @@ mod test {
"#}
);
}

#[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",
"src/main.rs:4:9: delete match arm",
]
);
}

#[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 with true",
"src/main.rs:3:17: replace match guard with false",
"src/main.rs:6:17: replace match guard with true",
"src/main.rs:6:17: replace match guard with false",
]
);
}

#[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",
"src/main.rs:3:17: replace match guard with true",
"src/main.rs:3:17: replace match guard with false",
]
);
}
}
4 changes: 0 additions & 4 deletions src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,6 @@ impl Workspace {
// in that case we'll just fall back to everything, for lack of a better option.
// TODO: Use the new cargo_metadata API that doesn't panic?
match catch_unwind(|| metadata.workspace_default_packages()) {
Ok(default_packages) if default_packages.is_empty() => {
debug!("manifest has no explicit default packages");
PackageSelection::All
}
Ok(default_packages) => {
let default_package_names: Vec<&str> = default_packages
.iter()
Expand Down
9 changes: 9 additions & 0 deletions testdata/many_patterns/src/binops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ pub fn binops() {
a -= 2;
a *= 3;
a /= 2;

let mut b;
b = a < 0;
b = a <= 0;
b = a > 0;
b = a >= 0;
b = a == 0;
b = a != 0;
}

pub fn bin_assign() -> i32 {
Expand All @@ -15,5 +23,6 @@ pub fn bin_assign() -> i32 {
a &= 0x0f;
a >>= 4;
a <<= 1;
a %= 1;
a
}
6 changes: 6 additions & 0 deletions testdata/workspace_default_members/Cargo_test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# This workspace has default-members set, which influences which packages are mutated by default.

[workspace]
members = ["main", "excluded"]
default-members = ["main"]
resolver = "2"
10 changes: 10 additions & 0 deletions testdata/workspace_default_members/excluded/Cargo_test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "excluded"
version = "0.1.0"
edition = "2021"
publish = false

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
mutants = "0.0.3"
3 changes: 3 additions & 0 deletions testdata/workspace_default_members/excluded/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() -> i32 {
100
}
10 changes: 10 additions & 0 deletions testdata/workspace_default_members/main/Cargo_test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "main"
version = "0.1.0"
edition = "2021"
publish = false

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
mutants = "0.0.3"
6 changes: 6 additions & 0 deletions testdata/workspace_default_members/main/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const X: u32 = 2 + 1;

#[test]
fn test() {
assert_eq!(X, 3);
}
12 changes: 12 additions & 0 deletions tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ fn show_version() {
.stdout(predicates::str::is_match(r"^cargo-mutants \d+\.\d+\.\d+(-.*)?\n$").unwrap());
}

#[test]
fn show_help() {
// Asserting on the entire help message would be a bit too annoying to maintain.
run()
.args(["mutants", "--help"])
.assert()
.success()
.stdout(predicates::str::contains(
"Usage: cargo mutants [OPTIONS] [-- <CARGO_TEST_ARGS>...]",
));
}

#[test]
fn uses_cargo_env_var_to_run_cargo_so_invalid_value_fails() {
let tmp_src_dir = copy_of_testdata("well_tested");
Expand Down
Loading