diff --git a/scripts/check-doc-count.py b/scripts/check-doc-count.py index 897d3fe2..0a0c9068 100644 --- a/scripts/check-doc-count.py +++ b/scripts/check-doc-count.py @@ -108,6 +108,16 @@ def _resolve_root() -> Path: # the reader straight into a second failure. A hint is only worth printing if it # runs; keep the spelling here and let tests/test_check_doc_count.py prove the # other sites agree with it. +# +# Scope of that measurement, added 2026-09-13 (cyc20260913-122923) after walking +# into it: "this form exits 0" is a property of a **synced** checkout. The +# measurement above was taken in the main clone, which is synced. In a fresh +# worktree the same spelling exits 2 without measuring anything, because +# `uv run --no-sync` has created an empty `.venv` there and both `python` and +# `python3` resolve to it - so the spelling is necessary but not sufficient, and +# the hint it appears in must not be printed when the environment, not the +# interpreter choice, is what is missing. See the pytest-missing branch in +# `measured_count`. INVOCATION = "uv run --no-sync python3 scripts/check-doc-count.py" # The stored form, as it appeared in Agent.md: the command followed by the count @@ -185,9 +195,31 @@ def measured_count() -> int: "(its output could not be decoded)" ) if proc.returncode != 0: + detail = (proc.stdout[-2000:] + proc.stderr[-2000:]).strip() + # Two causes, two remedies - and the second one is *not* "use the right + # interpreter". Measured 2026-09-13 (cyc20260913-122923) in a fresh + # review worktree: `uv run --no-sync` there had produced an empty `.venv` + # (`site-packages` holding only `_virtualenv.pth` and `_virtualenv.py`), + # and `python` and `python3` both resolve to it - so the invocation this + # tool prints as its own remedy failed with byte-identical output, rc 2, + # sending the reader in a circle. Reading that output as a wrong + # interpreter is the misdiagnosis: there is no interpreter in this + # checkout that has pytest, so advising a different one cannot help. + if "No module named pytest" in detail: + raise DocCountError( + "pytest is not installed in the interpreter running this tool " + f"({sys.executable}), so no test was collected:\n" + + detail + + "\n\nThis is an unsynced checkout, not a wrong-interpreter " + "problem - a fresh worktree or clone gets an empty `.venv`, and " + "`python` and `python3` both resolve to it, so re-running " + f"`{INVOCATION}` here fails identically. Run `uv sync` in this " + "checkout first, or run this tool from a checkout whose " + "environment is already synced." + ) raise DocCountError( f"pytest --collect-only failed (rc={proc.returncode}):\n" - + (proc.stdout[-2000:] + proc.stderr[-2000:]).strip() + + detail + "\n\nhint: run this with the project interpreter, e.g." f" `{INVOCATION} --measure`" ) diff --git a/tests/test_check_doc_count.py b/tests/test_check_doc_count.py index 58c2e6d1..ca3f36d4 100644 --- a/tests/test_check_doc_count.py +++ b/tests/test_check_doc_count.py @@ -528,6 +528,56 @@ class _Proc: assert f"`{mod.INVOCATION} --measure`" in str(excinfo.value) +def test_a_missing_pytest_is_diagnosed_as_an_unsynced_checkout(monkeypatch) -> None: + """The remedy must not be the command that just failed. + + Measured state this pins (2026-09-13, `cyc20260913-122923`): in a fresh + review worktree the tool printed its own `INVOCATION` as the fix, and running + that spelling produced byte-identical output, rc 2 - `uv run --no-sync` had + left an empty `.venv` there and both `python` and `python3` resolve to it, so + "use the project interpreter" is a circle. The message must name the + environment instead. + """ + fresh = _load_module() + + class _Proc: + returncode = 1 + stdout = "" + stderr = "/some/checkout/.venv/bin/python3: No module named pytest\n" + + monkeypatch.setattr(fresh.subprocess, "run", lambda *a, **k: _Proc()) + with pytest.raises(fresh.DocCountError) as excinfo: + fresh.measured_count() + message = str(excinfo.value) + assert "No module named pytest" in message + assert "/some/checkout/.venv/bin/python3" in message, "the child's own words" + assert "unsynced" in message, "the cause, named" + assert "uv sync" in message, "a remedy that can actually work here" + + +def test_a_real_collection_failure_keeps_the_invocation_hint(monkeypatch) -> None: + """The other cause of the same non-zero exit: pytest ran, and it failed. + + Without this arm, treating every non-zero collection as an unsynced checkout + would be green - which would take the invocation hint away from the case it + was written for. + """ + fresh = _load_module() + + class _Proc: + returncode = 2 + stdout = "ERROR: file or directory not found: tests/\n" + stderr = "" + + monkeypatch.setattr(fresh.subprocess, "run", lambda *a, **k: _Proc()) + with pytest.raises(fresh.DocCountError) as excinfo: + fresh.measured_count() + message = str(excinfo.value) + assert f"`{fresh.INVOCATION} --measure`" in message + assert "uv sync" not in message + assert "unsynced" not in message + + # --- which tree was scanned --------------------------------------------------