"""478-2 문서 사이트 검사 — 묶음 배정 · 손글 함수 · 함수 화면 도구 대 추출 · mkdocs --strict. 가짜 추출(fixtures/478_docs)은 늘 돎 · 진짜 추출(docs/_data)은 있을 때만. """ import json import re import sys import warnings from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "docs" / "tools")) import build # noqa: E402 import group # noqa: E402 FIXTURE = Path(__file__).parent / "fixtures" / "478_docs" REAL = ROOT / "docs" / "_data" def make_site(data_dir: Path, out: Path) -> tuple[build.Site, dict]: site = build.Site(build.load_data(data_dir), group.load_rules(), out) return site, site.build() def shown_anchors(site: build.Site) -> set[str]: text = "".join(p.read_text(encoding="utf-8") for p in (site.src).glob("*/*.md")) return set(re.findall(r"\{ #(f-[0-9a-f]{10}) \}", text)) def typedoc_functions(out: Path) -> set[tuple[str, str]]: project = json.loads((out / "typedoc.json").read_text(encoding="utf-8")) modules = [m for m in project.get("children", []) if m["kind"] == 2] or [project] return { (c["sources"][0]["fileName"], c["name"]) for m in modules for c in m.get("children", []) if c["kind"] == 64 } @pytest.fixture(scope="module") def fake(tmp_path_factory): return make_site(FIXTURE, tmp_path_factory.mktemp("fake")) @pytest.fixture(scope="module") def real(tmp_path_factory): if not (REAL / "functions.json").exists(): pytest.skip("추출 없음 — docs/_data") return make_site(REAL, tmp_path_factory.mktemp("real")) def test_assign_rules(): data = build.load_data(FIXTURE) page = group.load_rules()["pages"][0] gid = { f["name"]: g for g, fns in group.assign(data["functions"], data["registry"], page["groups"]).items() for f in fns } assert gid["stationKey"] == "stations" # 등록부 stations.key assert gid["station_key"] == "stations" # 등록부 pair assert gid["seatBornNodes"] == "profile" # 등록부 profile.seat assert gid["median"] == "profile" # 파일 B05_Profile_Grade_* assert gid["node_edits"] == "route_edit" # 파일 B05_Profile_Route_NodeEdits assert gid["atomic_write_bytes"] == group.NONE def test_fake_site(fake): site, rep = fake assert shown_anchors(site) == {build.anchor(f["id"]) for f in site.fns} assert rep["ungrouped"] == 2 assert rep["session_keys"] == 1 and rep["api"] == 1 assert "stations" in rep["stale_groups"] # 손글 지문 ≠ 가짜 목록 build.build_site(site.out, strict=True) def test_fake_tools_match_extract(fake): site, _ = fake ts = {(f["file"], f["name"]) for f in site.fns if f["lang"] == "ts" and f["exported"]} assert typedoc_functions(site.out) == ts py = {(f["file"], f["name"]) for f in site.fns if f["lang"] == "py"} assert {(file, n) for file, _ in py for n in build.griffe_names(file)} == py def test_real_hand_refs(real): _, rep = real assert rep["missing_refs"] == [], "손글이 가리키는 함수가 추출에 없음" def test_real_every_function_shown(real): site, _ = real assert shown_anchors(site) == {build.anchor(f["id"]) for f in site.fns}, "조용히 빠진 함수" def test_real_typedoc_matches_extract(real): site, _ = real ts = {(f["file"], f["name"]) for f in site.fns if f["lang"] == "ts" and f["exported"]} got = typedoc_functions(site.out) assert not (ts - got) and not (got - ts), ( f"추출만 {sorted(ts - got)[:10]} · TypeDoc 만 {sorted(got - ts)[:10]}" ) def test_real_griffe_matches_extract(real): site, _ = real py = {(f["file"], f["name"]) for f in site.fns if f["lang"] == "py"} got = {(file, n) for file in {f for f, _ in py} for n in build.griffe_names(file)} assert not (py - got) and not (got - py), ( f"추출만 {sorted(py - got)[:10]} · griffe 만 {sorted(got - py)[:10]}" ) def test_real_counts(real): _, rep = real if rep["undocumented_exports"]: warnings.warn(f"설명 없는 export 함수 {len(rep['undocumented_exports'])}") if rep["ungrouped"]: warnings.warn(f"묶음 없는 함수 {rep['ungrouped']}") assert rep["ungrouped"] < rep["functions"] def test_real_strict_build(real): site, _ = real build.build_site(site.out, strict=True)