cargo-litmus

Conservatively select the Rust tests affected by a Git change.
cargo-litmus indexes Rust source relationships, combines them with Cargo package and workspace dependencies from cargo-ferris-wheel, and emits focused cargo nextest commands. When it cannot prove that a narrow selection is safe, it widens to package or workspace tests instead of risking a false negative.
Install
cargo-litmus requires cargo-ferris-wheel for affected-package discovery. cargo-nextest is optional unless you use --validate-nextest or execute the generated commands.
Litmus invokes the cargo-ferris-wheel binary directly and requires version 1.1.4 or newer. Direct invocation works from monorepo roots without a root Cargo.toml, including environments where cargo is wrapped by a build cache such as mbx.
# Fastest when cargo-binstall is available.
# Or build the two required Cargo subcommands from source.
The minimum supported Rust version is 1.88.0.
Quick start
From the root of a Git repository containing one or more Cargo workspaces:
# Build or update target/cargo-litmus.rkyv.
# Select changes between the merge-base of origin/main and HEAD.
# Select current staged, unstaged, deleted, renamed, and untracked changes.
# Explain why a file or package selects tests.
The summary output includes selection and cache telemetry. Use --format json for the complete machine-readable report, or --json-output <path> to write that report while retaining summary telemetry on stdout.
Selecting changes
The affected command accepts explicit paths as well as Git-native inputs:
# Explicit paths.
# An exact revision range, including changed line ranges.
# Only changes staged in the Git index.
# Newline-delimited paths, a JSON array, or a JSON object on stdin.
|
--merge-base, --worktree, and --staged are mutually exclusive. Explicit --files can be combined with --base and --head to restrict revision analysis to those paths.
Index caches
The current index defaults to $CARGO_TARGET_DIR/cargo-litmus.rkyv, or target/cargo-litmus.rkyv when CARGO_TARGET_DIR is unset. Override it with --cache for index or --current-cache for affected and explain.
For revision-aware CI, build an index at the base revision and pass it to the head analysis:
base_sha=""
base_tmp=""
base_worktree="/repo"
LITMUS_BASE_CACHE provides the default --base-cache path. Cache metadata includes the repository root and source/Cargo fingerprints; incompatible or incomplete caches are rebuilt conservatively.
How inputs select tests
Litmus classifies every changed file, in this order:
- Configured inputs (
.cargo-litmus.toml) select the workspaces or packages they name. - Cargo inputs (
Cargo.toml,Cargo.lock,build.rs,.cargo/config.toml,rust-toolchain.toml) select their owning package's closure, their workspace, or every workspace for configuration and toolchain files, which resolve by walking up the directory tree. - Indexed sources map to their owning package and reverse dependents. Files embedded with
include!,include_str!,include_bytes!, or#[path]map to the packages that embed them. - Inert inputs select nothing and are not passed to
cargo-ferris-wheel, whose ripple walk widens on files it cannot map. Documentation, media, fonts, legal and community files, container recipes, and CI, agent, and editor metadata cannot be compiled, executed, or read by a build unless an indexed source embeds them, which case 3 already covers. Inert classification stops at package roots: a file beneath a package root keeps its package's closure, because package code can read it through paths litmus cannot index (CARGO_MANIFEST_DIR, computed paths). - Any other file is conservative: a file beneath a package root selects that package and its reverse dependents; a file inside a workspace but outside its packages (lockfile, workspace manifest, workspace data) widens to that workspace; a file outside every workspace widens to all of them.
The built-in inert classes are:
| class | examples |
|---|---|
| documentation and media | *.md, *.mdx, *.rst, *.adoc, *.png, *.svg, *.pdf, *.woff2 |
| JavaScript and TypeScript sources | *.ts, *.tsx, *.js, *.jsx, *.mjs, *.cjs, *.mts, *.cts |
| legal and community | LICENSE*, COPYING*, NOTICE*, CHANGELOG*, CONTRIBUTING*, SECURITY*, CODEOWNERS |
| CI, agent, and editor metadata | .github/**, .agents/**, .claude/**, .cursor/**, .devcontainer/**, .vscode/**, .idea/** |
| container recipes | Dockerfile*, *.dockerfile, .dockerignore, docker-compose*.yml |
JavaScript and TypeScript sources are classified inert because Cargo never compiles, runs, or consumes them; the toolchain that does is outside the index. Repositories whose Rust builds or tests read JavaScript or TypeScript trees map those paths back in with [[rules]]; configured rules win over inert classification.
Data and configuration files (*.toml, *.yaml, *.json, *.sql, *.snap, scripts, templates) are never inert, because a build or test can read them at runtime. Declare them with .cargo-litmus.toml rules, or add selection = "ignore" rules for repository-specific trees that cannot affect tests:
[[]]
= ["ts/**", "dev-docs/**"]
= "ignore"
Set default-inert-paths = false to disable the built-in classes and make every unmapped file conservative again. --format json reports every classification in input_explanations, including inert ones.
Repository-specific inputs
Rust syntax and Cargo metadata cannot express every input to a build or test. Add .cargo-litmus.toml at the analyzed repository root to map migrations, schemas, generated inputs, or other files explicitly:
[[]]
= ["migrations/**"]
= ["server"]
= "workspace"
[[]]
= ["schemas/**"]
= ["api-types", "api-server"]
= "packages"
[[]]
= ["vendor/tooling/**"]
= "ignore"
[[]]
= ["codegen/**"]
= ["generated-client"]
For selection = "workspace", a workspace whose root manifest declares [package] uses that package's Cargo name as its workspace identifier, regardless of its member count. Virtual workspaces use their root directory name.
Patterns are globset globs: *, **, ?, character classes ([abc], [!abc]), and {a,b} alternates (*.{js,jsx,ts,tsx}) are supported. Patterns match repository-relative paths.
A path matched by several rules is a real input if any matching rule selects workspaces or packages; it is inert only when every matching rule ignores it. A broad selection = "ignore" rule therefore cannot shadow a narrower mapping rule, whatever the order in the file.
Rules are strict: unknown fields, invalid globs, and empty or contradictory selections are errors. Inputs that remain unmapped and are not classified inert widen selection rather than being ignored.
Optional nextest validation
Pass --validate-nextest to affected or path-based explain to resolve the emitted filters with cargo nextest list. This can compile repository code and execute build scripts, so only enable it for trusted code.
Without validation, Litmus still emits conservative commands but does not execute Cargo builds or build scripts while selecting them.
Development
Scenario tests
tests/integration/scenarios.rs covers selection accuracy end to end. Each scenario
synthesizes a multi-workspace Cargo monorepo in a temporary Git repository, builds a
base index, commits a change, and runs the real cargo-litmus affected binary against
it; the harness in tests/integration/support/ provides the monorepo builders and
scripts cargo-ferris-wheel deterministically, so the suite needs no installed
ferris-wheel. The harness is split by concern: monorepo.rs (synthesis DSL and git
plumbing), ferris.rs (shim and payload model), report.rs (test-side mirror of the
JSON contract), expectations.rs (two-sided expectations and diagnostics),
scenario.rs (the check* drivers), and env.rs (PATH/binary resolution).
tests/integration/main.rs is the only integration-test root: Cargo compiles every
top-level file in tests/ into its own executable, so the suites and the harness live
under one root to keep the harness compiled once and its dead-code analysis global.
Scenario expectations are two-sided: required packages fail the run when they are
missing (a false negative) and allowed packages fail the run when they are exceeded
(over-selection past the scenario's declared conservative allowance). Add a scenario
whenever a change class can widen or narrow selection.
License
Licensed under the MIT License.