Skip to content

Commit

Permalink
Mutate +, -, *, /, %, &, ^, |, <<, >>, and their assignment versions (#…
Browse files Browse the repository at this point in the history
…198)

- [x] Docs
- [x] Maybe also assignment ops like `+=`, which is apparently also a
binop in the AST
- [x] Bitwise ops `& | << >>`
- [x] Add a tree that demonstrates all these patterns and that is only
listed by tests, not run (for speed)

Fixes #200
  • Loading branch information
sourcefrog authored Jan 14, 2024
2 parents cf48041 + 8485b07 commit e55257d
Show file tree
Hide file tree
Showing 58 changed files with 4,568 additions and 382 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ exclude = [
"testdata/hang_when_mutated",
"testdata/insta",
"testdata/integration_tests",
"testdata/many_patterns",
"testdata/missing_test",
"testdata/mut_ref",
"testdata/never_type",
Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

- New: Mutate `+, -, *, /, %, &, ^, |, <<, >>` binary ops, and their corresponding assignment ops like `+=`.

- Changed: Stop generating mutations of `||` and `&&` to `!=` and `||`, because it seems to raise too many low-value false positives that may be hard to test.

## 24.1.0

- New! `cargo mutants --test-tool nextest`, or `test_tool = "nextest"` in `.cargo/mutants.toml` runs tests under [Nextest](https://nexte.st/). Some trees have tests that only work under Nextest, and this allows them to be tested. In other cases Nextest may be significantly faster, because it will exit soon after the first test failure.
Expand Down
22 changes: 15 additions & 7 deletions book/src/mutants.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,24 @@ like `a == 0`.
| -------- | ------------------ |
| `==` | `!=` |
| `!=` | `==` |
| `&&` | `\|\|`, `==`, `!=` |
| `\|\|` | `&&`, `==`, `!=` |
| `&&` | `\|\|` |
| `\|\|` | `&&`, |
| `<` | `==`, `>` |
| `>` | `==`, `<` |
| `<=` | `==`, `>=` |
| `>=` | `==`, `<=` |
| `<=` | `>` |
| `>=` | `<` |
| `+` | `-`, `*` |
| `-` | `+`, `/` |
| `*` | `+`, `/` |
| `/` | `%`, `*` |
| `%` | `/`, `+` |
| `<<` | `>>` |
| `>>` | `<<` |
| `&` | `\|`,`^` |
| `\|` | `&`, `^` |
| `^` | `&`, `\|` |
| `+=` and similar assignments | assignment corresponding to the line above |

Equality operators are not currently replaced with comparisons like `<` or `<=`
because they are
too prone to generate false positives, for example when unsigned integers are compared to 0.

Logical `&&` and `||` are replaced with `==` and `!=` which function as XNOR and XOR respectively,
although they are fairly often unviable due to needing parenthesis when the original operator does not.
6 changes: 4 additions & 2 deletions src/mutate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ mod test {
let mutants = workspace
.mutants(&PackageFilter::All, &options, &Console::new())
.unwrap();
assert_eq!(mutants.len(), 3);
assert_eq!(mutants.len(), 5);
assert_eq!(
format!("{:#?}", mutants[0]),
indoc! {
Expand Down Expand Up @@ -318,6 +318,8 @@ mod test {
replace controlled_loop with ()
replace > with == in controlled_loop
replace > with < in controlled_loop
replace * with + in controlled_loop
replace * with / in controlled_loop
"###
);
}
Expand All @@ -330,7 +332,7 @@ mod test {
&Options::default(),
&Console::new(),
)?;
assert_eq!(mutants.len(), 3);
assert_eq!(mutants.len(), 5);

let mutated_code = mutants[0].mutated_code();
assert_eq!(mutants[0].function.as_ref().unwrap().function_name, "main");
Expand Down

Large diffs are not rendered by default.

25 changes: 23 additions & 2 deletions src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use std::collections::VecDeque;
use std::sync::Arc;
use std::vec;

use anyhow::Context;
use proc_macro2::{Ident, TokenStream};
Expand Down Expand Up @@ -361,12 +362,32 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> {
// because they require parenthesis for disambiguation in many expressions.
BinOp::Eq(_) => vec![quote! { != }],
BinOp::Ne(_) => vec![quote! { == }],
BinOp::And(_) => vec![quote! { || }, quote! {==}, quote! {!=}],
BinOp::Or(_) => vec![quote! { && }, quote! {==}, quote! {!=}],
BinOp::And(_) => vec![quote! { || }],
BinOp::Or(_) => vec![quote! { && }],
BinOp::Lt(_) => vec![quote! { == }, quote! {>}],
BinOp::Gt(_) => vec![quote! { == }, quote! {<}],
BinOp::Le(_) => vec![quote! {>}],
BinOp::Ge(_) => vec![quote! {<}],
BinOp::Add(_) => vec![quote! {-}, quote! {*}],
BinOp::AddAssign(_) => vec![quote! {-=}, quote! {*=}],
BinOp::Sub(_) => vec![quote! {+}, quote! {/}],
BinOp::SubAssign(_) => vec![quote! {+=}, quote! {/=}],
BinOp::Mul(_) => vec![quote! {+}, quote! {/}],
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! {|=}, quote! {^=}],
BinOp::BitOr(_) => vec![quote! {&}, quote! {^}],
BinOp::BitOrAssign(_) => vec![quote! {&=}, quote! {^=}],
BinOp::BitXor(_) => vec![quote! {|}, quote! {&}],
BinOp::BitXorAssign(_) => vec![quote! {|=}, quote! {&=}],
_ => {
trace!(
op = i.op.to_pretty_string(),
Expand Down
10 changes: 5 additions & 5 deletions testdata/error_value/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::result::Result;

pub fn even_is_ok(n: u32) -> Result<u32, &'static str> {
if n % 2 == 0 {
pub fn zero_is_ok(n: u32) -> Result<u32, &'static str> {
if n == 0 {
Ok(n)
} else {
Err("number is odd")
Err("not zero")
}
}

Expand All @@ -30,7 +30,7 @@ mod test {
fn bad_test_ignores_error_results() {
// A bit contrived but does the job: never checks that
// the code passes on values that it should accept.
assert!(even_is_ok(1).is_err());
assert!(even_is_ok(3).is_err());
assert!(zero_is_ok(1).is_err());
assert!(zero_is_ok(3).is_err());
}
}
8 changes: 8 additions & 0 deletions testdata/many_patterns/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "cargo-mutants-testdata-many-patterns"
version = "0.0.0"
edition = "2021"
authors = ["Martin Pool"]
publish = false

[lib]
5 changes: 5 additions & 0 deletions testdata/many_patterns/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# `many_patterns` testdata tree

This tree contains code that generates many different mutants, to exercise code that matches these patterns.

This tree is not tested from the test suite, because it may eventually generate many mutants and get slow, and there is in fact no test coverage.
19 changes: 19 additions & 0 deletions testdata/many_patterns/src/binops.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
pub fn binops() {
let _ = 1 + 2 * 3 / 4 % 5;
let _ = 1 & 2 | 3 ^ 4 << 5 >> 6;
let mut a = 0isize;
a += 1;
a -= 2;
a *= 3;
a /= 2;
}

pub fn bin_assign() -> i32 {
let mut a = 0;
a |= 0xfff7;
a ^= 0xffff;
a &= 0x0f;
a >>= 4;
a <<= 1;
a
}
1 change: 1 addition & 0 deletions testdata/many_patterns/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod binops;
5 changes: 5 additions & 0 deletions testdata/override_dependency/tests/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ fn zero_is_even() {
fn three_is_not_even() {
assert_eq!(is_even(3), false);
}

#[test]
fn two_is_even() {
assert!(is_even(2));
}
5 changes: 5 additions & 0 deletions testdata/patch_dependency/tests/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ fn zero_is_even() {
fn three_is_not_even() {
assert_eq!(is_even(3), false);
}

#[test]
fn two_is_even() {
assert!(is_even(2));
}
5 changes: 5 additions & 0 deletions testdata/replace_dependency/tests/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ fn zero_is_even() {
fn three_is_not_even() {
assert_eq!(is_even(3), false);
}

#[test]
fn two_is_even() {
assert!(is_even(2));
}
3 changes: 2 additions & 1 deletion testdata/with_child_directories/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ pub fn double(x: usize) -> usize {
#[cfg(test)]
mod test {
#[test]
fn double() {
fn test_double() {
assert_eq!(super::double(2), 4);
assert_eq!(super::double(8), 16);
}
}
2 changes: 2 additions & 0 deletions testdata/with_child_directories/src/module/module_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ mod test {
#[test]
fn double() {
assert_eq!(super::double(2), 4);
assert_eq!(super::double(0), 0);
assert_eq!(super::double(6), 12);
}
}
50 changes: 28 additions & 22 deletions tests/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use std::fs::{create_dir, write};

use indoc::indoc;
use insta::assert_snapshot;
use predicates::prelude::*;
use tempfile::TempDir;

Expand Down Expand Up @@ -163,15 +164,20 @@ fn list_with_config_file_regexps() {
exclude_re = ["-> bool with true"]
"#,
);
run()
let cmd = run()
.args(["mutants", "--list", "--line-col=false", "-d"])
.arg(testdata.path())
.assert()
.success()
.stdout(predicates::str::diff(indoc! {"\
src/simple_fns.rs: replace divisible_by_three -> bool with false
src/simple_fns.rs: replace == with != in divisible_by_three
"}));
.success();
assert_snapshot!(
String::from_utf8_lossy(&cmd.get_output().stdout),
@r###"
src/simple_fns.rs: replace divisible_by_three -> bool with false
src/simple_fns.rs: replace == with != in divisible_by_three
src/simple_fns.rs: replace % with / in divisible_by_three
src/simple_fns.rs: replace % with + in divisible_by_three
"###
);
}

#[test]
Expand All @@ -180,8 +186,8 @@ fn exclude_re_overrides_config() {
write_config_file(
&testdata,
r#"
exclude_re = [".*"] # would exclude everything
"#,
exclude_re = [".*"] # would exclude everything
"#,
);
run()
.args(["mutants", "--list", "-d"])
Expand All @@ -190,17 +196,23 @@ exclude_re = [".*"] # would exclude everything
.success()
.stdout(predicates::str::is_empty());
// Also tests that the alias --exclude-regex is accepted
run()
let cmd = run()
.args(["mutants", "--list", "--line-col=false", "-d"])
.arg(testdata.path())
.args(["--exclude-regex", " -> "])
.args(["-f", "src/simple_fns.rs"])
.assert()
.success()
.stdout(indoc! {"
src/simple_fns.rs: replace returns_unit with ()
src/simple_fns.rs: replace == with != in divisible_by_three
"});
.success();
assert_snapshot!(
String::from_utf8_lossy(&cmd.get_output().stdout),
@r###"
src/simple_fns.rs: replace returns_unit with ()
src/simple_fns.rs: replace += with -= in returns_unit
src/simple_fns.rs: replace += with *= in returns_unit
src/simple_fns.rs: replace == with != in divisible_by_three
src/simple_fns.rs: replace % with / in divisible_by_three
src/simple_fns.rs: replace % with + in divisible_by_three
"###);
}

#[test]
Expand All @@ -220,8 +232,6 @@ fn tree_fails_without_needed_feature() {

#[test]
fn additional_cargo_args() {
// The point of this tree is to check that Cargo features can be turned on,
// but let's make sure it does fail as intended if they're not.
let testdata = copy_of_testdata("fails_without_feature");
write_config_file(
&testdata,
Expand All @@ -233,14 +243,11 @@ fn additional_cargo_args() {
.args(["mutants", "-d"])
.arg(testdata.path())
.assert()
.success()
.stdout(predicates::str::contains("2 caught"));
.success();
}

#[test]
fn additional_cargo_test_args() {
// The point of this tree is to check that Cargo features can be turned on,
// but let's make sure it does fail as intended if they're not.
let testdata = copy_of_testdata("fails_without_feature");
write_config_file(
&testdata,
Expand All @@ -252,6 +259,5 @@ fn additional_cargo_test_args() {
.args(["mutants", "-d"])
.arg(testdata.path())
.assert()
.success()
.stdout(predicates::str::contains("2 caught"));
.success();
}
14 changes: 2 additions & 12 deletions tests/cli/error_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@ fn error_value_catches_untested_ok_case() {
.arg(tmp_src_dir.path())
.assert()
.code(2)
.stderr("")
.stdout(predicate::function(|stdout| {
insta::assert_snapshot!(stdout);
true
}));
.stderr("");
}

#[test]
Expand Down Expand Up @@ -94,13 +90,7 @@ fn warn_if_error_value_starts_with_err() {
.code(0)
.stderr(predicate::str::contains(
"error_value option gives the value of the error, and probably should not start with Err(: got Err(anyhow!(\"mutant\"))"
))
.stdout(indoc! { "\
src/lib.rs:4:5: replace even_is_ok -> Result<u32, &\'static str> with Ok(0)
src/lib.rs:4:5: replace even_is_ok -> Result<u32, &\'static str> with Ok(1)
src/lib.rs:4:5: replace even_is_ok -> Result<u32, &\'static str> with Err(Err(anyhow!(\"mutant\")))
src/lib.rs:4:14: replace == with != in even_is_ok
" });
));
}

#[test]
Expand Down
Loading

0 comments on commit e55257d

Please sign in to comment.