diff --git a/Cargo.lock b/Cargo.lock index a5dc3e8b..13344ec3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,7 +131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" dependencies = [ "memchr", - "regex-automata", + "regex-automata 0.3.8", "serde", ] @@ -873,13 +873,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.5" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata", + "regex-automata 0.4.3", "regex-syntax", ] @@ -888,6 +888,12 @@ name = "regex-automata" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", @@ -896,9 +902,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rustix" diff --git a/Cargo.toml b/Cargo.toml index c76310a2..8535bed8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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" diff --git a/NEWS.md b/NEWS.md index 936d633f..bca34f04 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 diff --git a/book/src/filter_mutants.md b/book/src/filter_mutants.md index 707d6913..93846013 100644 --- a/book/src/filter_mutants.md +++ b/book/src/filter_mutants.md @@ -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: @@ -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: @@ -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 ``` diff --git a/book/src/skip_files.md b/book/src/skip_files.md index 0639f34d..cfe083d4 100644 --- a/book/src/skip_files.md +++ b/book/src/skip_files.md @@ -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. @@ -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 @@ -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. @@ -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 +``` diff --git a/src/main.rs b/src/main.rs index ea6cc216..9092b89c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,7 +104,13 @@ struct Args { error: Vec, /// 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, /// glob for files to exclude; with no glob, all files are included; globs containing @@ -113,7 +119,7 @@ struct Args { exclude: Vec, /// 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, /// glob for files to examine; with no glob, all files are examined; globs containing diff --git a/src/options.rs b/src/options.rs index fb07b56c..2f502998 100644 --- a/src/options.rs +++ b/src/options.rs @@ -62,10 +62,10 @@ pub struct Options { pub exclude_globset: Option, /// Mutants to examine, as a regexp matched against the full name. - pub examine_names: Option, + pub examine_names: RegexSet, /// Mutants to skip, as a regexp matched against the full name. - pub exclude_names: Option, + pub exclude_names: RegexSet, /// Create `mutants.out` within this directory (by default, the source directory). pub output_in_dir: Option, @@ -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(), @@ -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, I: IntoIterator>( glob_set: I, ) -> Result> { diff --git a/src/snapshots/cargo_mutants__visit__test__expected_mutants_for_own_source_tree.snap b/src/snapshots/cargo_mutants__visit__test__expected_mutants_for_own_source_tree.snap index 03c93739..c3b4ede2 100644 --- a/src/snapshots/cargo_mutants__visit__test__expected_mutants_for_own_source_tree.snap +++ b/src/snapshots/cargo_mutants__visit__test__expected_mutants_for_own_source_tree.snap @@ -217,6 +217,8 @@ src/mutate.rs: replace ::serialize -> Result Vec with vec![] src/options.rs: replace join_slices -> Vec with vec![String::new()] src/options.rs: replace join_slices -> Vec 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> with Ok(None) src/options.rs: replace build_glob_set -> Result> with Ok(Some(Default::default())) src/options.rs: replace build_glob_set -> Result> with Err(::anyhow::anyhow!("mutated!")) diff --git a/src/visit.rs b/src/visit.rs index 4f731d8a..680cf31e 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -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); diff --git a/tests/cli/config.rs b/tests/cli/config.rs index e1a3870a..9bf0b1dc 100644 --- a/tests/cli/config.rs +++ b/tests/cli/config.rs @@ -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"); @@ -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,