diff --git a/tests/functional/diff_collect.rs b/tests/functional/diff_collect.rs index 063f0b6..2db72f1 100644 --- a/tests/functional/diff_collect.rs +++ b/tests/functional/diff_collect.rs @@ -242,15 +242,20 @@ fn diff_collect_errors_with_no_prior_collect() { ); } -/// When one sha out of several diverges, `run` proceeds with the rows still -/// anchored at reachable shas — only tests stranded at the diverged sha -/// rerun (as "new"). The diverged rows stay in the DB until the user runs -/// `cargo affected clean`. +/// A `collect_sha` that HEAD has moved off of is still *reachable* as long +/// as its commit sits in the object database — reachability is `git cat-file +/// -e`, not ancestry. So `run` diffs against the sibling like any other sha: +/// the test anchored there comes back as plain `affected`, nothing is +/// stranded, and the run doesn't widen. The sibling's rows stay in the DB +/// until the user runs `cargo affected clean`. +/// +/// The genuinely-missing case — where the commit is gone and its tests do +/// strand — is [`run_unions_affected_and_stranded_when_sha_is_missing`]. /// /// One-test-per-file isolates each test's row set per file so the /// structural-edit backstop doesn't pull unrelated tests in. #[test] -fn run_uses_reachable_shas_when_one_sha_diverges() { +fn run_uses_sibling_sha_without_stranding_or_widening() { let tmp = tempfile::tempdir().unwrap(); let dir = tmp.path(); write_three_one_test_modules(dir, "sample_diff_partial_diverge"); @@ -298,9 +303,11 @@ fn run_uses_reachable_shas_when_one_sha_diverges() { // Selection chose exactly one test (test_fa — anchored at sha1, which is // a sibling but reachable, and the diff against it picks up the edit). + // The breakdown pins the *category*: a sibling sha contributes `affected` + // hits, never `stranded` ones. assert!( - combined.contains("1 tests to run"), - "expected '1 tests to run' (test_fa affected via sha1), got:\n{combined}" + combined.contains("1 tests to run (1 affected + 0 config + 0 new + 0 stranded"), + "expected test_fa selected as 'affected' with nothing stranded, got:\n{combined}" ); assert!( combined.contains("test_fa"), @@ -334,12 +341,20 @@ fn run_uses_reachable_shas_when_one_sha_diverges() { ); } -/// Middle-case: one sha diverged, one reachable — and there's a real edit -/// against a file covered by the reachable sha. Verifies the union of -/// `affected` (over the reachable sha) and `new_tests` (the stranded one) -/// is what runs, not just one or the other. +/// Middle-case: one sha genuinely gone, one reachable — and there's a real +/// edit against a file covered by the reachable sha. Verifies the union of +/// `affected` (over the reachable sha) and `stranded` (the tests anchored +/// only at the missing one) is what runs, not just one or the other. +/// +/// Resetting HEAD is *not* enough to strand anything. Reachability is +/// `git cat-file -e`, so an orphaned commit still in the object database +/// stays reachable and its tests come back as ordinary `affected` hits — +/// that variant is +/// [`run_uses_sibling_sha_without_stranding_or_widening`]. Expiring the +/// reflog and running `git gc --prune=now` is what actually removes the +/// commit and drives the `stranded` path. #[test] -fn run_unions_affected_and_stranded_when_partially_diverged() { +fn run_unions_affected_and_stranded_when_sha_is_missing() { let tmp = tempfile::tempdir().unwrap(); let dir = tmp.path(); write_three_one_test_modules(dir, "sample_diff_partial_diverge_with_edit"); @@ -358,7 +373,7 @@ fn run_unions_affected_and_stranded_when_partially_diverged() { replace_in_file(&dir.join("src/a.rs"), "x + 1", "x + 1 /* edited */"); git(dir, &["add", "."]); git(dir, &["commit", "-q", "-m", "edit fa"]); - let _sha1 = git_head(dir); + let sha1 = git_head(dir); let diff = cargo_affected(dir, &["affected", "collect", "--diff"]); assert!( diff.status.success(), @@ -366,10 +381,29 @@ fn run_unions_affected_and_stranded_when_partially_diverged() { String::from_utf8_lossy(&diff.stderr), ); - // Reset HEAD to sha0 (orphans sha1) AND modify b.rs so there's a real - // diff against sha0. Selection should pick test_fb (overlap at sha0) - // AND test_fa (stranded → "new"). + // Reset HEAD to sha0, then expire the reflog and gc so sha1's commit + // object is really gone — the reset alone orphans it but leaves it + // findable, which would make it reachable and this scenario a duplicate + // of the sibling test above. git(dir, &["reset", "--hard", "-q", &sha0]); + git(dir, &["reflog", "expire", "--expire=now", "--all"]); + git(dir, &["gc", "--prune=now", "--quiet"]); + assert!( + // `.output()` rather than `.status()`: the probe is expected to fail, + // and this keeps git's "Not a valid object name" off the test log. + !std::process::Command::new("git") + .args(["cat-file", "-e", &format!("{sha1}^{{commit}}")]) + .current_dir(dir) + .output() + .unwrap() + .status + .success(), + "sha1 {sha1} should be pruned out of the object database", + ); + + // Modify b.rs so there's a real diff against sha0. Selection should pick + // test_fb (overlap at sha0) AND test_fa (anchored only at the missing + // sha1 → stranded). replace_in_file(&dir.join("src/b.rs"), "x + 1", "x + 1 /* run-time edit */"); let run = cargo_affected(dir, &["affected", "run", "-v"]); @@ -381,9 +415,21 @@ fn run_unions_affected_and_stranded_when_partially_diverged() { ); let combined = combined_output(&run); + // The breakdown is the assertion that matters: one hit from each + // category. A bare "2 tests to run" would also pass with both tests + // classified `affected`, which is exactly what happens if sha1 survives. + assert!( + combined.contains("2 tests to run (1 affected + 0 config + 0 new + 1 stranded"), + "expected the affected ∪ stranded union, got:\n{combined}" + ); + assert!( + combined.contains("test_fa (stranded)"), + "test_fa is anchored only at the missing sha → stranded, got:\n{combined}" + ); + // The missing sha is announced, not silently dropped. assert!( - combined.contains("2 tests to run"), - "expected '2 tests to run' (test_fb affected + test_fa as new), got:\n{combined}" + combined.contains("not in the repo"), + "expected the missing-sha notice for {sha1}, got:\n{combined}" ); for t in ["test_fa", "test_fb"] { assert!(