diff --git a/scripts/lint_base_counts.py b/scripts/lint_base_counts.py index 2b86d4ce55b..b710ac0ae1b 100644 --- a/scripts/lint_base_counts.py +++ b/scripts/lint_base_counts.py @@ -276,8 +276,9 @@ def base_counts_cached( ) -> Counts: """`compute` memoized on disk. The base tree at a given commit is immutable, so its counts are a pure function of the merge-base plus the checker's - fingerprints in the cache key; an empty result is never stored because it is - the signature of a crashed pass, not a clean tree. On a disk miss the counts + fingerprints in the cache key. An empty local result fails the gate instead of + being returned, because it is the signature of a crashed pass, not a clean + tree, and would make every existing violation look new. On a disk miss the counts CI already published for the merge-base are fetched before the expensive local base scan; a fetch miss of any kind computes locally.""" directory: Final = default_cache_dir() if cache_dir is None else cache_dir @@ -289,8 +290,14 @@ def base_counts_cached( store_counts(directory, checker, base_point, fetched) return fetched counts: Final = compute(base_point) - if counts: - store_counts(directory, checker, base_point, counts) + if not counts: + print( + f"FAIL: {checker.name} produced no violations for the base tree at {base_point[:12]}, " + "so every rule would look freshly added. The base pass almost certainly " + "crashed; refusing to blame this change for it." + ) + raise SystemExit(1) + store_counts(directory, checker, base_point, counts) return counts diff --git a/scripts/type_check_gate.py b/scripts/type_check_gate.py index 2a9f1c1229f..dbcce091ac6 100644 --- a/scripts/type_check_gate.py +++ b/scripts/type_check_gate.py @@ -279,15 +279,7 @@ def cmd_check(head: Mapping[str, int], base_ref: str) -> None: ) raise SystemExit(1) base_point: Final = resolve_base_point(base_ref) - base: Final = base_counts_cached(checker_identity(), base_point, base_counts) - if not base: - print( - f"FAIL: basedpyright produced no errors for the base tree at {base_point[:12]}, " - "so every rule would look freshly added. The base pass almost certainly " - "crashed; refusing to blame this change for it." - ) - raise SystemExit(1) - judge(head, base, base_point) + judge(head, base_counts_cached(checker_identity(), base_point, base_counts), base_point) def judge(head: Mapping[str, int], base: Mapping[str, int], base_point: str) -> None: diff --git a/tests/unit/test_lint_base_counts.py b/tests/unit/test_lint_base_counts.py index 84f8a0936c0..29fa57a6b4b 100644 --- a/tests/unit/test_lint_base_counts.py +++ b/tests/unit/test_lint_base_counts.py @@ -203,16 +203,15 @@ def test_base_counts_cached_keeps_each_checker_apart(tmp_path: Path) -> None: ) == {"reportAny": 4} -def test_an_empty_base_pass_is_never_cached(tmp_path: Path) -> None: - calls: Final[list[str]] = [] - - def crashed(ref: str) -> counts.Counts: - calls.append(ref) - return {} - - assert counts.base_counts_cached(_CHECKER, "abc123", crashed, cache_dir=tmp_path, fetch=_no_fetch) == {} - assert counts.base_counts_cached(_CHECKER, "abc123", crashed, cache_dir=tmp_path, fetch=_no_fetch) == {} - assert calls == ["abc123", "abc123"] +def test_an_empty_base_pass_fails_the_gate_instead_of_blaming_the_change( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + with pytest.raises(SystemExit) as exit_info: + counts.base_counts_cached( + _CHECKER, "abc123def4567", lambda ref: {}, cache_dir=tmp_path, fetch=lambda checker, base_point: {} + ) + assert exit_info.value.code == 1 + assert f"FAIL: {_CHECKER.name} produced no violations for the base tree at abc123def456," in capsys.readouterr().out assert list(tmp_path.iterdir()) == []