Repository: sourcefrog/cargo-mutants (rust)

Issue:
Support `cargo nextest`
<https://nexte.st/> promises to be faster and in some ways nicer than `cargo test` on trees where it works.

- [x] Do cargo-mutants's own tests pass under nextest?
- [x] Add an option to use it instead of `cargo test`, probably a `TestDriver` or `TestTool` enum.
- [x] Let this option be set in config or on the command line.
- [x] Document the new option.
- [x] Integration test with nextest, ~~probably off by default as it may not be installed~~.
- [x] Test PR mutants (and all mutants?) using nextest in cargo-mutants own CI.

Perhaps later:
- [ ] Add an option to run doctests separately through `cargo test`, since Nextest doesn't run them. (However, trees that require Nextest presumably don't care about this.) #229 
- [ ] [Consume machine-readable nextest output](https://nexte.st/book/run-machine-readable.html) (currently unstable in theory, maybe OK in practice)
- [ ] Build with `cargo test --no-run` and `nextest run --no-run` #237
- [ ] Look at nextest exit codes and report if the test one is not "tests failed" #238
- [ ] Send a PR adding it under https://nexte.st/book/integrations.html

Rather than a specific `--nextest` option, this perhaps should build on some general concept of configuring which tools to use, allied to #77. Although, it's an interesting edge case as it also has a lot in common with Cargo, and should use Cargo for package discovery, builds, etc.

Maybe later it would also be interesting to add a test tool that runs Miri.

Pull request description:
Add --test-tool=nextest
Fixes #85 

- [x] Update manual
- [x] Exercise from cargo-mutants' own CI
- [x] Integration test 
- [x] Check docs about passing down options to `cargo test` also cover nextest

## Performance on cargo-mutants

I tested this on cargo-mutants itself locally and in CI, and it works well. However, interestingly, it does currently seem to be significantly slower in sharded full-tree tests than is cargo test (about 8m vs >13m). ([Example CI run](https://github.com/sourcefrog/cargo-mutants/actions/runs/7391505570/job/20108488473?pr=223).)

It's also somewhat slower, although not as much, locally:

```
; c r --release mutants --shard 1/10 --test-tool=nextest
   Compiling cargo-mutants v23.12.2 (/home/mbp/src/mutants)
    Finished release [optimized] target(s) in 4.49s
     Running `target/release/cargo-mutants mutants --shard 1/10 --test-tool=nextest`
Found 56 mutants to test
ok       Unmutated baseline in 13.9s build + 22.3s test
Auto-set test timeout to 1m 51s
56 mutants tested in 4m 22s: 40 caught, 16 unviable


; c r --release mutants --shard 1/10
    Finished release [optimized] target(s) in 0.08s
     Running `target/release/cargo-mutants mutants --shard 1/10`
Found 56 mutants to test
ok       Unmutated baseline in 13.8s build + 21.7s test
Auto-set test timeout to 1m 48s
56 mutants tested in 3m 46s: 40 caught, 16 unviable
```

Although it's probably better at stopping early, it may be paying a higher per-test overhead or maybe making less use of parallelism?

## Performance on stackslib

I also tested this on a 1% sample of stackslib with 

```
; c r --release mutants --shard 1/100 -d ~/src/stacks-core/ --test-tool nextest
    Finished release [optimized] target(s) in 0.18s
     Running `target/release/cargo-mutants mutants --shard 1/100 -d /home/mbp/src/stacks-core/ --test-tool nextest`
Found 112 mutants to test
ok       Unmutated baseline in 116.1s build + 244.5s test
Auto-set test timeout to 20m 22s
MISSED   src/burnchains/bitcoin/spv.rs:1150:55: replace > with == in SpvClient::get_target in 34.1s build + 256.1s test
MISSED   src/chainstate/burn/db/sortdb.rs:2893:25: replace == with != in SortitionDB::is_db_version_supported_in_epoch in 25.8s build + 283.8s test
3358.435171577s  INFO timeout after 1222.4s, terminating child process...
TIMEOUT  src/net/connection.rs:1334:9: replace NetworkConnection<P>::send_data -> Result<usize, net_error> with Ok(1) in 34.8s build + 1222.4s test
MISSED   src/chainstate/coordinator/comm.rs:105:9: replace SignalBools::receive_signal -> u8 with 0 in 38.0s build + 278.8s test
MISSED   src/main.rs:957:16: replace == with != in main in 24.3s build + 258.5s test
MISSED   src/net/p2p.rs:4235:9: replace PeerNetwork::need_block_or_microblock_stream -> Result<bool, net_error> with Ok(true) in 28.7s build + 267.8s test
MISSED   src/net/p2p.rs:1280:9: replace PeerNetwork::num_peers -> usize with 1 in 26.8s build + 311.8s test
test     src/clarity_vm/database/mod.rs:174:9: replace <impl HeadersDB for ChainstateTx<'a>>::get_burnchain_tokens_spent_for_block -> Option<u128> with Some(1) ... 233.0s
MISSED   src/net/http.rs:1730:9: replace HttpRequestType::get_proof_query -> bool with true in 33.5s build + 291.0s test
MISSED   src/util_lib/db.rs:656:20: replace > with == in tx_busy_handler in 35.5s build + 290.3s test
test     src/chainstate/stacks/db/unconfirmed.rs:443:9: replace UnconfirmedState::num_mined_txs -> usize with 0 s
└                PASS [  28.317s]                     blockstack-core clarity_vm::tests::costs::epoch_21_test_alt
47/112 mutants tested, 8 MISSED, 1 timeout, 24 caught, 14 unviable, 1h 49m 40s elapsed, about 2h 32m remaining
```

It looks like some of those tests are still pretty slow, but it is making progress.

Patch:
diff --git a/.gitignore b/.gitignore
index ae711fe..c2966ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ mutants.out
 mutants.out.old
 .cargo/config.toml
 wiki
+.vscode/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dd01b50..9c4ad4e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -33,7 +33,13 @@ Please run `cargo fmt` and `cargo clippy`. These are checked in CI.
 
 ## Testing
 
-Of course, please add tests for new features or bug fixes, and see the _Testing_ section of [the design doc](DESIGN.md).
+Of course, please add tests for new features or bug fixes. See also the _Testing_ section of [the design doc](DESIGN.md).
+
+### Running the tests
+
+cargo-mutants tests require [`cargo-nextest`](https://nexte.st/) to be installed, so that they can exercise `--test-tool=nextest`.
+
+cargo-mutants tests can be run under either `cargo test` or `cargo nextest run`.
 
 ### Test naming
 
diff --git a/Cargo.lock b/Cargo.lock
index 2b44ce5..976d0b9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -191,6 +191,7 @@ dependencies = [
  "serde",
  "serde_json",
  "similar",
+ "strum",
  "subprocess",
  "syn",
  "tempfile",
@@ -958,6 +959,12 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
 [[package]]
 name = "ryu"
 version = "1.0.15"
@@ -1055,6 +1062,28 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
+[[package]]
+name = "strum"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.25.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
 [[package]]
 name = "subprocess"
 version = "0.2.9"
diff --git a/Cargo.toml b/Cargo.toml
index 68886ef..8ebb586 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,7 @@ version = "23.12.2"
 edition = "2021"
 authors = ["Martin Pool"]
 license = "MIT"
-description = "Find inadequately-tested code that can be removed without any tests failing."
+description = "Inject bugs and see if your tests catch them"
 repository = "https://github.com/sourcefrog/cargo-mutants"
 homepage = "https://mutants.rs/"
 categories = ["development-tools::testing"]
@@ -60,6 +60,7 @@ quote = "1.0"
 regex = "1.10"
 serde_json = "1"
 similar = "2.0"
+strum = { version = "0.25", features = ["derive"] }
 subprocess = "0.2.8"
 tempfile = "3.2"
 time = "0.3"
@@ -72,7 +73,6 @@ whoami = "1.2"
 [dependencies.nutmeg]
 version = "0.1.4"
 # git = "https://github.com/sourcefrog/nutmeg.git"
-# branch = "const-new"
 
 [dependencies.proc-macro2]
 features = ["span-locations"]
diff --git a/DESIGN.md b/DESIGN.md
index 09273e7..7a9ae4b 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -194,6 +194,18 @@ Cargo-mutants is primarily tested on its public interface, which is the command
 
 `cargo-mutants` runs as a subprocess of the test process so that we get the most realistic view of its behavior. In some cases it is run via the `cargo` command to test that this level of indirection works properly.
 
+### Test performance
+
+Since cargo-mutants itself runs the test suite of the program under test many times there is a risk that the test suite can get slow. Aside from slowing down developers and CI, this has a multiplicative effect on the time to run `cargo mutants` on itself.
+
+To manage test time:
+
+* Although key behaviour should be tested through integration tests that run the CLI, it's OK to handle additional cases with unit tests that run much faster.
+
+* Whenever reasonable, CLI tests can only list mutants with `--list` rather than actually testing all of them: we have just a small set of tests that check that the mutants that are listed are actually run.
+
+* Use relatively small testdata trees that are sufficient to test the right behavior.
+
 ### `testdata` trees
 
 The primary means of testing is Rust source trees under `testdata`: you can copy an existing tree and modify it to show the new behavior that you want to test.
@@ -214,7 +226,11 @@ Many features can be tested adequately by only looking at the list of mutants pr
 
 ### Unit tests
 
-Although we primarily want to test the public interface (which is the command line), unit tests can be added in a `mod test {}` within the source tree for any behavior that is inconvenient to exercise from the command line.
+Although we primarily want to test the public interface (which is the command line), unit tests can be added in a `mod test {}` within the source tree for any behavior that is inconvenient or overly slow to exercise from the command line.
+
+### Nextest tests
+
+cargo-mutants tests require `nextest` to be installed.
 
 ## UI Style
 
diff --git a/NEWS.md b/NEWS.md
index 05b670b..38d656a 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+- New! `cargo mutants --test-tool nextest`, or `test_tool = "nextest"` in `.cargo/mutants.toml` runs tests under [Nextest](https://nexte.st/). Some trees have tests that only work under Nextest, and this allows them to be tested. In other cases Nextest may be significantly faster, because it will exit soon after the first test failure.
+
 - Fixed: Fixed spurious "Patch input contains repeated filenames" error when `--in-diff` is given a patch that deletes multiple files.
 
 ## 23.12.2
diff --git a/README.md b/README.md
index d666a3f..2fb9702 100644
--- a/README.md
+++ b/README.md
@@ -22,12 +22,18 @@ the tests might be insufficient.
 
 **The main documentation is the user guide at <https://mutants.rs/>.**
 
+## Prerequisites
+
+cargo-mutants can help on trees with non-flaky tests that run under `cargo test` or [`cargo nextest run`](https://nexte.st/).
+
 ## Install
 
 ```sh
 cargo install --locked cargo-mutants
 ```
 
+You can also install using [cargo-binstall](https://github.com/cargo-bins/cargo-binstall) or from binaries attached to GitHub releases.
+
 ## Quick start
 
 From within a Rust source directory, just run
@@ -42,8 +48,6 @@ To generate mutants in only one file:
 cargo mutants -f src/something.rs
 ```
 
-**For more, see the user guide is at <https://mutants.rs/>.**
-
 ## Help advance cargo-mutants
 
 If you use cargo-mutants or just like the idea you can help it get better:
@@ -53,18 +57,20 @@ If you use cargo-mutants or just like the idea you can help it get better:
 
 ## Project status
 
-As of October 2023 this is an actively-maintained spare time project. I expect to make [releases](https://github.com/sourcefrog/cargo-mutants/releases) about every one or two months.
+As of January 2024 this is an actively-maintained spare time project. I expect to make [releases](https://github.com/sourcefrog/cargo-mutants/releases) about every one or two months.
 
 It's very usable at it is and there's room for lots more future improvement,
 especially in adding new types of mutation.
 
+This software is provided as-is with no warranty of any kind.
+
 ## Further reading
 
 See also:
 
-- [cargo-mutants manual](https://mutants.rs/)
-- [How cargo-mutants compares to other techniques and tools](https://github.com/sourcefrog/cargo-mutants/wiki/Compared).
-- [Design notes](DESIGN.md)
-- [Contributing](CONTRIBUTING.md)
-- [Release notes](NEWS.md)
-- [Discussions](https://github.com/sourcefrog/cargo-mutants/discussions)
+* [cargo-mutants manual](https://mutants.rs/)
+* [How cargo-mutants compares to other techniques and tools](https://github.com/sourcefrog/cargo-mutants/wiki/Compared).
+* [Design notes](DESIGN.md)
+* [Contributing](CONTRIBUTING.md)
+* [Release notes](NEWS.md)
+* [Discussions](https://github.com/sourcefrog/cargo-mutants/discussions)
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index dffedcc..da5a684 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -16,6 +16,7 @@
   - [Workspaces and packages](workspaces.md)
   - [Passing options to Cargo](cargo-args.md)
   - [Build directories](build-dirs.md)
+  - [Using nextest](nextest.md)
 - [Generating mutants](mutants.md)
   - [Error values](error-values.md)
 - [Improving performance](performance.md)
diff --git a/book/src/cargo-args.md b/book/src/cargo-args.md
index 0d39385..cfa0474 100644
--- a/book/src/cargo-args.md
+++ b/book/src/cargo-args.md
@@ -27,7 +27,9 @@ additional_cargo_args = ["--all-features"]
 ## Arguments to `cargo test`
 
 Command-line options following a `--` delimiter are passed through to
-`cargo test`. For example, this can be used to pass `--all-targets` which (unobviously)
+`cargo test` (or to [nextest](nextest.md), if you're using that).
+
+For example, this can be used to pass `--all-targets` which (unobviously)
 excludes doctests. (If the doctests are numerous and slow, and not relied upon to catch bugs, this can improve performance.)
 
 ```shell
diff --git a/book/src/getting-started.md b/book/src/getting-started.md
index a6e9dfa..2c5dc97 100644
--- a/book/src/getting-started.md
+++ b/book/src/getting-started.md
@@ -3,6 +3,17 @@
 Just run `cargo mutants` in a Rust source directory, and it will point out
 functions that may be inadequately tested.
 
+## Prerequisites
+
+For cargo-mutants to give useful results, your tree must already
+
+1. Be built with `cargo build`, and
+2. Have reliable non-flaky tests that run under either `cargo test` or `cargo nextest`.
+
+If the tests are flaky, meaning that they can pass or fail depending on factors other than the source tree, then the cargo-mutants results will be meaningless.
+
+Cross-compilation is not currently supported, so the tree must be buildable for the host platform.
+
 ## Example
 
 ```none
diff --git a/book/src/installation.md b/book/src/installation.md
index f4e1847..ff8354e 100644
--- a/book/src/installation.md
+++ b/book/src/installation.md
@@ -6,6 +6,8 @@ Install cargo-mutants from source:
 cargo install --locked cargo-mutants
 ```
 
+You can also use `cargo binstall` from [cargo-binstall](https://github.com/cargo-bins/cargo-binstall), or install binaries from GitHub releases.
+
 ## Supported Rust versions
 
 Building cargo-mutants requires a reasonably recent stable (or nightly or beta) Rust toolchain.
diff --git a/book/src/shards.md b/book/src/shards.md
index f7a3e6e..e93d231 100644
--- a/book/src/shards.md
+++ b/book/src/shards.md
@@ -87,3 +87,7 @@ If your CI system offers a choice of VM sizes you might experiment with using sm
 You should also think about cost and capacity constraints in your CI system, and the risk of starving out other users.
 
 cargo-mutants has no internal scaling constraints to prevent you from setting `k` very large, if cost, efficiency and CI capacity are not a concern.
+
+## Sampling mutants
+
+An option like `--shard 1/100` can be used to run 1% of all the generated mutants for testing cargo-mutants, to get a sense of whether it works or to see how it performs on some tree.
diff --git a/src/cargo.rs b/src/cargo.rs
index b4ee142..fae22e0 100644
--- a/src/cargo.rs
+++ b/src/cargo.rs
@@ -1,4 +1,4 @@
-// Copyright 2021-2023 Martin Pool
+// Copyright 2021-2024 Martin Pool
 
 //! Run Cargo as a subprocess, including timeouts and propagating signals.
 
@@ -10,6 +10,7 @@ use camino::Utf8Path;
 use itertools::Itertools;
 use tracing::{debug, debug_span};
 
+use crate::options::TestTool;
 use crate::outcome::PhaseResult;
 use crate::package::Package;
 use crate::process::Process;
@@ -63,8 +64,17 @@ fn cargo_argv(
     phase: Phase,
     options: &Options,
 ) -> Vec<String> {
-    let mut cargo_args = vec![cargo_bin(), phase.name().to_string()];
-    if phase == Phase::Check || phase == Phase::Build {
+    let mut cargo_args = vec![cargo_bin()];
+    if phase == Phase::Test {
+        match &options.test_tool {
+            TestTool::Cargo => cargo_args.push("test".to_string()),
+            TestTool::Nextest => {
+                cargo_args.push("nextest".to_string());
+                cargo_args.push("run".to_string());
+            }
+        }
+    } else {
+        cargo_args.push(phase.name().to_string());
         cargo_args.push("--tests".to_string());
     }
     if let Some([package]) = packages {
diff --git a/src/config.rs b/src/config.rs
index efcd4d6..b0cb8df 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,4 +1,4 @@
-// Copyright 2022-2023 Martin Pool.
+// Copyright 2022-2024 Martin Pool.
 
 //! `.cargo/mutants.toml` configuration file.
 //!
@@ -10,11 +10,13 @@
 
 use std::default::Default;
 use std::fs::read_to_string;
+use std::path::Path;
 
 use anyhow::Context;
 use camino::Utf8Path;
 use serde::Deserialize;
 
+use crate::options::TestTool;
 use crate::Result;
 
 /// Configuration read from a config file.
@@ -40,10 +42,12 @@ pub struct Config {
     pub additional_cargo_test_args: Vec<String>,
     /// Minimum test timeout, in seconds, as a floor on the autoset value.
     pub minimum_test_timeout: Option<f64>,
+    /// Choice of test tool: cargo or nextest.
+    pub test_tool: Option<TestTool>,
 }
 
 impl Config {
-    pub fn read_file(path: &Utf8Path) -> Result<Config> {
+    pub fn read_file(path: &Path) -> Result<Config> {
         let toml = read_to_string(path).with_context(|| format!("read config {path:?}"))?;
         toml::de::from_str(&toml).with_context(|| format!("parse toml from {path:?}"))
     }
@@ -53,7 +57,7 @@ impl Config {
     pub fn read_tree_config(workspace_dir: &Utf8Path) -> Result<Config> {
         let path = workspace_dir.join(".cargo").join("mutants.toml");
         if path.exists() {
-            Config::read_file(&path)
+            Config::read_file(path.as_ref())
         } else {
             Ok(Config::default())
         }
diff --git a/src/main.rs b/src/main.rs
index a8966b8..4d44d5f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,4 @@
-// Copyright 2021-2023 Martin Pool
+// Copyright 2021-2024 Martin Pool
 
 //! `cargo-mutants`: Find test gaps by inserting bugs.
 
@@ -51,7 +51,7 @@ use crate::list::{list_files, list_mutants, FmtToIoWrite};
 use crate::log_file::LogFile;
 use crate::manifest::fix_manifest;
 use crate::mutate::{Genre, Mutant};
-use crate::options::Options;
+use crate::options::{Options, TestTool};
 use crate::outcome::{Phase, ScenarioOutcome};
 use crate::scenario::Scenario;
 use crate::shard::Shard;
@@ -190,6 +190,10 @@ struct Args {
     #[arg(long, short = 'D')]
     in_diff: Option<Utf8PathBuf>,
 
+    /// minimum timeout for tests, in seconds, as a lower bound on the auto-set time.
+    #[arg(long, env = "CARGO_MUTANTS_MINIMUM_TEST_TIMEOUT")]
+    minimum_test_timeout: Option<f64>,
+
     /// only test mutants from these packages.
     #[arg(id = "package", long, short = 'p')]
     mutate_packages: Vec<String>,
@@ -206,14 +210,14 @@ struct Args {
     #[arg(long)]
     shard: Option<Shard>,
 
+    /// tool used to run test suites: cargo or nextest.
+    #[arg(long)]
+    test_tool: Option<TestTool>,
+
     /// maximum run time for all cargo commands, in seconds.
     #[arg(long, short = 't')]
     timeout: Option<f64>,
 
-    /// minimum timeout for tests, in seconds, as a lower bound on the auto-set time.
-    #[arg(long, env = "CARGO_MUTANTS_MINIMUM_TEST_TIMEOUT")]
-    minimum_test_timeout: Option<f64>,
-
     /// print mutations that failed to check or build.
     #[arg(long, short = 'V')]
     unviable: bool,
diff --git a/src/options.rs b/src/options.rs
index 0fcaf3f..54a971f 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -1,4 +1,4 @@
-// Copyright 2021-2023 Martin Pool
+// Copyright 2021-2024 Martin Pool
 
 //! Global in-process options for experimenting on mutants.
 //!
@@ -11,9 +11,12 @@ use anyhow::Context;
 use camino::Utf8PathBuf;
 use globset::{Glob, GlobSet, GlobSetBuilder};
 use regex::RegexSet;
+use serde::Deserialize;
+use strum::{Display, EnumString};
 use tracing::warn;
 
-use crate::{config::Config, *};
+use crate::config::Config;
+use crate::*;
 
 /// Options for mutation testing, based on both command-line arguments and the
 /// config file.
@@ -90,8 +93,25 @@ pub struct Options {
 
     /// Emit diffs showing just what changed.
     pub emit_diffs: bool,
+
+    /// The tool to use to run tests.
+    pub test_tool: TestTool,
+}
+
+/// Choice of tool to use to run tests.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, EnumString, Display, Deserialize)]
+#[strum(serialize_all = "snake_case")]
+#[serde(rename_all = "snake_case")]
+pub enum TestTool {
+    /// Use `cargo test`, the default.
+    #[default]
+    Cargo,
+
+    /// Use `cargo nextest`.
+    Nextest,
 }
 
+/// Join two slices into a new vector.
 fn join_slices(a: &[String], b: &[String]) -> Vec<String> {
     let mut v = Vec::with_capacity(a.len() + b.len());
     v.extend_from_slice(a);
@@ -119,6 +139,9 @@ impl Options {
                 &config.additional_cargo_test_args,
             ),
             check_only: args.check,
+            colors: true, // TODO: An option for this and use CLICOLORS.
+            emit_json: args.json,
+            emit_diffs: args.diff,
             error_values: join_slices(&args.error, &config.error_values),
             examine_names: RegexSet::new(or_slices(&args.examine_re, &config.examine_re))
                 .context("Failed to compile examine_re regex")?,
@@ -129,6 +152,7 @@ impl Options {
             gitignore: args.gitignore,
             jobs: args.jobs,
             leak_dirs: args.leak_dirs,
+            minimum_test_timeout,
             output_in_dir: args.output.clone(),
             print_caught: args.caught,
             print_unviable: args.unviable,
@@ -137,10 +161,7 @@ impl Options {
             show_times: !args.no_times,
             show_all_logs: args.all_logs,
             test_timeout: args.timeout.map(Duration::from_secs_f64),
-            emit_json: args.json,
-            colors: true, // TODO: An option for this and use CLICOLORS.
-            emit_diffs: args.diff,
-            minimum_test_timeout,
+            test_tool: args.test_tool.or(config.test_tool).unwrap_or_default(),
         };
         options.error_values.iter().for_each(|e| {
             if e.starts_with("Err(") {
@@ -182,3 +203,41 @@ fn build_glob_set<S: AsRef<str>, I: IntoIterator<Item = S>>(
     }
     Ok(Some(builder.build()?))
 }
+
+#[cfg(test)]
+mod test {
+    use std::io::Write;
+
+    use indoc::indoc;
+    use tempfile::NamedTempFile;
+
+    use super::*;
+
+    #[test]
+    fn default_options() {
+        let args = Args::parse_from(["mutants"]);
+        let options = Options::new(&args, &Config::default()).unwrap();
+        assert!(!options.check_only);
+        assert_eq!(options.test_tool, TestTool::Cargo);
+    }
+
+    #[test]
+    fn options_from_test_tool_arg() {
+        let args = Args::parse_from(["mutants", "--test-tool", "nextest"]);
+        let options = Options::new(&args, &Config::default()).unwrap();
+        assert_eq!(options.test_tool, TestTool::Nextest);
+    }
+
+    #[test]
+    fn test_tool_from_config() {
+        let config = indoc! { r#"
+            test_tool = "nextest"
+        "#};
+        let mut config_file = NamedTempFile::new().unwrap();
+        config_file.write_all(config.as_bytes()).unwrap();
+        let args = Args::parse_from(["mutants"]);
+        let config = Config::read_file(config_file.path()).unwrap();
+        let options = Options::new(&args, &config).unwrap();
+        assert_eq!(options.test_tool, TestTool::Nextest);
+    }
+}


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ae45fe4..4acad52 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -42,6 +42,10 @@ jobs:
       - uses: Swatinem/rust-cache@v2
       - name: rustfmt
         run: cargo fmt --all -- --check
+      - uses: taiki-e/install-action@v2
+        name: Install nextest using install-action
+        with:
+          tool: nextest
       - name: Build
         run: cargo build --all-targets
       - name: Test
@@ -70,6 +74,9 @@ jobs:
     runs-on: ubuntu-latest
     needs: [release-binary]
     if: github.event_name == 'pull_request'
+    strategy:
+      matrix:
+        test_tool: [cargo, nextest]
     steps:
       - uses: actions/checkout@v3
         with:
@@ -82,16 +89,21 @@ jobs:
         with:
           toolchain: beta
       - uses: Swatinem/rust-cache@v2
-      - name: Download release binary
+      - uses: taiki-e/install-action@v2
+        name: Install nextest using install-action
+        with:
+          tool: nextest
+      - name: Download cargo-mutants binary
         uses: actions/download-artifact@v3
         with:
           name: cargo-mutants-linux
-      - name: Install binary artifact
+      - name: Install cargo-mutants binary
         run: |
           install cargo-mutants $HOME/.cargo/bin/
-      - name: Mutants
-        run: |
-          cargo mutants --no-shuffle -vV --in-diff git.diff
+      - name: Mutants in-diff
+        run: >
+          cargo mutants --no-shuffle -vV --in-diff git.diff --test-tool
+          ${{matrix.test_tool}}
       - name: Archive mutants.out
         uses: actions/upload-artifact@v3
         if: always()
@@ -105,22 +117,28 @@ jobs:
     strategy:
       matrix:
         shard: [0, 1, 2, 3, 4, 5, 6, 7]
+        test_tool: [cargo, nextest]
     steps:
       - uses: actions/checkout@v3
       - uses: dtolnay/rust-toolchain@master
         with:
           toolchain: beta
       - uses: Swatinem/rust-cache@v2
-      - name: Download release binary
+      - uses: taiki-e/install-action@v2
+        name: Install nextest using install-action
+        with:
+          tool: nextest
+      - name: Download cargo-mutants binary
         uses: actions/download-artifact@v3
         with:
           name: cargo-mutants-linux
-      - name: Install binary artifact
+      - name: Install cargo-mutants binary
         run: |
           install cargo-mutants $HOME/.cargo/bin/
       - name: Mutants
-        run: |
+        run: >
           cargo mutants --no-shuffle -vV --shard ${{ matrix.shard }}/8
+          --test-tool ${{ matrix.test_tool }}
       - name: Archive mutants.out
         uses: actions/upload-artifact@v3
         if: always()
diff --git a/book/src/nextest.md b/book/src/nextest.md
new file mode 100644
index 0000000..17cc335
--- /dev/null
+++ b/book/src/nextest.md
@@ -0,0 +1,29 @@
+# cargo-mutants with nextest
+
+[nextest](https://nexte.st) is a tool for running Rust tests, as a replacement for `cargo test`.
+
+You can use nextest to run your tests with cargo-mutants, instead of `cargo test`, by either passing the `--test-tool=nextest` option, or setting `test_tool = "nextest"` in `.cargo/mutants.toml`.
+
+## How nextest works
+
+In the context of cargo-mutants the most important difference between cargo-test and nextest is that nextest runs each test in a separate process, and it can run tests from multiple test targets in parallel. (Nextest also has some nice UI improvements and other features, but they're not relevant here.)
+
+This means that nextest can stop faster if a single test fails, whereas cargo test will continue running all the tests within the test binary.
+
+This is beneficial for cargo-mutants, because it only needs to know whether at least one test caught the mutation, and so exiting as soon as there's a failure is better.
+
+However, running each test individually also means there is more per-test startup cost, and so on some trees nextest may be slower.
+
+In general, nextest will do relatively poorly on trees that have tests that are individually very fast, or on trees that establish shared or cached state across tests.
+
+## When to use nextest
+
+There are at least two reasons why you might want to use nextest:
+
+1. Some Rust source trees only support testing under nextest, and their tests fail under `cargo test`: in that case, you have to use this option! In particular, nextest's behavior of running each test in a separate process gives better isolation between tests.
+
+2. Some trees might be faster under nextest than under `cargo test`, because they have a lot of tests that fail quickly, and the startup time is a small fraction of the time for the average test. This may or may not be true for your tree, so you can try it and see. Some trees, including cargo-mutants itself, are slower under nextest.
+
+## nextest and doctests
+
+**Caution:** [nextest currently does not run doctests](https://github.com/nextest-rs/nextest/issues/16), so behaviors that are only caught by doctests will show as missed when using nextest. (cargo-mutants could separately run the doctests, but currently does not.)
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 4db3dc3..813a96d 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
@@ -14,10 +14,6 @@ src/cargo.rs: replace cargo_bin -> String with "xyzzy".into()
 src/cargo.rs: replace cargo_argv -> Vec<String> with vec![]
 src/cargo.rs: replace cargo_argv -> Vec<String> with vec![String::new()]
 src/cargo.rs: replace cargo_argv -> Vec<String> with vec!["xyzzy".into()]
-src/cargo.rs: replace || with && in cargo_argv
-src/cargo.rs: replace || with == in cargo_argv
-src/cargo.rs: replace || with != in cargo_argv
-src/cargo.rs: replace == with != in cargo_argv
 src/cargo.rs: replace == with != in cargo_argv
 src/cargo.rs: replace == with != in cargo_argv
 src/cargo.rs: replace rustflags -> String with String::new()
diff --git a/tests/cli/main.rs b/tests/cli/main.rs
index f94f497..7b1a5cc 100644
--- a/tests/cli/main.rs
+++ b/tests/cli/main.rs
@@ -26,6 +26,7 @@ mod config;
 mod error_value;
 mod in_diff;
 mod jobs;
+mod nextest;
 mod shard;
 mod trace;
 #[cfg(windows)]
diff --git a/tests/cli/nextest.rs b/tests/cli/nextest.rs
new file mode 100644
index 0000000..5ef14c9
--- /dev/null
+++ b/tests/cli/nextest.rs
@@ -0,0 +1,20 @@
+// Copyright 2024 Martin Pool
+
+//! Integration tests for cargo mutants calling nextest.
+
+use super::{copy_of_testdata, run};
+
+#[test]
+fn test_with_nextest_on_small_tree() {
+    let tmp_src_dir = copy_of_testdata("small_well_tested");
+    let assert = run()
+        .args(["mutants", "--test-tool", "nextest", "-vV", "--no-shuffle"])
+        .arg("-d")
+        .arg(tmp_src_dir.path())
+        .assert()
+        .success();
+    println!(
+        "stdout:\n{}",
+        String::from_utf8_lossy(&assert.get_output().stdout)
+    );
+}