diff --git a/NEWS.md b/NEWS.md index ed5bd2bb..e9605031 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,8 @@ ## Unreleased +- New: Generate more comparison operator mutant variants: `<` and `>` now also mutate to `>=` and `<=` respectively as a way of negating the condition, and `<=` and `>=` now mutate to `<` and `>` as well to catch off-by-one errors. + - New: `#[mutants::exclude_re("pattern")]` attribute to exclude specific mutations by regex, without disabling all mutations on the function. The attribute can be placed on functions, `impl` blocks, `trait` blocks, modules, files, and on expressions that can carry an attribute (such as `match`, struct literals, call expressions, method calls, and unary expressions). Multiple patterns can be applied. Also supported within `cfg_attr`. Requires the [mutants](https://crates.io/crates/mutants) crate version `0.0.5` or later. - Fixed: `#[mutants::skip]` (and `#[cfg_attr(..., mutants::skip)]`) is now honoured when placed on `const` and `static` items, including associated constants in `impl` and `trait` blocks. Previously the attribute was silently ignored on these items and operator mutants inside the initializer expression were still generated ([#508](https://github.com/sourcefrog/cargo-mutants/issues/508)). diff --git a/book/src/mutants.md b/book/src/mutants.md index 458a26cf..18b92108 100644 --- a/book/src/mutants.md +++ b/book/src/mutants.md @@ -81,10 +81,10 @@ like `a == 0`. | `!=` | `==` | | `&&` | `\|\|` | | `\|\|` | `&&`, | -| `<` | `==`, `>` | -| `>` | `==`, `<` | -| `<=` | `>` | -| `>=` | `<` | +| `<` | `==`, `>`, `<=`, `>=` | +| `>` | `==`, `<`, `>=`, `<=` | +| `<=` | `<`, `>` | +| `>=` | `>`, `<` | | `+` | `-`, `*` | | `-` | `+`, `/` | | `*` | `+`, `/` | diff --git a/src/mutant.rs b/src/mutant.rs index 83fd1fa4..6fc5f076 100644 --- a/src/mutant.rs +++ b/src/mutant.rs @@ -477,6 +477,7 @@ mod test { "replace > with == in controlled_loop", "replace > with < in controlled_loop", "replace > with >= in controlled_loop", + "replace > with <= in controlled_loop", "replace * with + in controlled_loop", "replace * with / in controlled_loop", ] diff --git a/src/visit.rs b/src/visit.rs index 904bb2bb..c245eea6 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -799,10 +799,10 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { 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::Lt(_) => vec![quote! { == }, quote! {>}, quote! { <= }, quote! { >= }], + BinOp::Gt(_) => vec![quote! { == }, quote! {<}, quote! { >= }, quote! { <= }], + BinOp::Le(_) => vec![quote! {<}, quote! {>}], + BinOp::Ge(_) => vec![quote! {>}, quote! {<}], BinOp::Add(_) => vec![quote! {-}, quote! {*}], BinOp::AddAssign(_) => vec![quote! {-=}, quote! {*=}], BinOp::Sub(_) | BinOp::Mul(_) => vec![quote! {+}, quote! {/}], @@ -1821,16 +1821,32 @@ mod test { fn mutate_comparisons() { assert_eq!( mutate_expr("a > b"), - &["replace > with ==", "replace > with <", "replace > with >="] + &[ + "replace > with ==", + "replace > with <", + "replace > with >=", + "replace > with <=" + ] ); assert_eq!( mutate_expr("a < b "), - &["replace < with ==", "replace < with >", "replace < with <="] + &[ + "replace < with ==", + "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 >"]); + assert_eq!( + mutate_expr("a >= b"), + &["replace >= with >", "replace >= with <"] + ); + assert_eq!( + mutate_expr("a <= b"), + &["replace <= with <", "replace <= with >"] + ); } #[test] diff --git a/tests/main.rs b/tests/main.rs index 0fb8973d..0f80c163 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -2605,15 +2605,16 @@ fn check_tree_with_mutants_skip() { .assert() .success() .stdout(indoc! { r" - Found 6 mutants to test + Found 7 mutants to test ok Unmutated baseline ok src/lib.rs:15:5: replace controlled_loop with () ok src/lib.rs:21:28: replace > with == in controlled_loop ok src/lib.rs:21:28: replace > with < in controlled_loop ok src/lib.rs:21:28: replace > with >= in controlled_loop + ok src/lib.rs:21:28: replace > with <= in controlled_loop ok src/lib.rs:21:53: replace * with + in controlled_loop ok src/lib.rs:21:53: replace * with / in controlled_loop - 6 mutants tested: 6 succeeded + 7 mutants tested: 7 succeeded "}) .stderr(""); assert_eq!( @@ -2621,10 +2622,10 @@ fn check_tree_with_mutants_skip() { serde_json::json!({ "caught": 0, "missed": 0, - "success": 6, + "success": 7, "timeout": 0, "unviable": 0, - "total_mutants": 6, + "total_mutants": 7, }) ); } diff --git a/tests/snapshots/main__cargo_mutants_in_relative_dependency_tree_passes.snap b/tests/snapshots/main__cargo_mutants_in_relative_dependency_tree_passes.snap index 4d935a63..279f1987 100644 --- a/tests/snapshots/main__cargo_mutants_in_relative_dependency_tree_passes.snap +++ b/tests/snapshots/main__cargo_mutants_in_relative_dependency_tree_passes.snap @@ -1,8 +1,7 @@ --- source: tests/main.rs expression: stdout -snapshot_kind: text --- -Found 7 mutants to test +Found 8 mutants to test ok Unmutated baseline -7 mutants tested: 7 caught +8 mutants tested: 8 caught diff --git a/tests/util/snapshots/main__util__list_mutants_json_well_tested.snap b/tests/util/snapshots/main__util__list_mutants_json_well_tested.snap index bfbe00b0..8f76a670 100644 --- a/tests/util/snapshots/main__util__list_mutants_json_well_tested.snap +++ b/tests/util/snapshots/main__util__list_mutants_json_well_tested.snap @@ -1,8 +1,6 @@ --- source: tests/util/mod.rs -assertion_line: 33 expression: "String::from_utf8_lossy(&output.stdout)" - --- [ { @@ -1893,6 +1891,38 @@ expression: "String::from_utf8_lossy(&output.stdout)" } } }, + { + "diff": "--- src/result.rs\n+++ replace < with >= in error_if_negative\n@@ -2,17 +2,17 @@\n // fn io_result() -> std::io::Result<\n \n /// Simple easily-recognizable Result.\n fn simple_result() -> Result<&'static str, ()> {\n Ok(\"success\")\n }\n \n fn error_if_negative(a: i32) -> Result<(), ()> {\n- if a < 0 {\n+ if a >= /* ~ changed by cargo-mutants ~ */ 0 {\n Err(())\n } else {\n Ok(())\n }\n }\n \n fn result_with_no_apparent_type_args() -> std::fmt::Result {\n Err(Default::default())\n", + "file": "src/result.rs", + "function": { + "function_name": "error_if_negative", + "return_type": "-> Result<(), ()>", + "span": { + "end": { + "column": 2, + "line": 15 + }, + "start": { + "column": 1, + "line": 9 + } + } + }, + "genre": "BinaryOperator", + "name": "src/result.rs:10:10: replace < with >= in error_if_negative", + "package": "cargo-mutants-testdata-well-tested", + "replacement": ">=", + "span": { + "end": { + "column": 11, + "line": 10 + }, + "start": { + "column": 10, + "line": 10 + } + } + }, { "diff": "--- src/result.rs\n+++ replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default())\n@@ -10,17 +10,17 @@\n if a < 0 {\n Err(())\n } else {\n Ok(())\n }\n }\n \n fn result_with_no_apparent_type_args() -> std::fmt::Result {\n- Err(Default::default())\n+ Ok(Default::default()) /* ~ changed by cargo-mutants ~ */\n }\n \n mod test {\n use super::*;\n \n #[test]\n fn simple_result_success() {\n assert_eq!(simple_result(), Ok(\"success\"));\n", "file": "src/result.rs", @@ -2661,6 +2691,38 @@ expression: "String::from_utf8_lossy(&output.stdout)" } } }, + { + "diff": "--- src/slices.rs\n+++ replace < with >= in pad\n@@ -1,13 +1,13 @@\n use std::borrow::Cow;\n \n fn pad<'a>(aa: &'a mut [Cow<'static, str>]) -> &'a [Cow<'static, str>] {\n for a in aa.iter_mut() {\n- if a.len() < 3 {\n+ if a.len() >= /* ~ changed by cargo-mutants ~ */ 3 {\n a.to_mut().push_str(\"___\");\n }\n }\n aa\n }\n \n fn return_mut_slice(a: &mut [usize]) -> &mut [usize] {\n for x in a.iter_mut() {\n", + "file": "src/slices.rs", + "function": { + "function_name": "pad", + "return_type": "-> &'a[Cow<'static, str>]", + "span": { + "end": { + "column": 2, + "line": 10 + }, + "start": { + "column": 1, + "line": 3 + } + } + }, + "genre": "BinaryOperator", + "name": "src/slices.rs:5:20: replace < with >= in pad", + "package": "cargo-mutants-testdata-well-tested", + "replacement": ">=", + "span": { + "end": { + "column": 21, + "line": 5 + }, + "start": { + "column": 20, + "line": 5 + } + } + }, { "diff": "--- src/slices.rs\n+++ replace return_mut_slice -> &mut[usize] with Vec::leak(Vec::new())\n@@ -5,20 +5,17 @@\n if a.len() < 3 {\n a.to_mut().push_str(\"___\");\n }\n }\n aa\n }\n \n fn return_mut_slice(a: &mut [usize]) -> &mut [usize] {\n- for x in a.iter_mut() {\n- *x *= 2\n- }\n- a\n+ Vec::leak(Vec::new()) /* ~ changed by cargo-mutants ~ */\n }\n \n #[cfg(test)]\n mod test {\n #[test]\n fn test_pad() {\n assert_eq!(\n super::pad(&mut [\"hello\".into(), \"ok\".into(), \"cat\".into()]),\n", "file": "src/slices.rs", diff --git a/tests/util/snapshots/main__util__list_mutants_well_tested.snap b/tests/util/snapshots/main__util__list_mutants_well_tested.snap index ceae44cf..b28f5ca8 100644 --- a/tests/util/snapshots/main__util__list_mutants_well_tested.snap +++ b/tests/util/snapshots/main__util__list_mutants_well_tested.snap @@ -61,6 +61,7 @@ src/result.rs:10:5: replace error_if_negative -> Result<(), ()> with Ok(()) src/result.rs:10:10: replace < with == in error_if_negative src/result.rs:10:10: replace < with > in error_if_negative src/result.rs:10:10: replace < with <= in error_if_negative +src/result.rs:10:10: replace < with >= in error_if_negative src/result.rs:18:5: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::new() src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) @@ -85,6 +86,7 @@ src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow src/slices.rs:5:20: replace < with == in pad src/slices.rs:5:20: replace < with > in pad src/slices.rs:5:20: replace < with <= in pad +src/slices.rs:5:20: replace < with >= in pad src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(Vec::new()) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![0]) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![1]) diff --git a/tests/util/snapshots/main__util__list_mutants_well_tested_exclude_name_filter.snap b/tests/util/snapshots/main__util__list_mutants_well_tested_exclude_name_filter.snap index 91419b1b..a077b6fe 100644 --- a/tests/util/snapshots/main__util__list_mutants_well_tested_exclude_name_filter.snap +++ b/tests/util/snapshots/main__util__list_mutants_well_tested_exclude_name_filter.snap @@ -61,6 +61,7 @@ src/result.rs:10:5: replace error_if_negative -> Result<(), ()> with Ok(()) src/result.rs:10:10: replace < with == in error_if_negative src/result.rs:10:10: replace < with > in error_if_negative src/result.rs:10:10: replace < with <= in error_if_negative +src/result.rs:10:10: replace < with >= in error_if_negative src/result.rs:18:5: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::new() src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) @@ -73,6 +74,7 @@ src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow src/slices.rs:5:20: replace < with == in pad src/slices.rs:5:20: replace < with > in pad src/slices.rs:5:20: replace < with <= in pad +src/slices.rs:5:20: replace < with >= in pad src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(Vec::new()) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![0]) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![1])