Files
skill-seekers-reference/tests/test_api_reference_builder.py
yusyus 43063dc0d2 feat(C2.4): Add API reference generator from code signatures
- 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)
2026-01-01 23:00:36 +03:00

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)