Skip to content

Commit

Permalink
Filter options override config file (#166)
Browse files Browse the repository at this point in the history
Add aliases for easily-misremembered options
  • Loading branch information
sourcefrog authored Nov 25, 2023
2 parents 61ed6ed + 0be82fe commit fff4987
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 58 deletions.
18 changes: 12 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ nix = "0.27"
patch = "0.7"
path-slash = "0.2"
quote = "1.0"
regex = "1.10"
serde_json = "1"
similar = "2.0"
subprocess = "0.2.8"
Expand All @@ -60,9 +61,6 @@ tracing-appender = "0.2"
tracing-subscriber = "0.3"
whoami = "1.2"

[dependencies.regex]
version = "1.5"

[dependencies.cp_r]
version = "0.5.1"

Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

## Unreleased

- Changed: If `--file` or `--exclude` are set on the command line, then they replace the corresponding config file options. Similarly, if `--re` is given then the `examine_re` config key is ignored, and if `--exclude-re` is given then `exclude_regex` is ignored. (Previously the values were combined.) This makes it easier to use the command line to test files or mutants that are normally not tested.

- Improved: Run `cargo metadata` with `--no-deps`, so that it doesn't download and compute dependency information, which can save time in some situations.

- Added: Alternative aliases for command line options, so you don't need to remember if it's "regex" or "re": `--regex`, `--examine-re`, `--examine-regex` (all for names to include) and `--exclude-regex`.

## 23.11.1

- New `--in-diff FILE` option tests only mutants that are in the diff from the
Expand Down
23 changes: 14 additions & 9 deletions book/src/filter_mutants.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Filtering functions and mutants

You can also filter mutants by name, using the `--re` and `--exclude-re` command line
You can filter mutants by name, using the `--re` and `--exclude-re` command line
options and the corresponding `examine_re` and `exclude_re` config file options.

These options are useful if you want to run cargo-mutants just once, focusing on a subset of functions or mutants.

These options filter mutants by the full name of the mutant, which includes the
function name, file name, and a description of the change, as shown in list.
function name, file name, and a description of the change, as shown in the output of `cargo mutants --list`.

For example, one mutant name might be:

Expand All @@ -22,17 +22,13 @@ Within this name, your regex can match any substring, including for example:
- The function name, `serialize`
- The mutated return value, `with Ok(Defualt::default())`, or any part of it.

Mutants can also be filtered by name in the `.cargo/mutants.toml` file, for example:

Regexes from the config file are appended to regexes from the command line.

The regex matches a substring, but can be anchored with `^` and `$` to require that
it match the whole name.

The regex syntax is defined by the [`regex`](https://docs.rs/regex/latest/regex/)
crate.

These filters are applied after filtering by filename, and `--re` is applied before
These filters are applied after [filtering by filename](skip_files.md), and `--re` is applied before
`--exclude-re`.

Examples:
Expand All @@ -43,9 +39,18 @@ Examples:
- `-F 'impl Serialize' -F 'impl Deserialize'` -- test implementations of these
two traits.

Or in `.cargo/mutants.toml`:
## Configuring filters by name

Mutants can be filtered by name in the `.cargo/mutants.toml` file. The `exclude_re` and `examine_re` keys are each a list of strings.

This can be helpful
if you want to systematically skip testing implementations of certain traits, or functions
with certain names.

From cargo-mutants 23.11.2 onwards, if the command line options are given then the corresponding config file option is ignored.

For example:

```toml
exclude_re = ["impl Debug"] # same as -E
examine_re = ["impl Serialize", "impl Deserialize"] # same as -F, test *only* matches
```
36 changes: 20 additions & 16 deletions book/src/skip_files.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,6 @@ Two options (each with short and long names) control which files are mutated:

These options may be repeated.

Files may also be filtered with the `exclude_globs` and `examine_globs` options in `.cargo/mutants.toml`, for example:

```toml
exclude_globs = ["src/main.rs", "src/cache/*.rs"] # same as -e
examine_globs = ["src/important/*.rs"] # same as -f, test *only* these files
```

Globs from the config file are appended to globs from the command line.

If any `-f` options are given, only source files that match are
considered; otherwise all files are considered. This list is then further
reduced by exclusions.
Expand All @@ -25,9 +16,7 @@ If the glob contains `/` (or on Windows, `\`), then it matches against the path
tree. For example, `src/*/*.rs` will exclude all files in subdirectories of `src`.

If the glob does not contain a path separator, it matches against filenames
in any directory.

`/` matches the path separator on both Unix and Windows.
in any directory. `/` matches the path separator on both Unix and Windows.

Note that the glob must contain `.rs` (or a matching wildcard) to match
source files with that suffix. For example, `-f network` will match
Expand All @@ -40,10 +29,6 @@ test mutants in other files referenced by `mod` statements in `main.rs`.

Since Rust does not currently allow attributes such as `#[mutants::skip]` on `mod` statements or at module scope filtering by filename is the only way to skip an entire module.

Exclusions in the config file may be particularly useful when there are modules that are
inherently hard to automatically test, and the project has made a decision to accept lower
test coverage for them.

The results of filters can be previewed with the `--list-files` and `--list`
options.

Expand All @@ -55,3 +40,22 @@ Examples:
- `cargo mutants -e console.rs` -- test mutants in any file except `console.rs`.

- `cargo mutants -f src/db/*.rs` -- test mutants in any file in this directory.

## Configuring filters by filename

Files may also be filtered with the `exclude_globs` and `examine_globs` options in `.cargo/mutants.toml`.

Exclusions in the config file may be particularly useful when there are modules that are
inherently hard to automatically test, and the project has made a decision to accept lower
test coverage for them.

From cargo-mutants 23.11.2 onwards, if the command line options are given then the corresponding config file option is ignored.
This allows you to use the config file to test files that are normally expected to pass, and then
to use the command line to test files that are not yet passing.

For example:

```toml
exclude_globs = ["src/main.rs", "src/cache/*.rs"] # like -e
examine_globs = ["src/important/*.rs"] # like -f: test *only* these files
```
10 changes: 8 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,13 @@ struct Args {
error: Vec<String>,

/// regex for mutations to examine, matched against the names shown by `--list`.
#[arg(long = "re", short = 'F')]
#[arg(
long = "re",
short = 'F',
alias = "regex",
alias = "examine-regex",
alias = "examine-re"
)]
examine_re: Vec<String>,

/// glob for files to exclude; with no glob, all files are included; globs containing
Expand All @@ -113,7 +119,7 @@ struct Args {
exclude: Vec<String>,

/// regex for mutations to exclude, matched against the names shown by `--list`.
#[arg(long, short = 'E')]
#[arg(long, short = 'E', alias = "exclude-regex")]
exclude_re: Vec<String>,

/// glob for files to examine; with no glob, all files are examined; globs containing
Expand Down
31 changes: 17 additions & 14 deletions src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ pub struct Options {
pub exclude_globset: Option<GlobSet>,

/// Mutants to examine, as a regexp matched against the full name.
pub examine_names: Option<RegexSet>,
pub examine_names: RegexSet,

/// Mutants to skip, as a regexp matched against the full name.
pub exclude_names: Option<RegexSet>,
pub exclude_names: RegexSet,

/// Create `mutants.out` within this directory (by default, the source directory).
pub output_in_dir: Option<Utf8PathBuf>,
Expand Down Expand Up @@ -114,18 +114,12 @@ impl Options {
),
check_only: args.check,
error_values: join_slices(&args.error, &config.error_values),
examine_names: Some(
RegexSet::new(args.examine_re.iter().chain(config.examine_re.iter()))
.context("Compiling examine_re regex")?,
),
examine_globset: build_glob_set(args.file.iter().chain(config.examine_globs.iter()))?,
exclude_names: Some(
RegexSet::new(args.exclude_re.iter().chain(config.exclude_re.iter()))
.context("Compiling exclude_re regex")?,
),
exclude_globset: build_glob_set(
args.exclude.iter().chain(config.exclude_globs.iter()),
)?,
examine_names: RegexSet::new(or_slices(&args.examine_re, &config.examine_re))
.context("Failed to compile examine_re regex")?,
exclude_names: RegexSet::new(or_slices(&args.exclude_re, &config.exclude_re))
.context("Failed to compile exclude_re regex")?,
examine_globset: build_glob_set(or_slices(&args.file, &config.examine_globs))?,
exclude_globset: build_glob_set(or_slices(&args.exclude, &config.exclude_globs))?,
jobs: args.jobs,
leak_dirs: args.leak_dirs,
output_in_dir: args.output.clone(),
Expand All @@ -152,6 +146,15 @@ impl Options {
}
}

/// If the first slices is non-empty, return that, otherwise the second.
fn or_slices<'a: 'c, 'b: 'c, 'c, T>(a: &'a [T], b: &'b [T]) -> &'c [T] {
if a.is_empty() {
b
} else {
a
}
}

fn build_glob_set<S: AsRef<str>, I: IntoIterator<Item = S>>(
glob_set: I,
) -> Result<Option<GlobSet>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ src/mutate.rs: replace <impl Serialize for Mutant>::serialize -> Result<S::Ok, S
src/options.rs: replace join_slices -> Vec<String> with vec![]
src/options.rs: replace join_slices -> Vec<String> with vec![String::new()]
src/options.rs: replace join_slices -> Vec<String> with vec!["xyzzy".into()]
src/options.rs: replace or_slices -> &'c[T] with Vec::leak(Vec::new())
src/options.rs: replace or_slices -> &'c[T] with Vec::leak(vec![Default::default()])
src/options.rs: replace build_glob_set -> Result<Option<GlobSet>> with Ok(None)
src/options.rs: replace build_glob_set -> Result<Option<GlobSet>> with Ok(Some(Default::default()))
src/options.rs: replace build_glob_set -> Result<Option<GlobSet>> with Err(::anyhow::anyhow!("mutated!"))
Expand Down
12 changes: 4 additions & 8 deletions src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,11 @@ pub fn walk_tree(
continue;
}
}
if let Some(examine_names) = &options.examine_names {
if !examine_names.is_empty() {
file_mutants.retain(|m| examine_names.is_match(&m.to_string()));
}
if !options.examine_names.is_empty() {
file_mutants.retain(|m| options.examine_names.is_match(&m.to_string()));
}
if let Some(exclude_names) = &options.exclude_names {
if !exclude_names.is_empty() {
file_mutants.retain(|m| !exclude_names.is_match(&m.to_string()));
}
if !options.exclude_names.is_empty() {
file_mutants.retain(|m| !options.exclude_names.is_match(&m.to_string()));
}
mutants.append(&mut file_mutants);
files.push(source_file);
Expand Down
79 changes: 79 additions & 0 deletions tests/cli/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,57 @@ fn list_with_config_file_inclusion() {
.stdout(predicates::str::contains("simple_fns.rs").not());
}

#[test]
fn file_argument_overrides_config_examine_globs_key() {
let testdata = copy_of_testdata("well_tested");
// This config key has no effect because the command line argument
// takes precedence.
write_config_file(
&testdata,
r#"examine_globs = ["src/*_mod.rs"]
"#,
);
run()
.args(["mutants", "--list-files", "-d"])
.arg(testdata.path())
.args(["--file", "src/simple_fns.rs"])
.assert()
.success()
.stdout(predicates::str::diff(indoc! { "\
src/simple_fns.rs
" }));
}

#[test]
fn exclude_file_argument_overrides_config() {
let testdata = copy_of_testdata("well_tested");
// This config key has no effect because the command line argument
// takes precedence.
write_config_file(
&testdata,
indoc! { r#"
examine_globs = ["src/*_mod.rs"]
exclude_globs = ["src/inside_mod.rs"]
"#},
);
run()
.args(["mutants", "--list-files", "-d"])
.arg(testdata.path())
.args(["--file", "src/*.rs"])
.args(["--exclude", "src/*_mod.rs"])
.args(["--exclude", "src/s*.rs"])
.args(["--exclude", "src/n*.rs"])
.assert()
.success()
.stdout(predicates::str::diff(indoc! { "\
src/lib.rs
src/arc.rs
src/empty_fns.rs
src/methods.rs
src/result.rs
" }));
}

#[test]
fn list_with_config_file_regexps() {
let testdata = copy_of_testdata("well_tested");
Expand All @@ -121,6 +172,34 @@ fn list_with_config_file_regexps() {
));
}

#[test]
fn exclude_re_overrides_config() {
let testdata = copy_of_testdata("well_tested");
write_config_file(
&testdata,
r#"
exclude_re = [".*"] # would exclude everything
"#,
);
run()
.args(["mutants", "--list", "-d"])
.arg(testdata.path())
.assert()
.success()
.stdout(predicates::str::is_empty());
// Also tests that the alias --exclude-regex is accepted
run()
.args(["mutants", "--list", "-d"])
.arg(testdata.path())
.args(["--exclude-regex", " -> "])
.args(["-f", "src/simple_fns.rs"])
.assert()
.success()
.stdout(indoc! {"
src/simple_fns.rs:7: replace returns_unit with ()
"});
}

#[test]
fn tree_fails_without_needed_feature() {
// The point of this tree is to check that Cargo features can be turned on,
Expand Down

0 comments on commit fff4987

Please sign in to comment.