Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutate +, -, *, /, %, &, ^, |, <<, >>, and their assignment versions #198

Merged
merged 16 commits into from
Jan 14, 2024
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
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