diff --git a/NEWS.md b/NEWS.md index 863e3ce8..fb4f3ad7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # cargo-mutants changelog +## Unreleased + +- 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, and files. Multiple patterns can be applied. Also supported within `cfg_attr`. + ## 27.0.0 Released 2026-03-07. diff --git a/book/src/attrs.md b/book/src/attrs.md index a85b00b1..ca8bda4e 100644 --- a/book/src/attrs.md +++ b/book/src/attrs.md @@ -49,3 +49,59 @@ mod test { } } ``` + +## Excluding specific mutations with an attribute + +If `#[mutants::skip]` is too broad (it disables _all_ mutations on a function) +you can use `#[mutants::exclude_re("pattern")]` to exclude only mutations +whose name matches a regex, while keeping the rest. + +The regex is matched against the full mutant name (the same string shown by +`cargo mutants --list`), using the same syntax as `--exclude-re` on the command +line. + +For example, to keep all mutations except the "replace with ()" return-value +mutation: + +```rust +#[mutants::exclude_re(r"with \(\)")] +fn do_something(x: i32) -> i32 { + x + 1 +} +``` + +Multiple attributes can be applied to exclude several patterns: + +```rust +#[mutants::exclude_re("with 0")] +#[mutants::exclude_re("with 1")] +fn compute(a: i32, b: i32) -> i32 { + a + b +} +``` + +As with `mutants::skip`, cargo-mutants also looks for `mutants::exclude_re` +within other attributes such as `cfg_attr`, without evaluating the outer +attribute: + +```rust +#[cfg_attr(test, mutants::exclude_re("replace .* -> bool"))] +fn is_valid(&self) -> bool { + // ... + true +} +``` + +### Scope + +`#[mutants::exclude_re]` 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::exclude_re("...")]`) — applies to the entire file. + +Patterns from outer scopes are inherited: if an `impl` block excludes a pattern, +all methods inside also exclude that pattern, in addition to any patterns on the +methods themselves. diff --git a/mutants_attrs/src/lib.rs b/mutants_attrs/src/lib.rs index eabc3b34..0b46649f 100644 --- a/mutants_attrs/src/lib.rs +++ b/mutants_attrs/src/lib.rs @@ -24,3 +24,25 @@ use proc_macro::TokenStream; pub fn skip(_attr: TokenStream, item: TokenStream) -> TokenStream { item } + +/// Exclude specific mutations matching a regex pattern. +/// +/// Unlike [macro@skip], which skips all mutations on a function, this attribute allows +/// you to exclude only mutations whose name matches the given regex, while keeping +/// other mutations active. +/// +/// This can be applied to functions, impl blocks, trait blocks, modules, etc. +/// +/// ``` +/// #[mutants::exclude_re("delete match arm")] +/// pub fn some_function() -> i32 { +/// // ... +/// # 0 +/// } +/// ``` +/// +/// This is a no-op during compilation, but is seen by cargo-mutants as it processes the source. +#[proc_macro_attribute] +pub fn exclude_re(_attr: TokenStream, item: TokenStream) -> TokenStream { + item +} diff --git a/src/visit.rs b/src/visit.rs index 932db260..9e28dfd4 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -14,9 +14,11 @@ use std::collections::VecDeque; use std::sync::Arc; use std::vec; +use anyhow::anyhow; use camino::{Utf8Path, Utf8PathBuf}; use proc_macro2::{Ident, TokenStream}; use quote::{ToTokens, quote}; +use regex::RegexSet; use syn::ext::IdentExt; use syn::spanned::Spanned; use syn::visit::Visit; @@ -142,6 +144,8 @@ pub fn walk_file( .with_context(|| format!("failed to parse {}", source_file.tree_relative_slashes()))?; let mut visitor = DiscoveryVisitor { error_exprs, + error: None, + exclude_re_stack: Vec::new(), external_mods: Vec::new(), mutants: Vec::new(), mod_namespace_stack: Vec::new(), @@ -151,6 +155,9 @@ pub fn walk_file( options, }; visitor.visit_file(&syn_file); + if let Some(err) = visitor.error { + return Err(err); + } Ok((visitor.mutants, visitor.external_mods)) } @@ -284,6 +291,18 @@ struct DiscoveryVisitor<'o> { error_exprs: &'o [Expr], options: &'o Options, + + /// Stack of compiled `RegexSet`s from `#[mutants::exclude_re("...")]` attributes. + /// + /// Each entry corresponds to a scope (file, mod, impl, trait, fn). + /// When collecting a mutant, all entries in the stack are checked. + exclude_re_stack: Vec, + + /// If set, an error occurred during visiting (e.g. invalid regex in an attribute). + /// + /// Since `Visit` trait methods cannot return errors, we store the error here + /// and propagate it after the visitor finishes. + error: Option, } impl DiscoveryVisitor<'_> { @@ -315,6 +334,42 @@ impl DiscoveryVisitor<'_> { ); } + /// Push `#[mutants::exclude_re("...")]` patterns from the given attributes onto the stack. + /// + /// Returns `true` if successful, `false` if an error was stored (invalid regex). + /// On error, the caller should return early without visiting children. + fn push_exclude_re(&mut self, attrs: &[Attribute]) -> bool { + let patterns = attrs_exclude_re_patterns(attrs); + if patterns.is_empty() { + self.exclude_re_stack.push(RegexSet::empty()); + return true; + } + match RegexSet::new(&patterns) { + Ok(re) => { + self.exclude_re_stack.push(re); + true + } + Err(err) => { + self.error.get_or_insert(anyhow!( + "invalid regex in #[mutants::exclude_re]: {err}" + )); + false + } + } + } + + fn pop_exclude_re(&mut self) { + self.exclude_re_stack + .pop() + .expect("exclude_re stack should not be empty"); + } + + /// Check whether a mutant name is excluded by any of the currently-active + /// `#[mutants::exclude_re("...")]` attributes on the stack. + fn excluded_by_attr_re(&self, name: &str) -> bool { + self.exclude_re_stack.iter().any(|re| re.is_match(name)) + } + /// Record that we generated some mutants. fn collect_mutant( &mut self, @@ -332,7 +387,12 @@ impl DiscoveryVisitor<'_> { genre, None, ); - if self.options.allows_mutant(&mutant) { + if self.excluded_by_attr_re(&mutant.name) { + trace!( + name = mutant.name(false), + "skip mutant by exclude_re attribute" + ); + } else if self.options.allows_mutant(&mutant) { self.mutants.push(mutant); } else { trace!(name = mutant.name(false), "skip mutant by options"); @@ -428,7 +488,11 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { trace!("file excluded by attrs"); return; } + if !self.push_exclude_re(&i.attrs) { + return; + } syn::visit::visit_file(self, i); + self.pop_exclude_re(); } /// Visit top-level `fn foo()`. @@ -444,10 +508,14 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { if fn_sig_excluded(&i.sig) || attrs_excluded(&i.attrs) || block_is_empty(&i.block) { return; } + if !self.push_exclude_re(&i.attrs) { + return; + } let function = self.enter_function(&i.sig.ident, &i.sig.output, i.span()); self.collect_fn_mutants(&i.sig, &i.block); syn::visit::visit_item_fn(self, i); self.leave_function(function); + self.pop_exclude_re(); } /// Visit `fn foo()` within an `impl`. @@ -468,10 +536,14 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { { return; } + if !self.push_exclude_re(&i.attrs) { + return; + } let function = self.enter_function(&i.sig.ident, &i.sig.output, i.span()); self.collect_fn_mutants(&i.sig, &i.block); syn::visit::visit_impl_item_fn(self, i); self.leave_function(function); + self.pop_exclude_re(); } /// Visit `fn foo() { ... }` within a trait, i.e. a default implementation of a function. @@ -490,10 +562,14 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { if block_is_empty(block) { return; } + if !self.push_exclude_re(&i.attrs) { + return; + } let function = self.enter_function(&i.sig.ident, &i.sig.output, i.span()); self.collect_fn_mutants(&i.sig, block); syn::visit::visit_trait_item_fn(self, i); self.leave_function(function); + self.pop_exclude_re(); } } @@ -502,10 +578,14 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { if attrs_excluded(&i.attrs) { return; } + if !self.push_exclude_re(&i.attrs) { + return; + } let type_name = i.self_ty.to_pretty_string(); let name = if let Some((_, trait_path, _)) = &i.trait_ { if path_ends_with(trait_path, "Default") { // Can't think of how to generate a viable different default. + self.pop_exclude_re(); return; } format!("", trait = trait_path.to_pretty_string()) @@ -513,6 +593,7 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { type_name }; self.in_namespace(&name, |v| syn::visit::visit_item_impl(v, i)); + self.pop_exclude_re(); } /// Visit `trait Foo { ... }` @@ -522,7 +603,11 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { if attrs_excluded(&i.attrs) { return; } + if !self.push_exclude_re(&i.attrs) { + return; + } self.in_namespace(&name, |v| syn::visit::visit_item_trait(v, i)); + self.pop_exclude_re(); } /// Visit `mod foo { ... }` or `mod foo;`. @@ -533,6 +618,9 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { trace!("mod excluded by attrs"); return; } + if !self.push_exclude_re(&node.attrs) { + return; + } let source_location = Span::from(node.span()); @@ -566,6 +654,7 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { } self.in_namespace(&mod_namespace.name, |v| syn::visit::visit_item_mod(v, node)); assert_eq!(self.mod_namespace_stack.pop(), Some(mod_namespace)); + self.pop_exclude_re(); } /// Visit `a op b` expressions. @@ -744,7 +833,9 @@ impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { struct_name: struct_name.clone(), }), ); - self.mutants.push(mutant); + if !self.excluded_by_attr_re(&mutant.name) { + self.mutants.push(mutant); + } } } } @@ -942,6 +1033,73 @@ fn attr_is_mutants_skip(attr: &Attribute) -> bool { skip } +/// Extract regex patterns from `#[mutants::exclude_re("...")]` attributes. +/// +/// This also handles `#[cfg_attr(test, mutants::exclude_re("..."))]`. +fn attrs_exclude_re_patterns(attrs: &[Attribute]) -> Vec { + attrs + .iter() + .filter_map(attr_mutants_exclude_re_pattern) + .collect() +} + +/// If this attribute is `#[mutants::exclude_re("pattern")]`, return the pattern string. +/// +/// Also matches `#[cfg_attr(..., mutants::exclude_re("pattern"))]`. +fn attr_mutants_exclude_re_pattern(attr: &Attribute) -> Option { + if path_is(attr.path(), &["mutants", "exclude_re"]) { + return extract_string_from_attr(attr); + } + if !path_is(attr.path(), &["cfg_attr"]) { + return None; + } + // For cfg_attr, we need to find mutants::exclude_re("...") in the token list. + // The tokens look like: `test, mutants::exclude_re("pattern")` + // We use syn to parse the inner attribute. + let tokens = match &attr.meta { + syn::Meta::List(list) => &list.tokens, + _ => return None, + }; + // Wrap the inner content after the condition as an attribute and try to parse it. + // We need to find the portion after the first comma that looks like mutants::exclude_re("...") + let token_str = tokens.to_string(); + // Find "mutants :: exclude_re" (with possible spaces around ::) + // by scanning for the pattern in the token representation. + let normalized = token_str.replace(" :: ", "::"); + let marker = "mutants::exclude_re"; + let start = normalized.find(marker)?; + // Extract from the marker onward in the original token string. + // Find the same position in the original string, accounting for space normalization. + let remaining = &normalized[start + marker.len()..]; + // remaining should start with something like `("pattern")` + let remaining = remaining.trim_start(); + if !remaining.starts_with('(') { + return None; + } + // Find matching close paren, accounting for the string content + let inner = &remaining[1..]; // skip '(' + // Parse the content as a string literal + let close_paren = inner.rfind(')')?; + let literal_str = inner[..close_paren].trim(); + // Parse as a Rust string literal + syn::parse_str::(literal_str) + .ok() + .map(|lit| lit.value()) +} + +/// Extract a string literal argument from an attribute like `#[something("value")]`. +fn extract_string_from_attr(attr: &Attribute) -> Option { + let meta = &attr.meta; + if let syn::Meta::List(list) = meta { + let tokens = &list.tokens; + // Parse the tokens as a single string literal + if let Ok(lit) = syn::parse2::(tokens.clone()) { + return Some(lit.value()); + } + } + None +} + /// Finds the first path attribute (`#[path = "..."]`) /// /// # Errors @@ -1653,4 +1811,269 @@ mod test { ] ); } + + #[test] + fn exclude_re_attr_filters_specific_mutants() { + let options = Options::default(); + let mutants = mutate_source_str( + indoc! {r#" + #[mutants::exclude_re("with \\(\\)")] + fn add(a: i32, b: i32) -> i32 { + a + b + } + "#}, + &options, + ) + .unwrap(); + let names: Vec<&str> = mutants.iter().map(|m| m.name.as_str()).collect(); + // The fn replacement "replace add -> i32 with 0" etc should remain, + // but "replace add -> i32 with ()" should be excluded. + // Also binary operator mutations remain. + assert!( + !names.iter().any(|n| n.contains("with ()")), + "should not contain 'with ()' mutant but got: {names:?}" + ); + assert!( + names.iter().any(|n| n.contains("replace + with")), + "should still contain binary op mutant: {names:?}" + ); + } + + #[test] + fn exclude_re_attr_keeps_all_when_no_match() { + let options = Options::default(); + let mutants_with_attr = mutate_source_str( + indoc! {r#" + #[mutants::exclude_re("this_matches_nothing")] + fn add(a: i32, b: i32) -> i32 { + a + b + } + "#}, + &options, + ) + .unwrap(); + let mutants_without_attr = mutate_source_str( + indoc! {" + fn add(a: i32, b: i32) -> i32 { + a + b + } + "}, + &options, + ) + .unwrap(); + assert_eq!(mutants_with_attr.len(), mutants_without_attr.len()); + } + + #[test] + fn exclude_re_attr_invalid_regex_returns_error() { + let options = Options::default(); + let result = mutate_source_str( + indoc! {r#" + #[mutants::exclude_re("(unclosed")] + fn add(a: i32, b: i32) -> i32 { + a + b + } + "#}, + &options, + ); + assert!(result.is_err(), "invalid regex should produce an error"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("invalid regex"), + "error should mention 'invalid regex': {err_msg}" + ); + } + + #[test] + fn exclude_re_attr_on_impl_block_applies_to_methods() { + let options = Options::default(); + let mutants = mutate_source_str( + indoc! {r#" + struct Foo; + #[mutants::exclude_re("replace .* -> bool")] + impl Foo { + fn is_ok(&self) -> bool { + true + } + fn count(&self) -> i32 { + 1 + 2 + } + } + "#}, + &options, + ) + .unwrap(); + let names: Vec<&str> = mutants.iter().map(|m| m.name.as_str()).collect(); + // is_ok fn replacement should be excluded, but count fn and binary ops should remain + assert!( + !names.iter().any(|n| n.contains("is_ok")), + "should not contain is_ok mutant but got: {names:?}" + ); + assert!( + names.iter().any(|n| n.contains("count")), + "should contain count mutant: {names:?}" + ); + } + + #[test] + fn cfg_attr_exclude_re_filters_mutants() { + let options = Options::default(); + let mutants = mutate_source_str( + indoc! {r#" + #[cfg_attr(test, mutants::exclude_re("replace .* with 0"))] + fn add(a: i32, b: i32) -> i32 { + a + b + } + "#}, + &options, + ) + .unwrap(); + let names: Vec<&str> = mutants.iter().map(|m| m.name.as_str()).collect(); + assert!( + !names.iter().any(|n| n.contains("with 0")), + "should not contain 'with 0' mutant but got: {names:?}" + ); + assert!( + names.iter().any(|n| n.contains("replace + with")), + "should still contain binary op mutant: {names:?}" + ); + } + + #[test] + fn exclude_re_attr_on_trait_block_applies_to_default_methods() { + let options = Options::default(); + let mutants = mutate_source_str( + indoc! {r#" + #[mutants::exclude_re("replace .* -> bool")] + trait Check { + fn is_ok(&self) -> bool { + true + } + fn count(&self) -> i32 { + 1 + 2 + } + } + "#}, + &options, + ) + .unwrap(); + let names: Vec<&str> = mutants.iter().map(|m| m.name.as_str()).collect(); + assert!( + !names.iter().any(|n| n.contains("is_ok")), + "should not contain is_ok mutant but got: {names:?}" + ); + assert!( + names.iter().any(|n| n.contains("count")), + "should contain count mutant: {names:?}" + ); + } + + #[test] + fn exclude_re_attr_on_mod_block_applies_to_functions() { + let options = Options::default(); + let mutants = mutate_source_str( + indoc! {r#" + #[mutants::exclude_re("replace .* -> bool")] + mod inner { + pub fn is_ok() -> bool { + true + } + pub fn count() -> i32 { + 1 + 2 + } + } + "#}, + &options, + ) + .unwrap(); + let names: Vec<&str> = mutants.iter().map(|m| m.name.as_str()).collect(); + assert!( + !names.iter().any(|n| n.contains("is_ok")), + "should not contain is_ok mutant but got: {names:?}" + ); + assert!( + names.iter().any(|n| n.contains("count")), + "should contain count mutant: {names:?}" + ); + } + + #[test] + fn exclude_re_attr_inner_file_attribute_applies_to_all_functions() { + let options = Options::default(); + let mutants = mutate_source_str( + indoc! {r#" + #![mutants::exclude_re("replace .* -> bool")] + + fn is_ok() -> bool { + true + } + fn count() -> i32 { + 1 + 2 + } + "#}, + &options, + ) + .unwrap(); + let names: Vec<&str> = mutants.iter().map(|m| m.name.as_str()).collect(); + assert!( + !names.iter().any(|n| n.contains("is_ok")), + "should not contain is_ok mutant but got: {names:?}" + ); + assert!( + names.iter().any(|n| n.contains("count")), + "should contain count mutant: {names:?}" + ); + } + + #[test] + fn exclude_re_attr_inherits_from_outer_scope() { + let options = Options::default(); + let mutants = mutate_source_str( + indoc! {r#" + struct Foo; + #[mutants::exclude_re("replace .* -> bool")] + impl Foo { + #[mutants::exclude_re("replace .* -> i32")] + fn count(&self) -> i32 { + 1 + 2 + } + fn is_ok(&self) -> bool { + true + } + fn add(&self, a: i32, b: i32) -> i32 { + a + b + } + } + "#}, + &options, + ) + .unwrap(); + let names: Vec<&str> = mutants.iter().map(|m| m.name.as_str()).collect(); + // is_ok: excluded by impl-level pattern (-> bool) + assert!( + !names.iter().any(|n| n.contains("is_ok")), + "should not contain is_ok mutant but got: {names:?}" + ); + // count: fn-value excluded by fn-level pattern (-> i32), but also + // impl-level pattern (-> bool) doesn't affect it; binary ops remain + assert!( + !names + .iter() + .any(|n| n.contains("replace count") || n.contains("replace Foo::count")), + "should not contain count fn-value mutant but got: {names:?}" + ); + assert!( + names + .iter() + .any(|n| n.contains("replace + with") && n.contains("count")), + "should contain binary op mutant in count: {names:?}" + ); + // add: fn-value NOT excluded (impl pattern only covers bool), binary ops remain + assert!( + names + .iter() + .any(|n| n.contains("add") && n.contains("-> i32")), + "should still contain add fn-value mutant: {names:?}" + ); + } } diff --git a/testdata/exclude_re_attr/Cargo_test.toml b/testdata/exclude_re_attr/Cargo_test.toml new file mode 100644 index 00000000..ce906318 --- /dev/null +++ b/testdata/exclude_re_attr/Cargo_test.toml @@ -0,0 +1,11 @@ +[package] +name = "cargo-mutants-testdata-exclude-re-attr" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +doctest = false + +[dependencies] +mutants = { path = "../../mutants_attrs" } diff --git a/testdata/exclude_re_attr/src/lib.rs b/testdata/exclude_re_attr/src/lib.rs new file mode 100644 index 00000000..9bdfa547 --- /dev/null +++ b/testdata/exclude_re_attr/src/lib.rs @@ -0,0 +1,155 @@ +//! Test tree for `#[mutants::exclude_re("...")]` attribute. +//! +//! This tests that specific mutations can be excluded by regex while +//! keeping other mutations active on the same function. + +/// This function has an exclude_re that filters out the "replace with ()" mutation +/// but keeps binary operator mutations. +/// Filtered: "replace add_numbers -> i32 with ()" +#[mutants::exclude_re(r"with \(\)")] +pub fn add_numbers(a: i32, b: i32) -> i32 { + a + b +} + +/// This function has no exclude_re, so all mutations should be generated. +pub fn multiply(a: i32, b: i32) -> i32 { + a * b +} + +/// This function uses cfg_attr form of exclude_re, filtering out "with 0" mutations. +/// Filtered: "replace subtract -> i32 with 0" +#[cfg_attr(test, mutants::exclude_re("with 0"))] +pub fn subtract(a: i32, b: i32) -> i32 { + a - b +} + +/// This function has an exclude_re that filters out binary operator mutations. +/// Filtered: "replace + with - in add_one", "replace + with * in add_one" +#[mutants::exclude_re("replace [+] with")] +pub fn add_one(a: i32) -> i32 { + a + 1 +} + +pub struct Calculator; + +/// exclude_re on an impl block applies to all methods inside. +/// Filtered: "replace Calculator::is_positive -> bool with true/false" +#[mutants::exclude_re("replace .* -> bool")] +impl Calculator { + pub fn is_positive(x: i32) -> bool { + x > 0 + } + + pub fn double(x: i32) -> i32 { + x + x + } +} + +/// exclude_re on a trait block applies to all default method implementations. +/// Filtered: "replace Checker::is_valid -> bool with true/false" +#[mutants::exclude_re("replace .* -> bool")] +pub trait Checker { + fn is_valid(&self) -> bool { + true + } + + fn score(&self) -> i32 { + 1 + 2 + } +} + +/// exclude_re on a mod block applies to all functions inside. +/// Filtered: "replace predicates::always_true -> bool with true/false" +#[mutants::exclude_re("replace .* -> bool")] +mod predicates { + pub fn always_true() -> bool { + true + } + + pub fn increment(x: i32) -> i32 { + x + 1 + } +} + +/// exclude_re on an impl block is inherited by methods; methods can add their own. +pub struct Combo; + +#[mutants::exclude_re("replace .* -> bool")] +impl Combo { + /// This method adds its own exclude_re on top of the impl-level one. + /// Filtered by impl: "replace Combo::count -> bool ..." (N/A, returns i32) + /// Filtered by method: "replace Combo::count -> i32 with 0/1/-1" + #[mutants::exclude_re("replace .* -> i32")] + pub fn count(&self) -> i32 { + 1 + 2 + } + + pub fn is_ok(&self) -> bool { + true + } + + /// This method is not excluded by the impl-level pattern (returns i32, not bool). + pub fn add(&self, a: i32, b: i32) -> i32 { + a + b + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_numbers() { + assert_eq!(add_numbers(2, 3), 5); + } + + #[test] + fn test_multiply() { + assert_eq!(multiply(2, 3), 6); + } + + #[test] + fn test_subtract() { + assert_eq!(subtract(5, 3), 2); + } + + #[test] + fn test_add_one() { + assert_eq!(add_one(5), 6); + } + + #[test] + fn test_is_positive() { + assert!(Calculator::is_positive(1)); + assert!(!Calculator::is_positive(-1)); + } + + #[test] + fn test_double() { + assert_eq!(Calculator::double(3), 6); + } + + struct MyChecker; + impl Checker for MyChecker {} + + #[test] + fn test_checker_defaults() { + let c = MyChecker; + assert!(c.is_valid()); + assert_eq!(c.score(), 3); + } + + #[test] + fn test_predicates() { + assert!(predicates::always_true()); + assert_eq!(predicates::increment(5), 6); + } + + #[test] + fn test_combo() { + let c = Combo; + assert_eq!(c.count(), 3); + assert!(c.is_ok()); + assert_eq!(c.add(2, 3), 5); + } +} diff --git a/tests/main.rs b/tests/main.rs index a4ef2a7a..81148cbe 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -3179,6 +3179,16 @@ fn list_mutants_in_cfg_attr_test_skip_json() { .assert_insta("list_mutants_in_cfg_attr_test_skip_json"); } +#[test] +fn list_mutants_in_exclude_re_attr() { + let tmp_src_dir = copy_of_testdata("exclude_re_attr"); + run() + .arg("mutants") + .arg("--list") + .current_dir(tmp_src_dir.path()) + .assert_insta("list_mutants_in_exclude_re_attr"); +} + #[test] fn list_mutants_with_dir_option() { let temp = copy_of_testdata("factorial"); diff --git a/tests/util/snapshots/main__util__list_mutants_in_exclude_re_attr.snap b/tests/util/snapshots/main__util__list_mutants_in_exclude_re_attr.snap new file mode 100644 index 00000000..8e71b398 --- /dev/null +++ b/tests/util/snapshots/main__util__list_mutants_in_exclude_re_attr.snap @@ -0,0 +1,49 @@ +--- +source: tests/util/mod.rs +assertion_line: 33 +expression: "String::from_utf8_lossy(&output.stdout)" + +--- +src/lib.rs:11:5: replace add_numbers -> i32 with 0 +src/lib.rs:11:5: replace add_numbers -> i32 with 1 +src/lib.rs:11:5: replace add_numbers -> i32 with -1 +src/lib.rs:11:7: replace + with - in add_numbers +src/lib.rs:11:7: replace + with * in add_numbers +src/lib.rs:16:5: replace multiply -> i32 with 0 +src/lib.rs:16:5: replace multiply -> i32 with 1 +src/lib.rs:16:5: replace multiply -> i32 with -1 +src/lib.rs:16:7: replace * with + in multiply +src/lib.rs:16:7: replace * with / in multiply +src/lib.rs:23:5: replace subtract -> i32 with 1 +src/lib.rs:23:5: replace subtract -> i32 with -1 +src/lib.rs:23:7: replace - with + in subtract +src/lib.rs:23:7: replace - with / in subtract +src/lib.rs:30:5: replace add_one -> i32 with 0 +src/lib.rs:30:5: replace add_one -> i32 with 1 +src/lib.rs:30:5: replace add_one -> i32 with -1 +src/lib.rs:40:11: replace > with == in Calculator::is_positive +src/lib.rs:40:11: replace > with < in Calculator::is_positive +src/lib.rs:40:11: replace > with >= in Calculator::is_positive +src/lib.rs:44:9: replace Calculator::double -> i32 with 0 +src/lib.rs:44:9: replace Calculator::double -> i32 with 1 +src/lib.rs:44:9: replace Calculator::double -> i32 with -1 +src/lib.rs:44:11: replace + with - in Calculator::double +src/lib.rs:44:11: replace + with * in Calculator::double +src/lib.rs:57:9: replace Checker::score -> i32 with 0 +src/lib.rs:57:9: replace Checker::score -> i32 with 1 +src/lib.rs:57:9: replace Checker::score -> i32 with -1 +src/lib.rs:57:11: replace + with - in Checker::score +src/lib.rs:57:11: replace + with * in Checker::score +src/lib.rs:70:9: replace predicates::increment -> i32 with 0 +src/lib.rs:70:9: replace predicates::increment -> i32 with 1 +src/lib.rs:70:9: replace predicates::increment -> i32 with -1 +src/lib.rs:70:11: replace + with - in predicates::increment +src/lib.rs:70:11: replace + with * in predicates::increment +src/lib.rs:84:11: replace + with - in Combo::count +src/lib.rs:84:11: replace + with * in Combo::count +src/lib.rs:93:9: replace Combo::add -> i32 with 0 +src/lib.rs:93:9: replace Combo::add -> i32 with 1 +src/lib.rs:93:9: replace Combo::add -> i32 with -1 +src/lib.rs:93:11: replace + with - in Combo::add +src/lib.rs:93:11: replace + with * in Combo::add +