From 91105cfda9fb09e545f189a8337fb1b85b005d7a Mon Sep 17 00:00:00 2001 From: cargo-affected-bot <282014906+cargo-affected-bot@users.noreply.github.com> Date: Wed, 20 May 2026 19:53:31 +0000 Subject: [PATCH 1/4] fix: forward filters to nextest list, key new-test detection off filter-match.status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo affected run -- some_test_name` dropped the positional filter from the `cargo nextest list` step used for new-test detection: the listing enumerated every test while the run only ran matching ones. A new test absent from the DB but not matching the positional was still flagged `(new)`, landed in the generated default-filter handed to `nextest run`, and tripped exit 4 once it was the only selected test. Replace `cargo_build_args` (build-flag allowlist) with `args_for_listing` (run-only denylist) so positionals, -E/--filterset expressions, and the libtest-compatible filter block all carry through to `nextest list`. Replace `Listing.ignored` with `Listing.excluded`, keyed off `filter-match.status == "mismatch"` — a strict superset of the old ignored predicate that unifies #[ignore]d tests, positional substring filters, -E expressions, and the project's own default-filter into one exclusion check across `new`/`stranded`/`affected`. Phantoms (in DB but absent from the listing entirely) remain in `affected` so `collect --diff`'s live-vs-phantom split in `handle_no_profraw_dirs` keeps working. Closes #39 Co-Authored-By: Claude --- src/collect.rs | 265 +++++++++++++++++++++-------------- src/run.rs | 14 +- src/selection.rs | 55 +++++--- tests/functional/new_test.rs | 50 +++++++ 4 files changed, 252 insertions(+), 132 deletions(-) diff --git a/src/collect.rs b/src/collect.rs index a00ed11..ff6a2d2 100644 --- a/src/collect.rs +++ b/src/collect.rs @@ -191,7 +191,7 @@ pub fn collect( project_root, Some(&rustflags), Some(&build_dir), - &cargo_build_args(nextest_args), + &args_for_listing(nextest_args), )?; eprintln!( "found {} tests across {} binaries", @@ -808,109 +808,119 @@ pub(crate) fn write_nextest_config(project_root: &Path, filter_expr: &str) -> Re Ok(path) } -/// Boolean cargo build flags accepted by both `cargo nextest list` and -/// `cargo nextest run` — no value token follows. -const BUILD_FLAGS_BARE: &[&str] = &[ - "--workspace", - "--all", - "--lib", - "--bins", - "--examples", - "--tests", - "--benches", - "--all-targets", - "--all-features", - "--no-default-features", - "--release", - "-r", - "--frozen", - "--locked", - "--offline", - "--ignore-rust-version", - "--future-incompat-report", - "--unit-graph", +/// Long `cargo nextest run`-only flags with no value token. Dropped from the +/// listing args; `list` would reject every one of them. +const RUN_ONLY_BARE: &[&str] = &[ + "--fail-fast", + "--no-fail-fast", + "--no-run", + "--no-capture", + "--no-input-handler", + "--no-output-indent", + "--hide-progress-bar", ]; -/// Long cargo build flags that consume a value — `--flag value` or the -/// joined `--flag=value`. +/// Long `cargo nextest run`-only flags that consume a value. The value may +/// follow as a separate token (`--flag value`) or be joined (`--flag=value`). /// -/// `--target-dir` is deliberately absent: it changes only where artifacts -/// land, not which tests exist, and `collect` already passes its own -/// `--target-dir` to `nextest_list` — forwarding a second one would make -/// `cargo nextest list` reject the duplicate. -const BUILD_FLAGS_VALUED: &[&str] = &[ - "--package", - "--exclude", - "--bin", - "--example", - "--test", - "--bench", - "--features", - "--cargo-profile", - "--target", - "--manifest-path", - "--build-jobs", - "--config", +/// `--message-format` appears on both `list` and `run` but with disjoint +/// value sets — `nextest_list` always passes its own `--message-format json`, +/// so forwarding the user's would either duplicate or break the listing. +const RUN_ONLY_VALUED: &[&str] = &[ + "--retries", + "--max-fail", + "--no-tests", + "--status-level", + "--final-status-level", + "--failure-output", + "--success-output", + "--message-format", + "--message-format-version", + "--max-progress-running", + "--show-progress", + "--stress-count", + "--stress-duration", + "--debugger", + "--tracer", + "--flaky-result", + "--test-threads", + "--jobs", ]; -/// Short cargo build flags that consume a value — `-p mycrate` or the -/// joined `-pmycrate`. -const BUILD_FLAGS_SHORT_VALUED: &[&str] = &["-p", "-F", "-Z"]; +/// Short `cargo nextest run`-only flags that consume a value. +const RUN_ONLY_SHORT_VALUED: &[&str] = &["-j"]; -/// Extract the cargo *build* flags from the post-`--` passthrough so the -/// `cargo nextest list` used for new-test detection builds the same test set -/// as the eventual `cargo nextest run`. +/// Drop `cargo nextest run`-only flags from the post-`--` passthrough so the +/// `cargo nextest list` used for new-test detection enumerates the same test +/// set as the eventual `cargo nextest run`. Everything else — cargo build +/// flags, positional substring filters, `-E`/`--filterset` expressions, +/// `--exact`/`--skip`/`--run-ignored` libtest-compatible options — is shared +/// between `list` and `run` and passes through unchanged. /// -/// `list` and `run` share cargo's build options (`--features`, `-p`, -/// `--release`, …) but `run` adds runner/reporter options (`--retries`, -/// `--no-fail-fast`, `--no-tests`, …) that `list` rejects outright. -/// Forwarding the whole passthrough to `list` would break on any of those; -/// forwarding nothing lists a feature-less build while `run` builds with the -/// user's features, so "listed minus DB = new" compares two different test -/// sets. Hence an allowlist of the build flags — anything else (run-only -/// flags, test-name filters, positionals) is dropped: it either doesn't -/// affect which test binaries get built or `list` wouldn't accept it. -pub(crate) fn cargo_build_args(nextest_args: &[String]) -> Vec { +/// Run-only flags govern execution: failure handling (`--retries`, +/// `--no-fail-fast`, `--max-fail`), test parallelism (`-j`/`--test-threads`), +/// output formatting (`--status-level`, `--message-format`), and progress +/// (`--show-progress`, `--hide-progress-bar`). `list` rejects every one of +/// them. Build flags and filters affect *which* test cases exist and which +/// match — `list` needs them to enumerate the right set with the right +/// `filter-match` tagging. +/// +/// The denylist must stay complete against nextest's CLI. A future +/// `cargo nextest run`-only flag not added here would be forwarded to +/// `cargo nextest list`, which would reject the unknown argument and exit +/// non-zero, surfacing the omission as a hard error during the listing +/// step. That matches this repo's prefer-loud-over-silent stance: the +/// previous build-flag *allowlist* dropped any unknown flag, so a future +/// build flag silently produced a listing that did not match the run. +pub(crate) fn args_for_listing(nextest_args: &[String]) -> Vec { let mut out = Vec::new(); let mut iter = nextest_args.iter(); while let Some(arg) = iter.next() { let name = arg.split('=').next().unwrap_or(arg); - if BUILD_FLAGS_BARE.contains(&name) { - out.push(arg.clone()); - } else if BUILD_FLAGS_VALUED.contains(&name) { - out.push(arg.clone()); - // `--flag value` carries the value in the next token; - // `--flag=value` carries it inline. + if RUN_ONLY_BARE.contains(&name) { + continue; + } + if RUN_ONLY_VALUED.contains(&name) { + // Drop the flag, and (when the value rides as a separate token) + // the value too. if !arg.contains('=') { - if let Some(value) = iter.next() { - out.push(value.clone()); - } - } - } else if BUILD_FLAGS_SHORT_VALUED.contains(&arg.as_str()) { - out.push(arg.clone()); - if let Some(value) = iter.next() { - out.push(value.clone()); + iter.next(); } - } else if BUILD_FLAGS_SHORT_VALUED.iter().any(|s| arg.starts_with(*s)) { - // Joined short form: `-pmycrate`, `-Ffeature`. - out.push(arg.clone()); + continue; + } + if RUN_ONLY_SHORT_VALUED.contains(&arg.as_str()) { + iter.next(); + continue; + } + // Joined short form `-j4`. `-j=4` is unusual but the prefix check + // catches it; the name-then-`=` split above already routed `-j=4` + // away from the bare/valued long-flag arms. + if RUN_ONLY_SHORT_VALUED.iter().any(|s| arg.starts_with(*s)) && arg.len() > 2 { + continue; } + out.push(arg.clone()); } out } /// Result of `cargo nextest list`: every testcase as a (binary_id, test_name) -/// pair, the subset that is ignored, plus per-binary metadata. +/// pair, the subset nextest's filter excludes, plus per-binary metadata. pub(crate) struct Listing { - /// Every testcase nextest enumerated, ignored or not. The complete set — - /// `collect --diff` prunes DB rows against it, so a merely-ignored test - /// must stay in here or its rows would be dropped. + /// Every testcase nextest enumerated, filter-matched or not. The + /// complete set — `collect --diff` prunes DB rows against it, so a test + /// merely excluded by the current filter must stay in here or its rows + /// would be dropped. pub(crate) tests: Vec, - /// Subset of `tests` that nextest reports as `#[ignore]`d on this - /// platform (covers conditional `#[cfg_attr(.., ignore)]` too). These - /// are skipped by `cargo nextest run`, so they never gain coverage; - /// new-test detection must exclude them or they read as "new" forever. - pub(crate) ignored: BTreeSet, + /// Subset of `tests` that nextest reports with `filter-match: { status: + /// "mismatch" }`. `cargo nextest run` skips every test in here, so + /// new-test detection must exclude them or each surfaces as `(new)` + /// every run, and the affected loop must exclude them or stale coverage + /// rows that overlap a hunk would pull a now-skipped test back in. + /// Unifies all four filter sources nextest knows about: `#[ignore]`d + /// tests (`reason: "ignored"`), positional substring filters + /// (`reason: "string"`), `-E`/`--filterset` expressions + /// (`reason: "expression"`), and the project's own `default-filter`. + pub(crate) excluded: BTreeSet, pub(crate) binaries: Vec, } @@ -933,16 +943,19 @@ pub(crate) struct BinaryEntry { /// rather than in the project root. Only collect passes this — run/status /// reuse the user's default target/. /// -/// `build_args` are the cargo build flags (`--features`, `-p`, …) extracted -/// from the post-`--` passthrough by [`cargo_build_args`]. They must match +/// `list_args` are the cargo build flags and nextest filters (`--features`, +/// `-p`, positional substrings, `-E` expressions, `--exact`, …) extracted +/// from the post-`--` passthrough by [`args_for_listing`]. They must match /// the build config of the subsequent `cargo nextest run`, or the listing /// enumerates a different test set than the run builds and new-test -/// detection ("listed minus DB") becomes unsound. +/// detection ("listed minus DB") becomes unsound. The filter args additionally +/// tag each testcase with `filter-match.status` so the selection layer can +/// honor positional/`-E` filters the same way nextest run will. pub(crate) fn nextest_list( project_root: &Path, rustflags_override: Option<&str>, build_dir: Option<&Path>, - build_args: &[String], + list_args: &[String], ) -> Result { let mut cmd = Command::new("cargo"); cmd.arg("nextest") @@ -959,7 +972,7 @@ pub(crate) fn nextest_list( cmd.arg("--target-dir").arg(dir); cmd.env("LLVM_PROFILE_FILE", dir.join("build-%p-%m.profraw")); } - for a in build_args { + for a in list_args { cmd.arg(a); } let output = cmd @@ -977,7 +990,7 @@ pub(crate) fn nextest_list( serde_json::from_str(stdout).context("failed to parse nextest list JSON")?; let mut tests = BTreeSet::new(); - let mut ignored = BTreeSet::new(); + let mut excluded = BTreeSet::new(); let mut binaries = Vec::new(); if let Some(suites) = json.get("rust-suites").and_then(|v| v.as_object()) { for suite in suites.values() { @@ -994,12 +1007,13 @@ pub(crate) fn nextest_list( }; for (name, case) in cases { let test_id = TestId::new(binary_id.clone(), name.clone()); - let is_ignored = case - .get("ignored") - .and_then(|v| v.as_bool()) - .context("nextest list testcase missing `ignored` flag")?; - if is_ignored { - ignored.insert(test_id.clone()); + let status = case + .get("filter-match") + .and_then(|v| v.get("status")) + .and_then(|v| v.as_str()) + .context("nextest list testcase missing `filter-match.status`")?; + if status != "matches" { + excluded.insert(test_id.clone()); } tests.insert(test_id); } @@ -1007,7 +1021,7 @@ pub(crate) fn nextest_list( } Ok(Listing { tests: tests.into_iter().collect(), - ignored, + excluded, binaries, }) } @@ -1213,7 +1227,7 @@ mod tests { } #[test] - fn cargo_build_args_keeps_build_flags_drops_run_only() { + fn args_for_listing_drops_run_only_flags() { let args: Vec = [ "--features", "shell-integration-tests", @@ -1226,39 +1240,74 @@ mod tests { .iter() .map(|s| s.to_string()) .collect(); - // `--features ` and `--release` survive; the run-only flags — - // and `--retries`'s separate value token — are dropped. + // Build flags survive; bare `--no-fail-fast`, valued `--retries 2` + // (with its separate value token), and joined `--no-tests=warn` are + // dropped because `cargo nextest list` rejects every one of them. assert_eq!( - cargo_build_args(&args), + args_for_listing(&args), vec!["--features", "shell-integration-tests", "--release"], ); } #[test] - fn cargo_build_args_handles_joined_and_short_forms() { + fn args_for_listing_forwards_positional_and_filterset() { let args: Vec = [ "--features=a,b", "-p", "mycrate", "-r", "--max-fail=3", + "-E", + "test(slow)", "some_test_filter", ] .iter() .map(|s| s.to_string()) .collect(); - // `--flag=value`, `-p `, and the `-r` short flag are build - // args; `--max-fail=3` is run-only and the bare positional filter is - // neither — both dropped. + // Build flags, `-E` filtersets, and positional substring filters all + // pass through — `cargo nextest list` accepts all of them and tags + // each testcase with `filter-match.status` accordingly. Only the + // run-only `--max-fail=3` is dropped. + assert_eq!( + args_for_listing(&args), + vec![ + "--features=a,b", + "-p", + "mycrate", + "-r", + "-E", + "test(slow)", + "some_test_filter", + ], + ); + } + + #[test] + fn args_for_listing_drops_short_and_joined_test_threads() { + // `-j N`, `-j4` (joined), `--jobs N`, and `--test-threads=N` are all + // the same run-only option. + let args: Vec = [ + "-j", + "4", + "--keep1", + "-j8", + "--test-threads=2", + "--jobs", + "1", + "--keep2", + ] + .iter() + .map(|s| s.to_string()) + .collect(); assert_eq!( - cargo_build_args(&args), - vec!["--features=a,b", "-p", "mycrate", "-r"], + args_for_listing(&args), + vec!["--keep1", "--keep2"], ); } #[test] - fn cargo_build_args_empty() { - assert!(cargo_build_args(&[]).is_empty()); + fn args_for_listing_empty() { + assert!(args_for_listing(&[]).is_empty()); } /// Regression for the Windows command-line overflow: a large affected set diff --git a/src/run.rs b/src/run.rs index c294ce2..cd2dcaf 100644 --- a/src/run.rs +++ b/src/run.rs @@ -28,7 +28,7 @@ use std::process::Command; use anyhow::{Context, Result}; use crate::collect::{ - cargo_build_args, nextest_filter_expr, nextest_list, require_nextest, + args_for_listing, nextest_filter_expr, nextest_list, require_nextest, write_nextest_config, }; use crate::db::{warn_untracked_rs_files, Db, TestId}; @@ -181,10 +181,14 @@ pub fn run( } eprintln!("checking for new tests..."); - // List with the same cargo build flags `run_tests` hands to `nextest - // run`, so new-test detection compares against the test set the run - // actually builds — not a feature-less one. - let listing = nextest_list(project_root, None, None, &cargo_build_args(nextest_args))?; + // List with the same args `run_tests` hands to `nextest run`, so + // new-test detection compares against the test set the run actually + // builds and admits — not a feature-less, filter-less one. Run-only + // flags (`--retries`, `--no-fail-fast`, …) are dropped because `list` + // rejects them; positional substring filters and `-E` filtersets pass + // through so each testcase is tagged with the same `filter-match` + // status `nextest run` will apply. + let listing = nextest_list(project_root, None, None, &args_for_listing(nextest_args))?; // Compute per-sha hunks once; selection consumes them and so does // the report builder if --report-json is set. The previous code // ran `git diff -U0 ` twice per reachable sha on the report diff --git a/src/selection.rs b/src/selection.rs index ccb5376..038a7ed 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -27,15 +27,20 @@ use crate::project::{git_changed_line_ranges, relation_to_head, LineRange, ShaRe /// Result of the selection computation. pub(crate) struct Selection { /// Known tests selected by line-range overlap with the changed hunks. - /// Excludes `#[ignore]`d tests: their coverage rows can persist from - /// an earlier (non-ignored) collect, but `nextest run` would skip them - /// — same all-ignored-selection rationale as [`new_tests`]. + /// Excludes tests with `filter-match.status == "mismatch"` — `#[ignore]`d + /// tests whose coverage rows persisted from an earlier collect, tests + /// not matched by a positional/`-E` filter the user passed, and tests + /// the project's own `default-filter` excludes. `nextest run` would + /// skip every one of them; selecting them anyway can collapse the + /// effective set to nothing and trip nextest's "no tests to run" exit. pub(crate) affected: BTreeSet, /// Tests present in the nextest listing but absent from the DB /// entirely under the current fingerprint — added since the last /// `collect`. Always selected because we have no coverage data. - /// Excludes `#[ignore]`d tests: `nextest run` skips them, so they - /// never gain coverage and would otherwise read as "new" on every run. + /// Excludes filter-mismatched tests for the same reason as + /// [`affected`] above: an `#[ignore]`d or filter-excluded test would + /// otherwise read as `(new)` forever, since it never runs and never + /// gains a coverage row. pub(crate) new_tests: BTreeSet, /// Tests present in the nextest listing AND in the DB, but only /// anchored at currently-missing collect_shas. Functionally identical @@ -289,11 +294,15 @@ pub(crate) fn select_with_precomputed_ranges( /// - `stranded_tests = listed ∩ (all_db_tests - reachable_known_tests)` /// (in DB but only at currently-missing shas). /// -/// `#[ignore]`d tests are dropped from all three sets: `nextest run` skips -/// them, so a selection of nothing but ignored tests makes `nextest run` exit -/// non-zero. New/stranded would re-select an ignored test on every run only -/// for it to be skipped again; `affected` would re-select a test whose -/// coverage rows survived from a previous (non-ignored) collect after a hunk +/// Filter-mismatched tests (`listing.excluded`) are dropped from all three +/// sets: `nextest run` skips every one of them, so a selection of nothing +/// but mismatched tests makes `nextest run` exit non-zero. `excluded` covers +/// `#[ignore]`d tests, tests not matching the user's positional substring +/// filter, tests not matching a `-E`/`--filterset` expression, and tests the +/// project's own `default-filter` excludes — `nextest run`'s exact filter +/// surface. New/stranded would re-select a mismatched test on every run +/// only for it to be skipped again; `affected` would re-select a test whose +/// coverage rows survived from a previous (matching) collect after a hunk /// happens to overlap them. /// /// Both `new` and `stranded` get rerun (and re-anchored, in `collect @@ -321,8 +330,9 @@ pub(crate) fn compute( let mut new_tests = BTreeSet::new(); let mut stranded_tests = BTreeSet::new(); for t in &listed { - if listing.ignored.contains(t) { - // Skipped by `nextest run`, so it never gains coverage — must + if listing.excluded.contains(t) { + // Skipped by `nextest run` (ignored, or excluded by a positional + // / `-E` / default-filter), so it never gains coverage — must // not be treated as a new/stranded test to rerun. Stays in // `listed` (above) so `collect --diff`'s prune keeps its rows. continue; @@ -355,14 +365,21 @@ pub(crate) fn compute( let hits = db.tests_covering_ranges(env_fingerprint, collect_sha, file, hunks)?; for hit in hits { - if listing.ignored.contains(&hit.test_id) { - // Coverage rows from a previous (non-ignored) collect - // can survive into a state where the test is now - // `#[ignore]`d (the `--diff` prune deliberately keeps - // them — see `diff_collect_keeps_ignored_test_rows`). - // Selecting it anyway produces the same all-ignored + if listing.excluded.contains(&hit.test_id) { + // Coverage rows from a previous matching collect can + // survive into a state where the test now fails the + // current filter (newly `#[ignore]`d, dropped by a + // positional / `-E` / default-filter); the `--diff` + // prune deliberately keeps them (see + // `diff_collect_keeps_ignored_test_rows`). Selecting + // such a test anyway produces the same all-excluded // → nextest exit 4 we filter against above for - // new/stranded. + // new/stranded. Phantoms (in the DB but absent from + // the listing entirely — renamed/deleted) are NOT in + // `listing.excluded` and stay in `affected`; the + // `collect --diff` flow uses the live/phantom split + // in `handle_no_profraw_dirs` to discriminate them + // from a runner-shim failure. continue; } affected.insert(hit.test_id.clone()); diff --git a/tests/functional/new_test.rs b/tests/functional/new_test.rs index bbc8f6f..00ecf45 100644 --- a/tests/functional/new_test.rs +++ b/tests/functional/new_test.rs @@ -174,6 +174,56 @@ fn newly_ignored_test_excluded_from_affected() { ); } +/// `cargo affected run -- ` forwards `` as a positional +/// test-name filter to `cargo nextest run`. New-test detection runs a separate +/// `cargo nextest list` whose build args came from a build-flag *allowlist* +/// that dropped the positional. A new test absent from the DB but not matching +/// `` was therefore still flagged `(new)`, landed in the generated +/// default-filter handed to `nextest run`, then failed to intersect the +/// positional filter — `nextest run` exited 4 (`no tests to run`) once that +/// new test was the only selected entry. +#[test] +fn run_with_positional_filter_excludes_unmatching_new_tests() { + let tmp = tempfile::tempdir().unwrap(); + let dir = tmp.path(); + write_two_module_project(dir, "sample_pos_filter_new"); + init_git_with_initial_commit(dir); + + let collect = cargo_affected(dir, &["affected", "collect"]); + assert!( + collect.status.success(), + "collect failed: {}", + String::from_utf8_lossy(&collect.stderr) + ); + + // Add a new test that doesn't match the positional filter we'll run with. + // `test_brand_new` has no `add` substring, so the positional filter will + // exclude it; if new-test detection still flags it `new`, the selection + // collapses to a single test the positional filter doesn't admit. + std::fs::create_dir_all(dir.join("tests")).unwrap(); + std::fs::write( + dir.join("tests/integration_new.rs"), + "#[test]\nfn test_brand_new() {\n assert_eq!(1 + 1, 2);\n}\n", + ) + .unwrap(); + + let run = cargo_affected(dir, &["affected", "run", "-v", "--", "add"]); + assert!( + run.status.success(), + "run with a positional filter that excludes the new test must exit 0, \ + got status={:?} stderr=\n{}\nstdout=\n{}", + run.status.code(), + String::from_utf8_lossy(&run.stderr), + String::from_utf8_lossy(&run.stdout), + ); + let combined = combined_output(&run); + assert!( + !combined.contains("test_brand_new"), + "the new test must not appear in the selection when the positional \ + filter excludes it, got:\n{combined}", + ); +} + /// Single-crate, single-test project: one `add` function and one `test_add` /// covering it. Used as the canonical "the only affected test got ignored" /// case where the selection collapses to empty. From 375b3fec102137f6f212ec4d823b4e2566ca512c Mon Sep 17 00:00:00 2001 From: cargo-affected-bot <282014906+cargo-affected-bot@users.noreply.github.com> Date: Thu, 18 Jun 2026 08:10:42 +0000 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20complete=20listing.ignored=E2=86=92?= =?UTF-8?q?excluded=20rename=20in=20test=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix renamed the `listing.ignored` field to `listing.excluded` everywhere in code but left one doc comment in new_test.rs referencing the now-removed field. Update it to match. Co-Authored-By: Claude --- tests/functional/new_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/new_test.rs b/tests/functional/new_test.rs index 00ecf45..22c2169 100644 --- a/tests/functional/new_test.rs +++ b/tests/functional/new_test.rs @@ -118,7 +118,7 @@ fn ignored_test_not_perpetually_new() { /// later edit to a line that test covered would otherwise pull it into /// `affected` and produce the same all-ignored selection that makes /// `nextest run` exit non-zero. The `affected` loop must filter the -/// `listing.ignored` set just like the new/stranded split does. +/// `listing.excluded` set just like the new/stranded split does. #[test] fn newly_ignored_test_excluded_from_affected() { let tmp = tempfile::tempdir().unwrap(); From 03fc62d17394c5f6e98a8f9c411e9a4fe6d40916 Mon Sep 17 00:00:00 2001 From: cargo-affected-bot <282014906+cargo-affected-bot@users.noreply.github.com> Date: Thu, 18 Jun 2026 08:20:51 +0000 Subject: [PATCH 3/4] fix: drop nextest run-only aliases (--nocapture/--ff/--nff) from list args The run-only denylist enumerated the canonical flags but missed the libtest-compatible aliases nextest keeps for them: --nocapture (alias of --no-capture), --ff (--fail-fast), and --nff (--no-fail-fast). Forwarding any of these to `cargo nextest list` fails the listing step, since list defines none of them. Add the aliases to RUN_ONLY_BARE and cover them with args_for_listing_drops_run_only_aliases. Co-Authored-By: Claude Opus 4.8 --- src/collect.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/collect.rs b/src/collect.rs index 81ab249..1af2ce4 100644 --- a/src/collect.rs +++ b/src/collect.rs @@ -792,9 +792,12 @@ pub(crate) fn write_nextest_config(project_root: &Path, filter_expr: &str) -> Re /// listing args; `list` would reject every one of them. const RUN_ONLY_BARE: &[&str] = &[ "--fail-fast", + "--ff", "--no-fail-fast", + "--nff", "--no-run", "--no-capture", + "--nocapture", "--no-input-handler", "--no-output-indent", "--hide-progress-bar", @@ -1252,6 +1255,19 @@ mod tests { ); } + #[test] + fn args_for_listing_drops_run_only_aliases() { + // nextest accepts `--nocapture` (alias of `--no-capture`), `--ff` + // (`--fail-fast`), and `--nff` (`--no-fail-fast`) for libtest muscle + // memory; `cargo nextest list` rejects each the same as its canonical + // form, so all three must be dropped. + let args: Vec = ["--nocapture", "--ff", "--keep", "--nff"] + .iter() + .map(|s| s.to_string()) + .collect(); + assert_eq!(args_for_listing(&args), vec!["--keep"]); + } + #[test] fn args_for_listing_empty() { assert!(args_for_listing(&[]).is_empty()); From a958d678113cbd4674c6f79b1bb2e57ec04cce73 Mon Sep 17 00:00:00 2001 From: cargo-affected-bot Date: Wed, 29 Jul 2026 07:28:22 +0000 Subject: [PATCH 4/4] fix: repair broken rustdoc intra-doc link on Selection::new_tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `new_tests` field doc referenced [`affected`] as a reference-style link but never defined the target, so `cargo doc` under `-D warnings` failed with an unresolved-link error. The orphaned [`new_tests`] definition left on the `affected` field (its text no longer references new_tests) is removed, and the [`affected`]: Self::affected definition is added to the new_tests block — matching the reference-style convention used by config_tests in the same struct. Co-Authored-By: Claude --- src/selection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/selection.rs b/src/selection.rs index b538cff..61ea749 100644 --- a/src/selection.rs +++ b/src/selection.rs @@ -40,8 +40,6 @@ pub(crate) struct Selection { /// the project's own `default-filter` excludes. `nextest run` would /// skip every one of them; selecting them anyway can collapse the /// effective set to nothing and trip nextest's "no tests to run" exit. - /// - /// [`new_tests`]: Self::new_tests pub(crate) affected: BTreeSet, /// Tests present in the nextest listing but absent from the DB /// entirely under the current fingerprint — added since the last @@ -50,6 +48,8 @@ pub(crate) struct Selection { /// [`affected`] above: an `#[ignore]`d or filter-excluded test would /// otherwise read as `(new)` forever, since it never runs and never /// gains a coverage row. + /// + /// [`affected`]: Self::affected pub(crate) new_tests: BTreeSet, /// Tests present in the nextest listing AND in the DB, but only /// anchored at currently-missing collect_shas. Functionally identical