Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --test-workspace and --test-packages #425

Merged
merged 38 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
933abc0
WIP add test_workspace
sourcefrog Oct 14, 2024
e113ab9
Merge remote-tracking branch 'origin/main' into 394-test-packages
sourcefrog Oct 24, 2024
18f57d6
Be clearer that --workspace configures what to mutate
sourcefrog Oct 28, 2024
9ca6e01
doc: new --test-workspace etc
sourcefrog Oct 28, 2024
9078a3d
Move book gitignore pattern
sourcefrog Nov 4, 2024
f4bdac7
Parse --test-options and --test-packages
sourcefrog Nov 4, 2024
05336b4
factor out Options.phases()
sourcefrog Nov 6, 2024
88a7149
Stop scenario phases on an internal error
sourcefrog Nov 6, 2024
253f84a
Start jobserver a little later
sourcefrog Nov 6, 2024
773a51b
Refactor/simplify lab code
sourcefrog Nov 6, 2024
af6570c
clippy: more cleanups
sourcefrog Nov 6, 2024
751b6e5
clean up
sourcefrog Nov 6, 2024
206aec8
Refactor BuildDir constructors
sourcefrog Nov 6, 2024
993d38c
clippy: pedantic cleanups for BuildDir
sourcefrog Nov 6, 2024
49e1bde
Tidy up
sourcefrog Nov 6, 2024
86d504b
Factor out Lab struct
sourcefrog Nov 6, 2024
6821d51
Comment
sourcefrog Nov 6, 2024
c21e931
Store just the package name in SourceFile
sourcefrog Nov 9, 2024
bb3912a
Simplify Workspace/package a bit
sourcefrog Nov 9, 2024
309eba1
Unify Package and PackageTop
sourcefrog Nov 9, 2024
763e2f0
Merge Package and PackageTop
sourcefrog Nov 9, 2024
14189de
Separate finding packages from filtering them
sourcefrog Nov 9, 2024
44992bb
Clean up auto package selection
sourcefrog Nov 9, 2024
fab5aab
clippy cleanups
sourcefrog Nov 9, 2024
888dd79
Run tests from named packages
sourcefrog Nov 9, 2024
5375fc8
Use PackageSelection more widely
sourcefrog Nov 9, 2024
e286dca
Copy testdata for shard test
sourcefrog Nov 9, 2024
6fefb2c
Check named test packages exist
sourcefrog Nov 9, 2024
4cb1bac
Rename to --test-package (singular)
sourcefrog Nov 9, 2024
72e00ab
Add testdata and tests for #394
sourcefrog Nov 9, 2024
f582061
News for #394
sourcefrog Nov 9, 2024
d2ef0e7
Fix cross-package tests for rename
sourcefrog Nov 9, 2024
61b2277
Comment
sourcefrog Nov 9, 2024
1993524
Tidy up
sourcefrog Nov 9, 2024
92eb8e1
Refactor Lab/Worker interface
sourcefrog Nov 9, 2024
0035ac4
cargo_argv does not need build_dir
sourcefrog Nov 9, 2024
d783f32
Run only shard 0/4 of cross-package tests
sourcefrog Nov 11, 2024
a8685cc
mutants::skip join_threads
sourcefrog Nov 11, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ mutants.out.old
.cargo/config.toml
wiki
.vscode/
book/book
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: `--test-workspace` and `--test-package` arguments and config options support projects whose tests live in a different package.

- New: Mutate `proc_macro` targets and functions.

- New: Write diffs to dedicated files under `mutants.out/diff/`. The filename is included in the mutant json output.
Expand Down
1 change: 0 additions & 1 deletion book/.gitignore

This file was deleted.

48 changes: 38 additions & 10 deletions book/src/workspaces.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
# Workspace and package support

cargo-mutants supports testing Cargo workspaces that contain multiple packages. The entire workspace tree is copied.
cargo-mutants supports testing Cargo workspaces that contain multiple packages.

