255 lines
9.7 KiB
Python
255 lines
9.7 KiB
Python
"""Tests for wiki generation."""
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from code_review_graph.communities import detect_communities, store_communities
|
|
from code_review_graph.graph import GraphStore
|
|
from code_review_graph.parser import EdgeInfo, NodeInfo
|
|
from code_review_graph.wiki import (
|
|
_generate_community_page,
|
|
_slugify,
|
|
generate_wiki,
|
|
get_wiki_page,
|
|
)
|
|
|
|
|
|
class TestWiki:
|
|
def setup_method(self):
|
|
self.tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
|
|
self.store = GraphStore(self.tmp.name)
|
|
self.wiki_dir = tempfile.mkdtemp()
|
|
|
|
def teardown_method(self):
|
|
self.store.close()
|
|
Path(self.tmp.name).unlink(missing_ok=True)
|
|
# Clean up wiki dir
|
|
wiki_path = Path(self.wiki_dir)
|
|
if wiki_path.exists():
|
|
for f in wiki_path.iterdir():
|
|
f.unlink(missing_ok=True)
|
|
wiki_path.rmdir()
|
|
|
|
def _seed_communities(self):
|
|
"""Seed graph data and detect/store communities."""
|
|
# Auth cluster
|
|
self.store.upsert_node(
|
|
NodeInfo(
|
|
kind="File", name="auth.py", file_path="auth.py",
|
|
line_start=1, line_end=100, language="python",
|
|
), file_hash="a1"
|
|
)
|
|
self.store.upsert_node(
|
|
NodeInfo(
|
|
kind="Function", name="login", file_path="auth.py",
|
|
line_start=5, line_end=20, language="python",
|
|
), file_hash="a1"
|
|
)
|
|
self.store.upsert_node(
|
|
NodeInfo(
|
|
kind="Function", name="logout", file_path="auth.py",
|
|
line_start=25, line_end=40, language="python",
|
|
), file_hash="a1"
|
|
)
|
|
self.store.upsert_node(
|
|
NodeInfo(
|
|
kind="Function", name="check_token", file_path="auth.py",
|
|
line_start=45, line_end=60, language="python",
|
|
), file_hash="a1"
|
|
)
|
|
self.store.upsert_edge(EdgeInfo(
|
|
kind="CALLS", source="auth.py::login",
|
|
target="auth.py::check_token", file_path="auth.py", line=10,
|
|
))
|
|
self.store.upsert_edge(EdgeInfo(
|
|
kind="CALLS", source="auth.py::logout",
|
|
target="auth.py::check_token", file_path="auth.py", line=30,
|
|
))
|
|
|
|
# DB cluster
|
|
self.store.upsert_node(
|
|
NodeInfo(
|
|
kind="File", name="db.py", file_path="db.py",
|
|
line_start=1, line_end=100, language="python",
|
|
), file_hash="b1"
|
|
)
|
|
self.store.upsert_node(
|
|
NodeInfo(
|
|
kind="Function", name="connect", file_path="db.py",
|
|
line_start=5, line_end=20, language="python",
|
|
), file_hash="b1"
|
|
)
|
|
self.store.upsert_node(
|
|
NodeInfo(
|
|
kind="Function", name="query", file_path="db.py",
|
|
line_start=25, line_end=40, language="python",
|
|
), file_hash="b1"
|
|
)
|
|
self.store.upsert_edge(EdgeInfo(
|
|
kind="CALLS", source="db.py::query",
|
|
target="db.py::connect", file_path="db.py", line=30,
|
|
))
|
|
self.store.commit()
|
|
|
|
communities = detect_communities(self.store, min_size=2)
|
|
store_communities(self.store, communities)
|
|
return communities
|
|
|
|
def test_generate_wiki_creates_files(self):
|
|
"""generate_wiki creates markdown files including index.md."""
|
|
self._seed_communities()
|
|
result = generate_wiki(self.store, self.wiki_dir)
|
|
|
|
wiki_path = Path(self.wiki_dir)
|
|
assert (wiki_path / "index.md").exists()
|
|
# At least one community page should be generated
|
|
md_files = list(wiki_path.glob("*.md"))
|
|
assert len(md_files) >= 2 # index + at least 1 community page
|
|
|
|
assert result["pages_generated"] >= 2
|
|
assert isinstance(result["pages_updated"], int)
|
|
assert isinstance(result["pages_unchanged"], int)
|
|
|
|
def test_generate_wiki_index_has_links(self):
|
|
"""index.md contains links to community pages."""
|
|
self._seed_communities()
|
|
generate_wiki(self.store, self.wiki_dir)
|
|
|
|
index_content = (Path(self.wiki_dir) / "index.md").read_text()
|
|
assert "# Code Wiki" in index_content
|
|
assert "Communities" in index_content
|
|
assert ".md" in index_content # contains links to .md files
|
|
|
|
def test_get_wiki_page_returns_content(self):
|
|
"""get_wiki_page returns content for an existing page."""
|
|
self._seed_communities()
|
|
generate_wiki(self.store, self.wiki_dir)
|
|
|
|
# Find any generated page
|
|
wiki_path = Path(self.wiki_dir)
|
|
pages = [f for f in wiki_path.glob("*.md") if f.name != "index.md"]
|
|
assert len(pages) > 0
|
|
|
|
# Get page by its stem (slug)
|
|
page_name = pages[0].stem
|
|
content = get_wiki_page(self.wiki_dir, page_name)
|
|
assert content is not None
|
|
assert len(content) > 0
|
|
|
|
def test_get_wiki_page_returns_none_for_missing(self):
|
|
"""get_wiki_page returns None for non-existent page."""
|
|
content = get_wiki_page(self.wiki_dir, "nonexistent-page")
|
|
assert content is None
|
|
|
|
def test_community_page_has_expected_sections(self):
|
|
"""Generated community pages contain expected sections."""
|
|
communities = self._seed_communities()
|
|
assert len(communities) > 0
|
|
|
|
from code_review_graph.communities import get_communities
|
|
stored = get_communities(self.store)
|
|
assert len(stored) > 0
|
|
|
|
page = _generate_community_page(self.store, stored[0])
|
|
assert "## Overview" in page
|
|
assert "## Members" in page
|
|
assert "## Execution Flows" in page
|
|
assert "## Dependencies" in page
|
|
|
|
def test_slugify(self):
|
|
"""_slugify converts names to safe filenames."""
|
|
assert _slugify("auth-login") == "auth-login"
|
|
assert _slugify("My Community Name") == "my-community-name"
|
|
assert _slugify("") == "unnamed"
|
|
assert _slugify("auth/sub-cluster") == "auth-sub-cluster"
|
|
|
|
def test_generate_wiki_force_regenerates(self):
|
|
"""generate_wiki with force=True regenerates all pages."""
|
|
self._seed_communities()
|
|
|
|
# First generation
|
|
result1 = generate_wiki(self.store, self.wiki_dir)
|
|
assert result1["pages_generated"] >= 2
|
|
|
|
# Second generation without force - should be unchanged
|
|
result2 = generate_wiki(self.store, self.wiki_dir)
|
|
assert result2["pages_unchanged"] >= 1
|
|
|
|
# Third generation with force - should update all
|
|
result3 = generate_wiki(self.store, self.wiki_dir, force=True)
|
|
assert result3["pages_generated"] + result3["pages_updated"] >= 1
|
|
|
|
def test_generate_wiki_empty_graph(self):
|
|
"""generate_wiki on empty graph creates index with no communities."""
|
|
result = generate_wiki(self.store, self.wiki_dir)
|
|
assert result["pages_generated"] >= 1 # at least index.md
|
|
|
|
index_content = (Path(self.wiki_dir) / "index.md").read_text()
|
|
assert "Total communities" in index_content
|
|
assert "0" in index_content # 0 communities
|
|
|
|
def test_generate_wiki_handles_slug_collisions(self, monkeypatch):
|
|
"""Communities whose names slugify to the same string must each get
|
|
their own page — earlier behaviour silently overwrote the first
|
|
community's file with the second's content and counted the second
|
|
as an 'updated' page (see #222 follow-up).
|
|
"""
|
|
# Fake three communities whose _slugify outputs collide:
|
|
# "Data Processing" -> data-processing
|
|
# "data processing" -> data-processing
|
|
# "Data Processing" -> data-processing
|
|
colliding_communities = [
|
|
{
|
|
"name": "Data Processing", "size": 5, "cohesion": 0.9,
|
|
"dominant_language": "python", "description": "first",
|
|
"members": [], "member_qns": set(),
|
|
},
|
|
{
|
|
"name": "data processing", "size": 4, "cohesion": 0.8,
|
|
"dominant_language": "python", "description": "second",
|
|
"members": [], "member_qns": set(),
|
|
},
|
|
{
|
|
"name": "Data Processing", "size": 3, "cohesion": 0.7,
|
|
"dominant_language": "python", "description": "third",
|
|
"members": [], "member_qns": set(),
|
|
},
|
|
]
|
|
|
|
import code_review_graph.wiki as wiki_mod
|
|
monkeypatch.setattr(
|
|
wiki_mod, "get_communities", lambda store: colliding_communities,
|
|
)
|
|
|
|
result = generate_wiki(self.store, self.wiki_dir)
|
|
|
|
# 3 unique .md pages + 1 index.md should land on disk.
|
|
wiki_files = sorted(p.name for p in Path(self.wiki_dir).glob("*.md"))
|
|
expected = {
|
|
"data-processing.md",
|
|
"data-processing-2.md",
|
|
"data-processing-3.md",
|
|
"index.md",
|
|
}
|
|
assert set(wiki_files) == expected, wiki_files
|
|
|
|
# Counter must match what actually hit the disk.
|
|
page_total = (
|
|
result["pages_generated"]
|
|
+ result["pages_updated"]
|
|
+ result["pages_unchanged"]
|
|
)
|
|
assert page_total == len(wiki_files), (
|
|
f"counter {result} but {len(wiki_files)} files on disk"
|
|
)
|
|
|
|
# Every community's description must survive (no data loss).
|
|
content_first = (Path(self.wiki_dir) / "data-processing.md").read_text()
|
|
content_second = (Path(self.wiki_dir) / "data-processing-2.md").read_text()
|
|
content_third = (Path(self.wiki_dir) / "data-processing-3.md").read_text()
|
|
# Descriptions are rendered in the page body; make sure all three
|
|
# communities produced distinct content.
|
|
assert content_first != content_second
|
|
assert content_first != content_third
|
|
assert content_second != content_third
|