Files
chemenu/tools/chemenu/tests/test_source_hygiene.py
T
torben 9a1be6acde
CI / verify (push) Successful in 55s
Release / release (push) Successful in 37s
docs: die Zahl der nachgezogenen Pfadliterale korrigiert (33, nicht 27)
Files changed:
- CHANGES.md
- VERSION
- tools/chemenu/tests/test_source_hygiene.py
2026-09-17 09:01:50 +02:00

71 lines
2.9 KiB
Python

"""Guard against a renamed stage leaving its old path standing in source.
`wiki/` was renamed to `kb/` on 2026-08-21. The directory moved; the string did
not, in 33 places - error messages, `--help` text, docstrings and the lint
report's own header, all naming a directory that no longer exists. Nothing
caught it, because no check reads a path literal in source.
This is that check. It is deliberately a plain substring scan over the source
tree rather than a `docs verify` check: `docs verify` reads `shipped_prose()`,
which is markdown only, and the bulk of the defect sat in `.py` strings.
"""
from __future__ import annotations
from pathlib import Path
TOOLS_DIR = Path(__file__).resolve().parents[2]
# Retired stage path -> what replaced it. A future rename adds a row here in the
# same change that does the renaming, which is what makes the next occurrence a
# test failure instead of a string nobody reads for a year.
RETIRED_STAGE_PATHS = {
"wiki/": "kb/",
}
# Occurrences that are not stage paths at all. Kept as an explicit list with a
# reason rather than dodged by a cleverer regex: an exception a reader can see
# is worth more than one a pattern hides.
ALLOWED = {
# A fixture release URL, where `wiki` is a repository name in `owner/repo`.
("tests/test_dist_cmd.py", "https://example/torben/wiki/releases/tag/v0.3.1"),
}
SELF = Path(__file__).resolve()
def _scanned_files() -> list[Path]:
"""Every source file in `tools/` a stale path literal could hide in.
This module is excluded, and has to be: it is the one file whose job is to
name the retired paths, so scanning it would make the guard fail on its own
declaration.
"""
candidates = [*(TOOLS_DIR / "chemenu").rglob("*.py"), TOOLS_DIR / "wikitool"]
return sorted(path for path in candidates if path.resolve() != SELF)
def test_no_retired_stage_path_survives_in_source():
findings = []
for path in _scanned_files():
relative = path.relative_to(TOOLS_DIR / "chemenu" if path.suffix == ".py" else TOOLS_DIR)
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
for retired, replacement in RETIRED_STAGE_PATHS.items():
if retired not in line:
continue
if any(key == str(relative) and excerpt in line for key, excerpt in ALLOWED):
continue
findings.append(
f"{relative}:{number} names the retired path `{retired}` "
f"(now `{replacement}`): {line.strip()}"
)
assert findings == [], "Retired stage paths still in source:\n" + "\n".join(findings)
def test_the_guard_actually_scans_something():
"""A scan that silently matches no file passes for the wrong reason."""
scanned = _scanned_files()
assert len(scanned) > 40
assert TOOLS_DIR / "wikitool" in scanned
assert SELF not in scanned