Skip to content
Open
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
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- New: The configuration can also be read from `mutants.toml`, `.mutants.toml`, or `.config/mutants.toml` in the root of the source tree, or from a `[workspace.metadata.mutants]` or `[package.metadata.mutants]` table in the root `Cargo.toml`. These are searched in that order after `.cargo/mutants.toml`, and only the first one that exists is read ([#645](https://github.com/sourcefrog/cargo-mutants/issues/645)).

- 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, files, and on expressions that can carry an attribute (such as `match`, struct literals, call expressions, method calls, and unary expressions). Multiple patterns can be applied. Also supported within `cfg_attr`. Requires the [mutants](https://crates.io/crates/mutants) crate version `0.0.5` or later.

- Fixed: `#[mutants::skip]` (and `#[cfg_attr(..., mutants::skip)]`) is now honoured when placed on `const` and `static` items, including associated constants in `impl` and `trait` blocks. Previously the attribute was silently ignored on these items and operator mutants inside the initializer expression were still generated ([#508](https://github.com/sourcefrog/cargo-mutants/issues/508)).
Expand Down
29 changes: 25 additions & 4 deletions book/src/config-file.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
# Config file

Many options for cargo-mutants can be set in a config file. By default, the config file is read from
Many options for cargo-mutants can be set in a config file. By default, the config is read from
`.cargo/mutants.toml` in the source tree root.

It's recommended that the config file be checked in to the source tree with values that will
It's recommended that the config be checked in to the source tree with values that will
allow developers to run `cargo mutants` with no other options.

`--no-config` can be used to disable reading the configuration file.
`--no-config` can be used to disable reading the configuration from the source tree.

`--config FILE` can be used to read configuration from a custom file instead of the default location.
`--config FILE` can be used to read configuration from a custom file instead of the default locations.
This is useful for having different configurations for different scenarios (e.g., CI/CD, development,
specific testing requirements).

## Where the config is read from

The config may be stored in any one of these locations, relative to the root of the source tree.
They are searched in this order and only the first one that exists is read: configuration from
several locations is never merged.

1. `.cargo/mutants.toml`
2. `mutants.toml`
3. `.mutants.toml`
4. `.config/mutants.toml`
5. The `[workspace.metadata.mutants]` table in `Cargo.toml`
6. The `[package.metadata.mutants]` table in `Cargo.toml`

The metadata tables hold the same keys as the config file, for example:

```toml
[workspace.metadata.mutants]
exclude_globs = ["src/generated/**/*.rs"]
timeout_multiplier = 2.0
```

For a full list of keys, see <https://github.com/sourcefrog/cargo-mutants/blob/main/src/config.rs>.

An example config file with detailed comments can be found at
Expand Down
77 changes: 64 additions & 13 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
// Copyright 2022-2026 Martin Pool.

//! `.cargo/mutants.toml` configuration file.
//! `mutants.toml` configuration file.
//!
//! The config file is read after parsing command line arguments,
//! The config is read after parsing command line arguments,
//! and after finding the source tree, because these together
//! determine its location.
//!
//! The config file is then merged in to the [`Options`].
//! Within the tree, the config is read from the first of
//! [`CONFIG_FILE_NAMES`] that exists, or otherwise from a
//! `[workspace.metadata.mutants]` or `[package.metadata.mutants]`
//! table in the root `Cargo.toml`.
//!
//! The config is then merged in to the [`Options`].

use std::default::Default;
use std::fs::read_to_string;
Expand All @@ -30,7 +35,7 @@ use crate::options::Common;
#[schemars(extend("$id" = "https://json.schemastore.org/cargo-mutants-config.json"))]
#[schemars(title = "cargo-mutants configuration")]
#[schemars(
description = "cargo-mutants configuration, read by default from `.cargo/mutants.toml`. See <https://mutants.rs/>."
description = "cargo-mutants configuration, read by default from `.cargo/mutants.toml` or another standard location in the source tree. See <https://mutants.rs/>."
)]
pub struct Config {
/// Pass extra args to every cargo invocation.
Expand Down Expand Up @@ -93,6 +98,22 @@ pub struct Config {
pub common: Common,
}

/// Paths, relative to the root of the source tree, from which the config is read,
/// in order of precedence: the first one that exists is used.
const CONFIG_FILE_NAMES: &[&str] = &[
".cargo/mutants.toml",
"mutants.toml",
".mutants.toml",
".config/mutants.toml",
];

/// Tables in the root `Cargo.toml` from which the config is read if none of
/// [`CONFIG_FILE_NAMES`] exists, in order of precedence.
const MANIFEST_METADATA_TABLES: &[&[&str]] = &[
&["workspace", "metadata", "mutants"],
&["package", "metadata", "mutants"],
];

impl Config {
pub fn read_file(path: &Path) -> Result<Config> {
debug!(?path, "Read config");
Expand All @@ -101,17 +122,47 @@ impl Config {
Config::from_str(&toml).with_context(|| format!("parse toml from {}", path.display()))
}

/// Read the config from a tree's `.cargo/mutants.toml`, and return a default (empty)
/// Config is the file does not exist.
/// Read the config from the first of the standard locations in a tree that
/// exists, and return a default (empty) Config if there is none.
pub fn read_tree_config(workspace_dir: &Utf8Path) -> Result<Config> {
let path = workspace_dir.join(".cargo").join("mutants.toml");
if path.exists() {
debug!(?path, "Found config in source tree");
Config::read_file(path.as_ref())
} else {
debug!("No config found in workspace");
Ok(Config::default())
for name in CONFIG_FILE_NAMES {
let path = workspace_dir.join(name);
if path.is_file() {
debug!(?path, "Found config in source tree");
return Config::read_file(path.as_ref());
}
}
if let Some(config) = Config::read_manifest_metadata(workspace_dir)? {
return Ok(config);
}
debug!("No config found in workspace");
Ok(Config::default())
}

/// Read the config from a metadata table in the tree's root `Cargo.toml`,
/// if there is one.
fn read_manifest_metadata(workspace_dir: &Utf8Path) -> Result<Option<Config>> {
let path = workspace_dir.join("Cargo.toml");
if !path.is_file() {
return Ok(None);
}
let toml = read_to_string(&path).with_context(|| format!("read manifest {path}"))?;
let manifest: toml::Table =
toml::de::from_str(&toml).with_context(|| format!("parse toml from {path}"))?;
for keys in MANIFEST_METADATA_TABLES {
let mut value = manifest.get(keys[0]);
for key in &keys[1..] {
value = value.and_then(|value| value.get(key));
}
if let Some(value) = value {
let table = keys.join(".");
debug!(?path, table, "Found config in manifest");
return Config::deserialize(value.clone())
.with_context(|| format!("parse `[{table}]` from {path}"))
.map(Some);
}
}
Ok(None)
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ pub struct Args {
profile: Option<String>,

// Config ============================================================
/// Read configuration from this file instead of .cargo/mutants.toml.
/// Read configuration from this file instead of the config in the source tree.
#[arg(
long,
help_heading = "Config",
Expand All @@ -168,7 +168,7 @@ pub struct Args {
)]
config: Option<Utf8PathBuf>,

/// Don't read .cargo/mutants.toml.
/// Don't read any configuration from the source tree.
#[arg(long, help_heading = "Config", conflicts_with = "config")]
no_config: bool,

Expand Down
2 changes: 1 addition & 1 deletion src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! The [`Options`] structure is built by combining, in priority order:
//!
//! 1. Command line options
//! 2. Config options (read from `.cargo/mutants.toml`)
//! 2. Config options (read from `.cargo/mutants.toml` or another location in the tree)
//! 3. Built-in defaults

use std::env;
Expand Down
191 changes: 191 additions & 0 deletions tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3462,6 +3462,197 @@ fn invalid_field_rejected() {
);
}

/// Write a config into any of the locations that are searched within the tree.
fn write_config_to(tempdir: &TempDir, relative_path: &str, config: &str) {
let path = tempdir.path().join(relative_path);
create_dir_all(path.parent().unwrap()).unwrap();
write(path, config.as_bytes()).unwrap();
}

/// Append a table to the tree's root `Cargo.toml`.
fn append_to_manifest(tempdir: &TempDir, toml: &str) {
let path = tempdir.path().join("Cargo.toml");
let mut manifest = read_to_string(&path).unwrap();
manifest.push_str(toml);
write(path, manifest.as_bytes()).unwrap();
}

/// A config that makes `--list-files` list only the `_mod.rs` files.
const CONFIG_EXAMINING_MOD_FILES: &str = "examine_globs = [\"src/*_mod.rs\"]\n";

/// A config that makes `--list-files` list only `src/simple_fns.rs`.
const CONFIG_EXAMINING_SIMPLE_FNS: &str = "examine_globs = [\"src/simple_fns.rs\"]\n";

const MOD_FILES_LISTED: &str = "src/inside_mod.rs\nsrc/item_mod.rs\n";

fn assert_list_files(tempdir: &TempDir, expected: &'static str) {
run()
.args(["mutants", "--list-files", "-d"])
.arg(tempdir.path())
.assert()
.success()
.stdout(predicates::str::diff(expected));
}

#[test]
fn config_is_read_from_mutants_toml_in_tree_root() {
let testdata = copy_of_testdata("well_tested");
write_config_to(&testdata, "mutants.toml", CONFIG_EXAMINING_MOD_FILES);
assert_list_files(&testdata, MOD_FILES_LISTED);
}

#[test]
fn config_is_read_from_dot_mutants_toml_in_tree_root() {
let testdata = copy_of_testdata("well_tested");
write_config_to(&testdata, ".mutants.toml", CONFIG_EXAMINING_MOD_FILES);
assert_list_files(&testdata, MOD_FILES_LISTED);
}

#[test]
fn config_is_read_from_dot_config_mutants_toml() {
let testdata = copy_of_testdata("well_tested");
write_config_to(
&testdata,
".config/mutants.toml",
CONFIG_EXAMINING_MOD_FILES,
);
assert_list_files(&testdata, MOD_FILES_LISTED);
}

#[test]
fn config_is_read_from_workspace_metadata_mutants_in_cargo_toml() {
let testdata = copy_of_testdata("well_tested");
append_to_manifest(
&testdata,
indoc! { r#"

[workspace]

[workspace.metadata.mutants]
examine_globs = ["src/*_mod.rs"]
"#},
);
assert_list_files(&testdata, MOD_FILES_LISTED);
}

#[test]
fn config_is_read_from_package_metadata_mutants_in_cargo_toml() {
let testdata = copy_of_testdata("well_tested");
append_to_manifest(
&testdata,
indoc! { r#"

[package.metadata.mutants]
examine_globs = ["src/*_mod.rs"]
"#},
);
assert_list_files(&testdata, MOD_FILES_LISTED);
}

#[test]
fn cargo_mutants_toml_takes_precedence_over_mutants_toml() {
let testdata = copy_of_testdata("well_tested");
write_config_file(&testdata, CONFIG_EXAMINING_MOD_FILES);
write_config_to(&testdata, "mutants.toml", CONFIG_EXAMINING_SIMPLE_FNS);
assert_list_files(&testdata, MOD_FILES_LISTED);
}

#[test]
fn mutants_toml_takes_precedence_over_dot_mutants_toml() {
let testdata = copy_of_testdata("well_tested");
write_config_to(&testdata, "mutants.toml", CONFIG_EXAMINING_MOD_FILES);
write_config_to(&testdata, ".mutants.toml", CONFIG_EXAMINING_SIMPLE_FNS);
assert_list_files(&testdata, MOD_FILES_LISTED);
}

#[test]
fn dot_mutants_toml_takes_precedence_over_dot_config_mutants_toml() {
let testdata = copy_of_testdata("well_tested");
write_config_to(&testdata, ".mutants.toml", CONFIG_EXAMINING_MOD_FILES);
write_config_to(
&testdata,
".config/mutants.toml",
CONFIG_EXAMINING_SIMPLE_FNS,
);
assert_list_files(&testdata, MOD_FILES_LISTED);
}

#[test]
fn dot_config_mutants_toml_takes_precedence_over_workspace_metadata_mutants() {
let testdata = copy_of_testdata("well_tested");
write_config_to(
&testdata,
".config/mutants.toml",
CONFIG_EXAMINING_MOD_FILES,
);
append_to_manifest(
&testdata,
indoc! { r#"

[workspace]

[workspace.metadata.mutants]
examine_globs = ["src/simple_fns.rs"]
"#},
);
assert_list_files(&testdata, MOD_FILES_LISTED);
}

#[test]
fn workspace_metadata_mutants_takes_precedence_over_package_metadata_mutants() {
let testdata = copy_of_testdata("well_tested");
append_to_manifest(
&testdata,
indoc! { r#"

[package.metadata.mutants]
examine_globs = ["src/simple_fns.rs"]

[workspace]

[workspace.metadata.mutants]
examine_globs = ["src/*_mod.rs"]
"#},
);
assert_list_files(&testdata, MOD_FILES_LISTED);
}

#[test]
fn invalid_field_in_workspace_metadata_mutants_rejected() {
let testdata = copy_of_testdata("well_tested");
append_to_manifest(
&testdata,
indoc! { r#"

[workspace]

[workspace.metadata.mutants]
wobble = false
"#},
);
run()
.args(["mutants", "--list-files", "-d"])
.arg(testdata.path())
.assert()
.failure()
.stderr(
predicates::str::contains("parse `[workspace.metadata.mutants]` from ")
.and(predicates::str::contains("unknown field `wobble`")),
);
}

#[test]
fn no_config_ignores_mutants_toml() {
let testdata = copy_of_testdata("well_tested");
write_config_to(&testdata, "mutants.toml", CONFIG_EXAMINING_SIMPLE_FNS);
run()
.args(["mutants", "--no-config", "--list-files", "-d"])
.arg(testdata.path())
.assert()
.success()
.stdout(predicates::str::contains("src/methods.rs"));
}

#[test]
fn list_with_config_file_exclusion() {
let testdata = copy_of_testdata("well_tested");
Expand Down