- Created src/skill_seekers/cli/api_reference_builder.py (330 lines) - Generates markdown API documentation from code analysis results - Supports Python, JavaScript/TypeScript, and C++ code signatures Features: - Class documentation with inheritance and methods - Function/method signatures with parameters and return types - Parameter tables with types and defaults - Async function indicators - Decorators display (for Python) - Standalone CLI tool for generating API docs from JSON Tests: - Created tests/test_api_reference_builder.py with 7 tests - All tests passing ✅ - Test coverage: Class formatting, function formatting, parameter tables, markdown structure, code analyzer integration, async indicators Output Format: - One .md file per analyzed source file - Organized: Classes → Methods, then standalone Functions - Professional markdown tables for parameters CLI Usage: python -m skill_seekers.cli.api_reference_builder \ code_analysis.json output/api_reference/ Related Issues: - Closes #66 (C2.4 Build API reference from code) - Part of C2 Local Codebase Scraping roadmap (TIER 3)
335 lines
12 KiB
Python
335 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for api_reference_builder.py - Markdown API documentation generation.
|
|
|
|
Test Coverage:
|
|
- Class formatting
|
|
- Function formatting
|
|
- Parameter table generation
|
|
- Markdown output structure
|
|
- Integration with code analysis results
|
|
"""
|
|
|
|
import unittest
|
|
import tempfile
|
|
import shutil
|
|
from pathlib import Path
|
|
import sys
|
|
import os
|
|
|
|
# Add src to path
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
|
|
from skill_seekers.cli.api_reference_builder import APIReferenceBuilder
|
|
|
|
|
|
class TestAPIReferenceBuilder(unittest.TestCase):
|
|
"""Tests for API reference builder"""
|
|
|
|
def setUp(self):
|
|
"""Set up test environment"""
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.output_dir = Path(self.temp_dir) / "api_reference"
|
|
|
|
def tearDown(self):
|
|
"""Clean up test environment"""
|
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
|
|
def test_class_formatting(self):
|
|
"""Test markdown formatting for class signatures."""
|
|
code_analysis = {
|
|
'files': [{
|
|
'file': 'test.py',
|
|
'language': 'Python',
|
|
'classes': [{
|
|
'name': 'Calculator',
|
|
'docstring': 'A simple calculator class.',
|
|
'base_classes': ['object'],
|
|
'methods': [{
|
|
'name': 'add',
|
|
'parameters': [
|
|
{'name': 'a', 'type_hint': 'int', 'default': None},
|
|
{'name': 'b', 'type_hint': 'int', 'default': None}
|
|
],
|
|
'return_type': 'int',
|
|
'docstring': 'Add two numbers.',
|
|
'is_async': False,
|
|
'is_method': True,
|
|
'decorators': []
|
|
}]
|
|
}],
|
|
'functions': []
|
|
}]
|
|
}
|
|
|
|
builder = APIReferenceBuilder(code_analysis)
|
|
generated = builder.build_reference(self.output_dir)
|
|
|
|
# Verify file was generated
|
|
self.assertEqual(len(generated), 1)
|
|
output_file = list(generated.values())[0]
|
|
self.assertTrue(output_file.exists())
|
|
|
|
# Verify content
|
|
content = output_file.read_text()
|
|
self.assertIn('### Calculator', content)
|
|
self.assertIn('A simple calculator class', content)
|
|
self.assertIn('**Inherits from**: object', content)
|
|
self.assertIn('##### add', content)
|
|
self.assertIn('Add two numbers', content)
|
|
|
|
def test_function_formatting(self):
|
|
"""Test markdown formatting for function signatures."""
|
|
code_analysis = {
|
|
'files': [{
|
|
'file': 'utils.py',
|
|
'language': 'Python',
|
|
'classes': [],
|
|
'functions': [{
|
|
'name': 'calculate_sum',
|
|
'parameters': [
|
|
{'name': 'numbers', 'type_hint': 'list', 'default': None}
|
|
],
|
|
'return_type': 'int',
|
|
'docstring': 'Calculate sum of numbers.',
|
|
'is_async': False,
|
|
'is_method': False,
|
|
'decorators': []
|
|
}]
|
|
}]
|
|
}
|
|
|
|
builder = APIReferenceBuilder(code_analysis)
|
|
generated = builder.build_reference(self.output_dir)
|
|
|
|
# Verify content
|
|
output_file = list(generated.values())[0]
|
|
content = output_file.read_text()
|
|
|
|
self.assertIn('## Functions', content)
|
|
self.assertIn('### calculate_sum', content)
|
|
self.assertIn('Calculate sum of numbers', content)
|
|
self.assertIn('**Returns**: `int`', content)
|
|
|
|
def test_parameter_table_generation(self):
|
|
"""Test parameter table formatting."""
|
|
code_analysis = {
|
|
'files': [{
|
|
'file': 'test.py',
|
|
'language': 'Python',
|
|
'classes': [],
|
|
'functions': [{
|
|
'name': 'create_user',
|
|
'parameters': [
|
|
{'name': 'name', 'type_hint': 'str', 'default': None},
|
|
{'name': 'age', 'type_hint': 'int', 'default': '18'},
|
|
{'name': 'active', 'type_hint': 'bool', 'default': 'True'}
|
|
],
|
|
'return_type': 'dict',
|
|
'docstring': 'Create a user object.',
|
|
'is_async': False,
|
|
'is_method': False,
|
|
'decorators': []
|
|
}]
|
|
}]
|
|
}
|
|
|
|
builder = APIReferenceBuilder(code_analysis)
|
|
generated = builder.build_reference(self.output_dir)
|
|
|
|
# Verify parameter table
|
|
output_file = list(generated.values())[0]
|
|
content = output_file.read_text()
|
|
|
|
self.assertIn('**Parameters**:', content)
|
|
self.assertIn('| Name | Type | Default | Description |', content)
|
|
self.assertIn('| name | str | - |', content) # Parameters with no default show "-"
|
|
self.assertIn('| age | int | 18 |', content)
|
|
self.assertIn('| active | bool | True |', content)
|
|
|
|
def test_markdown_output_structure(self):
|
|
"""Test overall markdown document structure."""
|
|
code_analysis = {
|
|
'files': [{
|
|
'file': 'module.py',
|
|
'language': 'Python',
|
|
'classes': [{
|
|
'name': 'TestClass',
|
|
'docstring': 'Test class.',
|
|
'base_classes': [],
|
|
'methods': []
|
|
}],
|
|
'functions': [{
|
|
'name': 'test_func',
|
|
'parameters': [],
|
|
'return_type': None,
|
|
'docstring': 'Test function.',
|
|
'is_async': False,
|
|
'is_method': False,
|
|
'decorators': []
|
|
}]
|
|
}]
|
|
}
|
|
|
|
builder = APIReferenceBuilder(code_analysis)
|
|
generated = builder.build_reference(self.output_dir)
|
|
|
|
# Verify structure
|
|
output_file = list(generated.values())[0]
|
|
content = output_file.read_text()
|
|
|
|
# Check header
|
|
self.assertIn('# API Reference: module.py', content)
|
|
self.assertIn('**Language**: Python', content)
|
|
self.assertIn('**Source**: `module.py`', content)
|
|
|
|
# Check sections in order
|
|
classes_pos = content.find('## Classes')
|
|
functions_pos = content.find('## Functions')
|
|
|
|
self.assertNotEqual(classes_pos, -1)
|
|
self.assertNotEqual(functions_pos, -1)
|
|
self.assertLess(classes_pos, functions_pos)
|
|
|
|
def test_integration_with_code_analyzer(self):
|
|
"""Test integration with actual code analyzer output format."""
|
|
# Simulate real code analyzer output
|
|
code_analysis = {
|
|
'files': [
|
|
{
|
|
'file': 'calculator.py',
|
|
'language': 'Python',
|
|
'classes': [{
|
|
'name': 'Calculator',
|
|
'base_classes': [],
|
|
'methods': [
|
|
{
|
|
'name': 'add',
|
|
'parameters': [
|
|
{'name': 'a', 'type_hint': 'float', 'default': None},
|
|
{'name': 'b', 'type_hint': 'float', 'default': None}
|
|
],
|
|
'return_type': 'float',
|
|
'docstring': 'Add two numbers.',
|
|
'decorators': [],
|
|
'is_async': False,
|
|
'is_method': True
|
|
}
|
|
],
|
|
'docstring': 'Calculator class.',
|
|
'line_number': 1
|
|
}],
|
|
'functions': []
|
|
},
|
|
{
|
|
'file': 'utils.js',
|
|
'language': 'JavaScript',
|
|
'classes': [],
|
|
'functions': [{
|
|
'name': 'formatDate',
|
|
'parameters': [
|
|
{'name': 'date', 'type_hint': None, 'default': None}
|
|
],
|
|
'return_type': None,
|
|
'docstring': None,
|
|
'is_async': False,
|
|
'is_method': False,
|
|
'decorators': []
|
|
}]
|
|
}
|
|
]
|
|
}
|
|
|
|
builder = APIReferenceBuilder(code_analysis)
|
|
generated = builder.build_reference(self.output_dir)
|
|
|
|
# Verify multiple files generated
|
|
self.assertEqual(len(generated), 2)
|
|
|
|
# Verify filenames
|
|
filenames = [f.name for f in generated.values()]
|
|
self.assertIn('calculator.md', filenames)
|
|
self.assertIn('utils.md', filenames)
|
|
|
|
# Verify Python file content
|
|
py_file = next(f for f in generated.values() if f.name == 'calculator.md')
|
|
py_content = py_file.read_text()
|
|
self.assertIn('Calculator class', py_content)
|
|
self.assertIn('add(a: float, b: float) → float', py_content)
|
|
|
|
# Verify JavaScript file content
|
|
js_file = next(f for f in generated.values() if f.name == 'utils.md')
|
|
js_content = js_file.read_text()
|
|
self.assertIn('formatDate', js_content)
|
|
self.assertIn('**Language**: JavaScript', js_content)
|
|
|
|
def test_async_function_indicator(self):
|
|
"""Test that async functions are marked in output."""
|
|
code_analysis = {
|
|
'files': [{
|
|
'file': 'async_utils.py',
|
|
'language': 'Python',
|
|
'classes': [],
|
|
'functions': [{
|
|
'name': 'fetch_data',
|
|
'parameters': [
|
|
{'name': 'url', 'type_hint': 'str', 'default': None}
|
|
],
|
|
'return_type': 'dict',
|
|
'docstring': 'Fetch data from URL.',
|
|
'is_async': True,
|
|
'is_method': False,
|
|
'decorators': []
|
|
}]
|
|
}]
|
|
}
|
|
|
|
builder = APIReferenceBuilder(code_analysis)
|
|
generated = builder.build_reference(self.output_dir)
|
|
|
|
# Verify async indicator
|
|
output_file = list(generated.values())[0]
|
|
content = output_file.read_text()
|
|
|
|
self.assertIn('**Async function**', content)
|
|
self.assertIn('fetch_data', content)
|
|
|
|
def test_empty_analysis_skipped(self):
|
|
"""Test that files with no analysis are skipped."""
|
|
code_analysis = {
|
|
'files': [
|
|
{
|
|
'file': 'empty.py',
|
|
'language': 'Python',
|
|
'classes': [],
|
|
'functions': []
|
|
},
|
|
{
|
|
'file': 'valid.py',
|
|
'language': 'Python',
|
|
'classes': [],
|
|
'functions': [{
|
|
'name': 'test',
|
|
'parameters': [],
|
|
'return_type': None,
|
|
'docstring': None,
|
|
'is_async': False,
|
|
'is_method': False,
|
|
'decorators': []
|
|
}]
|
|
}
|
|
]
|
|
}
|
|
|
|
builder = APIReferenceBuilder(code_analysis)
|
|
generated = builder.build_reference(self.output_dir)
|
|
|
|
# Only valid.py should be generated
|
|
self.assertEqual(len(generated), 1)
|
|
self.assertIn('valid.py', list(generated.keys())[0])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# Run tests with verbose output
|
|
unittest.main(verbosity=2)
|