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
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- New: mutate `NonZero<T>` into `1`, and also `-1` when `T` is or may be signed.

- Docs: Clarify that `#[mutants::skip]` is honoured at every scope where attributes can be placed, not only on functions: `impl` blocks, `trait` declarations, modules, files (as `#![mutants::skip]`), and expressions that can carry an outer attribute. Only the documentation was misleading; the behaviour is unchanged.
- Fixed: Set the mtime on files copied using reflinks to the scratch directory, so that they're not deleted prematurely by tools that delete old files from `/tmp`.

## 27.0.0
Expand Down
32 changes: 23 additions & 9 deletions book/src/attrs.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# Skipping functions with an attribute
# Skipping mutations with an attribute

To mark functions as skipped, so they are not mutated:
To mark items as skipped, so they are not mutated:

1. Add a Cargo dependency on the [mutants](https://crates.io/crates/mutants)
crate, version "0.0.3" or later. (This must be a regular `dependency` not a
`dev-dependency`, because the annotation will be on non-test code.)

2. Mark functions with `#[mutants::skip]` or other attributes containing
`mutants::skip` (e.g. `#[cfg_attr(test, mutants::skip)]`).
2. Mark items with `#[mutants::skip]`, or with `mutants::skip` nested inside a
`cfg_attr` (e.g. `#[cfg_attr(test, mutants::skip)]`).

The `mutants` crate is tiny and the attribute has no effect on the compiled
code. It only flags the function for cargo-mutants. However, you can avoid the
dependency by using the slightly longer `#[cfg_attr(test, mutants::skip)]` form.
code. It only flags the item for cargo-mutants.

**Note:** Currently, `cargo-mutants` does not (yet) evaluate attributes like
`cfg_attr`, it only looks for the sequence `mutants::skip` in the attribute.
**Note:** `cargo-mutants` does not evaluate the `cfg_attr` condition; the
inner `mutants::skip` is always honoured regardless of whether the condition
would hold during compilation.

You may want to also add a comment explaining why the function is skipped.
You may want to also add a comment explaining why the item is skipped.

For example:

Expand Down Expand Up @@ -49,3 +49,17 @@ mod test {
}
}
```

## Scope

`#[mutants::skip]` can be placed on:

