Skip to content

Commit c722338

Browse files
committed
tests
1 parent ce45134 commit c722338

File tree

9 files changed

+274
-97
lines changed

9 files changed

+274
-97
lines changed

.github/workflows/main.yml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,19 @@ jobs:
7676
steps:
7777
- name: Checkout source
7878
uses: actions/checkout@v4
79-
- name: Set up Python 3.8
79+
with:
80+
fetch-depth: 0
81+
- name: Set up Python 3.11
8082
uses: actions/setup-python@v5
8183
with:
82-
python-version: "3.8"
83-
- name: install flit
84+
python-version: "3.11"
85+
- name: Install flit
86+
run: pip install flit
87+
- name: Verify version
8488
run: |
85-
pip install flit~=3.4
89+
python -c "from src.autodoc2 import __version__; print(f'Publishing version: {__version__}')"
8690
- name: Build and publish
87-
run: |
88-
flit publish
91+
run: flit publish
8992
env:
9093
FLIT_USERNAME: __token__
91-
FLIT_PASSWORD: ${{ secrets.PYPI_TOKEN }}
94+
FLIT_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,5 @@ cython_debug/
153153
.vscode/
154154
docs/_build/
155155
_autodoc/
156+
157+
**/.DS_Store

src/autodoc2/__init__.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,23 @@
33
A simplified fork of sphinx-autodoc2 focused purely on Python → Fern markdown output.
44
"""
55

6-
__version__ = "0.1.1"
6+
import subprocess
7+
import os
8+
9+
def _get_version():
10+
"""Get version from git tag or fallback to default."""
11+
try:
12+
if os.path.exists('.git'):
13+
result = subprocess.run(
14+
['git', 'describe', '--tags', '--exact-match', 'HEAD'],
15+
capture_output=True, text=True, check=True
16+
)
17+
version = result.stdout.strip()
18+
return version[1:] if version.startswith('v') else version
19+
except (subprocess.CalledProcessError, FileNotFoundError):
20+
pass
21+
22+
# Fallback version for development
23+
return "0.1.2-dev"
24+
25+
__version__ = _get_version()

src/autodoc2/cli.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,23 @@ def _warn(msg: str, type_: WarningSubtypes) -> None:
283283
nav_path.write_text(nav_content, "utf8")
284284
console.print(f"Navigation written to: {nav_path}")
285285

286+
# Validate all links
287+
console.print("[bold]Validating links...[/bold]")
288+
validation_results = renderer_instance.validate_all_links(str(output))
289+
290+
if validation_results["errors"]:
291+
console.print(f"[red]❌ {len(validation_results['errors'])} link errors found:[/red]")
292+
for error in validation_results["errors"]:
293+
console.print(f" [red]• {error}[/red]")
294+
295+
if validation_results["warnings"]:
296+
console.print(f"[yellow]⚠️ {len(validation_results['warnings'])} link warnings:[/yellow]")
297+
for warning in validation_results["warnings"]:
298+
console.print(f" [yellow]• {warning}[/yellow]")
299+
300+
if not validation_results["errors"] and not validation_results["warnings"]:
301+
console.print("[green]✅ All links validated successfully![/green]")
302+
286303
# remove any files that are no longer needed
287304
if clean:
288305
console.print("[bold]Cleaning old files[/bold]")

tests/test_render.py

Lines changed: 225 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from pathlib import Path
44
from textwrap import dedent
5+
import yaml
6+
import re
57

68
from autodoc2.analysis import analyse_module
79
from autodoc2.config import Config
@@ -12,26 +14,243 @@
1214

1315

1416
def test_basic(tmp_path: Path, file_regression):
15-
"""Test basic rendering."""
17+
"""Test basic rendering - minimal snapshot test for regression detection."""
1618
package = build_package(tmp_path)
1719
db = InMemoryDb()
1820
for path, modname in yield_modules(package):
1921
for item in analyse_module(path, modname):
2022
db.add(item)
2123
content = "\n".join(FernRenderer(db, Config()).render_item(package.name))
22-
file_regression.check(content, extension=".md")
24+
file_regression.check(content, extension=".mdx")
2325

2426

25-
def test_config_options(tmp_path: Path, file_regression):
26-
"""Test basic rendering."""
27+
def test_link_validation(tmp_path: Path):
28+
"""Test that all generated links are valid and follow correct patterns."""
2729
package = build_package(tmp_path)
2830
db = InMemoryDb()
2931
for path, modname in yield_modules(package):
3032
for item in analyse_module(path, modname):
3133
db.add(item)
34+
35+
renderer = FernRenderer(db, Config())
36+
37+
# Test link validation method works (if available)
38+
if hasattr(renderer, 'validate_all_links'):
39+
validation_results = renderer.validate_all_links()
40+
assert isinstance(validation_results, dict), "validate_all_links should return dict"
41+
assert "errors" in validation_results, "Should have errors key"
42+
assert "warnings" in validation_results, "Should have warnings key"
43+
assert isinstance(validation_results["errors"], list), "Errors should be list"
44+
assert isinstance(validation_results["warnings"], list), "Warnings should be list"
45+
46+
# Should have no errors on our test package
47+
assert not validation_results["errors"], f"Found link errors: {validation_results['errors']}"
48+
49+
# Test specific link patterns in rendered content
50+
content = "\n".join(renderer.render_item(package.name))
51+
52+
# Check for same-page anchor links (items within same package)
53+
assert re.search(r'\[`\w+`\]\(#\w+\)', content), "Should have same-page anchor links"
54+
55+
# Check for cross-page links (subpackages/submodules)
56+
if "package-a" in content:
57+
assert re.search(r'\[`\w+`\]\([\w-]+\)', content), "Should have cross-page links"
58+
59+
60+
def test_anchor_generation(tmp_path: Path):
61+
"""Test that anchor IDs are generated correctly."""
62+
package = build_package(tmp_path)
63+
db = InMemoryDb()
64+
for path, modname in yield_modules(package):
65+
for item in analyse_module(path, modname):
66+
db.add(item)
67+
68+
renderer = FernRenderer(db, Config())
69+
content = "\n".join(renderer.render_item(package.name))
70+
71+
# Test that anchors exist in the rendered content
72+
# Look for anchor patterns like #packageclass, #packagefunc
73+
assert re.search(r'#package\w+', content), "Should have anchor links with package prefix"
74+
75+
# Test that rendered content has some consistent anchor pattern
76+
anchor_matches = re.findall(r'\(#(\w+)\)', content)
77+
assert len(anchor_matches) > 0, "Should have at least one anchor link"
78+
79+
# Anchors should be lowercase and contain no dots or special chars
80+
for anchor in anchor_matches:
81+
assert anchor.islower(), f"Anchor should be lowercase: {anchor}"
82+
assert '.' not in anchor, f"Anchor should not contain dots: {anchor}"
83+
84+
85+
def test_cross_reference_linking(tmp_path: Path):
86+
"""Test that cross-reference links work correctly with context awareness."""
87+
package = build_package(tmp_path)
88+
db = InMemoryDb()
89+
for path, modname in yield_modules(package):
90+
for item in analyse_module(path, modname):
91+
db.add(item)
92+
93+
renderer = FernRenderer(db, Config())
94+
content = "\n".join(renderer.render_item(package.name))
95+
96+
# Test that we have both types of links
97+
# Same-page links (just anchor): [`Class`](#packageclass)
98+
same_page_links = re.findall(r'\[`\w+`\]\(#\w+\)', content)
99+
assert len(same_page_links) > 0, "Should have same-page anchor links"
100+
101+
# Cross-page links (page + anchor): [`submod`](package-submod#anchor)
102+
cross_page_links = re.findall(r'\[`\w+`\]\([\w-]+(?:#\w+)?\)', content)
103+
# Note: cross-page links may or may not have anchors depending on target type
104+
105+
# Test link format consistency
106+
for link in same_page_links:
107+
# Should start with backticks and have # anchor
108+
assert link.startswith('[`') and ')' in link and '#' in link, f"Malformed same-page link: {link}"
109+
110+
# Test that items in summary tables link correctly
111+
# Classes and functions should link to their anchors on the same page
112+
class_item = renderer.get_item("package.Class")
113+
func_item = renderer.get_item("package.func")
114+
115+
if class_item:
116+
# Should find Class linked with an anchor in the content
117+
assert re.search(r'\[`Class`\]\(#package\w*class\w*\)', content), "Class should be linked with anchor"
118+
119+
if func_item:
120+
# Should find func linked with an anchor in the content
121+
assert re.search(r'\[`func`\]\(#package\w*func\w*\)', content), "Function should be linked with anchor"
122+
123+
124+
def test_rendering_pipeline(tmp_path: Path):
125+
"""Test that the full rendering pipeline works without crashes."""
126+
package = build_package(tmp_path)
127+
db = InMemoryDb()
128+
for path, modname in yield_modules(package):
129+
for item in analyse_module(path, modname):
130+
db.add(item)
131+
132+
renderer = FernRenderer(db, Config())
133+
134+
# Test each item type can be rendered without crashes
135+
for item_type in ["package", "module", "class", "function", "data"]:
136+
items = list(db.get_by_type(item_type))
137+
if items:
138+
item = items[0]
139+
try:
140+
content_lines = list(renderer.render_item(item["full_name"]))
141+
assert content_lines, f"Empty output for {item_type}: {item['full_name']}"
142+
content = "\n".join(content_lines)
143+
assert len(content) > 10, f"Suspiciously short content for {item_type}"
144+
except Exception as e:
145+
pytest.fail(f"Rendering {item_type} {item['full_name']} crashed: {e}")
146+
147+
148+
def test_frontmatter_structure(tmp_path: Path):
149+
"""Test that frontmatter is generated correctly."""
150+
package = build_package(tmp_path)
151+
db = InMemoryDb()
152+
for path, modname in yield_modules(package):
153+
for item in analyse_module(path, modname):
154+
db.add(item)
155+
156+
renderer = FernRenderer(db, Config())
157+
content = "\n".join(renderer.render_item(package.name))
158+
159+
# Should start with frontmatter
160+
assert content.startswith("---\n"), "Content should start with frontmatter"
161+
162+
# Extract frontmatter
163+
parts = content.split("---\n")
164+
assert len(parts) >= 3, "Should have opening ---, frontmatter, closing ---, content"
165+
166+
frontmatter = parts[1].strip()
167+
assert frontmatter, "Frontmatter should not be empty"
168+
169+
# Parse as valid YAML
170+
try:
171+
fm_data = yaml.safe_load(frontmatter)
172+
assert isinstance(fm_data, dict), "Frontmatter should be valid YAML dict"
173+
assert "layout" in fm_data, "Should have layout field"
174+
assert "slug" in fm_data, "Should have slug field"
175+
assert fm_data["layout"] == "overview", "Layout should be overview"
176+
except yaml.YAMLError as e:
177+
pytest.fail(f"Invalid frontmatter YAML: {e}")
178+
179+
180+
def test_code_block_structure(tmp_path: Path):
181+
"""Test that code blocks are properly formatted and closed."""
182+
package = build_package(tmp_path)
183+
db = InMemoryDb()
184+
for path, modname in yield_modules(package):
185+
for item in analyse_module(path, modname):
186+
db.add(item)
187+
188+
renderer = FernRenderer(db, Config())
189+
content = "\n".join(renderer.render_item(package.name))
190+
191+
# Count code block delimiters
192+
python_blocks = content.count("```python")
193+
closing_blocks = content.count("```\n") + content.count("```</CodeBlock>")
194+
195+
assert python_blocks > 0, "Should have at least one Python code block"
196+
assert python_blocks <= closing_blocks, f"Unmatched code blocks: {python_blocks} opening, {closing_blocks} closing"
197+
198+
# Check for proper code block content
199+
assert "def " in content or "class " in content, "Should have function or class definitions in code blocks"
200+
201+
202+
def test_navigation_generation(tmp_path: Path):
203+
"""Test that navigation.yml is generated correctly."""
204+
package = build_package(tmp_path)
205+
db = InMemoryDb()
206+
for path, modname in yield_modules(package):
207+
for item in analyse_module(path, modname):
208+
db.add(item)
209+
210+
renderer = FernRenderer(db, Config())
211+
nav_yaml = renderer.generate_navigation_yaml()
212+
213+
assert nav_yaml, "Navigation YAML should not be empty"
214+
215+
# Parse as valid YAML
216+
try:
217+
nav_data = yaml.safe_load(nav_yaml)
218+
assert isinstance(nav_data, dict), "Navigation should be a dict with 'navigation' key"
219+
assert "navigation" in nav_data, "Should have 'navigation' key"
220+
221+
nav_list = nav_data["navigation"]
222+
assert isinstance(nav_list, list), "Navigation value should be a list"
223+
assert len(nav_list) > 0, "Navigation should have at least one item"
224+
225+
# Check structure of navigation items
226+
for item in nav_list:
227+
assert isinstance(item, dict), "Navigation items should be dicts"
228+
# Navigation can have 'section' or 'page' entries
229+
assert "section" in item or "page" in item, f"Navigation item missing 'section' or 'page': {item}"
230+
231+
except yaml.YAMLError as e:
232+
pytest.fail(f"Invalid navigation YAML: {e}")
233+
234+
235+
def test_config_options_functional(tmp_path: Path):
236+
"""Test that config options work correctly (functional test, not snapshot)."""
237+
package = build_package(tmp_path)
238+
db = InMemoryDb()
239+
for path, modname in yield_modules(package):
240+
for item in analyse_module(path, modname):
241+
db.add(item)
242+
243+
# Test with no_index=True
32244
config = Config(no_index=True)
33-
content = "\n".join(FernRenderer(db, config).render_item(package.name + ".func"))
34-
file_regression.check(content, extension=".md")
245+
renderer = FernRenderer(db, config)
246+
247+
func_content = "\n".join(renderer.render_item(package.name + ".func"))
248+
assert func_content, "Should render function content"
249+
assert "```python" in func_content, "Should contain code block"
250+
assert "This is a function" in func_content, "Should contain docstring"
251+
252+
# Test basic rendering works
253+
assert len(func_content.split('\n')) > 3, "Should have multiple lines of content"
35254

36255

37256

File renamed without changes.

0 commit comments

Comments
 (0)