1"""Check that ``src:path#L42`` links in docs/*.md actually point at real files. 2 3The docs were authored with hand-rolled line numbers. Code moves, line 4numbers drift. This test surfaces the drift at CI time so we don't ship 5broken navigation in the in-dashboard doc viewer. 6 7The contract enforced here: 8 9 * The file referenced by ``src:path`` exists. 10 * If a line number is supplied, it's within the file's line count. 11 12We don't try to verify that the SYMBOL on that line is still the one 13described in the doc — that would need a full AST cross-walk and is out 14of scope. Stale-but-still-valid links are caught on read. 15""" 16 17from __future__ import annotations 18 19import re 20from pathlib import Path 21 22import pytest 23 24 25_REPO = Path(__file__).resolve().parents[1] 26_DOCS_DIR = _REPO / "docs" 27_SRC_LINK_RE = re.compile(r"src:([A-Za-z0-9_./\-]+?)(?:#L(\d+))?(?=[\s\)#])") 28 29 30def _collect_links() -> list[tuple[Path, int, str, int | None]]: 31 """Walk every .md under docs/ and return (doc, lineno, target, line_anchor).""" 32 found: list[tuple[Path, int, str, int | None]] = [] 33 for md in sorted(_DOCS_DIR.glob("*.md")): 34 for lineno, line in enumerate(md.read_text(encoding="utf-8").splitlines(), 1): 35 for m in _SRC_LINK_RE.finditer(line): 36 target = m.group(1) 37 anchor = int(m.group(2)) if m.group(2) else None 38 found.append((md, lineno, target, anchor)) 39 return found 40 41 42# Use parametrize so each broken link is its own test failure — far easier 43# to read in CI output than one big concatenated assertion. 44_LINKS = _collect_links() 45 46 47@pytest.mark.parametrize( 48 "doc,doc_line,target,line_anchor", 49 _LINKS, 50 ids=[f"{p.name}:{ln}:{t}" + (f"#L{a}" if a else "") for p, ln, t, a in _LINKS], 51) 52def test_doc_src_link_resolves(doc: Path, doc_line: int, target: str, line_anchor: int | None) -> None: 53 target_path = _REPO / target 54 assert target_path.exists(), ( 55 f"{doc.name}:{doc_line} → src:{target} does not exist. " 56 "If you renamed/moved the file, update the doc." 57 ) 58 if line_anchor is not None: 59 total_lines = sum(1 for _ in target_path.open(encoding="utf-8", errors="replace")) 60 assert 1 <= line_anchor <= total_lines, ( 61 f"{doc.name}:{doc_line} → src:{target}#L{line_anchor} is out of range " 62 f"(file has {total_lines} lines). Likely line numbers drifted; " 63 "either retarget or use a symbol-based pointer." 64 ) 65 66 67def test_at_least_one_src_link_in_docs() -> None: 68 """Sanity: make sure the regex actually matched something. Catches the 69 case where someone renames the src: scheme and the test silently passes 70 with zero links found.""" 71 assert len(_LINKS) > 0, "no src:path#L42 links found — did the link scheme change?"