Skip to content

Commit 3d00bf9

Browse files
Copilotsourcefrog
andauthored
Skip functions with #[anything::test] (#559)
Fixes functions annotated with async testing framework attributes like `#[tokio::test]`, `#[sqlx::test]`, and similar being incorrectly mutated. ## Problem Previously, cargo-mutants only skipped functions with the exact `#[test]` attribute. This meant that test functions using async testing frameworks were being mutated, leading to false positives. For example: ```rust #[test] fn test() { println!("Hello") // ✅ Correctly skipped } #[tokio::test] async fn tokio_test() { println!("Hello") // ❌ Was being mutated (incorrect) } ``` ## Solution Generalized the test attribute detection to match any attribute whose path ends with `test`. This is consistent with the project's goal of "doing something reasonable on most Rust trees without needing any setup" since Tokio, sqlx, and similar testing frameworks are widely used in the Rust ecosystem. The implementation now uses the existing `path_ends_with()` helper function to check if an attribute path ends with `test`, which matches: - `#[test]` (standard test attribute) - `#[tokio::test]` (Tokio async tests) - `#[sqlx::test]` (sqlx database tests) - `#[any_framework::test]` (any testing framework following this convention) ## Changes - Modified `attr_is_test()` in `src/visit.rs` to use `path_ends_with()` instead of exact identifier matching - Added 5 comprehensive tests verifying the new behavior - Updated documentation in `NEWS.md` and `book/src/mutants.md` - All existing tests continue to pass This change is backward compatible and requires no configuration changes. Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sourcefrog <346355+sourcefrog@users.noreply.github.com> Co-authored-by: Martin Pool <mbp@sourcefrog.net>
1 parent 81357d8 commit 3d00bf9

File tree

3 files changed

+108
-3
lines changed

3 files changed

+108
-3
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
- New: `start_time` and `end_time` fields in `outcomes.json`.
1212

13+
- Changed: Functions with attributes whose path ends with `test` are now skipped, not just those with the plain `#[test]` attribute. This means functions with `#[tokio::test]`, `#[sqlx::test]`, and similar testing framework attributes are automatically excluded from mutation testing.
14+
1315
- Changed: The bitwise assignment operators `&=` and `|=` are no longer mutated to `^=`. In code that accumulates bits into a bitmap starting from zero (e.g., `bitmap |= new_bits`), `|=` and `^=` produce the same result, making such mutations uninformative.
1416

1517
## 25.3.1 2025-08-10

book/src/mutants.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ that is likely to compile but have different behavior.
66

77
Mutants each have a "genre", each of which is described below.
88

9+
## Functions that are excluded from mutation
10+
11+
Some functions are automatically excluded from mutation:
12+
13+
- Functions marked with `#[cfg(test)]` or in files marked with `#![cfg(test)]`
14+
- Test functions: functions with attributes whose path ends with `test`, including `#[test]`, `#[tokio::test]`, `#[sqlx::test]`, and similar testing framework attributes
15+
- Functions marked with `#[mutants::skip]`
16+
- `unsafe` functions
17+
18+
You can also explicitly [skip functions](skip.md) or [filter which functions are mutated](filter_mutants.md).
19+
920
## Replace function body with value
1021

1122
The `FnValue` genre of mutants replaces a function's body with a value that is guessed to be of the right type.

src/visit.rs

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ fn fn_sig_excluded(sig: &syn::Signature) -> bool {
793793

794794
/// True if any of the attrs indicate that we should skip this node and everything inside it.
795795
///
796-
/// This checks for `#[cfg(test)]`, `#[test]`, and `#[mutants::skip]`.
796+
/// This checks for `#[cfg(test)]`, attributes whose path ends with `test` (like `#[test]` or `#[tokio::test]`), and `#[mutants::skip]`.
797797
fn attrs_excluded(attrs: &[Attribute]) -> bool {
798798
attrs
799799
.iter()
@@ -828,9 +828,11 @@ fn attr_is_cfg_test(attr: &Attribute) -> bool {
828828
contains_test
829829
}
830830

831-
/// True if the attribute is `#[test]`.
831+
/// True if the attribute path ends with `test`.
832+
///
833+
/// This matches `#[test]`, `#[tokio::test]`, `#[sqlx::test]`, etc.
832834
fn attr_is_test(attr: &Attribute) -> bool {
833-
attr.path().is_ident("test")
835+
path_ends_with(attr.path(), "test")
834836
}
835837

836838
fn path_is(path: &syn::Path, idents: &[&str]) -> bool {
@@ -1081,6 +1083,96 @@ mod test {
10811083
assert_eq!(mutants, []);
10821084
}
10831085

1086+
#[test]
1087+
fn skip_functions_with_test_attribute() {
1088+
let mutants = mutate_source_str(
1089+
indoc! {"
1090+
#[test]
1091+
fn test_function() {
1092+
println!(\"test\");
1093+
}
1094+
"},
1095+
&Options::default(),
1096+
)
1097+
.unwrap();
1098+
assert_eq!(mutants, []);
1099+
}
1100+
1101+
#[test]
1102+
fn skip_functions_with_tokio_test_attribute() {
1103+
let mutants = mutate_source_str(
1104+
indoc! {"
1105+
#[tokio::test]
1106+
async fn tokio_test() {
1107+
println!(\"test\");
1108+
}
1109+
"},
1110+
&Options::default(),
1111+
)
1112+
.unwrap();
1113+
assert_eq!(mutants, []);
1114+
}
1115+
1116+
#[test]
1117+
fn skip_functions_with_sqlx_test_attribute() {
1118+
let mutants = mutate_source_str(
1119+
indoc! {"
1120+
#[sqlx::test]
1121+
async fn sqlx_test() {
1122+
println!(\"test\");
1123+
}
1124+
"},
1125+
&Options::default(),
1126+
)
1127+
.unwrap();
1128+
assert_eq!(mutants, []);
1129+
}
1130+
1131+
#[test]
1132+
fn skip_functions_with_arbitrary_test_attribute() {
1133+
let mutants = mutate_source_str(
1134+
indoc! {"
1135+
#[my_framework::test]
1136+
fn custom_test() {
1137+
println!(\"test\");
1138+
}
1139+
"},
1140+
&Options::default(),
1141+
)
1142+
.unwrap();
1143+
assert_eq!(mutants, []);
1144+
}
1145+
1146+
#[test]
1147+
fn do_not_skip_functions_with_non_test_attributes() {
1148+
let mutants = mutate_source_str(
1149+
indoc! {"
1150+
#[derive(Debug)]
1151+
pub fn testing_something() -> i32 {
1152+
42
1153+
}
1154+
1155+
#[some_crate::test]
1156+
fn some_test() {
1157+
println!(\"test\");
1158+
}
1159+
1160+
#[some_attr]
1161+
pub fn regular_function() -> i32 {
1162+
100
1163+
}
1164+
"},
1165+
&Options::default(),
1166+
)
1167+
.unwrap();
1168+
// Should have mutants for testing_something and regular_function, but not for some_test
1169+
let mutant_names = mutants.iter().map(|m| m.name(false)).collect_vec();
1170+
assert_eq!(mutant_names.len(), 6); // 3 mutants each for the two non-test functions
1171+
assert!(mutant_names.iter().any(|n| n.contains("testing_something")));
1172+
assert!(mutant_names.iter().any(|n| n.contains("regular_function")));
1173+
assert!(!mutant_names.iter().any(|n| n.contains("some_test")));
1174+
}
1175+
10841176
/// Skip mutating arguments to a particular named function.
10851177
#[test]
10861178
fn skip_named_fn() {

0 commit comments

Comments
 (0)