By default, cargo-mutants has [the same behavior as Cargo](https://doc.rust-lang.org/cargo/reference/workspaces.html):
The entire workspace tree is copied to the temporary directory (unless `--in-place` is used).

* If `--workspace` is given, all packages in the workspace are tested.
* If `--package` is given, the named packages are tested.
* If the starting directory (or `-d` directory) is in a package, that package is tested.
* Otherwise, the starting directory must be in a virtual workspace. If it specifies default members, they are tested. Otherwise, all packages are tested.
In workspaces with multiple packages, there are two considerations:

For each mutant, only the containing package's tests are run, on the theory that
each package's tests are responsible for testing the package's code.
1. Which packages to generate mutants in, and
2. Which tests to run on those mutants.

The baseline tests exercise all and only the packages for which mutants will
be generated.
## Selecting packages to mutate

By default, cargo-mutants selects packages to mutate using [similar heuristics to other Cargo commands](https://doc.rust-lang.org/cargo/reference/workspaces.html).

These rules work from the "starting directory", which is the directory selected by `--dir` or the current working directory.

* If `--workspace` is given, all packages in the workspace are mutated.
* If `--package` is given, the named packages are mutated.
* If the starting directory is in a package, that package is mutated. Concretely, this means: if the starting directory or its parents contain a `Cargo.toml` containing a `[package]` section.
* If the starting directory's parents contain a `Cargo.toml` with a `[workspace]` section but no `[package]` section, then the directory is said to be in a "virtual workspace". If the `[workspace]` section has a `default-members` key then these packages are mutated. Otherwise, all packages are mutated.

Selection of packages can be combined with [`--file`](skip_files.md) and other filters.

You can also use the `--file` options to restrict cargo-mutants to testing only files
from some subdirectory, e.g. with `-f "utils/**/*.rs"`. (Remember to quote globs
on the command line, so that the shell doesn't expand them.) You can use `--list` or
`--list-files` to preview the effect of filters.

## Selecting tests to run

For each baseline and mutant scenario, cargo-mutants selects some tests to see if the mutant is caught.
These selections turn into `--package` or `--workspace` arguments to `cargo test`.

There are different behaviors for the baseline tests (before mutation), which run once for all packages, and then for the tests applied to each mutant.

These behaviors can be controlled by the `--test-workspace` and `--test-package` command line options and the corresponding configuration options.

By default, the baseline runs the tests from all and only the packages for which mutants will be generated. That is, if the whole workspace is being tested, then it runs `cargo test --workspace`, and otherwise runs tests for each selected package.

By default, each mutant runs only the tests from the package that's being mutated.

If the `--test-workspace=true` argument or `test_workspace` configuration key is set, then all tests from the workspace are run for the baseline and against each mutant.

If the `--test-package` argument or `test_package` configuration key is set then the specified packages are tested for the baseline and all mutants.

As for other options, the command line arguments have priority over the configuration file.

Like `--package`, the argument to `--test-package` can be a comma-separated list, or the option can be repeated.
103 changes: 78 additions & 25 deletions src/build_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,29 @@

//! A directory containing mutated source to run cargo builds and tests.

use std::fmt::{self, Debug};
#![warn(clippy::pedantic)]

use std::fs::write;

use anyhow::{ensure, Context};
use camino::{Utf8Path, Utf8PathBuf};
use tempfile::TempDir;
use tracing::info;

use crate::copy_tree::copy_tree;
use crate::manifest::fix_cargo_config;
use crate::*;
use crate::{
console::Console,
copy_tree::copy_tree,
manifest::{fix_cargo_config, fix_manifest},
options::Options,
workspace::Workspace,
Result,
};

/// A directory containing source, that can be mutated, built, and tested.
///
/// Depending on how its constructed, this might be a copy in a tempdir
/// or the original source directory.
#[derive(Debug)]
pub struct BuildDir {
/// The path of the root of the build directory.
path: Utf8PathBuf,
Expand All @@ -25,42 +34,45 @@ pub struct BuildDir {
temp_dir: Option<TempDir>,
}

impl Debug for BuildDir {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BuildDir")
.field("path", &self.path)
.finish()
}
}

impl BuildDir {
/// Make a new build dir, copying from a source directory, subject to exclusions.
pub fn copy_from(
source: &Utf8Path,
gitignore: bool,
leak_temp_dir: bool,
/// Make the build dir for the baseline.
///
/// Depending on the options, this might be either a copy of the source directory
/// or in-place.
pub fn for_baseline(
workspace: &Workspace,
options: &Options,
console: &Console,
) -> Result<BuildDir> {
if options.in_place {
BuildDir::in_place(workspace.root())
} else {
BuildDir::copy_from(workspace.root(), options, console)
}
}

/// Make a new build dir, copying from a source directory, subject to exclusions.
pub fn copy_from(source: &Utf8Path, options: &Options, console: &Console) -> Result<BuildDir> {
let name_base = format!("cargo-mutants-{}-", source.file_name().unwrap_or("unnamed"));
let source_abs = source
.canonicalize_utf8()
.context("canonicalize source path")?;
let temp_dir = copy_tree(source, &name_base, gitignore, console)?;
let temp_dir = copy_tree(source, &name_base, options.gitignore, console)?;
let path: Utf8PathBuf = temp_dir
.path()
.to_owned()
.try_into()
.context("tempdir path to UTF-8")?;
fix_manifest(&path.join("Cargo.toml"), &source_abs)?;
fix_cargo_config(&path, &source_abs)?;
let temp_dir = if leak_temp_dir {
let temp_dir = if options.leak_dirs {
let _ = temp_dir.into_path();
info!(?path, "Build directory will be leaked for inspection");
None
} else {
Some(temp_dir)
};
let build_dir = BuildDir { temp_dir, path };
let build_dir = BuildDir { path, temp_dir };
Ok(build_dir)
}

Expand All @@ -70,8 +82,7 @@ impl BuildDir {
temp_dir: None,
path: source_path
.canonicalize_utf8()
.context("canonicalize source path")?
.to_owned(),
.context("canonicalize source path")?,
})
}

Expand All @@ -90,16 +101,21 @@ impl BuildDir {

#[cfg(test)]
mod test {
use test_util::copy_of_testdata;
use crate::test_util::copy_of_testdata;

use super::*;

#[test]
fn build_dir_copy_from() {
let tmp = copy_of_testdata("factorial");
let workspace = Workspace::open(tmp.path()).unwrap();
let build_dir =
BuildDir::copy_from(workspace.root(), true, false, &Console::new()).unwrap();
let options = Options {
in_place: false,
gitignore: true,
leak_dirs: false,
..Default::default()
};
let build_dir = BuildDir::copy_from(workspace.root(), &options, &Console::new()).unwrap();
let debug_form = format!("{build_dir:?}");
println!("debug form is {debug_form:?}");
assert!(debug_form.starts_with("BuildDir { path: "));
Expand All @@ -108,6 +124,43 @@ mod test {
assert!(build_dir.path().join("src").is_dir());
}

#[test]
fn for_baseline_in_place() -> Result<()> {
let tmp = copy_of_testdata("factorial");
let workspace = Workspace::open(tmp.path())?;
let options = Options {
in_place: true,
..Default::default()
};
let build_dir = BuildDir::for_baseline(&workspace, &options, &Console::new())?;
assert_eq!(
build_dir.path().canonicalize_utf8()?,
workspace.root().canonicalize_utf8()?
);
assert!(build_dir.temp_dir.is_none());
Ok(())
}

#[test]
fn for_baseline_copied() -> Result<()> {
let tmp = copy_of_testdata("factorial");
let workspace = Workspace::open(tmp.path())?;
let options = Options {
in_place: false,
..Default::default()
};
let build_dir = BuildDir::for_baseline(&workspace, &options, &Console::new())?;
assert!(build_dir.path().is_dir());
assert!(build_dir.path().join("Cargo.toml").is_file());
assert!(build_dir.path().join("src").is_dir());
assert!(build_dir.temp_dir.is_some());
assert_ne!(
build_dir.path().canonicalize_utf8()?,
workspace.root().canonicalize_utf8()?
);
Ok(())
}

#[test]
fn build_dir_in_place() -> Result<()> {
let tmp = copy_of_testdata("factorial");
Expand Down
Loading