- **Functions** — applies to all mutations within that function.
- **`impl` blocks** — applies to all methods within the block.
- **`trait` blocks** — applies to all default method implementations.
- **`mod` blocks** — applies to all items within the module.
- **Files** (as an inner attribute `#![mutants::skip]`) — applies to the entire file.
- **Expressions** that can syntactically carry an outer attribute, including
`match`, struct literal (`Foo { ... }`), call (`foo(...)`), method-call
(`x.foo(...)`), and unary expressions (`!x`, `-x`) — applies to the
expression and everything nested inside it.
10 changes: 10 additions & 0 deletions src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,16 @@ mod test {

use super::*;

mod skip_attr_cfg_attr;
mod skip_attr_expr_call;
mod skip_attr_expr_match;
mod skip_attr_expr_method_call;
mod skip_attr_expr_struct;
mod skip_attr_expr_unary;
mod skip_attr_file;
mod skip_attr_impl;
mod skip_attr_trait;

#[test]
fn path_ends_with() {
use super::path_ends_with;
Expand Down
73 changes: 73 additions & 0 deletions src/visit/test/skip_attr_cfg_attr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Tests that `#[cfg_attr(<cond>, mutants::skip)]` is honoured at scopes
//! other than top-level `fn` — for example on an `impl` block and on a
//! `mod`. The visitor ignores the cfg condition and always treats the
//! attribute as an instruction to skip, matching the documented behaviour
//! of `#[cfg_attr(test, mutants::skip)]` on functions.

use indoc::indoc;
use test_log::test;

use crate::Options;
use crate::visit::mutate_source_str;

#[test]
fn cfg_attr_mutants_skip_on_impl_block_suppresses_all_methods() {
let mutants = mutate_source_str(
indoc! {r#"
struct S;

#[cfg_attr(test, mutants::skip)]
impl S {
fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
}

fn outside(a: i32, b: i32) -> i32 {
a * b
}
"#},
&Options::default(),
)
.unwrap();
let names: Vec<String> = mutants.iter().map(|m| m.name(false)).collect();

assert!(
!names.iter().any(|n| n.contains("S::add")),
"cfg_attr(mutants::skip) on impl block should suppress its methods: {names:?}"
);
assert!(
names.iter().any(|n| n.contains("outside")),
"sibling function should still produce mutants: {names:?}"
);
}

#[test]
fn cfg_attr_mutants_skip_on_mod_suppresses_inner_items() {
let mutants = mutate_source_str(
indoc! {r#"
#[cfg_attr(test, mutants::skip)]
mod inner {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}

fn outside(a: i32, b: i32) -> i32 {
a * b
}
"#},
&Options::default(),
)
.unwrap();
let names: Vec<String> = mutants.iter().map(|m| m.name(false)).collect();

assert!(
!names.iter().any(|n| n.contains("inner::add")),
"cfg_attr(mutants::skip) on mod should suppress its items: {names:?}"
);
assert!(
names.iter().any(|n| n.contains("outside")),
"sibling function outside the mod should still produce mutants: {names:?}"
);
}
36 changes: 36 additions & 0 deletions src/visit/test/skip_attr_expr_call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Tests that `#[mutants::skip]` placed on a call expression suppresses
//! mutants generated inside that call's arguments, while sibling calls in
//! the same function remain mutated.

use indoc::indoc;
use test_log::test;

use crate::Options;
use crate::visit::mutate_source_str;

#[test]
fn skip_attr_on_call_expression_suppresses_nested_arg_mutants() {
let mutants = mutate_source_str(
indoc! {r#"
fn helper(_x: i32) {}

fn driver(a: i32, b: i32, c: i32, d: i32) {
#[mutants::skip]
helper(a + b);
helper(c - d);
}
"#},
&Options::default(),
)
.unwrap();
let names: Vec<String> = mutants.iter().map(|m| m.name(false)).collect();

assert!(
!names.iter().any(|n| n.contains("replace + with")),
"`+` inside skipped call should not produce mutants: {names:?}"
);
assert!(
names.iter().any(|n| n.contains("replace - with")),
"`-` in the unannotated sibling call should still produce mutants: {names:?}"
);
}
36 changes: 36 additions & 0 deletions src/visit/test/skip_attr_expr_match.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Tests that `#[mutants::skip]` on a match expression suppresses both
//! arm-deletion and guard-replacement mutants for that match.

use indoc::indoc;
use test_log::test;

use crate::Options;
use crate::visit::mutate_source_str;

#[test]
fn skip_attr_on_match_expression_suppresses_arm_and_guard_mutants() {
let mutants = mutate_source_str(
indoc! {r#"
fn pick(x: i32, y: i32) -> &'static str {
#[mutants::skip]
match x {
0 => "zero",
n if n > y => "gt",
_ => "other",
}
}
"#},
&Options::default(),
)
.unwrap();
let names: Vec<String> = mutants.iter().map(|m| m.name(false)).collect();

assert!(
!names.iter().any(|n| n.contains("delete match arm")),
"match arm deletion mutants should be suppressed: {names:?}"
);
assert!(
!names.iter().any(|n| n.contains("replace match guard")),
"match guard mutants should be suppressed: {names:?}"
);
}
40 changes: 40 additions & 0 deletions src/visit/test/skip_attr_expr_method_call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Tests that `#[mutants::skip]` placed on a method-call expression
//! suppresses mutants generated inside the receiver and arguments, while
//! sibling method calls in the same function remain mutated.

use indoc::indoc;
use test_log::test;

use crate::Options;
use crate::visit::mutate_source_str;

#[test]
fn skip_attr_on_method_call_expression_suppresses_nested_mutants() {
let mutants = mutate_source_str(
indoc! {r#"
struct S;

impl S {
fn frob(&self, _x: i32) {}
}

fn driver(s: &S, a: i32, b: i32, c: i32, d: i32) {
#[mutants::skip]
s.frob(a + b);
s.frob(c - d);
}
"#},
&Options::default(),
)
.unwrap();
let names: Vec<String> = mutants.iter().map(|m| m.name(false)).collect();

assert!(
!names.iter().any(|n| n.contains("replace + with")),
"`+` inside skipped method call should not produce mutants: {names:?}"
);
assert!(
names.iter().any(|n| n.contains("replace - with")),
"`-` in the unannotated sibling method call should still produce mutants: {names:?}"
);
}
38 changes: 38 additions & 0 deletions src/visit/test/skip_attr_expr_struct.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! Tests that `#[mutants::skip]` on a struct literal expression suppresses
//! the field-deletion mutants generated for that literal.

use indoc::indoc;
use test_log::test;

use crate::Options;
use crate::visit::mutate_source_str;

#[test]
fn skip_attr_on_struct_literal_expression_suppresses_field_deletions() {
let mutants = mutate_source_str(
indoc! {r#"
#[derive(Default)]
struct Settings {
enabled: bool,
count: i32,
}

fn make() -> Settings {
#[mutants::skip]
Settings {
enabled: true,
count: 1,
..Default::default()
}
}
"#},
&Options::default(),
)
.unwrap();
let names: Vec<String> = mutants.iter().map(|m| m.name(false)).collect();

assert!(
!names.iter().any(|n| n.contains("delete field")),
"delete field mutants should be suppressed for skipped struct literal: {names:?}"
);
}
29 changes: 29 additions & 0 deletions src/visit/test/skip_attr_expr_unary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! Tests that `#[mutants::skip]` on a unary operator expression suppresses
//! the unary mutant, while a sibling unary in the same function remains.

use indoc::indoc;
use test_log::test;

use crate::Options;
use crate::visit::mutate_source_str;

#[test]
fn skip_attr_on_unary_expression_suppresses_only_that_unary_mutant() {
let mutants = mutate_source_str(
indoc! {r#"
fn invert(b: bool) -> bool {
let _ = #[mutants::skip] !b;
!b
}
"#},
&Options::default(),
)
.unwrap();
let names: Vec<String> = mutants.iter().map(|m| m.name(false)).collect();

let delete_bang_count = names.iter().filter(|n| n.contains("delete !")).count();
assert_eq!(
delete_bang_count, 1,
"exactly one `delete !` mutant should remain (only the unannotated one): {names:?}"
);
}
31 changes: 31 additions & 0 deletions src/visit/test/skip_attr_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! Tests that `#![mutants::skip]` as a file-level inner attribute
//! suppresses every mutant in the file.

use indoc::indoc;
use test_log::test;

use crate::Options;
use crate::visit::mutate_source_str;

#[test]
fn skip_attr_as_file_inner_attribute_suppresses_all_mutants() {
let mutants = mutate_source_str(
indoc! {r#"
#![mutants::skip]

fn add(a: i32, b: i32) -> i32 {
a + b
}

fn sub(a: i32, b: i32) -> i32 {
a - b
}
"#},
&Options::default(),
)
.unwrap();
assert!(
mutants.is_empty(),
"file inner #![mutants::skip] should suppress all mutants, got: {mutants:?}"
);
}
Loading