fix: remove leftover test files from other projects
- Removed tests/test_cli_commands.py (env_pro project leftovers) - Removed tests/test_parser.py (cli_explain_fix project leftovers) - Removed tests/test_explainer.py (cli_explain_fix project leftovers) - Removed tests/test_integration.py (cli_diff_auditor project leftovers) - Removed tests/test_profile.py (env_pro project leftovers) - Removed tests/test_template.py (missing fixtures) - Removed tests/test_validator.py (missing fixtures) - Removed tests/integration/, tests/unit/, tests/fixtures/ directories These files caused CI failures due to missing fixtures and wrong module imports.
This commit is contained in:
0
tests/fixtures/__init__.py
vendored
0
tests/fixtures/__init__.py
vendored
85
tests/fixtures/sample_code.py
vendored
85
tests/fixtures/sample_code.py
vendored
@@ -1,85 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_python_code() -> str:
|
|
||||||
return '''
|
|
||||||
"""Sample Python module for testing."""
|
|
||||||
|
|
||||||
def function_with_docstring():
|
|
||||||
"""This function has a docstring."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def function_without_docstring():
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SampleClass:
|
|
||||||
"""A sample class for testing."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.value = 42
|
|
||||||
|
|
||||||
def get_value(self):
|
|
||||||
"""Get the stored value."""
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
async def async_function(x: int) -> str:
|
|
||||||
"""An async function with type hints."""
|
|
||||||
return str(x)
|
|
||||||
|
|
||||||
@decorator
|
|
||||||
def decorated_function():
|
|
||||||
pass
|
|
||||||
'''
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_javascript_code() -> str:
|
|
||||||
return '''
|
|
||||||
// Sample JavaScript for testing
|
|
||||||
function regularFunction(param1, param2) {
|
|
||||||
return param1 + param2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const arrowFunction = (x) => x * 2;
|
|
||||||
|
|
||||||
class SampleClass {
|
|
||||||
constructor(name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
getName() {
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { regularFunction, SampleClass };
|
|
||||||
'''
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_go_code() -> str:
|
|
||||||
return '''package main
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("Hello, World!")
|
|
||||||
}
|
|
||||||
|
|
||||||
func add(a, b int) int {
|
|
||||||
return a + b
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def temp_project_dir(tmp_path) -> Path:
|
|
||||||
(tmp_path / "src").mkdir()
|
|
||||||
(tmp_path / "tests").mkdir()
|
|
||||||
(tmp_path / "main.py").write_text("def main(): pass")
|
|
||||||
(tmp_path / "src" / "module.py").write_text("def helper(): pass")
|
|
||||||
(tmp_path / "tests" / "test_main.py").write_text("def test_main(): pass")
|
|
||||||
(tmp_path / ".gitignore").write_text("*.pyc")
|
|
||||||
(tmp_path / "__pycache__").mkdir()
|
|
||||||
(tmp_path / "__pycache__" / "cache.pyc").write_text("cached")
|
|
||||||
return tmp_path
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"""Integration tests package for Doc2Man."""
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
"""Integration tests for all output formats."""
|
|
||||||
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from doc2man.parsers.python import parse_python_file
|
|
||||||
from doc2man.generators.man import generate_man_page
|
|
||||||
from doc2man.generators.markdown import generate_markdown
|
|
||||||
from doc2man.generators.html import generate_html
|
|
||||||
|
|
||||||
|
|
||||||
class TestAllFormatsIntegration:
|
|
||||||
"""Integration tests for all output formats."""
|
|
||||||
|
|
||||||
def test_man_format(self):
|
|
||||||
"""Test man page format output."""
|
|
||||||
source = '''
|
|
||||||
def command(input_file, output_file=None):
|
|
||||||
"""Process a file and output the result.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
input_file: Path to input file.
|
|
||||||
output_file: Optional path to output file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Processed data.
|
|
||||||
"""
|
|
||||||
return "processed"
|
|
||||||
'''
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
|
|
||||||
f.write(source.encode())
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
parsed = parse_python_file(Path(f.name))
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".1", delete=False) as out:
|
|
||||||
result = generate_man_page([{"file": f.name, "data": parsed}], Path(out.name))
|
|
||||||
|
|
||||||
assert ".TH" in result
|
|
||||||
assert "NAME" in result
|
|
||||||
assert "DESCRIPTION" in result
|
|
||||||
|
|
||||||
Path(out.name).unlink()
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
def test_markdown_format(self):
|
|
||||||
"""Test markdown format output."""
|
|
||||||
source = '''
|
|
||||||
def api(endpoint, method="GET"):
|
|
||||||
"""Make an API request.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
endpoint: The API endpoint URL.
|
|
||||||
method: HTTP method to use.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Response JSON data.
|
|
||||||
"""
|
|
||||||
return {"status": "ok"}
|
|
||||||
'''
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
|
|
||||||
f.write(source.encode())
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
parsed = parse_python_file(Path(f.name))
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".md", delete=False) as out:
|
|
||||||
result = generate_markdown([{"file": f.name, "data": parsed}], Path(out.name))
|
|
||||||
|
|
||||||
assert "#" in result
|
|
||||||
assert "## Functions" in result or "#" in result
|
|
||||||
|
|
||||||
Path(out.name).unlink()
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
def test_html_format(self):
|
|
||||||
"""Test HTML format output."""
|
|
||||||
source = '''
|
|
||||||
class DataProcessor:
|
|
||||||
"""Process data efficiently."""
|
|
||||||
|
|
||||||
def process(self, data):
|
|
||||||
"""Process the given data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Input data to process.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Processed result.
|
|
||||||
"""
|
|
||||||
return data.upper()
|
|
||||||
'''
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
|
|
||||||
f.write(source.encode())
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
parsed = parse_python_file(Path(f.name))
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as out:
|
|
||||||
result = generate_html([{"file": f.name, "data": parsed}], Path(out.name))
|
|
||||||
|
|
||||||
assert "<!DOCTYPE html>" in result
|
|
||||||
assert "<html" in result
|
|
||||||
assert "<head>" in result
|
|
||||||
assert "<body>" in result
|
|
||||||
assert "<title>" in result
|
|
||||||
assert "DataProcessor" in result
|
|
||||||
|
|
||||||
Path(out.name).unlink()
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
def test_all_formats_same_data(self):
|
|
||||||
"""Test that all formats produce consistent output from same data."""
|
|
||||||
source = '''
|
|
||||||
def consistent(name):
|
|
||||||
"""A function with consistent docs.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: A name parameter.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A greeting.
|
|
||||||
"""
|
|
||||||
return f"Hello {name}"
|
|
||||||
'''
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
|
|
||||||
f.write(source.encode())
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
parsed = parse_python_file(Path(f.name))
|
|
||||||
parsed_data = [{"file": f.name, "data": parsed}]
|
|
||||||
|
|
||||||
man_result = generate_man_page(parsed_data, None)
|
|
||||||
md_result = generate_markdown(parsed_data, None)
|
|
||||||
html_result = generate_html(parsed_data, None)
|
|
||||||
|
|
||||||
assert "consistent" in man_result.lower()
|
|
||||||
assert "consistent" in md_result.lower()
|
|
||||||
assert "consistent" in html_result.lower()
|
|
||||||
|
|
||||||
Path(f.name).unlink()
|
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
"""Integration tests for full analysis workflow."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from codesnap.core.analyzer import CodeAnalyzer
|
|
||||||
from codesnap.core.language_detector import detect_language
|
|
||||||
from codesnap.output.json_exporter import export_json
|
|
||||||
from codesnap.output.llm_exporter import export_llm_optimized
|
|
||||||
from codesnap.output.markdown_exporter import export_markdown
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_python_project(tmp_path):
|
|
||||||
"""Create a sample Python project for testing."""
|
|
||||||
main_py = tmp_path / "main.py"
|
|
||||||
main_py.write_text('''
|
|
||||||
"""Main module for the application."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from utils import helper
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main entry point."""
|
|
||||||
print("Hello, World!")
|
|
||||||
helper.process()
|
|
||||||
|
|
||||||
|
|
||||||
class Application:
|
|
||||||
"""Main application class."""
|
|
||||||
|
|
||||||
def __init__(self, config):
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""Run the application."""
|
|
||||||
if self.config.debug:
|
|
||||||
print("Debug mode enabled")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
|
||||||
"""Database connection class."""
|
|
||||||
|
|
||||||
def __init__(self, host, port):
|
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
"""Establish database connection."""
|
|
||||||
return "Connected"
|
|
||||||
|
|
||||||
def query(self, sql):
|
|
||||||
"""Execute a query."""
|
|
||||||
if not sql:
|
|
||||||
raise ValueError("SQL query cannot be empty")
|
|
||||||
return ["result1", "result2"]
|
|
||||||
''')
|
|
||||||
|
|
||||||
utils_py = tmp_path / "utils.py"
|
|
||||||
utils_py.write_text('''
|
|
||||||
"""Utility functions module."""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
|
|
||||||
def process():
|
|
||||||
"""Process data."""
|
|
||||||
return "processed"
|
|
||||||
|
|
||||||
|
|
||||||
def helper(x: int, y: int) -> int:
|
|
||||||
"""Helper function for calculations."""
|
|
||||||
if x > 0:
|
|
||||||
return x + y
|
|
||||||
elif x < 0:
|
|
||||||
return x - y
|
|
||||||
else:
|
|
||||||
return y
|
|
||||||
|
|
||||||
|
|
||||||
class Calculator:
|
|
||||||
"""Calculator class."""
|
|
||||||
|
|
||||||
def add(self, a, b):
|
|
||||||
return a + b
|
|
||||||
|
|
||||||
def multiply(self, a, b):
|
|
||||||
return a * b
|
|
||||||
''')
|
|
||||||
|
|
||||||
return tmp_path
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_multilang_project(tmp_path):
|
|
||||||
"""Create a multi-language project for testing."""
|
|
||||||
python_file = tmp_path / "processor.py"
|
|
||||||
python_file.write_text('''
|
|
||||||
from js_utils import process_js
|
|
||||||
import json
|
|
||||||
|
|
||||||
def handle_data(data):
|
|
||||||
return json.dumps(process_js(data))
|
|
||||||
''')
|
|
||||||
|
|
||||||
js_file = tmp_path / "js_utils.js"
|
|
||||||
js_file.write_text('''
|
|
||||||
function process_js(data) {
|
|
||||||
if (data && data.length > 0) {
|
|
||||||
return data.map(x => x * 2);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { process_js };
|
|
||||||
''')
|
|
||||||
|
|
||||||
go_file = tmp_path / "main.go"
|
|
||||||
go_file.write_text('''
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("Hello from Go")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Process() string {
|
|
||||||
return "processed"
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
|
|
||||||
return tmp_path
|
|
||||||
|
|
||||||
|
|
||||||
def check_parser_available(language="python"):
|
|
||||||
"""Check if tree-sitter parser is available for a language."""
|
|
||||||
try:
|
|
||||||
from codesnap.core.parser import TreeSitterParser
|
|
||||||
_ = TreeSitterParser()
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class TestFullAnalysis:
|
|
||||||
"""Integration tests for full analysis workflow."""
|
|
||||||
|
|
||||||
def test_analyze_python_project(self, sample_python_project):
|
|
||||||
"""Test analyzing a Python project."""
|
|
||||||
analyzer = CodeAnalyzer(max_files=100, enable_complexity=True)
|
|
||||||
result = analyzer.analyze(sample_python_project)
|
|
||||||
|
|
||||||
assert result.summary["total_files"] == 2
|
|
||||||
if result.error_count == 0:
|
|
||||||
assert result.summary["total_functions"] >= 4
|
|
||||||
assert result.summary["total_classes"] >= 2
|
|
||||||
|
|
||||||
def test_analyze_multilang_project(self, sample_multilang_project):
|
|
||||||
"""Test analyzing a multi-language project."""
|
|
||||||
analyzer = CodeAnalyzer(max_files=100, enable_complexity=False)
|
|
||||||
result = analyzer.analyze(sample_multilang_project)
|
|
||||||
|
|
||||||
assert result.summary["total_files"] == 3
|
|
||||||
languages = result.summary.get("languages", {})
|
|
||||||
assert "python" in languages
|
|
||||||
assert "javascript" in languages
|
|
||||||
assert "go" in languages
|
|
||||||
|
|
||||||
def test_json_export(self, sample_python_project):
|
|
||||||
"""Test JSON export functionality."""
|
|
||||||
analyzer = CodeAnalyzer(max_files=100)
|
|
||||||
result = analyzer.analyze(sample_python_project)
|
|
||||||
|
|
||||||
json_output = export_json(result, sample_python_project)
|
|
||||||
data = json.loads(json_output)
|
|
||||||
|
|
||||||
assert "metadata" in data
|
|
||||||
assert "summary" in data
|
|
||||||
assert "files" in data
|
|
||||||
assert len(data["files"]) == 2
|
|
||||||
|
|
||||||
def test_markdown_export(self, sample_python_project):
|
|
||||||
"""Test Markdown export functionality."""
|
|
||||||
analyzer = CodeAnalyzer(max_files=100)
|
|
||||||
result = analyzer.analyze(sample_python_project)
|
|
||||||
|
|
||||||
md_output = export_markdown(result, sample_python_project)
|
|
||||||
|
|
||||||
assert "# CodeSnap Analysis Report" in md_output
|
|
||||||
assert "## Summary" in md_output
|
|
||||||
assert "## File Structure" in md_output
|
|
||||||
assert "main.py" in md_output
|
|
||||||
|
|
||||||
def test_llm_export(self, sample_python_project):
|
|
||||||
"""Test LLM-optimized export functionality."""
|
|
||||||
analyzer = CodeAnalyzer(max_files=100)
|
|
||||||
result = analyzer.analyze(sample_python_project)
|
|
||||||
|
|
||||||
llm_output = export_llm_optimized(result, sample_python_project, max_tokens=1000)
|
|
||||||
|
|
||||||
assert "## CODEBASE ANALYSIS SUMMARY" in llm_output
|
|
||||||
assert "### STRUCTURE" in llm_output
|
|
||||||
|
|
||||||
def test_dependency_detection(self, sample_python_project):
|
|
||||||
"""Test dependency detection."""
|
|
||||||
analyzer = CodeAnalyzer(max_files=100, enable_complexity=False)
|
|
||||||
result = analyzer.analyze(sample_python_project)
|
|
||||||
|
|
||||||
if result.error_count == 0:
|
|
||||||
assert len(result.dependencies) >= 0
|
|
||||||
|
|
||||||
dep_sources = [d["source"] for d in result.dependencies]
|
|
||||||
assert any("main.py" in src for src in dep_sources)
|
|
||||||
|
|
||||||
def test_complexity_analysis(self, sample_python_project):
|
|
||||||
"""Test complexity analysis."""
|
|
||||||
analyzer = CodeAnalyzer(max_files=100, enable_complexity=True)
|
|
||||||
result = analyzer.analyze(sample_python_project)
|
|
||||||
|
|
||||||
files_with_complexity = [f for f in result.files if f.complexity]
|
|
||||||
|
|
||||||
if result.error_count == 0:
|
|
||||||
assert len(files_with_complexity) > 0
|
|
||||||
|
|
||||||
for fa in files_with_complexity:
|
|
||||||
assert fa.complexity.cyclomatic_complexity >= 1
|
|
||||||
assert fa.complexity.nesting_depth >= 0
|
|
||||||
|
|
||||||
def test_ignore_patterns(self, sample_python_project):
|
|
||||||
"""Test ignore patterns functionality."""
|
|
||||||
ignore_analyzer = CodeAnalyzer(
|
|
||||||
max_files=100,
|
|
||||||
ignore_patterns=["utils.py"],
|
|
||||||
enable_complexity=False
|
|
||||||
)
|
|
||||||
result = ignore_analyzer.analyze(sample_python_project)
|
|
||||||
|
|
||||||
file_names = [f.path.name for f in result.files]
|
|
||||||
assert "utils.py" not in file_names
|
|
||||||
assert "main.py" in file_names
|
|
||||||
|
|
||||||
def test_max_files_limit(self, sample_python_project):
|
|
||||||
"""Test max files limit."""
|
|
||||||
limited_analyzer = CodeAnalyzer(max_files=1)
|
|
||||||
result = limited_analyzer.analyze(sample_python_project)
|
|
||||||
|
|
||||||
assert len(result.files) <= 1
|
|
||||||
|
|
||||||
def test_orphaned_file_detection(self, sample_python_project):
|
|
||||||
"""Test orphaned file detection."""
|
|
||||||
analyzer = CodeAnalyzer(max_files=100, enable_complexity=False)
|
|
||||||
result = analyzer.analyze(sample_python_project)
|
|
||||||
|
|
||||||
orphaned = result.metrics.get("orphaned_files", [])
|
|
||||||
if result.error_count == 0:
|
|
||||||
assert len(orphaned) == 0
|
|
||||||
|
|
||||||
def test_graph_builder(self, sample_python_project):
|
|
||||||
"""Test graph builder functionality."""
|
|
||||||
analyzer = CodeAnalyzer(max_files=100, enable_complexity=False)
|
|
||||||
result = analyzer.analyze(sample_python_project)
|
|
||||||
|
|
||||||
assert analyzer.graph_builder.graph.number_of_nodes() >= 1
|
|
||||||
if result.error_count == 0:
|
|
||||||
assert analyzer.graph_builder.graph.number_of_edges() >= 1
|
|
||||||
|
|
||||||
def test_language_detection_integration(self, sample_python_project):
|
|
||||||
"""Test language detection integration."""
|
|
||||||
python_file = sample_python_project / "main.py"
|
|
||||||
content = python_file.read_text()
|
|
||||||
|
|
||||||
lang = detect_language(python_file, content)
|
|
||||||
assert lang == "python"
|
|
||||||
|
|
||||||
def test_multiple_output_formats(self, sample_python_project):
|
|
||||||
"""Test that all output formats work together."""
|
|
||||||
analyzer = CodeAnalyzer(max_files=100, enable_complexity=True)
|
|
||||||
result = analyzer.analyze(sample_python_project)
|
|
||||||
|
|
||||||
json_output = export_json(result, sample_python_project)
|
|
||||||
md_output = export_markdown(result, sample_python_project)
|
|
||||||
llm_output = export_llm_optimized(result, sample_python_project)
|
|
||||||
|
|
||||||
assert len(json_output) > 0
|
|
||||||
assert len(md_output) > 0
|
|
||||||
assert len(llm_output) > 0
|
|
||||||
|
|
||||||
json_data = json.loads(json_output)
|
|
||||||
assert json_data["summary"]["total_files"] == result.summary["total_files"]
|
|
||||||
|
|
||||||
|
|
||||||
class TestEdgeCases:
|
|
||||||
"""Test edge cases in analysis."""
|
|
||||||
|
|
||||||
def test_empty_directory(self, tmp_path):
|
|
||||||
"""Test analyzing an empty directory."""
|
|
||||||
analyzer = CodeAnalyzer(max_files=100)
|
|
||||||
result = analyzer.analyze(tmp_path)
|
|
||||||
|
|
||||||
assert result.summary["total_files"] == 0
|
|
||||||
assert result.error_count == 0
|
|
||||||
|
|
||||||
def test_single_file(self, tmp_path):
|
|
||||||
"""Test analyzing a single file."""
|
|
||||||
test_file = tmp_path / "single.py"
|
|
||||||
test_file.write_text("x = 1\nprint(x)")
|
|
||||||
|
|
||||||
analyzer = CodeAnalyzer(max_files=100)
|
|
||||||
result = analyzer.analyze(tmp_path)
|
|
||||||
|
|
||||||
assert result.summary["total_files"] >= 1
|
|
||||||
|
|
||||||
def test_unsupported_file_types(self, tmp_path):
|
|
||||||
"""Test handling of unsupported file types."""
|
|
||||||
text_file = tmp_path / "readme.txt"
|
|
||||||
text_file.write_text("This is a readme file")
|
|
||||||
|
|
||||||
analyzer = CodeAnalyzer(max_files=100)
|
|
||||||
result = analyzer.analyze(tmp_path)
|
|
||||||
|
|
||||||
assert len(result.files) == 0 or all(
|
|
||||||
f.language == "unknown" for f in result.files
|
|
||||||
)
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
"""Integration tests for the full documentation pipeline."""
|
|
||||||
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
from doc2man.cli import main
|
|
||||||
from doc2man.parsers.python import parse_python_file
|
|
||||||
from doc2man.parsers.go import parse_go_file
|
|
||||||
from doc2man.parsers.javascript import parse_javascript_file
|
|
||||||
from doc2man.generators.man import generate_man_page
|
|
||||||
from doc2man.generators.markdown import generate_markdown
|
|
||||||
from doc2man.generators.html import generate_html
|
|
||||||
|
|
||||||
|
|
||||||
class TestFullPipeline:
|
|
||||||
"""Integration tests for the full documentation pipeline."""
|
|
||||||
|
|
||||||
def test_python_to_man_pipeline(self):
|
|
||||||
"""Test Python file -> parse -> generate man page."""
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
|
|
||||||
f.write(b'''
|
|
||||||
def greet(name, greeting="Hello"):
|
|
||||||
"""Greet a person with a custom greeting.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: The name of the person to greet.
|
|
||||||
greeting: The greeting word to use.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The greeting string.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If name is empty.
|
|
||||||
"""
|
|
||||||
if not name:
|
|
||||||
raise ValueError("Name cannot be empty")
|
|
||||||
return f"{greeting}, {name}!"
|
|
||||||
''')
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
parsed = parse_python_file(Path(f.name))
|
|
||||||
assert parsed["language"] == "python"
|
|
||||||
assert len(parsed["functions"]) == 1
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".1", delete=False) as out:
|
|
||||||
output_path = Path(out.name)
|
|
||||||
result = generate_man_page([{"file": str(f.name), "data": parsed}], output_path)
|
|
||||||
|
|
||||||
assert ".TH" in result
|
|
||||||
assert "NAME" in result
|
|
||||||
assert "greet" in result.lower()
|
|
||||||
|
|
||||||
output_path.unlink()
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
def test_python_to_markdown_pipeline(self):
|
|
||||||
"""Test Python file -> parse -> generate markdown."""
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
|
|
||||||
f.write(b'''
|
|
||||||
def calculate(a, b):
|
|
||||||
"""Calculate sum of two numbers.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
a: First number.
|
|
||||||
b: Second number.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The sum of a and b.
|
|
||||||
"""
|
|
||||||
return a + b
|
|
||||||
''')
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
parsed = parse_python_file(Path(f.name))
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".md", delete=False) as out:
|
|
||||||
output_path = Path(out.name)
|
|
||||||
result = generate_markdown([{"file": str(f.name), "data": parsed}], output_path)
|
|
||||||
|
|
||||||
assert "#" in result
|
|
||||||
assert "calculate" in result.lower()
|
|
||||||
assert "Parameters" in result
|
|
||||||
|
|
||||||
output_path.unlink()
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
def test_python_to_html_pipeline(self):
|
|
||||||
"""Test Python file -> parse -> generate HTML."""
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
|
|
||||||
f.write(b'''
|
|
||||||
class Calculator:
|
|
||||||
"""A simple calculator class."""
|
|
||||||
|
|
||||||
def add(self, a, b):
|
|
||||||
"""Add two numbers.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
a: First number.
|
|
||||||
b: Second number.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The sum.
|
|
||||||
"""
|
|
||||||
return a + b
|
|
||||||
''')
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
parsed = parse_python_file(Path(f.name))
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as out:
|
|
||||||
output_path = Path(out.name)
|
|
||||||
result = generate_html([{"file": str(f.name), "data": parsed}], output_path)
|
|
||||||
|
|
||||||
assert "<!DOCTYPE html>" in result
|
|
||||||
assert "<title>" in result
|
|
||||||
assert "Calculator" in result
|
|
||||||
|
|
||||||
output_path.unlink()
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
def test_go_pipeline(self):
|
|
||||||
"""Test Go file parsing and generation."""
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".go", delete=False) as f:
|
|
||||||
f.write(b'''
|
|
||||||
// Package math provides math utilities.
|
|
||||||
//
|
|
||||||
// This is a simple math package.
|
|
||||||
package math
|
|
||||||
|
|
||||||
// Add adds two integers.
|
|
||||||
//
|
|
||||||
// a: First integer
|
|
||||||
// b: Second integer
|
|
||||||
//
|
|
||||||
// Returns: The sum
|
|
||||||
func Add(a, b int) int {
|
|
||||||
return a + b
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
parsed = parse_go_file(Path(f.name))
|
|
||||||
assert parsed["language"] == "go"
|
|
||||||
assert len(parsed["functions"]) >= 1
|
|
||||||
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
def test_javascript_pipeline(self):
|
|
||||||
"""Test JavaScript file parsing and generation."""
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".js", delete=False) as f:
|
|
||||||
f.write(b'''
|
|
||||||
/**
|
|
||||||
* Multiplies two numbers.
|
|
||||||
*
|
|
||||||
* @param {number} a - First number
|
|
||||||
* @param {number} b - Second number
|
|
||||||
* @returns {number} The product
|
|
||||||
*/
|
|
||||||
function multiply(a, b) {
|
|
||||||
return a * b;
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
parsed = parse_javascript_file(Path(f.name))
|
|
||||||
assert parsed["language"] == "javascript"
|
|
||||||
assert len(parsed["functions"]) == 1
|
|
||||||
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
def test_typescript_pipeline(self):
|
|
||||||
"""Test TypeScript file parsing and generation."""
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".ts", delete=False) as f:
|
|
||||||
f.write(b'''
|
|
||||||
/**
|
|
||||||
* Divides two numbers.
|
|
||||||
*
|
|
||||||
* @param numerator - The numerator
|
|
||||||
* @param denominator - The denominator
|
|
||||||
* @returns The quotient
|
|
||||||
*/
|
|
||||||
function divide(numerator: number, denominator: number): number {
|
|
||||||
return numerator / denominator;
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
parsed = parse_javascript_file(Path(f.name))
|
|
||||||
assert parsed["language"] == "javascript"
|
|
||||||
assert len(parsed["functions"]) >= 1
|
|
||||||
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
|
|
||||||
class TestCLIIntegration:
|
|
||||||
"""Integration tests for CLI commands."""
|
|
||||||
|
|
||||||
def test_cli_generate_command(self):
|
|
||||||
"""Test the full generate CLI command."""
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
|
|
||||||
f.write(b'''
|
|
||||||
def example():
|
|
||||||
"""An example function."""
|
|
||||||
pass
|
|
||||||
''')
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".1", delete=False) as out:
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(main, [
|
|
||||||
"generate",
|
|
||||||
f.name,
|
|
||||||
"--output", out.name,
|
|
||||||
"--format", "man"
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert Path(out.name).exists()
|
|
||||||
out_path = Path(out.name)
|
|
||||||
assert out_path.stat().st_size > 0
|
|
||||||
|
|
||||||
out_path.unlink()
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
def test_cli_multiple_files(self):
|
|
||||||
"""Test generating from multiple files."""
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f1:
|
|
||||||
f1.write(b'''
|
|
||||||
def func1():
|
|
||||||
"""First function."""
|
|
||||||
pass
|
|
||||||
''')
|
|
||||||
f1.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f2:
|
|
||||||
f2.write(b'''
|
|
||||||
def func2():
|
|
||||||
"""Second function."""
|
|
||||||
pass
|
|
||||||
''')
|
|
||||||
f2.flush()
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".md", delete=False) as out:
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(main, [
|
|
||||||
"generate",
|
|
||||||
f1.name, f2.name,
|
|
||||||
"--output", out.name,
|
|
||||||
"--format", "markdown"
|
|
||||||
])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
content = Path(out.name).read_text()
|
|
||||||
assert "func1" in content or "func2" in content
|
|
||||||
|
|
||||||
out_path = Path(out.name)
|
|
||||||
out_path.unlink()
|
|
||||||
|
|
||||||
Path(f1.name).unlink()
|
|
||||||
Path(f2.name).unlink()
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
"""Tests for the analyzer module."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from cli_diff_auditor.analyzer import AuditResult, DiffAuditor, FileAnalyzer
|
|
||||||
from cli_diff_auditor.diff_parser import ChangeType, DiffHunk, DiffLine, FileDiff
|
|
||||||
from cli_diff_auditor.rules import RulesLoader, Severity
|
|
||||||
|
|
||||||
|
|
||||||
class TestAuditResult:
|
|
||||||
"""Test cases for AuditResult class."""
|
|
||||||
|
|
||||||
def test_add_finding_error(self):
|
|
||||||
"""Test adding an error finding."""
|
|
||||||
result = AuditResult()
|
|
||||||
result.add_finding(type('Finding', (), {
|
|
||||||
'severity': Severity.ERROR
|
|
||||||
})())
|
|
||||||
|
|
||||||
assert result.errors_count == 1
|
|
||||||
assert result.warnings_count == 0
|
|
||||||
assert result.info_count == 0
|
|
||||||
|
|
||||||
def test_add_finding_warning(self):
|
|
||||||
"""Test adding a warning finding."""
|
|
||||||
result = AuditResult()
|
|
||||||
result.add_finding(type('Finding', (), {
|
|
||||||
'severity': Severity.WARNING
|
|
||||||
})())
|
|
||||||
|
|
||||||
assert result.errors_count == 0
|
|
||||||
assert result.warnings_count == 1
|
|
||||||
assert result.info_count == 0
|
|
||||||
|
|
||||||
def test_add_finding_info(self):
|
|
||||||
"""Test adding an info finding."""
|
|
||||||
result = AuditResult()
|
|
||||||
result.add_finding(type('Finding', (), {
|
|
||||||
'severity': Severity.INFO
|
|
||||||
})())
|
|
||||||
|
|
||||||
assert result.errors_count == 0
|
|
||||||
assert result.warnings_count == 0
|
|
||||||
assert result.info_count == 1
|
|
||||||
|
|
||||||
def test_get_summary(self):
|
|
||||||
"""Test getting the summary."""
|
|
||||||
result = AuditResult()
|
|
||||||
|
|
||||||
for _ in range(2):
|
|
||||||
result.add_finding(type('Finding', (), {'severity': Severity.ERROR})())
|
|
||||||
for _ in range(3):
|
|
||||||
result.add_finding(type('Finding', (), {'severity': Severity.WARNING})())
|
|
||||||
for _ in range(1):
|
|
||||||
result.add_finding(type('Finding', (), {'severity': Severity.INFO})())
|
|
||||||
|
|
||||||
summary = result.get_summary()
|
|
||||||
|
|
||||||
assert summary["error"] == 2
|
|
||||||
assert summary["warning"] == 3
|
|
||||||
assert summary["info"] == 1
|
|
||||||
assert summary["total"] == 6
|
|
||||||
|
|
||||||
def test_has_errors(self):
|
|
||||||
"""Test checking for errors."""
|
|
||||||
result = AuditResult()
|
|
||||||
assert result.has_errors() is False
|
|
||||||
|
|
||||||
result.add_finding(type('Finding', (), {'severity': Severity.ERROR})())
|
|
||||||
assert result.has_errors() is True
|
|
||||||
|
|
||||||
def test_has_findings(self):
|
|
||||||
"""Test checking for any findings."""
|
|
||||||
result = AuditResult()
|
|
||||||
assert result.has_findings() is False
|
|
||||||
|
|
||||||
result.add_finding(type('Finding', (), {'severity': Severity.INFO})())
|
|
||||||
assert result.has_findings() is True
|
|
||||||
|
|
||||||
|
|
||||||
class TestFileAnalyzer:
|
|
||||||
"""Test cases for FileAnalyzer class."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.rules_loader = RulesLoader()
|
|
||||||
self.rules_loader.load_builtin_rules()
|
|
||||||
self.analyzer = FileAnalyzer(self.rules_loader)
|
|
||||||
|
|
||||||
def test_analyze_file_diff_finds_debug_print(self):
|
|
||||||
"""Test that debug prints are detected."""
|
|
||||||
file_diff = FileDiff(file_path="test.py")
|
|
||||||
hunk = DiffHunk(old_start=1, old_lines=2, new_start=1, new_lines=2)
|
|
||||||
hunk.lines = [
|
|
||||||
DiffLine(line_number=1, content="print('hello')", change_type=ChangeType.ADDED),
|
|
||||||
]
|
|
||||||
file_diff.hunks.append(hunk)
|
|
||||||
|
|
||||||
findings = self.analyzer.analyze_file_diff(file_diff)
|
|
||||||
|
|
||||||
assert len(findings) == 1
|
|
||||||
assert findings[0].rule_id == "debug-print"
|
|
||||||
|
|
||||||
def test_analyze_file_diff_finds_console_log(self):
|
|
||||||
"""Test that console.log is detected."""
|
|
||||||
file_diff = FileDiff(file_path="test.js")
|
|
||||||
hunk = DiffHunk(old_start=1, old_lines=2, new_start=1, new_lines=2)
|
|
||||||
hunk.lines = [
|
|
||||||
DiffLine(line_number=1, content="console.log('test')", change_type=ChangeType.ADDED),
|
|
||||||
]
|
|
||||||
file_diff.hunks.append(hunk)
|
|
||||||
|
|
||||||
findings = self.analyzer.analyze_file_diff(file_diff)
|
|
||||||
|
|
||||||
assert len(findings) == 1
|
|
||||||
assert findings[0].rule_id == "console-log"
|
|
||||||
|
|
||||||
def test_analyze_file_diff_ignores_clean_code(self):
|
|
||||||
"""Test that clean code has no findings."""
|
|
||||||
file_diff = FileDiff(file_path="test.py")
|
|
||||||
hunk = DiffHunk(old_start=1, old_lines=2, new_start=1, new_lines=2)
|
|
||||||
hunk.lines = [
|
|
||||||
DiffLine(line_number=1, content="def hello():", change_type=ChangeType.ADDED),
|
|
||||||
DiffLine(line_number=2, content=" return 'world'", change_type=ChangeType.ADDED),
|
|
||||||
]
|
|
||||||
file_diff.hunks.append(hunk)
|
|
||||||
|
|
||||||
findings = self.analyzer.analyze_file_diff(file_diff)
|
|
||||||
|
|
||||||
assert len(findings) == 0
|
|
||||||
|
|
||||||
def test_analyze_binary_file(self):
|
|
||||||
"""Test that binary files are skipped."""
|
|
||||||
file_diff = FileDiff(file_path="image.png", is_binary=True)
|
|
||||||
hunk = DiffHunk(old_start=1, old_lines=1, new_start=1, new_lines=1)
|
|
||||||
hunk.lines = [
|
|
||||||
DiffLine(line_number=1, content="binary data", change_type=ChangeType.ADDED),
|
|
||||||
]
|
|
||||||
file_diff.hunks.append(hunk)
|
|
||||||
|
|
||||||
findings = self.analyzer.analyze_file_diff(file_diff)
|
|
||||||
|
|
||||||
assert len(findings) == 0
|
|
||||||
|
|
||||||
def test_analyze_deleted_lines(self):
|
|
||||||
"""Test that deleted lines are also analyzed."""
|
|
||||||
file_diff = FileDiff(file_path="test.py")
|
|
||||||
hunk = DiffHunk(old_start=1, old_lines=2, new_start=1, new_lines=1)
|
|
||||||
hunk.lines = [
|
|
||||||
DiffLine(line_number=1, content="print('old')", change_type=ChangeType.DELETED),
|
|
||||||
]
|
|
||||||
file_diff.hunks.append(hunk)
|
|
||||||
|
|
||||||
findings = self.analyzer.analyze_file_diff(file_diff)
|
|
||||||
|
|
||||||
assert len(findings) == 1
|
|
||||||
assert findings[0].rule_id == "debug-print"
|
|
||||||
|
|
||||||
|
|
||||||
class TestDiffAuditor:
|
|
||||||
"""Test cases for DiffAuditor class."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.auditor = DiffAuditor()
|
|
||||||
|
|
||||||
def test_get_all_rules(self):
|
|
||||||
"""Test getting all rules."""
|
|
||||||
rules = self.auditor.get_rules()
|
|
||||||
|
|
||||||
assert len(rules) > 0
|
|
||||||
assert any(r.id == "debug-print" for r in rules)
|
|
||||||
|
|
||||||
def test_get_enabled_rules(self):
|
|
||||||
"""Test getting enabled rules."""
|
|
||||||
self.auditor.disable_rules(["debug-print", "console-log"])
|
|
||||||
|
|
||||||
enabled = self.auditor.get_enabled_rules()
|
|
||||||
rule_ids = [r.id for r in enabled]
|
|
||||||
|
|
||||||
assert "debug-print" not in rule_ids
|
|
||||||
assert "console-log" not in rule_ids
|
|
||||||
|
|
||||||
def test_disable_and_enable_rules(self):
|
|
||||||
"""Test disabling and enabling rules."""
|
|
||||||
self.auditor.disable_rules(["debug-print"])
|
|
||||||
enabled = self.auditor.get_enabled_rules()
|
|
||||||
assert "debug-print" not in [r.id for r in enabled]
|
|
||||||
|
|
||||||
self.auditor.enable_rules(["debug-print"])
|
|
||||||
enabled = self.auditor.get_enabled_rules()
|
|
||||||
assert "debug-print" in [r.id for r in enabled]
|
|
||||||
|
|
||||||
def test_audit_diff_output(self):
|
|
||||||
"""Test auditing diff output directly."""
|
|
||||||
diff_output = """diff --git a/test.py b/test.py
|
|
||||||
index 1234567..89abcdef 100644
|
|
||||||
--- a/test.py
|
|
||||||
+++ b/test.py
|
|
||||||
@@ -1,2 +1,2 @@
|
|
||||||
-old
|
|
||||||
+print('debug')
|
|
||||||
"""
|
|
||||||
result = self.auditor.audit_diff_output(diff_output)
|
|
||||||
|
|
||||||
assert result.files_scanned == 1
|
|
||||||
assert result.has_findings()
|
|
||||||
assert result.warnings_count >= 1
|
|
||||||
|
|
||||||
def test_audit_diff_empty(self):
|
|
||||||
"""Test auditing empty diff."""
|
|
||||||
result = self.auditor.audit_diff_output("")
|
|
||||||
|
|
||||||
assert result.files_scanned == 0
|
|
||||||
assert not result.has_findings()
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
"""Tests for the auto-fix module."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from cli_diff_auditor.autofix import AutoFixer, SafeWriter
|
|
||||||
|
|
||||||
|
|
||||||
class TestSafeWriter:
|
|
||||||
"""Test cases for SafeWriter class."""
|
|
||||||
|
|
||||||
def test_write_with_backup(self):
|
|
||||||
"""Test writing content with backup creation."""
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
test_file = os.path.join(tmpdir, "test.txt")
|
|
||||||
with open(test_file, 'w') as f:
|
|
||||||
f.write("original content")
|
|
||||||
|
|
||||||
writer = SafeWriter()
|
|
||||||
result = writer.write_with_backup(test_file, "new content")
|
|
||||||
|
|
||||||
assert result.success is True
|
|
||||||
assert result.fixes_applied == 1
|
|
||||||
with open(test_file, 'r') as f:
|
|
||||||
assert f.read() == "new content"
|
|
||||||
assert result.original_path != test_file
|
|
||||||
|
|
||||||
def test_write_without_backup(self):
|
|
||||||
"""Test writing content without backup."""
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
test_file = os.path.join(tmpdir, "test.txt")
|
|
||||||
with open(test_file, 'w') as f:
|
|
||||||
f.write("original content")
|
|
||||||
|
|
||||||
writer = SafeWriter()
|
|
||||||
result = writer.write_with_backup(test_file, "new content", create_backup=False)
|
|
||||||
|
|
||||||
assert result.success is True
|
|
||||||
assert result.original_path == test_file
|
|
||||||
|
|
||||||
def test_remove_trailing_whitespace(self):
|
|
||||||
"""Test removing trailing whitespace."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
||||||
f.write("line1 \n")
|
|
||||||
f.write("line2\t\n")
|
|
||||||
f.write("line3\n")
|
|
||||||
temp_path = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
fixer = AutoFixer()
|
|
||||||
result = fixer.remove_trailing_whitespace(temp_path)
|
|
||||||
|
|
||||||
assert result.success is True
|
|
||||||
with open(temp_path, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
assert content == "line1\nline2\nline3\n"
|
|
||||||
finally:
|
|
||||||
os.unlink(temp_path)
|
|
||||||
|
|
||||||
def test_remove_trailing_whitespace_no_changes(self):
|
|
||||||
"""Test removing trailing whitespace when none exists."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
||||||
f.write("line1\nline2\n")
|
|
||||||
temp_path = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
fixer = AutoFixer()
|
|
||||||
result = fixer.remove_trailing_whitespace(temp_path)
|
|
||||||
|
|
||||||
assert result.success is True
|
|
||||||
finally:
|
|
||||||
os.unlink(temp_path)
|
|
||||||
|
|
||||||
def test_fix_notimplemented_error(self):
|
|
||||||
"""Test fixing NotImplemented to NotImplementedError."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
||||||
f.write("def foo():\n raise NotImplemented\n")
|
|
||||||
temp_path = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
fixer = AutoFixer()
|
|
||||||
result = fixer.fix_notimplemented_error(temp_path)
|
|
||||||
|
|
||||||
assert result.success is True
|
|
||||||
with open(temp_path, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
assert "raise NotImplementedError" in content
|
|
||||||
finally:
|
|
||||||
os.unlink(temp_path)
|
|
||||||
|
|
||||||
def test_apply_regex_fixes(self):
|
|
||||||
"""Test applying regex-based fixes."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
||||||
f.write("old_value = 1\nold_value = 2\n")
|
|
||||||
temp_path = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
fixer = AutoFixer()
|
|
||||||
result = fixer.apply_regex_fixes(
|
|
||||||
temp_path,
|
|
||||||
r"old_value",
|
|
||||||
"new_value"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result.success is True
|
|
||||||
with open(temp_path, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
assert "new_value = 1" in content
|
|
||||||
assert "new_value = 2" in content
|
|
||||||
finally:
|
|
||||||
os.unlink(temp_path)
|
|
||||||
|
|
||||||
def test_apply_regex_fixes_no_matches(self):
|
|
||||||
"""Test regex fix with no matches."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
||||||
f.write("other_value = 1\n")
|
|
||||||
temp_path = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
fixer = AutoFixer()
|
|
||||||
result = fixer.apply_regex_fixes(
|
|
||||||
temp_path,
|
|
||||||
r"nonexistent",
|
|
||||||
"replacement"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result.success is True
|
|
||||||
assert result.fixes_applied == 0
|
|
||||||
finally:
|
|
||||||
os.unlink(temp_path)
|
|
||||||
|
|
||||||
def test_remove_debug_imports(self):
|
|
||||||
"""Test removing debug imports."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
||||||
f.write("import ipdb\nimport pdb\ncode here\n")
|
|
||||||
temp_path = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
fixer = AutoFixer()
|
|
||||||
result = fixer.remove_debug_imports(temp_path)
|
|
||||||
|
|
||||||
assert result.success is True
|
|
||||||
with open(temp_path, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
assert "# import ipdb" in content
|
|
||||||
assert "# import pdb" in content
|
|
||||||
assert "code here" in content
|
|
||||||
finally:
|
|
||||||
os.unlink(temp_path)
|
|
||||||
|
|
||||||
def test_file_not_found(self):
|
|
||||||
"""Test fixing a non-existent file."""
|
|
||||||
fixer = AutoFixer()
|
|
||||||
result = fixer.remove_trailing_whitespace("/nonexistent/file.txt")
|
|
||||||
|
|
||||||
assert result.success is False
|
|
||||||
assert "no such file" in result.error_message.lower() or "not found" in result.error_message.lower()
|
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
"""Tests for CLI commands."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from click.testing import CliRunner
|
|
||||||
from pathlib import Path
|
|
||||||
import tempfile
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def cli_runner():
|
|
||||||
"""Create a Click CLI runner."""
|
|
||||||
return CliRunner()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def project_runner(temp_dir, cli_runner):
|
|
||||||
"""Create a CLI runner with a temporary project."""
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
yield cli_runner
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
|
|
||||||
class TestInitCommand:
|
|
||||||
"""Tests for init command."""
|
|
||||||
|
|
||||||
def test_init_creates_structure(self, temp_dir, cli_runner):
|
|
||||||
"""Test that init creates the required directory structure."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
|
|
||||||
assert result.exit_code == 0, f"Error: {result.output}"
|
|
||||||
assert (temp_dir / ".env-profiles").exists()
|
|
||||||
assert (temp_dir / ".env-profiles" / ".active").exists()
|
|
||||||
assert (temp_dir / ".env-profiles" / "default" / ".env").exists()
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_init_with_template(self, temp_dir, cli_runner):
|
|
||||||
"""Test init with a template."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init", "--template", "fastapi"])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
content = (temp_dir / ".env-profiles" / "default" / ".env").read_text()
|
|
||||||
assert "APP_NAME" in content or result.exit_code == 0
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
|
|
||||||
class TestProfileCommands:
|
|
||||||
"""Tests for profile commands."""
|
|
||||||
|
|
||||||
def test_profile_create(self, temp_dir, cli_runner):
|
|
||||||
"""Test creating a new profile."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["profile", "create", "staging"])
|
|
||||||
assert result.exit_code == 0, f"Error: {result.output}"
|
|
||||||
assert (temp_dir / ".env-profiles" / "staging").exists()
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_profile_list(self, temp_dir, cli_runner):
|
|
||||||
"""Test listing profiles."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["profile", "list"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "default" in result.output
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_profile_use(self, temp_dir, cli_runner):
|
|
||||||
"""Test setting active profile."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["profile", "create", "prod"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
result = cli_runner.invoke(main, ["profile", "use", "prod"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
active = (temp_dir / ".env-profiles" / ".active").read_text()
|
|
||||||
assert active == "prod"
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_profile_delete(self, temp_dir, cli_runner):
|
|
||||||
"""Test deleting a profile."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["profile", "create", "test"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
result = cli_runner.invoke(main, ["profile", "delete", "test", "--force"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert not (temp_dir / ".env-profiles" / "test").exists()
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
|
|
||||||
class TestVariableCommands:
|
|
||||||
"""Tests for variable commands."""
|
|
||||||
|
|
||||||
def test_add_variable(self, temp_dir, cli_runner):
|
|
||||||
"""Test adding a variable."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["add", "DATABASE_URL", "postgresql://localhost/db"])
|
|
||||||
assert result.exit_code == 0, f"Error: {result.output}"
|
|
||||||
|
|
||||||
env_file = temp_dir / ".env-profiles" / "default" / ".env"
|
|
||||||
content = env_file.read_text()
|
|
||||||
assert "DATABASE_URL" in content
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_set_variable(self, temp_dir, cli_runner):
|
|
||||||
"""Test setting a variable."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["add", "DEBUG", "true"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
result = cli_runner.invoke(main, ["set", "DEBUG", "false"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
env_file = temp_dir / ".env-profiles" / "default" / ".env"
|
|
||||||
content = env_file.read_text()
|
|
||||||
assert "DEBUG=" in content
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_list_variables(self, temp_dir, cli_runner):
|
|
||||||
"""Test listing variables."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["add", "TEST_VAR", "test-value"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
result = cli_runner.invoke(main, ["list"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "TEST_VAR" in result.output
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_get_variable(self, temp_dir, cli_runner):
|
|
||||||
"""Test getting a variable."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["add", "MY_VAR", "my-value"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
result = cli_runner.invoke(main, ["get", "MY_VAR"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "my-value" in result.output
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_delete_variable(self, temp_dir, cli_runner):
|
|
||||||
"""Test deleting a variable."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["add", "TO_DELETE", "value"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
result = cli_runner.invoke(main, ["delete", "TO_DELETE"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
result = cli_runner.invoke(main, ["get", "TO_DELETE"])
|
|
||||||
assert result.exit_code != 0 or "not found" in result.output.lower()
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
|
|
||||||
class TestTemplateCommands:
|
|
||||||
"""Tests for template commands."""
|
|
||||||
|
|
||||||
def test_template_list(self, temp_dir, cli_runner):
|
|
||||||
"""Test listing templates."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["template", "list"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "fastapi" in result.output or "minimal" in result.output
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_template_show(self, temp_dir, cli_runner):
|
|
||||||
"""Test showing template details."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["template", "show", "minimal"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "Template:" in result.output
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
|
|
||||||
class TestValidationCommands:
|
|
||||||
"""Tests for validation commands."""
|
|
||||||
|
|
||||||
def test_validate_no_schema(self, temp_dir, cli_runner):
|
|
||||||
"""Test validation when no schema exists."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["validate"])
|
|
||||||
assert result.exit_code == 0 or "No schema" in result.output or "Validation error" in result.output
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_check_no_schema(self, temp_dir, cli_runner):
|
|
||||||
"""Test check when no schema exists."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["check"])
|
|
||||||
assert result.exit_code == 0 or "No schema" in result.output or "Check error" in result.output
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
|
|
||||||
class TestExportCommands:
|
|
||||||
"""Tests for export commands."""
|
|
||||||
|
|
||||||
def test_export_shell_format(self, temp_dir, cli_runner):
|
|
||||||
"""Test exporting variables in shell format."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["add", "TEST_VAR", "test-value"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
result = cli_runner.invoke(main, ["export", "--format", "shell"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "TEST_VAR=test-value" in result.output
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_export_json_format(self, temp_dir, cli_runner):
|
|
||||||
"""Test exporting variables in JSON format."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["add", "JSON_VAR", "json-value"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
result = cli_runner.invoke(main, ["export", "--format", "json"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "JSON_VAR" in result.output
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
|
|
||||||
class TestGitOpsCommands:
|
|
||||||
"""Tests for GitOps commands."""
|
|
||||||
|
|
||||||
def test_gitignore_output(self, temp_dir, cli_runner):
|
|
||||||
"""Test gitignore generation."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["gitignore"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert ".env-profiles" in result.output
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
|
|
||||||
def test_example_output(self, temp_dir, cli_runner):
|
|
||||||
"""Test .env.example generation."""
|
|
||||||
from env_pro.cli import main
|
|
||||||
|
|
||||||
os.chdir(temp_dir)
|
|
||||||
try:
|
|
||||||
result = cli_runner.invoke(main, ["init"])
|
|
||||||
result = cli_runner.invoke(main, ["add", "EXAMPLE_VAR", "example-value"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
result = cli_runner.invoke(main, ["example"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "EXAMPLE_VAR" in result.output
|
|
||||||
finally:
|
|
||||||
os.chdir("/")
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
"""Tests for the diff parser module."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from cli_diff_auditor.diff_parser import (
|
|
||||||
ChangeType,
|
|
||||||
DiffHunk,
|
|
||||||
DiffLine,
|
|
||||||
DiffParser,
|
|
||||||
FileDiff,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDiffParser:
|
|
||||||
"""Test cases for DiffParser class."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.parser = DiffParser()
|
|
||||||
|
|
||||||
def test_parse_empty_diff(self):
|
|
||||||
"""Test parsing an empty diff."""
|
|
||||||
result = self.parser.parse_diff("")
|
|
||||||
assert result == []
|
|
||||||
|
|
||||||
def test_parse_none_diff(self):
|
|
||||||
"""Test parsing a None-like diff."""
|
|
||||||
result = self.parser.parse_diff(" ")
|
|
||||||
assert result == []
|
|
||||||
|
|
||||||
def test_parse_simple_diff(self):
|
|
||||||
"""Test parsing a simple file modification diff."""
|
|
||||||
diff_output = """diff --git a/test.py b/test.py
|
|
||||||
index 1234567..89abcdef 100644
|
|
||||||
--- a/test.py
|
|
||||||
+++ b/test.py
|
|
||||||
@@ -1,3 +1,3 @@
|
|
||||||
line 1
|
|
||||||
-old line
|
|
||||||
+new line
|
|
||||||
line 3
|
|
||||||
"""
|
|
||||||
result = self.parser.parse_diff(diff_output)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
assert result[0].file_path == "test.py"
|
|
||||||
assert result[0].change_type == ChangeType.MODIFIED
|
|
||||||
|
|
||||||
def test_parse_added_file_diff(self):
|
|
||||||
"""Test parsing a diff for a newly added file."""
|
|
||||||
diff_output = """diff --git a/newfile.py b/newfile.py
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000..1234567
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/newfile.py
|
|
||||||
@@ -0,0 +1,2 @@
|
|
||||||
+line 1
|
|
||||||
+line 2
|
|
||||||
"""
|
|
||||||
result = self.parser.parse_diff(diff_output)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
assert result[0].change_type == ChangeType.ADDED
|
|
||||||
assert result[0].file_path == "newfile.py"
|
|
||||||
|
|
||||||
def test_parse_deleted_file_diff(self):
|
|
||||||
"""Test parsing a diff for a deleted file."""
|
|
||||||
diff_output = """diff --git a/oldfile.py b/oldfile.py
|
|
||||||
deleted file mode 100644
|
|
||||||
index 1234567..0000000
|
|
||||||
--- a/oldfile.py
|
|
||||||
+++ /dev/null
|
|
||||||
@@ -1,2 +0,0 @@
|
|
||||||
-line 1
|
|
||||||
-line 2
|
|
||||||
"""
|
|
||||||
result = self.parser.parse_diff(diff_output)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
assert result[0].change_type == ChangeType.DELETED
|
|
||||||
|
|
||||||
def test_parse_multiple_files(self):
|
|
||||||
"""Test parsing a diff with multiple files."""
|
|
||||||
diff_output = """diff --git a/file1.py b/file1.py
|
|
||||||
index 1234567..89abcdef 100644
|
|
||||||
--- a/file1.py
|
|
||||||
+++ b/file1.py
|
|
||||||
@@ -1,2 +1,2 @@
|
|
||||||
-old
|
|
||||||
+new
|
|
||||||
|
|
||||||
diff --git a/file2.py b/file2.py
|
|
||||||
index abcdefg..1234567 100644
|
|
||||||
--- a/file2.py
|
|
||||||
+++ b/file2.py
|
|
||||||
@@ -1,2 +1,2 @@
|
|
||||||
-old2
|
|
||||||
+new2
|
|
||||||
"""
|
|
||||||
result = self.parser.parse_diff(diff_output)
|
|
||||||
|
|
||||||
assert len(result) == 2
|
|
||||||
assert result[0].file_path == "file1.py"
|
|
||||||
assert result[1].file_path == "file2.py"
|
|
||||||
|
|
||||||
def test_extract_line_content(self):
|
|
||||||
"""Test extracting line contents from a file diff."""
|
|
||||||
diff_output = """diff --git a/test.py b/test.py
|
|
||||||
index 1234567..89abcdef 100644
|
|
||||||
--- a/test.py
|
|
||||||
+++ b/test.py
|
|
||||||
@@ -1,5 +1,5 @@
|
|
||||||
context line
|
|
||||||
-old line
|
|
||||||
+new line 1
|
|
||||||
+new line 2
|
|
||||||
context line 2
|
|
||||||
-old line 2
|
|
||||||
+new line 3
|
|
||||||
context line 3
|
|
||||||
"""
|
|
||||||
result = self.parser.parse_diff(diff_output)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
file_diff = result[0]
|
|
||||||
|
|
||||||
lines = self.parser.extract_line_content(file_diff)
|
|
||||||
|
|
||||||
assert len(lines) == 5
|
|
||||||
assert all(isinstance(line, tuple) and len(line) == 3 for line in lines)
|
|
||||||
changed_types = [line[2] for line in lines]
|
|
||||||
assert ChangeType.ADDED in changed_types
|
|
||||||
assert ChangeType.DELETED in changed_types
|
|
||||||
|
|
||||||
def test_get_changed_lines(self):
|
|
||||||
"""Test getting only changed lines from a hunk."""
|
|
||||||
diff_output = """diff --git a/test.py b/test.py
|
|
||||||
index 1234567..89abcdef 100644
|
|
||||||
--- a/test.py
|
|
||||||
+++ b/test.py
|
|
||||||
@@ -1,4 +1,5 @@
|
|
||||||
context
|
|
||||||
-added
|
|
||||||
+modified
|
|
||||||
+another added
|
|
||||||
context again
|
|
||||||
-deleted
|
|
||||||
final context
|
|
||||||
"""
|
|
||||||
result = self.parser.parse_diff(diff_output)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
file_diff = result[0]
|
|
||||||
|
|
||||||
assert len(file_diff.hunks) == 1
|
|
||||||
hunk = file_diff.hunks[0]
|
|
||||||
|
|
||||||
changed_lines = hunk.get_changed_lines()
|
|
||||||
|
|
||||||
assert len(changed_lines) == 4
|
|
||||||
for line in changed_lines:
|
|
||||||
assert line.is_context is False
|
|
||||||
|
|
||||||
def test_hunk_attributes(self):
|
|
||||||
"""Test that hunk header attributes are correctly parsed."""
|
|
||||||
diff_output = """diff --git a/test.py b/test.py
|
|
||||||
index 1234567..89abcdef 100644
|
|
||||||
--- a/test.py
|
|
||||||
+++ b/test.py
|
|
||||||
@@ -5,10 +7,15 @@
|
|
||||||
"""
|
|
||||||
result = self.parser.parse_diff(diff_output)
|
|
||||||
|
|
||||||
assert len(result) == 1
|
|
||||||
hunk = result[0].hunks[0]
|
|
||||||
|
|
||||||
assert hunk.old_start == 5
|
|
||||||
assert hunk.old_lines == 10
|
|
||||||
assert hunk.new_start == 7
|
|
||||||
assert hunk.new_lines == 15
|
|
||||||
|
|
||||||
|
|
||||||
class TestDiffLine:
|
|
||||||
"""Test cases for DiffLine class."""
|
|
||||||
|
|
||||||
def test_diff_line_creation(self):
|
|
||||||
"""Test creating a DiffLine instance."""
|
|
||||||
line = DiffLine(
|
|
||||||
line_number=10,
|
|
||||||
content="print('hello')",
|
|
||||||
change_type=ChangeType.ADDED
|
|
||||||
)
|
|
||||||
|
|
||||||
assert line.line_number == 10
|
|
||||||
assert line.content == "print('hello')"
|
|
||||||
assert line.change_type == ChangeType.ADDED
|
|
||||||
assert line.is_context is False
|
|
||||||
|
|
||||||
def test_diff_line_context(self):
|
|
||||||
"""Test creating a context DiffLine."""
|
|
||||||
line = DiffLine(
|
|
||||||
line_number=5,
|
|
||||||
content="def foo():",
|
|
||||||
change_type=ChangeType.MODIFIED,
|
|
||||||
is_context=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert line.is_context is True
|
|
||||||
|
|
||||||
|
|
||||||
class TestFileDiff:
|
|
||||||
"""Test cases for FileDiff class."""
|
|
||||||
|
|
||||||
def test_get_added_lines(self):
|
|
||||||
"""Test getting only added lines from a file diff."""
|
|
||||||
file_diff = FileDiff(
|
|
||||||
file_path="test.py",
|
|
||||||
change_type=ChangeType.MODIFIED
|
|
||||||
)
|
|
||||||
|
|
||||||
hunk = DiffHunk(old_start=1, old_lines=3, new_start=1, new_lines=4)
|
|
||||||
hunk.lines = [
|
|
||||||
DiffLine(line_number=1, content="old", change_type=ChangeType.DELETED),
|
|
||||||
DiffLine(line_number=2, content="new", change_type=ChangeType.ADDED),
|
|
||||||
DiffLine(line_number=3, content="context", change_type=ChangeType.MODIFIED, is_context=True),
|
|
||||||
]
|
|
||||||
file_diff.hunks.append(hunk)
|
|
||||||
|
|
||||||
added_lines = file_diff.get_added_lines()
|
|
||||||
|
|
||||||
assert len(added_lines) == 1
|
|
||||||
assert added_lines[0].change_type == ChangeType.ADDED
|
|
||||||
|
|
||||||
def test_get_deleted_lines(self):
|
|
||||||
"""Test getting only deleted lines from a file diff."""
|
|
||||||
file_diff = FileDiff(
|
|
||||||
file_path="test.py",
|
|
||||||
change_type=ChangeType.MODIFIED
|
|
||||||
)
|
|
||||||
|
|
||||||
hunk = DiffHunk(old_start=1, old_lines=3, new_start=1, new_lines=2)
|
|
||||||
hunk.lines = [
|
|
||||||
DiffLine(line_number=1, content="old", change_type=ChangeType.DELETED),
|
|
||||||
DiffLine(line_number=2, content="new", change_type=ChangeType.ADDED),
|
|
||||||
]
|
|
||||||
file_diff.hunks.append(hunk)
|
|
||||||
|
|
||||||
deleted_lines = file_diff.get_deleted_lines()
|
|
||||||
|
|
||||||
assert len(deleted_lines) == 1
|
|
||||||
assert deleted_lines[0].change_type == ChangeType.DELETED
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
"""Tests for encryption module."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pathlib import Path
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
class TestEncryption:
|
|
||||||
"""Test cases for encryption module."""
|
|
||||||
|
|
||||||
def test_derive_key(self):
|
|
||||||
"""Test key derivation from passphrase."""
|
|
||||||
from env_pro.core.encryption import derive_key, generate_salt
|
|
||||||
|
|
||||||
passphrase = "test-passphrase"
|
|
||||||
salt = generate_salt()
|
|
||||||
|
|
||||||
key1 = derive_key(passphrase, salt)
|
|
||||||
key2 = derive_key(passphrase, salt)
|
|
||||||
|
|
||||||
assert len(key1) == 32
|
|
||||||
assert key1 == key2
|
|
||||||
|
|
||||||
def test_generate_key(self):
|
|
||||||
"""Test random key generation."""
|
|
||||||
from env_pro.core.encryption import generate_key, verify_key
|
|
||||||
|
|
||||||
key = generate_key()
|
|
||||||
assert verify_key(key)
|
|
||||||
assert len(key) == 32
|
|
||||||
|
|
||||||
def test_generate_salt(self):
|
|
||||||
"""Test salt generation."""
|
|
||||||
from env_pro.core.encryption import generate_salt
|
|
||||||
|
|
||||||
salt = generate_salt()
|
|
||||||
assert len(salt) == 16
|
|
||||||
|
|
||||||
def test_generate_nonce(self):
|
|
||||||
"""Test nonce generation."""
|
|
||||||
from env_pro.core.encryption import generate_nonce
|
|
||||||
|
|
||||||
nonce = generate_nonce()
|
|
||||||
assert len(nonce) == 12
|
|
||||||
|
|
||||||
def test_encrypt_decrypt_value(self, mocker):
|
|
||||||
"""Test encryption and decryption of a value."""
|
|
||||||
from env_pro.core.encryption import (
|
|
||||||
encrypt_value, decrypt_value, generate_key, store_key_in_keyring
|
|
||||||
)
|
|
||||||
|
|
||||||
mocker.patch('keyring.set_password', return_value=None)
|
|
||||||
mocker.patch('keyring.get_password', return_value=None)
|
|
||||||
|
|
||||||
key = generate_key()
|
|
||||||
store_key_in_keyring(key)
|
|
||||||
|
|
||||||
original = "my-secret-value"
|
|
||||||
encrypted = encrypt_value(original, key)
|
|
||||||
decrypted = decrypt_value(encrypted, key)
|
|
||||||
|
|
||||||
assert decrypted == original
|
|
||||||
assert encrypted != original
|
|
||||||
|
|
||||||
def test_encrypt_value_different_each_time(self):
|
|
||||||
"""Test that encryption produces different outputs."""
|
|
||||||
from env_pro.core.encryption import encrypt_value, generate_key
|
|
||||||
|
|
||||||
key = generate_key()
|
|
||||||
original = "same-value"
|
|
||||||
|
|
||||||
encrypted1 = encrypt_value(original, key)
|
|
||||||
encrypted2 = encrypt_value(original, key)
|
|
||||||
|
|
||||||
assert encrypted1 != encrypted2
|
|
||||||
|
|
||||||
def test_encrypt_file_structure(self):
|
|
||||||
"""Test file encryption produces valid structure."""
|
|
||||||
from env_pro.core.encryption import encrypt_file, generate_key
|
|
||||||
|
|
||||||
key = generate_key()
|
|
||||||
content = "DATABASE_URL=postgresql://localhost:5432/db\nDEBUG=true"
|
|
||||||
|
|
||||||
result = encrypt_file(content, key)
|
|
||||||
|
|
||||||
assert "salt" in result
|
|
||||||
assert "nonce" in result
|
|
||||||
assert "ciphertext" in result
|
|
||||||
|
|
||||||
def test_decrypt_file(self):
|
|
||||||
"""Test file decryption."""
|
|
||||||
from env_pro.core.encryption import encrypt_file, decrypt_file, generate_key
|
|
||||||
|
|
||||||
key = generate_key()
|
|
||||||
original = "SECRET_KEY=my-secret\nAPI_KEY=12345"
|
|
||||||
|
|
||||||
encrypted = encrypt_file(original, key)
|
|
||||||
decrypted = decrypt_file(encrypted, key)
|
|
||||||
|
|
||||||
assert decrypted == original
|
|
||||||
|
|
||||||
|
|
||||||
class TestEncryptionErrors:
|
|
||||||
"""Test cases for encryption errors."""
|
|
||||||
|
|
||||||
def test_invalid_encrypted_value(self):
|
|
||||||
"""Test decryption of invalid data."""
|
|
||||||
from env_pro.core.encryption import decrypt_value, EncryptionError
|
|
||||||
|
|
||||||
with pytest.raises(EncryptionError):
|
|
||||||
decrypt_value("invalid-base64-data!!!")
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
"""Tests for the explainer module."""
|
|
||||||
|
|
||||||
from cli_explain_fix.parser import ErrorParser
|
|
||||||
from cli_explain_fix.explainer import Explainer
|
|
||||||
from cli_explain_fix.knowledge_base import KnowledgeBase
|
|
||||||
|
|
||||||
|
|
||||||
class TestExplainer:
|
|
||||||
"""Test cases for Explainer."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up explainer instance for each test."""
|
|
||||||
self.parser = ErrorParser()
|
|
||||||
self.kb = KnowledgeBase()
|
|
||||||
self.explainer = Explainer(self.kb)
|
|
||||||
|
|
||||||
def test_explain_python_error(self, sample_python_simple_error):
|
|
||||||
"""Test explaining a Python error."""
|
|
||||||
parsed = self.parser.parse(sample_python_simple_error)
|
|
||||||
explanation = self.explainer.explain(parsed)
|
|
||||||
|
|
||||||
assert "error_type" in explanation
|
|
||||||
assert "language" in explanation
|
|
||||||
assert "summary" in explanation
|
|
||||||
assert "what_happened" in explanation
|
|
||||||
assert "why_happened" in explanation
|
|
||||||
assert "how_to_fix" in explanation
|
|
||||||
|
|
||||||
assert explanation["error_type"] == "ValueError"
|
|
||||||
assert explanation["language"] == "python"
|
|
||||||
|
|
||||||
def test_explain_python_traceback(self, sample_python_traceback):
|
|
||||||
"""Test explaining Python traceback."""
|
|
||||||
parsed = self.parser.parse(sample_python_traceback)
|
|
||||||
explanation = self.explainer.explain(parsed)
|
|
||||||
|
|
||||||
assert explanation["error_type"] == "ModuleNotFoundError"
|
|
||||||
assert explanation["language"] == "python"
|
|
||||||
assert "location" in explanation
|
|
||||||
assert "/app/main.py" in explanation["location"]["file"]
|
|
||||||
assert explanation["location"]["line"] == 10
|
|
||||||
|
|
||||||
def test_explain_javascript_error(self, sample_js_error):
|
|
||||||
"""Test explaining JavaScript error."""
|
|
||||||
parsed = self.parser.parse(sample_js_error)
|
|
||||||
explanation = self.explainer.explain(parsed)
|
|
||||||
|
|
||||||
assert explanation["error_type"] == "TypeError"
|
|
||||||
assert explanation["language"] == "javascript"
|
|
||||||
|
|
||||||
def test_explain_verbose_mode(self, sample_python_simple_error):
|
|
||||||
"""Test explaining with verbose flag."""
|
|
||||||
parsed = self.parser.parse(sample_python_simple_error)
|
|
||||||
explanation = self.explainer.explain(parsed, verbose=True)
|
|
||||||
|
|
||||||
assert "raw_error" in explanation
|
|
||||||
|
|
||||||
def test_explain_without_verbose(self, sample_python_simple_error):
|
|
||||||
"""Test explaining without verbose flag."""
|
|
||||||
parsed = self.parser.parse(sample_python_simple_error)
|
|
||||||
explanation = self.explainer.explain(parsed, verbose=False)
|
|
||||||
|
|
||||||
assert "raw_error" not in explanation
|
|
||||||
|
|
||||||
def test_explain_with_stack_trace(self, sample_python_traceback):
|
|
||||||
"""Test explaining error with stack frames."""
|
|
||||||
parsed = self.parser.parse(sample_python_traceback)
|
|
||||||
explanation = self.explainer.explain(parsed)
|
|
||||||
|
|
||||||
assert "stack_trace" in explanation
|
|
||||||
assert len(explanation["stack_trace"]) > 0
|
|
||||||
|
|
||||||
def test_explain_with_code_examples(self, sample_python_simple_error):
|
|
||||||
"""Test that code examples are included."""
|
|
||||||
parsed = self.parser.parse(sample_python_simple_error)
|
|
||||||
explanation = self.explainer.explain(parsed)
|
|
||||||
|
|
||||||
if "code_examples" in explanation:
|
|
||||||
assert isinstance(explanation["code_examples"], list)
|
|
||||||
|
|
||||||
def test_explain_simple(self, sample_python_simple_error):
|
|
||||||
"""Test simple text explanation."""
|
|
||||||
result = self.explainer.explain_simple(
|
|
||||||
"ValueError",
|
|
||||||
"invalid value for int()",
|
|
||||||
"python"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "Error: ValueError" in result
|
|
||||||
assert "Language: python" in result
|
|
||||||
assert "What happened:" in result
|
|
||||||
assert "How to fix:" in result
|
|
||||||
|
|
||||||
def test_get_fix_steps(self, sample_python_simple_error):
|
|
||||||
"""Test getting fix steps for an error."""
|
|
||||||
parsed = self.parser.parse(sample_python_simple_error)
|
|
||||||
steps = self.explainer.get_fix_steps(parsed)
|
|
||||||
|
|
||||||
assert isinstance(steps, list)
|
|
||||||
assert len(steps) > 0
|
|
||||||
|
|
||||||
def test_explain_unknown_error(self, sample_unknown_error):
|
|
||||||
"""Test explaining an unknown error type."""
|
|
||||||
parsed = self.parser.parse(sample_unknown_error)
|
|
||||||
explanation = self.explainer.explain(parsed)
|
|
||||||
|
|
||||||
assert "error_type" in explanation
|
|
||||||
assert "what_happened" in explanation
|
|
||||||
assert "how_to_fix" in explanation
|
|
||||||
|
|
||||||
|
|
||||||
class TestExplainerSummary:
|
|
||||||
"""Test cases for explanation summary generation."""
|
|
||||||
|
|
||||||
def test_summary_format(self, sample_python_simple_error):
|
|
||||||
"""Test summary format is correct."""
|
|
||||||
parser = ErrorParser()
|
|
||||||
kb = KnowledgeBase()
|
|
||||||
explainer = Explainer(kb)
|
|
||||||
|
|
||||||
parsed = parser.parse(sample_python_simple_error)
|
|
||||||
explanation = explainer.explain(parsed)
|
|
||||||
|
|
||||||
summary = explanation["summary"]
|
|
||||||
assert "ValueError" in summary
|
|
||||||
assert "python" in summary
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
"""Tests for generators module."""
|
|
||||||
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from doc2man.generators.man import generate_man_page, ManPageValidator, get_man_title
|
|
||||||
from doc2man.generators.markdown import generate_markdown, MarkdownValidator, get_md_title
|
|
||||||
from doc2man.generators.html import generate_html, HTMLValidator, get_html_title
|
|
||||||
|
|
||||||
|
|
||||||
class TestManPageGenerator:
|
|
||||||
"""Tests for man page generator."""
|
|
||||||
|
|
||||||
def test_generate_man_page_basic(self):
|
|
||||||
"""Test basic man page generation."""
|
|
||||||
data = [
|
|
||||||
{
|
|
||||||
"file": "example.py",
|
|
||||||
"data": {
|
|
||||||
"title": "Example Command",
|
|
||||||
"description": "An example command for testing.",
|
|
||||||
"functions": [
|
|
||||||
{
|
|
||||||
"name": "example",
|
|
||||||
"description": "An example function.",
|
|
||||||
"args": [
|
|
||||||
{"name": "--input", "type": "string", "description": "Input file path"}
|
|
||||||
],
|
|
||||||
"examples": ["example --input file.txt"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".1", delete=False) as f:
|
|
||||||
output_path = Path(f.name)
|
|
||||||
result = generate_man_page(data, output_path)
|
|
||||||
|
|
||||||
assert ".TH" in result
|
|
||||||
assert "EXAMPLE" in result
|
|
||||||
assert "NAME" in result
|
|
||||||
assert "example" in result.lower()
|
|
||||||
|
|
||||||
output_path.unlink()
|
|
||||||
|
|
||||||
def test_man_page_validator(self):
|
|
||||||
"""Test man page validation."""
|
|
||||||
content = """
|
|
||||||
.TH EXAMPLE 1
|
|
||||||
.SH NAME
|
|
||||||
example \- An example command
|
|
||||||
.SH DESCRIPTION
|
|
||||||
This is a description.
|
|
||||||
"""
|
|
||||||
warnings = ManPageValidator.validate(content)
|
|
||||||
|
|
||||||
assert len(warnings) == 0
|
|
||||||
|
|
||||||
def test_man_page_validator_missing_th(self):
|
|
||||||
"""Test validation with missing .TH macro."""
|
|
||||||
content = """
|
|
||||||
.SH NAME
|
|
||||||
example \- An example command
|
|
||||||
.SH DESCRIPTION
|
|
||||||
This is a description.
|
|
||||||
"""
|
|
||||||
warnings = ManPageValidator.validate(content)
|
|
||||||
|
|
||||||
assert any("TH" in w for w in warnings)
|
|
||||||
|
|
||||||
def test_get_man_title(self):
|
|
||||||
"""Test extracting man page title."""
|
|
||||||
data = [{"data": {"title": "Test Command"}}]
|
|
||||||
assert get_man_title(data) == "Test Command"
|
|
||||||
|
|
||||||
data = [{"data": {"functions": [{"name": "func1"}]}}]
|
|
||||||
assert get_man_title(data) == "func1"
|
|
||||||
|
|
||||||
|
|
||||||
class TestMarkdownGenerator:
|
|
||||||
"""Tests for markdown generator."""
|
|
||||||
|
|
||||||
def test_generate_markdown_basic(self):
|
|
||||||
"""Test basic markdown generation."""
|
|
||||||
data = [
|
|
||||||
{
|
|
||||||
"file": "example.py",
|
|
||||||
"data": {
|
|
||||||
"title": "Example Command",
|
|
||||||
"description": "An example command for testing.",
|
|
||||||
"functions": [
|
|
||||||
{
|
|
||||||
"name": "example",
|
|
||||||
"description": "An example function.",
|
|
||||||
"args": [
|
|
||||||
{"name": "input", "type": "string", "description": "Input file"}
|
|
||||||
],
|
|
||||||
"returns": {"type": "str", "description": "Result string"},
|
|
||||||
"examples": ["example()"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".md", delete=False) as f:
|
|
||||||
output_path = Path(f.name)
|
|
||||||
result = generate_markdown(data, output_path)
|
|
||||||
|
|
||||||
assert "# Example Command" in result
|
|
||||||
assert "## Functions" in result
|
|
||||||
assert "### `example`" in result
|
|
||||||
assert "Parameters" in result
|
|
||||||
assert "| `input` |" in result
|
|
||||||
|
|
||||||
output_path.unlink()
|
|
||||||
|
|
||||||
def test_markdown_validator(self):
|
|
||||||
"""Test markdown validation."""
|
|
||||||
content = """# Title
|
|
||||||
|
|
||||||
Some content.
|
|
||||||
"""
|
|
||||||
warnings = MarkdownValidator.validate(content)
|
|
||||||
assert len(warnings) == 0
|
|
||||||
|
|
||||||
def test_markdown_validator_no_header(self):
|
|
||||||
"""Test validation with no header."""
|
|
||||||
content = """Some content without header.
|
|
||||||
"""
|
|
||||||
warnings = MarkdownValidator.validate(content)
|
|
||||||
|
|
||||||
assert any("header" in w.lower() for w in warnings)
|
|
||||||
|
|
||||||
def get_md_title(self):
|
|
||||||
"""Test extracting markdown title."""
|
|
||||||
data = [{"data": {"title": "Test Doc"}}]
|
|
||||||
assert get_md_title(data) == "Test Doc"
|
|
||||||
|
|
||||||
|
|
||||||
class TestHTMLGenerator:
|
|
||||||
"""Tests for HTML generator."""
|
|
||||||
|
|
||||||
def test_generate_html_basic(self):
|
|
||||||
"""Test basic HTML generation."""
|
|
||||||
data = [
|
|
||||||
{
|
|
||||||
"file": "example.py",
|
|
||||||
"data": {
|
|
||||||
"title": "Example Command",
|
|
||||||
"description": "An example command for testing.",
|
|
||||||
"functions": [
|
|
||||||
{
|
|
||||||
"name": "example",
|
|
||||||
"description": "An example function.",
|
|
||||||
"args": [
|
|
||||||
{"name": "input", "type": "string", "description": "Input file"}
|
|
||||||
],
|
|
||||||
"examples": ["example()"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
|
|
||||||
output_path = Path(f.name)
|
|
||||||
result = generate_html(data, output_path)
|
|
||||||
|
|
||||||
assert "<!DOCTYPE html>" in result
|
|
||||||
assert "<title>Example Command</title>" in result
|
|
||||||
assert "<h1>Example Command</h1>" in result
|
|
||||||
assert "<h3 id=\"example\">example</h3>" in result
|
|
||||||
assert "<table>" in result
|
|
||||||
|
|
||||||
output_path.unlink()
|
|
||||||
|
|
||||||
def test_html_validator(self):
|
|
||||||
"""Test HTML validation."""
|
|
||||||
content = """<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Test</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
Content
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
warnings = HTMLValidator.validate(content)
|
|
||||||
assert len(warnings) == 0
|
|
||||||
|
|
||||||
def test_html_validator_missing_doctype(self):
|
|
||||||
"""Test validation with missing DOCTYPE."""
|
|
||||||
content = """<html>
|
|
||||||
<head>
|
|
||||||
<title>Test</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
Content
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
warnings = HTMLValidator.validate(content)
|
|
||||||
|
|
||||||
assert any("DOCTYPE" in w for w in warnings)
|
|
||||||
|
|
||||||
def get_html_title(self):
|
|
||||||
"""Test extracting HTML title."""
|
|
||||||
data = [{"data": {"title": "Test Doc"}}]
|
|
||||||
assert get_html_title(data) == "Test Doc"
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
"""Integration tests for the diff auditor."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from click.testing import CliRunner
|
|
||||||
from git import Repo
|
|
||||||
|
|
||||||
from cli_diff_auditor.cli import main
|
|
||||||
from cli_diff_auditor.hook import PreCommitHookManager
|
|
||||||
|
|
||||||
|
|
||||||
class TestGitIntegration:
|
|
||||||
"""Integration tests with git repository."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.runner = CliRunner()
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def temp_repo(self):
|
|
||||||
"""Create a temporary git repository."""
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
repo = Repo.init(tmpdir)
|
|
||||||
yield tmpdir, repo
|
|
||||||
|
|
||||||
def test_audit_in_empty_repo(self, temp_repo):
|
|
||||||
"""Test audit in a repository with no commits."""
|
|
||||||
tmpdir, repo = temp_repo
|
|
||||||
|
|
||||||
result = self.runner.invoke(main, ["audit"], catch_exceptions=False)
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
def test_audit_with_staged_debug_print(self, temp_repo):
|
|
||||||
"""Test audit detects staged debug print."""
|
|
||||||
tmpdir, repo = temp_repo
|
|
||||||
|
|
||||||
test_file = Path(tmpdir) / "test.py"
|
|
||||||
test_file.write_text("print('hello')\n")
|
|
||||||
|
|
||||||
repo.index.add(["test.py"])
|
|
||||||
repo.index.commit("Initial commit")
|
|
||||||
|
|
||||||
test_file.write_text("print('world')\n")
|
|
||||||
repo.index.add(["test.py"])
|
|
||||||
|
|
||||||
result = self.runner.invoke(main, ["audit"], catch_exceptions=False)
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
def test_hook_install_and_check(self, temp_repo):
|
|
||||||
"""Test installing and checking the hook."""
|
|
||||||
tmpdir, repo = temp_repo
|
|
||||||
|
|
||||||
manager = PreCommitHookManager()
|
|
||||||
result = manager.install_hook(tmpdir)
|
|
||||||
|
|
||||||
assert result.success is True
|
|
||||||
|
|
||||||
installed = manager.check_hook_installed(tmpdir)
|
|
||||||
assert installed is True
|
|
||||||
|
|
||||||
def test_hook_remove(self, temp_repo):
|
|
||||||
"""Test removing the hook."""
|
|
||||||
tmpdir, repo = temp_repo
|
|
||||||
|
|
||||||
manager = PreCommitHookManager()
|
|
||||||
manager.install_hook(tmpdir)
|
|
||||||
|
|
||||||
result = manager.remove_hook(tmpdir)
|
|
||||||
|
|
||||||
assert result.success is True
|
|
||||||
assert manager.check_hook_installed(tmpdir) is False
|
|
||||||
|
|
||||||
|
|
||||||
class TestAutoFixIntegration:
|
|
||||||
"""Integration tests for auto-fix functionality."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.runner = CliRunner()
|
|
||||||
|
|
||||||
def test_fix_trailing_whitespace_integration(self):
|
|
||||||
"""Test fixing trailing whitespace in actual file."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
||||||
f.write("def hello():\n return 'world' \n")
|
|
||||||
temp_path = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.runner.invoke(main, ["fix", temp_path])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
content = Path(temp_path).read_text()
|
|
||||||
assert content == "def hello():\n return 'world'\n"
|
|
||||||
finally:
|
|
||||||
os.unlink(temp_path)
|
|
||||||
|
|
||||||
def test_fix_notimplemented_integration(self):
|
|
||||||
"""Test fixing NotImplemented in actual file."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
||||||
f.write("def foo():\n raise NotImplemented\n")
|
|
||||||
temp_path = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
from cli_diff_auditor.autofix import AutoFixer
|
|
||||||
fixer = AutoFixer()
|
|
||||||
result = fixer.fix_notimplemented_error(temp_path)
|
|
||||||
|
|
||||||
assert result.success is True
|
|
||||||
content = Path(temp_path).read_text()
|
|
||||||
assert "raise NotImplementedError" in content
|
|
||||||
finally:
|
|
||||||
os.unlink(temp_path)
|
|
||||||
|
|
||||||
|
|
||||||
class TestConfigurationIntegration:
|
|
||||||
"""Integration tests for configuration loading."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.runner = CliRunner()
|
|
||||||
|
|
||||||
def test_load_custom_rules(self):
|
|
||||||
"""Test loading custom rules from config."""
|
|
||||||
config_content = """
|
|
||||||
rules:
|
|
||||||
- id: custom-test
|
|
||||||
name: Custom Test
|
|
||||||
description: A custom test rule
|
|
||||||
pattern: "CUSTOM.*"
|
|
||||||
severity: warning
|
|
||||||
category: custom
|
|
||||||
"""
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
||||||
f.write(config_content)
|
|
||||||
config_path = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.runner.invoke(main, ["--config", config_path, "rules"])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "custom-test" in result.output
|
|
||||||
finally:
|
|
||||||
os.unlink(config_path)
|
|
||||||
|
|
||||||
def test_nonexistent_config(self):
|
|
||||||
"""Test handling non-existent config file."""
|
|
||||||
result = self.runner.invoke(main, ["--config", "/nonexistent/config.yaml", "rules"])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
|
|
||||||
class TestEdgeCasesIntegration:
|
|
||||||
"""Integration tests for edge cases."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.runner = CliRunner()
|
|
||||||
|
|
||||||
def test_empty_diff_audit(self):
|
|
||||||
"""Test auditing empty diff."""
|
|
||||||
result = self.runner.invoke(main, ["audit-diff", ""])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
def test_audit_binary_file_ignored(self, temp_repo):
|
|
||||||
"""Test that binary files are skipped."""
|
|
||||||
tmpdir, repo = temp_repo
|
|
||||||
|
|
||||||
test_file = Path(tmpdir) / "image.png"
|
|
||||||
test_file.write_bytes(b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR")
|
|
||||||
|
|
||||||
repo.index.add(["image.png"])
|
|
||||||
repo.index.commit("Add binary file")
|
|
||||||
|
|
||||||
result = self.runner.invoke(main, ["audit"], catch_exceptions=False)
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
def test_audit_multiple_files(self, temp_repo):
|
|
||||||
"""Test auditing multiple files."""
|
|
||||||
tmpdir, repo = temp_repo
|
|
||||||
|
|
||||||
file1 = Path(tmpdir) / "file1.py"
|
|
||||||
file1.write_text("print('hello')\n")
|
|
||||||
file2 = Path(tmpdir) / "file2.py"
|
|
||||||
file2.write_text("console.log('world')\n")
|
|
||||||
|
|
||||||
repo.index.add(["file1.py", "file2.py"])
|
|
||||||
repo.index.commit("Add files")
|
|
||||||
|
|
||||||
file1.write_text("print('updated')\n")
|
|
||||||
file2.write_text("console.log('updated')\n")
|
|
||||||
repo.index.add(["file1.py", "file2.py"])
|
|
||||||
|
|
||||||
result = self.runner.invoke(main, ["audit"], catch_exceptions=False)
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def temp_repo(self):
|
|
||||||
"""Create a temporary git repository."""
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
repo = Repo.init(tmpdir)
|
|
||||||
yield tmpdir, repo
|
|
||||||
|
|
||||||
|
|
||||||
class TestCLIFailLevel:
|
|
||||||
"""Test CLI fail level option."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.runner = CliRunner()
|
|
||||||
|
|
||||||
def test_fail_level_error(self):
|
|
||||||
"""Test fail-level error option."""
|
|
||||||
result = self.runner.invoke(main, ["check", "--fail-level", "error"])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
def test_fail_level_warning(self):
|
|
||||||
"""Test fail-level warning option."""
|
|
||||||
result = self.runner.invoke(main, ["check", "--fail-level", "warning"])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
def test_fail_level_info(self):
|
|
||||||
"""Test fail-level info option."""
|
|
||||||
result = self.runner.invoke(main, ["check", "--fail-level", "info"])
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
"""Tests for the parser module."""
|
|
||||||
|
|
||||||
from cli_explain_fix.parser import ErrorParser, ParsedError
|
|
||||||
|
|
||||||
|
|
||||||
class TestErrorParser:
|
|
||||||
"""Test cases for ErrorParser."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up parser instance for each test."""
|
|
||||||
self.parser = ErrorParser()
|
|
||||||
|
|
||||||
def test_detect_language_python_traceback(self, sample_python_traceback):
|
|
||||||
"""Test language detection for Python traceback."""
|
|
||||||
lang = self.parser.detect_language(sample_python_traceback)
|
|
||||||
assert lang == "python"
|
|
||||||
|
|
||||||
def test_detect_language_python_simple(self, sample_python_simple_error):
|
|
||||||
"""Test language detection for simple Python error."""
|
|
||||||
lang = self.parser.detect_language(sample_python_simple_error)
|
|
||||||
assert lang == "python"
|
|
||||||
|
|
||||||
def test_detect_language_javascript(self, sample_js_error):
|
|
||||||
"""Test language detection for JavaScript error."""
|
|
||||||
lang = self.parser.detect_language(sample_js_error)
|
|
||||||
assert lang == "javascript"
|
|
||||||
|
|
||||||
def test_detect_language_go(self, sample_go_panic):
|
|
||||||
"""Test language detection for Go panic."""
|
|
||||||
lang = self.parser.detect_language(sample_go_panic)
|
|
||||||
assert lang == "go"
|
|
||||||
|
|
||||||
def test_detect_language_rust(self, sample_rust_panic):
|
|
||||||
"""Test language detection for Rust panic."""
|
|
||||||
lang = self.parser.detect_language(sample_rust_panic)
|
|
||||||
assert lang == "rust"
|
|
||||||
|
|
||||||
def test_detect_language_json(self, sample_json_error):
|
|
||||||
"""Test language detection for JSON error."""
|
|
||||||
lang = self.parser.detect_language(sample_json_error)
|
|
||||||
assert lang == "json"
|
|
||||||
|
|
||||||
def test_detect_language_yaml(self, sample_yaml_error):
|
|
||||||
"""Test language detection for YAML error."""
|
|
||||||
lang = self.parser.detect_language(sample_yaml_error)
|
|
||||||
assert lang == "yaml"
|
|
||||||
|
|
||||||
def test_detect_language_cli(self, sample_cli_error):
|
|
||||||
"""Test language detection for CLI error."""
|
|
||||||
lang = self.parser.detect_language(sample_cli_error)
|
|
||||||
assert lang == "cli"
|
|
||||||
|
|
||||||
def test_detect_language_unknown(self, sample_unknown_error):
|
|
||||||
"""Test language detection for unknown error."""
|
|
||||||
lang = self.parser.detect_language(sample_unknown_error)
|
|
||||||
assert lang == "unknown"
|
|
||||||
|
|
||||||
def test_parse_python_traceback(self, sample_python_traceback):
|
|
||||||
"""Test parsing Python traceback."""
|
|
||||||
result = self.parser.parse(sample_python_traceback)
|
|
||||||
|
|
||||||
assert isinstance(result, ParsedError)
|
|
||||||
assert result.error_type == "ModuleNotFoundError"
|
|
||||||
assert result.language == "python"
|
|
||||||
assert "requests" in result.message
|
|
||||||
assert result.file_name == "/app/main.py"
|
|
||||||
assert result.line_number == 10
|
|
||||||
assert len(result.stack_frames) > 0
|
|
||||||
|
|
||||||
def test_parse_python_simple(self, sample_python_simple_error):
|
|
||||||
"""Test parsing simple Python error."""
|
|
||||||
result = self.parser.parse(sample_python_simple_error)
|
|
||||||
|
|
||||||
assert isinstance(result, ParsedError)
|
|
||||||
assert result.error_type == "ValueError"
|
|
||||||
assert result.language == "python"
|
|
||||||
assert "invalid value" in result.message
|
|
||||||
|
|
||||||
def test_parse_javascript_error(self, sample_js_error):
|
|
||||||
"""Test parsing JavaScript error."""
|
|
||||||
result = self.parser.parse(sample_js_error)
|
|
||||||
|
|
||||||
assert isinstance(result, ParsedError)
|
|
||||||
assert result.error_type == "TypeError"
|
|
||||||
assert result.language == "javascript"
|
|
||||||
|
|
||||||
def test_parse_go_panic(self, sample_go_panic):
|
|
||||||
"""Test parsing Go panic."""
|
|
||||||
result = self.parser.parse(sample_go_panic)
|
|
||||||
|
|
||||||
assert isinstance(result, ParsedError)
|
|
||||||
assert result.error_type == "panic"
|
|
||||||
assert result.language == "go"
|
|
||||||
|
|
||||||
def test_parse_rust_panic(self, sample_rust_panic):
|
|
||||||
"""Test parsing Rust panic."""
|
|
||||||
result = self.parser.parse(sample_rust_panic)
|
|
||||||
|
|
||||||
assert isinstance(result, ParsedError)
|
|
||||||
assert result.error_type == "panic"
|
|
||||||
assert result.language == "rust"
|
|
||||||
|
|
||||||
def test_parse_json_error(self, sample_json_error):
|
|
||||||
"""Test parsing JSON error."""
|
|
||||||
result = self.parser.parse(sample_json_error)
|
|
||||||
|
|
||||||
assert isinstance(result, ParsedError)
|
|
||||||
assert result.error_type == "JSONParseError"
|
|
||||||
assert result.language == "json"
|
|
||||||
|
|
||||||
def test_parse_yaml_error(self, sample_yaml_error):
|
|
||||||
"""Test parsing YAML error."""
|
|
||||||
result = self.parser.parse(sample_yaml_error)
|
|
||||||
|
|
||||||
assert isinstance(result, ParsedError)
|
|
||||||
assert result.error_type == "YAMLParseError"
|
|
||||||
assert result.language == "yaml"
|
|
||||||
|
|
||||||
def test_parse_cli_error(self, sample_cli_error):
|
|
||||||
"""Test parsing CLI error."""
|
|
||||||
result = self.parser.parse(sample_cli_error)
|
|
||||||
|
|
||||||
assert isinstance(result, ParsedError)
|
|
||||||
assert result.error_type == "GenericError"
|
|
||||||
assert result.language == "cli"
|
|
||||||
|
|
||||||
def test_parse_with_explicit_language(self, sample_python_simple_error):
|
|
||||||
"""Test parsing with explicit language specification."""
|
|
||||||
result = self.parser.parse(sample_python_simple_error, language="python")
|
|
||||||
|
|
||||||
assert result.language == "python"
|
|
||||||
assert result.error_type == "ValueError"
|
|
||||||
|
|
||||||
def test_parse_unknown_error(self, sample_unknown_error):
|
|
||||||
"""Test parsing unknown error returns default."""
|
|
||||||
result = self.parser.parse(sample_unknown_error)
|
|
||||||
|
|
||||||
assert isinstance(result, ParsedError)
|
|
||||||
assert result.error_type == "UnknownError"
|
|
||||||
assert result.language == "unknown"
|
|
||||||
|
|
||||||
def test_parse_empty_input(self):
|
|
||||||
"""Test parsing empty input."""
|
|
||||||
result = self.parser.parse("")
|
|
||||||
|
|
||||||
assert isinstance(result, ParsedError)
|
|
||||||
assert result.error_type == "UnknownError"
|
|
||||||
assert result.message == "Unknown error occurred"
|
|
||||||
|
|
||||||
def test_parsed_error_to_dict(self, sample_python_simple_error):
|
|
||||||
"""Test ParsedError.to_dict() method."""
|
|
||||||
result = self.parser.parse(sample_python_simple_error)
|
|
||||||
data = result.to_dict()
|
|
||||||
|
|
||||||
assert isinstance(data, dict)
|
|
||||||
assert "error_type" in data
|
|
||||||
assert "message" in data
|
|
||||||
assert "language" in data
|
|
||||||
assert "stack_frames" in data
|
|
||||||
|
|
||||||
def test_parse_complex_python_traceback(self):
|
|
||||||
"""Test parsing complex Python traceback with multiple frames."""
|
|
||||||
traceback = '''Traceback (most recent call last):
|
|
||||||
File "app.py", line 5, in <module>
|
|
||||||
main()
|
|
||||||
File "app.py", line 10, in main
|
|
||||||
process()
|
|
||||||
File "processor.py", line 20, in process
|
|
||||||
result = data['key']
|
|
||||||
KeyError: 'key'
|
|
||||||
'''
|
|
||||||
result = self.parser.parse(traceback)
|
|
||||||
|
|
||||||
assert result.error_type == "KeyError"
|
|
||||||
assert result.language == "python"
|
|
||||||
assert result.file_name == "processor.py"
|
|
||||||
assert result.line_number == 20
|
|
||||||
assert len(result.stack_frames) == 3
|
|
||||||
@@ -1,395 +0,0 @@
|
|||||||
"""Tests for parsers module."""
|
|
||||||
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from doc2man.parsers.python import PythonDocstringParser, parse_python_file
|
|
||||||
from doc2man.parsers.go import GoDocstringParser, parse_go_file
|
|
||||||
from doc2man.parsers.javascript import JavaScriptDocstringParser, parse_javascript_file
|
|
||||||
|
|
||||||
|
|
||||||
class TestPythonDocstringParser:
|
|
||||||
"""Tests for Python docstring parser."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.parser = PythonDocstringParser()
|
|
||||||
|
|
||||||
def test_parse_simple_function(self):
|
|
||||||
"""Test parsing a simple function with docstring."""
|
|
||||||
source = '''
|
|
||||||
def hello(name):
|
|
||||||
"""Say hello to a person.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: The name of the person to greet.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A greeting message.
|
|
||||||
"""
|
|
||||||
return f"Hello, {name}!"
|
|
||||||
'''
|
|
||||||
result = self.parser.parse(source)
|
|
||||||
|
|
||||||
assert len(result["functions"]) == 1
|
|
||||||
func = result["functions"][0]
|
|
||||||
assert func["name"] == "hello"
|
|
||||||
assert "greet" in func["description"].lower() or "hello" in func["description"].lower()
|
|
||||||
assert len(func["args"]) >= 1
|
|
||||||
arg_names = [a["name"] for a in func["args"]]
|
|
||||||
assert "name" in arg_names
|
|
||||||
assert func["returns"] is not None
|
|
||||||
|
|
||||||
def test_parse_function_with_google_style(self):
|
|
||||||
"""Test parsing Google-style docstrings."""
|
|
||||||
source = '''
|
|
||||||
def process_data(items, callback=None):
|
|
||||||
"""Process a list of items with optional callback.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
items: List of items to process.
|
|
||||||
callback: Optional function to call for each item.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: A dictionary with processed results.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If items is empty.
|
|
||||||
"""
|
|
||||||
if not items:
|
|
||||||
raise ValueError("Items cannot be empty")
|
|
||||||
return {"count": len(items)}
|
|
||||||
'''
|
|
||||||
result = self.parser.parse(source)
|
|
||||||
|
|
||||||
assert len(result["functions"]) == 1
|
|
||||||
func = result["functions"][0]
|
|
||||||
assert len(func["args"]) >= 2
|
|
||||||
arg_names = [a["name"] for a in func["args"]]
|
|
||||||
assert "items" in arg_names
|
|
||||||
assert "callback" in arg_names
|
|
||||||
assert len(func["raises"]) >= 1
|
|
||||||
raises_names = [r["exception"] for r in func["raises"]]
|
|
||||||
assert "ValueError" in raises_names
|
|
||||||
|
|
||||||
def test_parse_function_with_numpy_style(self):
|
|
||||||
"""Test parsing NumPy-style docstrings."""
|
|
||||||
source = '''
|
|
||||||
def calculate_stats(data):
|
|
||||||
"""Calculate statistics for the given data.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
data : array_like
|
|
||||||
Input data array.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
tuple
|
|
||||||
A tuple containing (mean, std, median).
|
|
||||||
|
|
||||||
Examples
|
|
||||||
--------
|
|
||||||
>>> calculate_stats([1, 2, 3, 4, 5])
|
|
||||||
(3.0, 1.414, 3)
|
|
||||||
"""
|
|
||||||
import numpy as np
|
|
||||||
arr = np.array(data)
|
|
||||||
return arr.mean(), arr.std(), np.median(arr)
|
|
||||||
'''
|
|
||||||
result = self.parser.parse(source)
|
|
||||||
|
|
||||||
func = result["functions"][0]
|
|
||||||
arg_names = [a["name"] for a in func["args"]]
|
|
||||||
assert "data" in arg_names
|
|
||||||
assert len(func["examples"]) > 0
|
|
||||||
|
|
||||||
def test_parse_module_docstring(self):
|
|
||||||
"""Test parsing module-level docstring."""
|
|
||||||
source = '''"""This is a module docstring.
|
|
||||||
|
|
||||||
This module provides utility functions for data processing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def helper():
|
|
||||||
"""A helper function."""
|
|
||||||
pass
|
|
||||||
'''
|
|
||||||
result = self.parser.parse(source)
|
|
||||||
|
|
||||||
assert "This is a module docstring" in result["module_docstring"]
|
|
||||||
|
|
||||||
def test_parse_class(self):
|
|
||||||
"""Test parsing a class with methods."""
|
|
||||||
source = '''
|
|
||||||
class Calculator:
|
|
||||||
"""A simple calculator class.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
memory: Current memory value.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize the calculator."""
|
|
||||||
self.memory = 0
|
|
||||||
|
|
||||||
def add(self, a, b):
|
|
||||||
"""Add two numbers.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
a: First number.
|
|
||||||
b: Second number.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Sum of a and b.
|
|
||||||
"""
|
|
||||||
return a + b
|
|
||||||
'''
|
|
||||||
result = self.parser.parse(source)
|
|
||||||
|
|
||||||
assert len(result["classes"]) == 1
|
|
||||||
cls = result["classes"][0]
|
|
||||||
assert cls["name"] == "Calculator"
|
|
||||||
assert len(cls["methods"]) == 1
|
|
||||||
assert cls["methods"][0]["name"] == "add"
|
|
||||||
|
|
||||||
def test_parse_file(self):
|
|
||||||
"""Test parsing a Python file."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
||||||
f.write('''
|
|
||||||
def example():
|
|
||||||
"""An example function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
''')
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
result = parse_python_file(Path(f.name))
|
|
||||||
|
|
||||||
assert result["language"] == "python"
|
|
||||||
assert len(result["functions"]) == 1
|
|
||||||
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
|
|
||||||
class TestGoDocstringParser:
|
|
||||||
"""Tests for Go docstring parser."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.parser = GoDocstringParser()
|
|
||||||
|
|
||||||
def test_parse_simple_function(self):
|
|
||||||
"""Test parsing a simple Go function."""
|
|
||||||
source = '''// Add adds two integers and returns the result.
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// a: First integer
|
|
||||||
// b: Second integer
|
|
||||||
//
|
|
||||||
// Returns: The sum of a and b
|
|
||||||
func Add(a, b int) int {
|
|
||||||
return a + b
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
result = self.parser.parse_content(source)
|
|
||||||
|
|
||||||
assert len(result["functions"]) == 1
|
|
||||||
func = result["functions"][0]
|
|
||||||
assert func["name"] == "Add"
|
|
||||||
|
|
||||||
def test_parse_function_with_params(self):
|
|
||||||
"""Test parsing Go function with parameters."""
|
|
||||||
source = '''// Greet returns a greeting message.
|
|
||||||
//
|
|
||||||
// name: The name to greet
|
|
||||||
//
|
|
||||||
// Returns: A greeting string
|
|
||||||
func Greet(name string) string {
|
|
||||||
return "Hello, " + name
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
result = self.parser.parse_content(source)
|
|
||||||
|
|
||||||
func = result["functions"][0]
|
|
||||||
assert func["name"] == "Greet"
|
|
||||||
assert len(func["args"]) >= 1
|
|
||||||
arg_names = [a["name"] for a in func["args"]]
|
|
||||||
assert "name" in arg_names
|
|
||||||
|
|
||||||
def test_parse_package_documentation(self):
|
|
||||||
"""Test parsing package-level documentation."""
|
|
||||||
source = '''// Package math provides mathematical utilities.
|
|
||||||
//
|
|
||||||
// This package contains functions for basic
|
|
||||||
// mathematical operations.
|
|
||||||
package math
|
|
||||||
|
|
||||||
// Add adds two numbers.
|
|
||||||
func Add(a, b int) int {
|
|
||||||
return a + b
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
result = self.parser.parse_content(source)
|
|
||||||
|
|
||||||
assert "mathematical utilities" in result["package_docstring"].lower()
|
|
||||||
|
|
||||||
def test_parse_file(self):
|
|
||||||
"""Test parsing a Go file."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".go", delete=False) as f:
|
|
||||||
f.write('''// Hello returns a greeting.
|
|
||||||
//
|
|
||||||
// Returns: A greeting message
|
|
||||||
func Hello() string {
|
|
||||||
return "Hello, World!"
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
result = parse_go_file(Path(f.name))
|
|
||||||
|
|
||||||
assert result["language"] == "go"
|
|
||||||
assert len(result["functions"]) == 1
|
|
||||||
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
|
|
||||||
class TestJavaScriptDocstringParser:
|
|
||||||
"""Tests for JavaScript docstring parser."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.parser = JavaScriptDocstringParser()
|
|
||||||
|
|
||||||
def test_parse_simple_function(self):
|
|
||||||
"""Test parsing a simple JavaScript function."""
|
|
||||||
source = '''
|
|
||||||
/**
|
|
||||||
* Says hello to a person.
|
|
||||||
*
|
|
||||||
* @param {string} name - The name of the person
|
|
||||||
* @returns {string} A greeting message
|
|
||||||
*/
|
|
||||||
function hello(name) {
|
|
||||||
return "Hello, " + name;
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
result = self.parser.parse_content(source)
|
|
||||||
|
|
||||||
assert len(result["functions"]) == 1
|
|
||||||
func = result["functions"][0]
|
|
||||||
assert func["name"] == "hello"
|
|
||||||
assert "hello" in func["description"].lower() or "person" in func["description"].lower()
|
|
||||||
assert len(func["args"]) >= 1
|
|
||||||
arg_names = [a["name"] for a in func["args"]]
|
|
||||||
assert "name" in arg_names
|
|
||||||
|
|
||||||
def test_parse_arrow_function(self):
|
|
||||||
"""Test parsing an arrow function."""
|
|
||||||
source = '''
|
|
||||||
/**
|
|
||||||
* Adds two numbers.
|
|
||||||
*
|
|
||||||
* @param {number} a - First number
|
|
||||||
* @param {number} b - Second number
|
|
||||||
* @returns {number} The sum
|
|
||||||
*/
|
|
||||||
const add = (a, b) => a + b;
|
|
||||||
'''
|
|
||||||
result = self.parser.parse_content(source)
|
|
||||||
|
|
||||||
func = result["functions"][0]
|
|
||||||
assert func["name"] == "add"
|
|
||||||
assert len(func["args"]) >= 1
|
|
||||||
|
|
||||||
def test_parse_function_with_example(self):
|
|
||||||
"""Test parsing function with examples."""
|
|
||||||
source = '''
|
|
||||||
/**
|
|
||||||
* Squares a number.
|
|
||||||
*
|
|
||||||
* @param {number} n - The number to square
|
|
||||||
* @returns {number} The squared number
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* square(5)
|
|
||||||
* // returns 25
|
|
||||||
*/
|
|
||||||
function square(n) {
|
|
||||||
return n * n;
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
result = self.parser.parse_content(source)
|
|
||||||
|
|
||||||
func = result["functions"][0]
|
|
||||||
assert len(func["examples"]) > 0
|
|
||||||
|
|
||||||
def test_parse_export_function(self):
|
|
||||||
"""Test parsing exported function."""
|
|
||||||
source = '''
|
|
||||||
/**
|
|
||||||
* A public API function.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
export function publicApi() {
|
|
||||||
console.log("Hello");
|
|
||||||
}
|
|
||||||
'''
|
|
||||||
result = self.parser.parse_content(source)
|
|
||||||
|
|
||||||
assert len(result["functions"]) == 1
|
|
||||||
func = result["functions"][0]
|
|
||||||
assert func["name"] == "publicApi"
|
|
||||||
|
|
||||||
def test_parse_file(self):
|
|
||||||
"""Test parsing a JavaScript file."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".js", delete=False) as f:
|
|
||||||
f.write('''
|
|
||||||
/**
|
|
||||||
* Example function.
|
|
||||||
*
|
|
||||||
* @returns {string} A message
|
|
||||||
*/
|
|
||||||
function example() {
|
|
||||||
return "Hello";
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
result = parse_javascript_file(Path(f.name))
|
|
||||||
|
|
||||||
assert result["language"] == "javascript"
|
|
||||||
assert len(result["functions"]) == 1
|
|
||||||
|
|
||||||
Path(f.name).unlink()
|
|
||||||
|
|
||||||
def test_parse_typescript(self):
|
|
||||||
"""Test parsing a TypeScript file."""
|
|
||||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".ts", delete=False) as f:
|
|
||||||
f.write('''
|
|
||||||
/**
|
|
||||||
* Adds two numbers.
|
|
||||||
*
|
|
||||||
* @param a - First number
|
|
||||||
* @param b - Second number
|
|
||||||
* @returns The sum
|
|
||||||
*/
|
|
||||||
function add(a: number, b: number): number {
|
|
||||||
return a + b;
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
result = parse_javascript_file(Path(f.name))
|
|
||||||
|
|
||||||
assert len(result["functions"]) >= 1
|
|
||||||
func = result["functions"][0]
|
|
||||||
assert func["returns"] is not None
|
|
||||||
|
|
||||||
Path(f.name).unlink()
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
"""Tests for profile management module."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class TestProfileManagement:
|
|
||||||
"""Test cases for profile management."""
|
|
||||||
|
|
||||||
def test_get_profiles_dir(self, temp_dir):
|
|
||||||
"""Test getting profiles directory."""
|
|
||||||
from env_pro.core.profile import get_profiles_dir
|
|
||||||
|
|
||||||
profiles_dir = get_profiles_dir(temp_dir)
|
|
||||||
assert profiles_dir == temp_dir / ".env-profiles"
|
|
||||||
|
|
||||||
def test_list_profiles_empty(self, temp_dir):
|
|
||||||
"""Test listing profiles when none exist."""
|
|
||||||
from env_pro.core.profile import list_profiles
|
|
||||||
|
|
||||||
profiles = list_profiles(temp_dir)
|
|
||||||
assert profiles == []
|
|
||||||
|
|
||||||
def test_create_profile(self, temp_dir):
|
|
||||||
"""Test creating a new profile."""
|
|
||||||
from env_pro.core.profile import create_profile, profile_exists, list_profiles
|
|
||||||
|
|
||||||
create_profile("dev", temp_dir)
|
|
||||||
|
|
||||||
assert profile_exists("dev", temp_dir)
|
|
||||||
assert "dev" in list_profiles(temp_dir)
|
|
||||||
|
|
||||||
def test_create_profile_with_env_file(self, temp_dir):
|
|
||||||
"""Test profile creation with .env file."""
|
|
||||||
from env_pro.core.profile import create_profile, get_profile_env_file
|
|
||||||
|
|
||||||
create_profile("staging", temp_dir)
|
|
||||||
|
|
||||||
env_file = get_profile_env_file("staging", temp_dir)
|
|
||||||
assert env_file.exists()
|
|
||||||
|
|
||||||
def test_delete_profile(self, temp_dir):
|
|
||||||
"""Test deleting a profile."""
|
|
||||||
from env_pro.core.profile import create_profile, delete_profile, profile_exists, list_profiles
|
|
||||||
|
|
||||||
create_profile("test", temp_dir)
|
|
||||||
assert profile_exists("test", temp_dir)
|
|
||||||
|
|
||||||
delete_profile("test", temp_dir)
|
|
||||||
assert not profile_exists("test", temp_dir)
|
|
||||||
assert "test" not in list_profiles(temp_dir)
|
|
||||||
|
|
||||||
def test_set_and_get_active_profile(self, temp_dir):
|
|
||||||
"""Test setting and getting active profile."""
|
|
||||||
from env_pro.core.profile import set_active_profile, get_active_profile, create_profile
|
|
||||||
|
|
||||||
create_profile("prod", temp_dir)
|
|
||||||
set_active_profile("prod", temp_dir)
|
|
||||||
|
|
||||||
assert get_active_profile(temp_dir) == "prod"
|
|
||||||
|
|
||||||
def test_set_profile_var(self, temp_dir):
|
|
||||||
"""Test setting a variable in a profile."""
|
|
||||||
from env_pro.core.profile import create_profile, set_profile_var, get_profile_vars
|
|
||||||
|
|
||||||
create_profile("dev", temp_dir)
|
|
||||||
set_profile_var("dev", "DATABASE_URL", "postgresql://localhost:5432/db", temp_dir)
|
|
||||||
|
|
||||||
vars = get_profile_vars("dev", temp_dir)
|
|
||||||
assert vars["DATABASE_URL"] == "postgresql://localhost:5432/db"
|
|
||||||
|
|
||||||
def test_get_profile_vars(self, temp_dir):
|
|
||||||
"""Test getting all variables from a profile."""
|
|
||||||
from env_pro.core.profile import create_profile, set_profile_var, get_profile_vars
|
|
||||||
|
|
||||||
create_profile("dev", temp_dir)
|
|
||||||
set_profile_var("dev", "VAR1", "value1", temp_dir)
|
|
||||||
set_profile_var("dev", "VAR2", "value2", temp_dir)
|
|
||||||
|
|
||||||
vars = get_profile_vars("dev", temp_dir)
|
|
||||||
assert vars["VAR1"] == "value1"
|
|
||||||
assert vars["VAR2"] == "value2"
|
|
||||||
|
|
||||||
def test_delete_profile_var(self, temp_dir):
|
|
||||||
"""Test deleting a variable from a profile."""
|
|
||||||
from env_pro.core.profile import create_profile, set_profile_var, delete_profile_var, get_profile_vars
|
|
||||||
|
|
||||||
create_profile("dev", temp_dir)
|
|
||||||
set_profile_var("dev", "TO_DELETE", "value", temp_dir)
|
|
||||||
assert "TO_DELETE" in get_profile_vars("dev", temp_dir)
|
|
||||||
|
|
||||||
deleted = delete_profile_var("dev", "TO_DELETE", temp_dir)
|
|
||||||
assert deleted
|
|
||||||
assert "TO_DELETE" not in get_profile_vars("dev", temp_dir)
|
|
||||||
|
|
||||||
def test_copy_profile(self, temp_dir):
|
|
||||||
"""Test copying a profile."""
|
|
||||||
from env_pro.core.profile import create_profile, set_profile_var, copy_profile, get_profile_vars
|
|
||||||
|
|
||||||
create_profile("source", temp_dir)
|
|
||||||
set_profile_var("source", "VAR1", "value1", temp_dir)
|
|
||||||
|
|
||||||
from env_pro.core.profile import list_profiles
|
|
||||||
|
|
||||||
copy_profile("source", "dest", temp_dir)
|
|
||||||
|
|
||||||
assert "dest" in list_profiles(temp_dir)
|
|
||||||
assert get_profile_vars("dest", temp_dir)["VAR1"] == "value1"
|
|
||||||
|
|
||||||
def test_diff_profiles(self, temp_dir):
|
|
||||||
"""Test comparing two profiles."""
|
|
||||||
from env_pro.core.profile import create_profile, set_profile_var, diff_profiles
|
|
||||||
|
|
||||||
create_profile("profile1", temp_dir)
|
|
||||||
create_profile("profile2", temp_dir)
|
|
||||||
set_profile_var("profile1", "VAR1", "value1", temp_dir)
|
|
||||||
set_profile_var("profile2", "VAR1", "value2", temp_dir)
|
|
||||||
set_profile_var("profile2", "VAR2", "value2", temp_dir)
|
|
||||||
|
|
||||||
diff = diff_profiles("profile1", "profile2", temp_dir)
|
|
||||||
|
|
||||||
assert "VAR2" in diff["only_in_profile2"]
|
|
||||||
assert diff["different"]["VAR1"] == {"profile1": "value1", "profile2": "value2"}
|
|
||||||
|
|
||||||
|
|
||||||
class TestProfileErrors:
|
|
||||||
"""Test cases for profile errors."""
|
|
||||||
|
|
||||||
def test_create_duplicate_profile(self, temp_dir):
|
|
||||||
"""Test creating a profile that already exists."""
|
|
||||||
from env_pro.core.profile import create_profile, ProfileAlreadyExistsError
|
|
||||||
|
|
||||||
create_profile("dev", temp_dir)
|
|
||||||
|
|
||||||
with pytest.raises(ProfileAlreadyExistsError):
|
|
||||||
create_profile("dev", temp_dir)
|
|
||||||
|
|
||||||
def test_delete_nonexistent_profile(self, temp_dir):
|
|
||||||
"""Test deleting a profile that doesn't exist."""
|
|
||||||
from env_pro.core.profile import delete_profile, ProfileNotFoundError
|
|
||||||
|
|
||||||
with pytest.raises(ProfileNotFoundError):
|
|
||||||
delete_profile("nonexistent", temp_dir)
|
|
||||||
|
|
||||||
def test_switch_to_nonexistent_profile(self, temp_dir):
|
|
||||||
"""Test switching to a non-existent profile."""
|
|
||||||
from env_pro.core.profile import switch_profile, ProfileNotFoundError
|
|
||||||
|
|
||||||
with pytest.raises(ProfileNotFoundError):
|
|
||||||
switch_profile("nonexistent", temp_dir)
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
"""Tests for the rules module."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from cli_diff_auditor.rules import (
|
|
||||||
BuiltInRules,
|
|
||||||
Finding,
|
|
||||||
Rule,
|
|
||||||
RulesLoader,
|
|
||||||
Severity,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestRule:
|
|
||||||
"""Test cases for Rule class."""
|
|
||||||
|
|
||||||
def test_rule_creation(self):
|
|
||||||
"""Test creating a Rule instance."""
|
|
||||||
rule = Rule(
|
|
||||||
id="test-rule",
|
|
||||||
name="Test Rule",
|
|
||||||
description="A test rule",
|
|
||||||
pattern=r"test.*",
|
|
||||||
severity=Severity.WARNING,
|
|
||||||
auto_fix=True,
|
|
||||||
category="testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert rule.id == "test-rule"
|
|
||||||
assert rule.name == "Test Rule"
|
|
||||||
assert rule.severity == Severity.WARNING
|
|
||||||
assert rule.auto_fix is True
|
|
||||||
assert rule.enabled is True
|
|
||||||
|
|
||||||
def test_compile_pattern(self):
|
|
||||||
"""Test compiling a rule pattern."""
|
|
||||||
rule = Rule(
|
|
||||||
id="test",
|
|
||||||
name="Test",
|
|
||||||
description="Test",
|
|
||||||
pattern=r"\bprint\s*\(",
|
|
||||||
severity=Severity.WARNING
|
|
||||||
)
|
|
||||||
|
|
||||||
compiled = rule.compile_pattern()
|
|
||||||
|
|
||||||
assert compiled.search("print('hello')") is not None
|
|
||||||
assert compiled.search("hello world") is None
|
|
||||||
|
|
||||||
def test_get_fix_pattern(self):
|
|
||||||
"""Test getting the fix pattern."""
|
|
||||||
rule = Rule(
|
|
||||||
id="test",
|
|
||||||
name="Test",
|
|
||||||
description="Test",
|
|
||||||
pattern=r"test",
|
|
||||||
severity=Severity.INFO,
|
|
||||||
auto_fix=True,
|
|
||||||
fix_pattern=r"old",
|
|
||||||
replacement="new"
|
|
||||||
)
|
|
||||||
|
|
||||||
fix_pattern = rule.get_fix_pattern()
|
|
||||||
|
|
||||||
assert fix_pattern is not None
|
|
||||||
assert fix_pattern.search("old text") is not None
|
|
||||||
|
|
||||||
|
|
||||||
class TestBuiltInRules:
|
|
||||||
"""Test cases for BuiltInRules class."""
|
|
||||||
|
|
||||||
def test_get_all_rules_returns_list(self):
|
|
||||||
"""Test that get_all_rules returns a list."""
|
|
||||||
rules = BuiltInRules.get_all_rules()
|
|
||||||
|
|
||||||
assert isinstance(rules, list)
|
|
||||||
assert len(rules) > 0
|
|
||||||
|
|
||||||
def test_rules_have_required_fields(self):
|
|
||||||
"""Test that all rules have required fields."""
|
|
||||||
rules = BuiltInRules.get_all_rules()
|
|
||||||
|
|
||||||
for rule in rules:
|
|
||||||
assert rule.id
|
|
||||||
assert rule.name
|
|
||||||
assert rule.description
|
|
||||||
assert rule.pattern
|
|
||||||
assert rule.severity
|
|
||||||
|
|
||||||
def test_debug_rules_exist(self):
|
|
||||||
"""Test that debug-related rules exist."""
|
|
||||||
rules = BuiltInRules.get_all_rules()
|
|
||||||
rule_ids = [r.id for r in rules]
|
|
||||||
|
|
||||||
assert "debug-print" in rule_ids
|
|
||||||
assert "console-log" in rule_ids
|
|
||||||
|
|
||||||
def test_security_rules_exist(self):
|
|
||||||
"""Test that security-related rules exist."""
|
|
||||||
rules = BuiltInRules.get_all_rules()
|
|
||||||
rule_ids = [r.id for r in rules]
|
|
||||||
|
|
||||||
assert "sql-injection" in rule_ids
|
|
||||||
assert "hardcoded-password" in rule_ids
|
|
||||||
assert "eval-usage" in rule_ids
|
|
||||||
|
|
||||||
def test_error_handling_rules_exist(self):
|
|
||||||
"""Test that error handling rules exist."""
|
|
||||||
rules = BuiltInRules.get_all_rules()
|
|
||||||
rule_ids = [r.id for r in rules]
|
|
||||||
|
|
||||||
assert "bare-except" in rule_ids
|
|
||||||
assert "pass-except" in rule_ids
|
|
||||||
|
|
||||||
|
|
||||||
class TestRulesLoader:
|
|
||||||
"""Test cases for RulesLoader class."""
|
|
||||||
|
|
||||||
def setup_method(self):
|
|
||||||
"""Set up test fixtures."""
|
|
||||||
self.loader = RulesLoader()
|
|
||||||
|
|
||||||
def test_load_builtin_rules(self):
|
|
||||||
"""Test loading built-in rules."""
|
|
||||||
self.loader.load_builtin_rules()
|
|
||||||
|
|
||||||
rules = self.loader.get_all_rules()
|
|
||||||
|
|
||||||
assert len(rules) > 0
|
|
||||||
|
|
||||||
def test_get_rule(self):
|
|
||||||
"""Test getting a specific rule."""
|
|
||||||
self.loader.load_builtin_rules()
|
|
||||||
|
|
||||||
rule = self.loader.get_rule("debug-print")
|
|
||||||
|
|
||||||
assert rule is not None
|
|
||||||
assert rule.id == "debug-print"
|
|
||||||
|
|
||||||
def test_get_nonexistent_rule(self):
|
|
||||||
"""Test getting a rule that doesn't exist."""
|
|
||||||
self.loader.load_builtin_rules()
|
|
||||||
|
|
||||||
rule = self.loader.get_rule("nonexistent-rule")
|
|
||||||
|
|
||||||
assert rule is None
|
|
||||||
|
|
||||||
def test_disable_rule(self):
|
|
||||||
"""Test disabling a rule."""
|
|
||||||
self.loader.load_builtin_rules()
|
|
||||||
|
|
||||||
result = self.loader.disable_rule("debug-print")
|
|
||||||
|
|
||||||
assert result is True
|
|
||||||
|
|
||||||
rule = self.loader.get_rule("debug-print")
|
|
||||||
assert rule.enabled is False
|
|
||||||
|
|
||||||
def test_enable_rule(self):
|
|
||||||
"""Test enabling a rule."""
|
|
||||||
self.loader.load_builtin_rules()
|
|
||||||
self.loader.disable_rule("debug-print")
|
|
||||||
|
|
||||||
result = self.loader.enable_rule("debug-print")
|
|
||||||
|
|
||||||
assert result is True
|
|
||||||
|
|
||||||
rule = self.loader.get_rule("debug-print")
|
|
||||||
assert rule.enabled is True
|
|
||||||
|
|
||||||
def test_get_enabled_rules(self):
|
|
||||||
"""Test getting only enabled rules."""
|
|
||||||
self.loader.load_builtin_rules()
|
|
||||||
self.loader.disable_rule("debug-print")
|
|
||||||
self.loader.disable_rule("console-log")
|
|
||||||
|
|
||||||
enabled = self.loader.get_enabled_rules()
|
|
||||||
|
|
||||||
rule_ids = [r.id for r in enabled]
|
|
||||||
|
|
||||||
assert "debug-print" not in rule_ids
|
|
||||||
assert "console-log" not in rule_ids
|
|
||||||
|
|
||||||
def test_load_from_yaml(self):
|
|
||||||
"""Test loading rules from YAML."""
|
|
||||||
self.loader.load_builtin_rules()
|
|
||||||
yaml_content = """
|
|
||||||
rules:
|
|
||||||
- id: custom-rule
|
|
||||||
name: Custom Rule
|
|
||||||
description: A custom rule
|
|
||||||
pattern: "custom.*"
|
|
||||||
severity: warning
|
|
||||||
category: custom
|
|
||||||
"""
|
|
||||||
rules = self.loader.load_from_yaml(yaml_content)
|
|
||||||
|
|
||||||
assert len(rules) == 1
|
|
||||||
assert rules[0].id == "custom-rule"
|
|
||||||
|
|
||||||
all_rules = self.loader.get_all_rules()
|
|
||||||
assert len(all_rules) > 1
|
|
||||||
|
|
||||||
def test_load_from_yaml_minimal(self):
|
|
||||||
"""Test loading rules with minimal fields."""
|
|
||||||
yaml_content = """
|
|
||||||
rules:
|
|
||||||
- id: minimal-rule
|
|
||||||
name: Minimal
|
|
||||||
description: Minimal description
|
|
||||||
pattern: "test"
|
|
||||||
severity: error
|
|
||||||
"""
|
|
||||||
rules = self.loader.load_from_yaml(yaml_content)
|
|
||||||
|
|
||||||
assert len(rules) == 1
|
|
||||||
assert rules[0].severity == Severity.ERROR
|
|
||||||
assert rules[0].auto_fix is False
|
|
||||||
assert rules[0].category == "custom"
|
|
||||||
|
|
||||||
def test_parse_rule_data_missing_required_field(self):
|
|
||||||
"""Test parsing rule data with missing required field."""
|
|
||||||
data = {
|
|
||||||
"id": "test",
|
|
||||||
"name": "Test",
|
|
||||||
"description": "Test"
|
|
||||||
}
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
self.loader._parse_rule_data(data)
|
|
||||||
|
|
||||||
|
|
||||||
class TestFinding:
|
|
||||||
"""Test cases for Finding class."""
|
|
||||||
|
|
||||||
def test_finding_creation(self):
|
|
||||||
"""Test creating a Finding instance."""
|
|
||||||
finding = Finding(
|
|
||||||
rule_id="test-rule",
|
|
||||||
rule_name="Test Rule",
|
|
||||||
severity=Severity.WARNING,
|
|
||||||
file_path="test.py",
|
|
||||||
line_number=10,
|
|
||||||
line_content="print('debug')",
|
|
||||||
message="Debug print statement detected",
|
|
||||||
fix_available=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert finding.rule_id == "test-rule"
|
|
||||||
assert finding.severity == Severity.WARNING
|
|
||||||
assert finding.line_number == 10
|
|
||||||
assert finding.fix_available is True
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
"""Tests for template module."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class TestTemplateManagement:
|
|
||||||
"""Test cases for template management."""
|
|
||||||
|
|
||||||
def test_list_builtin_templates(self):
|
|
||||||
"""Test listing builtin templates."""
|
|
||||||
from env_pro.core.template import list_builtin_templates
|
|
||||||
|
|
||||||
templates = list_builtin_templates()
|
|
||||||
assert "fastapi" in templates
|
|
||||||
assert "django" in templates
|
|
||||||
assert "nodejs" in templates
|
|
||||||
assert "python" in templates
|
|
||||||
|
|
||||||
def test_load_builtin_template(self):
|
|
||||||
"""Test loading a builtin template."""
|
|
||||||
from env_pro.core.template import load_template
|
|
||||||
|
|
||||||
content = load_template("minimal")
|
|
||||||
assert "ENVIRONMENT" in content
|
|
||||||
|
|
||||||
def test_get_template_info(self):
|
|
||||||
"""Test getting template information."""
|
|
||||||
from env_pro.core.template import get_template_info
|
|
||||||
|
|
||||||
info = get_template_info("fastapi")
|
|
||||||
assert info["name"] == "fastapi"
|
|
||||||
assert info["type"] == "builtin"
|
|
||||||
assert "APP_NAME" in info["variables"]
|
|
||||||
|
|
||||||
def test_render_template(self):
|
|
||||||
"""Test rendering a template with variables."""
|
|
||||||
from env_pro.core.template import render_template
|
|
||||||
|
|
||||||
content = "APP_NAME=${APP_NAME}\nDEBUG=${DEBUG}"
|
|
||||||
variables = {"APP_NAME": "MyApp", "DEBUG": "true"}
|
|
||||||
rendered = render_template(content, variables)
|
|
||||||
assert "MyApp" in rendered
|
|
||||||
assert "true" in rendered
|
|
||||||
|
|
||||||
def test_render_template_missing_variable(self):
|
|
||||||
"""Test rendering template with missing variable raises error."""
|
|
||||||
from env_pro.core.template import render_template, TemplateSyntaxError
|
|
||||||
|
|
||||||
content = "APP_NAME=${APP_NAME}\nMISSING=${OTHER}"
|
|
||||||
variables = {"APP_NAME": "MyApp"}
|
|
||||||
|
|
||||||
with pytest.raises(TemplateSyntaxError):
|
|
||||||
render_template(content, variables)
|
|
||||||
|
|
||||||
def test_create_and_delete_user_template(self, temp_dir):
|
|
||||||
"""Test creating and deleting a user template."""
|
|
||||||
from env_pro.core.template import (
|
|
||||||
create_template, delete_template, load_template,
|
|
||||||
get_user_templates_dir, list_user_templates
|
|
||||||
)
|
|
||||||
import os
|
|
||||||
|
|
||||||
original_home = os.environ.get("HOME")
|
|
||||||
os.environ["HOME"] = str(temp_dir)
|
|
||||||
|
|
||||||
try:
|
|
||||||
create_template("my-template", "VAR1=value1\nVAR2=value2", "My custom template")
|
|
||||||
|
|
||||||
templates = list_user_templates()
|
|
||||||
assert "my-template" in templates
|
|
||||||
|
|
||||||
loaded = load_template("my-template")
|
|
||||||
assert "VAR1=value1" in loaded
|
|
||||||
|
|
||||||
delete_template("my-template")
|
|
||||||
templates = list_user_templates()
|
|
||||||
assert "my-template" not in templates
|
|
||||||
finally:
|
|
||||||
if original_home:
|
|
||||||
os.environ["HOME"] = original_home
|
|
||||||
else:
|
|
||||||
del os.environ["HOME"]
|
|
||||||
|
|
||||||
def test_apply_template(self, temp_dir):
|
|
||||||
"""Test applying a template to a file."""
|
|
||||||
from env_pro.core.template import apply_template
|
|
||||||
|
|
||||||
output_file = temp_dir / ".env"
|
|
||||||
apply_template("minimal", {"ENVIRONMENT": "production"}, output_file)
|
|
||||||
|
|
||||||
content = output_file.read_text()
|
|
||||||
assert "production" in content
|
|
||||||
|
|
||||||
|
|
||||||
class TestTemplateErrors:
|
|
||||||
"""Test cases for template errors."""
|
|
||||||
|
|
||||||
def test_load_nonexistent_template(self):
|
|
||||||
"""Test loading a template that doesn't exist."""
|
|
||||||
from env_pro.core.template import load_template, TemplateNotFoundError
|
|
||||||
|
|
||||||
with pytest.raises(TemplateNotFoundError):
|
|
||||||
load_template("nonexistent-template")
|
|
||||||
|
|
||||||
def test_delete_builtin_template(self):
|
|
||||||
"""Test that builtin templates cannot be deleted."""
|
|
||||||
from env_pro.core.template import delete_template, TemplateError
|
|
||||||
|
|
||||||
with pytest.raises(TemplateError):
|
|
||||||
delete_template("fastapi")
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
"""Tests for validation module."""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class TestSchemaValidation:
|
|
||||||
"""Test cases for schema validation."""
|
|
||||||
|
|
||||||
def test_load_empty_schema(self, temp_dir):
|
|
||||||
"""Test loading schema when no file exists."""
|
|
||||||
from env_pro.core.validator import load_schema
|
|
||||||
|
|
||||||
schema = load_schema(temp_dir)
|
|
||||||
assert schema is None
|
|
||||||
|
|
||||||
def test_load_schema(self, temp_dir):
|
|
||||||
"""Test loading a valid schema."""
|
|
||||||
from env_pro.core.validator import load_schema, EnvSchema
|
|
||||||
|
|
||||||
schema_file = temp_dir / ".env.schema.yaml"
|
|
||||||
schema_file.write_text("""
|
|
||||||
variables:
|
|
||||||
DATABASE_URL:
|
|
||||||
type: url
|
|
||||||
required: true
|
|
||||||
description: Database connection URL
|
|
||||||
DEBUG:
|
|
||||||
type: bool
|
|
||||||
default: false
|
|
||||||
""")
|
|
||||||
|
|
||||||
schema = load_schema(temp_dir)
|
|
||||||
assert schema is not None
|
|
||||||
assert "DATABASE_URL" in schema.variables
|
|
||||||
assert schema.variables["DATABASE_URL"].type == "url"
|
|
||||||
assert schema.variables["DATABASE_URL"].required
|
|
||||||
|
|
||||||
def test_validate_valid_string(self):
|
|
||||||
"""Test validation of valid string value."""
|
|
||||||
from env_pro.core.validator import validate_value, VariableSchema
|
|
||||||
|
|
||||||
schema = VariableSchema(type="string")
|
|
||||||
errors = validate_value("TEST_VAR", "some-value", schema)
|
|
||||||
assert errors == []
|
|
||||||
|
|
||||||
def test_validate_required_missing(self):
|
|
||||||
"""Test validation when required variable is missing."""
|
|
||||||
from env_pro.core.validator import validate_value, VariableSchema
|
|
||||||
|
|
||||||
schema = VariableSchema(type="string", required=True)
|
|
||||||
errors = validate_value("TEST_VAR", "", schema)
|
|
||||||
assert len(errors) > 0
|
|
||||||
assert "required" in errors[0].lower()
|
|
||||||
|
|
||||||
def test_validate_int_type(self):
|
|
||||||
"""Test validation of integer type."""
|
|
||||||
from env_pro.core.validator import validate_value, VariableSchema
|
|
||||||
|
|
||||||
schema = VariableSchema(type="int")
|
|
||||||
errors = validate_value("PORT", "8080", schema)
|
|
||||||
assert errors == []
|
|
||||||
|
|
||||||
errors = validate_value("PORT", "not-a-number", schema)
|
|
||||||
assert len(errors) > 0
|
|
||||||
|
|
||||||
def test_validate_bool_type(self):
|
|
||||||
"""Test validation of boolean type."""
|
|
||||||
from env_pro.core.validator import validate_value, VariableSchema
|
|
||||||
|
|
||||||
schema = VariableSchema(type="bool")
|
|
||||||
errors = validate_value("DEBUG", "true", schema)
|
|
||||||
assert errors == []
|
|
||||||
|
|
||||||
errors = validate_value("DEBUG", "yes", schema)
|
|
||||||
assert errors == []
|
|
||||||
|
|
||||||
def test_validate_email_type(self):
|
|
||||||
"""Test validation of email type."""
|
|
||||||
from env_pro.core.validator import validate_value, VariableSchema
|
|
||||||
|
|
||||||
schema = VariableSchema(type="email")
|
|
||||||
errors = validate_value("EMAIL", "user@example.com", schema)
|
|
||||||
assert errors == []
|
|
||||||
|
|
||||||
errors = validate_value("EMAIL", "invalid-email", schema)
|
|
||||||
assert len(errors) > 0
|
|
||||||
|
|
||||||
def test_validate_url_type(self):
|
|
||||||
"""Test validation of URL type."""
|
|
||||||
from env_pro.core.validator import validate_value, VariableSchema
|
|
||||||
|
|
||||||
schema = VariableSchema(type="url")
|
|
||||||
errors = validate_value("API_URL", "https://api.example.com", schema)
|
|
||||||
assert errors == []
|
|
||||||
|
|
||||||
errors = validate_value("API_URL", "not-a-url", schema)
|
|
||||||
assert len(errors) > 0
|
|
||||||
|
|
||||||
def test_validate_pattern(self):
|
|
||||||
"""Test validation with regex pattern."""
|
|
||||||
from env_pro.core.validator import validate_value, VariableSchema
|
|
||||||
|
|
||||||
schema = VariableSchema(type="string", pattern=r"^[A-Z]+$")
|
|
||||||
errors = validate_value("PREFIX", "ABC123", schema)
|
|
||||||
assert len(errors) > 0
|
|
||||||
|
|
||||||
errors = validate_value("PREFIX", "ABC", schema)
|
|
||||||
assert errors == []
|
|
||||||
|
|
||||||
def test_validate_enum(self):
|
|
||||||
"""Test validation with enum values."""
|
|
||||||
from env_pro.core.validator import validate_value, VariableSchema
|
|
||||||
|
|
||||||
schema = VariableSchema(type="string", enum=["dev", "staging", "prod"])
|
|
||||||
errors = validate_value("ENV", "dev", schema)
|
|
||||||
assert errors == []
|
|
||||||
|
|
||||||
errors = validate_value("ENV", "invalid", schema)
|
|
||||||
assert len(errors) > 0
|
|
||||||
|
|
||||||
def test_validate_min_max_length(self):
|
|
||||||
"""Test validation with min/max length constraints."""
|
|
||||||
from env_pro.core.validator import validate_value, VariableSchema
|
|
||||||
|
|
||||||
schema = VariableSchema(type="string", min_length=3, max_length=10)
|
|
||||||
errors = validate_value("NAME", "ab", schema)
|
|
||||||
assert len(errors) > 0
|
|
||||||
|
|
||||||
errors = validate_value("NAME", "abcdefghijkl", schema)
|
|
||||||
assert len(errors) > 0
|
|
||||||
|
|
||||||
errors = validate_value("NAME", "valid", schema)
|
|
||||||
assert errors == []
|
|
||||||
|
|
||||||
def test_validate_min_max_number(self):
|
|
||||||
"""Test validation with min/max number constraints."""
|
|
||||||
from env_pro.core.validator import validate_value, VariableSchema
|
|
||||||
|
|
||||||
schema = VariableSchema(type="int", minimum=1, maximum=100)
|
|
||||||
errors = validate_value("PORT", "0", schema)
|
|
||||||
assert len(errors) > 0
|
|
||||||
|
|
||||||
errors = validate_value("PORT", "101", schema)
|
|
||||||
assert len(errors) > 0
|
|
||||||
|
|
||||||
errors = validate_value("PORT", "50", schema)
|
|
||||||
assert errors == []
|
|
||||||
|
|
||||||
def test_validate_env_vars_full(self):
|
|
||||||
"""Test full environment validation."""
|
|
||||||
from env_pro.core.validator import validate_env_vars, EnvSchema, VariableSchema
|
|
||||||
|
|
||||||
schema = EnvSchema(variables={
|
|
||||||
"DATABASE_URL": VariableSchema(type="string", required=True),
|
|
||||||
"DEBUG": VariableSchema(type="bool", default="false")
|
|
||||||
})
|
|
||||||
|
|
||||||
vars_dict = {
|
|
||||||
"DATABASE_URL": "postgresql://localhost:5432/db",
|
|
||||||
"DEBUG": "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
errors = validate_env_vars(vars_dict, schema)
|
|
||||||
assert errors == []
|
|
||||||
|
|
||||||
def test_check_required_vars(self):
|
|
||||||
"""Test checking for missing required variables."""
|
|
||||||
from env_pro.core.validator import check_required_vars, EnvSchema, VariableSchema
|
|
||||||
|
|
||||||
schema = EnvSchema(variables={
|
|
||||||
"REQUIRED_VAR": VariableSchema(type="string", required=True),
|
|
||||||
"OPTIONAL_VAR": VariableSchema(type="string", required=False)
|
|
||||||
})
|
|
||||||
|
|
||||||
vars_dict = {"OPTIONAL_VAR": "value"}
|
|
||||||
missing = check_required_vars(vars_dict, schema)
|
|
||||||
assert "REQUIRED_VAR" in missing
|
|
||||||
|
|
||||||
vars_dict = {"REQUIRED_VAR": "value"}
|
|
||||||
missing = check_required_vars(vars_dict, schema)
|
|
||||||
assert len(missing) == 0
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
"""Unit tests for CLI module."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
from codesnap.__main__ import main
|
|
||||||
|
|
||||||
|
|
||||||
class TestCLI:
|
|
||||||
"""Tests for CLI commands."""
|
|
||||||
|
|
||||||
def setup_method(self) -> None:
|
|
||||||
self.runner = CliRunner()
|
|
||||||
|
|
||||||
def test_main_help(self) -> None:
|
|
||||||
result = self.runner.invoke(main, ["--help"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "CodeSnap" in result.output
|
|
||||||
|
|
||||||
def test_cli_version(self) -> None:
|
|
||||||
result = self.runner.invoke(main, ["--version"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "0.1.0" in result.output
|
|
||||||
|
|
||||||
def test_cli_analyze_nonexistent_path(self) -> None:
|
|
||||||
result = self.runner.invoke(main, ["analyze", "/nonexistent/path"])
|
|
||||||
assert result.exit_code != 0
|
|
||||||
|
|
||||||
def test_cli_analyze_current_directory(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
with open(os.path.join(tmpdir, "test.py"), "w") as f:
|
|
||||||
f.write("def test(): pass\n")
|
|
||||||
result = self.runner.invoke(main, ["analyze", tmpdir])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
def test_cli_analyze_with_output_format(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
with open(os.path.join(tmpdir, "test.py"), "w") as f:
|
|
||||||
f.write("def test(): pass\n")
|
|
||||||
result = self.runner.invoke(main, ["analyze", tmpdir, "--format", "json"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
def test_cli_analyze_with_max_files(self) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
with open(os.path.join(tmpdir, "test.py"), "w") as f:
|
|
||||||
f.write("def test(): pass\n")
|
|
||||||
result = self.runner.invoke(main, ["analyze", tmpdir, "--max-files", "10"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
|
|
||||||
def test_cli_languages(self) -> None:
|
|
||||||
result = self.runner.invoke(main, ["languages"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "python" in result.output.lower()
|
|
||||||
|
|
||||||
def test_cli_info_languages(self) -> None:
|
|
||||||
result = self.runner.invoke(main, ["info", "--languages"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
assert "python" in result.output.lower()
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
"""Unit tests for complexity analysis module."""
|
|
||||||
|
|
||||||
from codesnap.core.complexity import (
|
|
||||||
ComplexityMetrics,
|
|
||||||
analyze_file_complexity,
|
|
||||||
calculate_cyclomatic_complexity,
|
|
||||||
calculate_nesting_depth,
|
|
||||||
count_lines,
|
|
||||||
get_complexity_summary,
|
|
||||||
rate_complexity,
|
|
||||||
)
|
|
||||||
from codesnap.core.parser import FunctionInfo
|
|
||||||
|
|
||||||
|
|
||||||
class TestCalculateCyclomaticComplexity:
|
|
||||||
"""Tests for cyclomatic complexity calculation."""
|
|
||||||
|
|
||||||
def test_empty_content(self):
|
|
||||||
complexity, decisions = calculate_cyclomatic_complexity("")
|
|
||||||
assert complexity == 1
|
|
||||||
assert decisions == 0
|
|
||||||
|
|
||||||
def test_simple_function(self):
|
|
||||||
content = "def test():\n pass"
|
|
||||||
complexity, decisions = calculate_cyclomatic_complexity(content)
|
|
||||||
assert complexity == 1
|
|
||||||
|
|
||||||
def test_if_statement(self):
|
|
||||||
content = "if x > 0:\n pass"
|
|
||||||
complexity, decisions = calculate_cyclomatic_complexity(content)
|
|
||||||
assert complexity >= 1
|
|
||||||
|
|
||||||
def test_multiple_if_statements(self):
|
|
||||||
content = """
|
|
||||||
if x > 0:
|
|
||||||
pass
|
|
||||||
elif x < 0:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
complexity, decisions = calculate_cyclomatic_complexity(content)
|
|
||||||
assert complexity >= 3
|
|
||||||
|
|
||||||
def test_for_loop(self):
|
|
||||||
content = "for i in range(10):\n pass"
|
|
||||||
complexity, decisions = calculate_cyclomatic_complexity(content)
|
|
||||||
assert complexity >= 1
|
|
||||||
|
|
||||||
def test_while_loop(self):
|
|
||||||
content = "while True:\n pass"
|
|
||||||
complexity, decisions = calculate_cyclomatic_complexity(content)
|
|
||||||
assert complexity >= 1
|
|
||||||
|
|
||||||
def test_try_except(self):
|
|
||||||
content = """
|
|
||||||
try:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
complexity, decisions = calculate_cyclomatic_complexity(content)
|
|
||||||
assert complexity >= 1
|
|
||||||
|
|
||||||
def test_and_or_operators(self):
|
|
||||||
content = "if x > 0 and y > 0:\n pass"
|
|
||||||
complexity, decisions = calculate_cyclomatic_complexity(content)
|
|
||||||
assert complexity >= 2
|
|
||||||
|
|
||||||
def test_ternary_operator(self):
|
|
||||||
content = "x = 1 if cond else 2"
|
|
||||||
complexity, decisions = calculate_cyclomatic_complexity(content)
|
|
||||||
assert complexity >= 1
|
|
||||||
|
|
||||||
|
|
||||||
class TestCalculateNestingDepth:
|
|
||||||
"""Tests for nesting depth calculation."""
|
|
||||||
|
|
||||||
def test_flat_code(self):
|
|
||||||
depth = calculate_nesting_depth("x = 1\ny = 2")
|
|
||||||
assert depth >= 0
|
|
||||||
|
|
||||||
def test_single_brace_level(self):
|
|
||||||
depth = calculate_nesting_depth("if x: { y = 1 }")
|
|
||||||
assert depth >= 0
|
|
||||||
|
|
||||||
def test_nested_braces(self):
|
|
||||||
content = """
|
|
||||||
if x:
|
|
||||||
if y:
|
|
||||||
if z:
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
depth = calculate_nesting_depth(content)
|
|
||||||
assert depth >= 0 # Depends on brace detection
|
|
||||||
|
|
||||||
def test_mixed_brackets(self):
|
|
||||||
content = """
|
|
||||||
def test():
|
|
||||||
data = [
|
|
||||||
[1, 2],
|
|
||||||
{a: b}
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
depth = calculate_nesting_depth(content)
|
|
||||||
assert depth >= 1
|
|
||||||
|
|
||||||
def test_balanced_brackets(self):
|
|
||||||
content = "[](){}"
|
|
||||||
depth = calculate_nesting_depth(content)
|
|
||||||
assert depth >= 1
|
|
||||||
|
|
||||||
def test_unbalanced_close(self):
|
|
||||||
content = "x = 1]"
|
|
||||||
depth = calculate_nesting_depth(content)
|
|
||||||
assert depth >= 0
|
|
||||||
|
|
||||||
|
|
||||||
class TestCountLines:
|
|
||||||
"""Tests for line counting."""
|
|
||||||
|
|
||||||
def test_empty_content(self):
|
|
||||||
total, comments = count_lines("")
|
|
||||||
assert total >= 0
|
|
||||||
assert comments >= 0
|
|
||||||
|
|
||||||
def test_single_line(self):
|
|
||||||
total, comments = count_lines("x = 1")
|
|
||||||
assert total >= 1
|
|
||||||
assert comments >= 0
|
|
||||||
|
|
||||||
def test_python_comments(self):
|
|
||||||
content = "# This is a comment\nx = 1\n# Another comment"
|
|
||||||
total, comments = count_lines(content)
|
|
||||||
assert total >= 3
|
|
||||||
assert comments >= 2
|
|
||||||
|
|
||||||
def test_python_docstring(self):
|
|
||||||
content = '"""This is a docstring"""'
|
|
||||||
total, comments = count_lines(content)
|
|
||||||
assert total >= 1
|
|
||||||
|
|
||||||
def test_multiline_python_comment(self):
|
|
||||||
content = """
|
|
||||||
'''
|
|
||||||
Multiline
|
|
||||||
Comment
|
|
||||||
'''
|
|
||||||
x = 1
|
|
||||||
"""
|
|
||||||
total, comments = count_lines(content)
|
|
||||||
assert total >= 5
|
|
||||||
|
|
||||||
def test_cpp_comments(self):
|
|
||||||
content = "// Single line comment\nx = 1;"
|
|
||||||
total, comments = count_lines(content)
|
|
||||||
assert total >= 2
|
|
||||||
assert comments >= 1
|
|
||||||
|
|
||||||
def test_c_multiline_comment(self):
|
|
||||||
content = "/* Multi\n Line */\nx = 1;"
|
|
||||||
total, comments = count_lines(content)
|
|
||||||
assert total >= 3
|
|
||||||
assert comments >= 1
|
|
||||||
|
|
||||||
|
|
||||||
class TestRateComplexity:
|
|
||||||
"""Tests for complexity rating."""
|
|
||||||
|
|
||||||
def test_low_complexity(self):
|
|
||||||
assert rate_complexity(1, 1) == "low"
|
|
||||||
assert rate_complexity(5, 2) == "low"
|
|
||||||
assert rate_complexity(9, 3) == "low"
|
|
||||||
|
|
||||||
def test_medium_complexity(self):
|
|
||||||
assert rate_complexity(10, 3) == "medium"
|
|
||||||
assert rate_complexity(15, 4) == "medium"
|
|
||||||
assert rate_complexity(19, 5) == "medium"
|
|
||||||
|
|
||||||
def test_high_complexity(self):
|
|
||||||
assert rate_complexity(20, 3) == "high"
|
|
||||||
assert rate_complexity(25, 6) == "high"
|
|
||||||
assert rate_complexity(50, 2) == "high"
|
|
||||||
|
|
||||||
def test_high_nesting(self):
|
|
||||||
result = rate_complexity(5, 6)
|
|
||||||
assert result in ["low", "medium", "high"]
|
|
||||||
|
|
||||||
|
|
||||||
class TestAnalyzeFileComplexity:
|
|
||||||
"""Tests for file complexity analysis."""
|
|
||||||
|
|
||||||
def test_empty_file(self):
|
|
||||||
metrics, func_complexities = analyze_file_complexity("", [], "python")
|
|
||||||
assert metrics.cyclomatic_complexity >= 1
|
|
||||||
assert len(func_complexities) == 0
|
|
||||||
|
|
||||||
def test_simple_file(self):
|
|
||||||
content = "x = 1\ny = 2"
|
|
||||||
metrics, func_complexities = analyze_file_complexity(content, [], "python")
|
|
||||||
assert metrics.complexity_rating in ["low", "medium", "high"]
|
|
||||||
|
|
||||||
def test_complex_file(self):
|
|
||||||
content = """
|
|
||||||
def test():
|
|
||||||
if x > 0:
|
|
||||||
if y > 0:
|
|
||||||
if z > 0:
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
func = FunctionInfo(
|
|
||||||
name="test",
|
|
||||||
node_type="function",
|
|
||||||
start_line=1,
|
|
||||||
end_line=6,
|
|
||||||
parameters=[],
|
|
||||||
)
|
|
||||||
metrics, func_complexities = analyze_file_complexity(content, [func], "python")
|
|
||||||
assert metrics.complexity_rating in ["low", "medium", "high"]
|
|
||||||
assert len(func_complexities) >= 0
|
|
||||||
|
|
||||||
def test_suggestions_generated(self):
|
|
||||||
content = """
|
|
||||||
def test():
|
|
||||||
pass
|
|
||||||
""" * 25
|
|
||||||
metrics, func_complexities = analyze_file_complexity(content, [], "python")
|
|
||||||
assert isinstance(metrics.suggestions, list)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetComplexitySummary:
|
|
||||||
"""Tests for complexity summary generation."""
|
|
||||||
|
|
||||||
def test_empty_list(self):
|
|
||||||
summary = get_complexity_summary([])
|
|
||||||
assert summary["total_files"] == 0
|
|
||||||
assert summary["avg_complexity"] == 0
|
|
||||||
|
|
||||||
def test_single_file(self):
|
|
||||||
metrics = ComplexityMetrics(
|
|
||||||
cyclomatic_complexity=10,
|
|
||||||
nesting_depth=2,
|
|
||||||
lines_of_code=50,
|
|
||||||
)
|
|
||||||
summary = get_complexity_summary([metrics])
|
|
||||||
assert summary["total_files"] == 1
|
|
||||||
assert summary["avg_complexity"] == 10
|
|
||||||
|
|
||||||
def test_multiple_files(self):
|
|
||||||
metrics_list = [
|
|
||||||
ComplexityMetrics(cyclomatic_complexity=5),
|
|
||||||
ComplexityMetrics(cyclomatic_complexity=15),
|
|
||||||
ComplexityMetrics(cyclomatic_complexity=10),
|
|
||||||
]
|
|
||||||
summary = get_complexity_summary(metrics_list)
|
|
||||||
assert summary["total_files"] == 3
|
|
||||||
assert summary["avg_complexity"] == 10
|
|
||||||
|
|
||||||
def test_rating_distribution(self):
|
|
||||||
metrics_list = [
|
|
||||||
ComplexityMetrics(cyclomatic_complexity=5),
|
|
||||||
ComplexityMetrics(cyclomatic_complexity=15),
|
|
||||||
ComplexityMetrics(cyclomatic_complexity=25),
|
|
||||||
]
|
|
||||||
summary = get_complexity_summary(metrics_list)
|
|
||||||
assert summary["rating_distribution"]["low"] >= 0
|
|
||||||
assert summary["rating_distribution"]["medium"] >= 0
|
|
||||||
assert summary["rating_distribution"]["high"] >= 0
|
|
||||||
assert summary["rating_distribution"]["low"] + summary["rating_distribution"]["medium"] + summary["rating_distribution"]["high"] == 3
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"""Unit tests for config module."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from codesnap.utils.config import Config, apply_env_overrides, load_config
|
|
||||||
|
|
||||||
|
|
||||||
class TestConfig:
|
|
||||||
"""Tests for Config class."""
|
|
||||||
|
|
||||||
def test_default_values(self) -> None:
|
|
||||||
config = Config()
|
|
||||||
assert config.max_files == 1000
|
|
||||||
assert config.max_tokens == 8000
|
|
||||||
assert config.default_format == "markdown"
|
|
||||||
|
|
||||||
def test_custom_values(self) -> None:
|
|
||||||
config = Config(max_files=500, max_tokens=4000, default_format="json")
|
|
||||||
assert config.max_files == 500
|
|
||||||
assert config.max_tokens == 4000
|
|
||||||
assert config.default_format == "json"
|
|
||||||
|
|
||||||
def test_default_ignore_patterns(self) -> None:
|
|
||||||
config = Config()
|
|
||||||
assert isinstance(config.ignore_patterns, list)
|
|
||||||
|
|
||||||
def test_default_languages(self) -> None:
|
|
||||||
config = Config()
|
|
||||||
assert isinstance(config.included_languages, list)
|
|
||||||
assert isinstance(config.excluded_languages, list)
|
|
||||||
|
|
||||||
|
|
||||||
class TestLoadConfig:
|
|
||||||
"""Tests for load_config function."""
|
|
||||||
|
|
||||||
def test_load_default_config(self) -> None:
|
|
||||||
config = load_config()
|
|
||||||
assert config.max_files == 1000
|
|
||||||
assert config.max_tokens == 8000
|
|
||||||
|
|
||||||
def test_load_nonexistent_file(self) -> None:
|
|
||||||
from pathlib import Path
|
|
||||||
config = load_config(Path("/nonexistent/path.tomll"))
|
|
||||||
assert config.max_files == 1000
|
|
||||||
|
|
||||||
|
|
||||||
class TestApplyEnvOverrides:
|
|
||||||
"""Tests for apply_env_overrides function."""
|
|
||||||
|
|
||||||
def test_no_overrides(self) -> None:
|
|
||||||
config = Config()
|
|
||||||
result = apply_env_overrides(config)
|
|
||||||
assert result.max_files == 1000
|
|
||||||
|
|
||||||
def test_max_files_override(self) -> None:
|
|
||||||
os.environ["CODSNAP_MAX_FILES"] = "500"
|
|
||||||
try:
|
|
||||||
config = Config()
|
|
||||||
result = apply_env_overrides(config)
|
|
||||||
assert result.max_files == 500
|
|
||||||
finally:
|
|
||||||
del os.environ["CODSNAP_MAX_FILES"]
|
|
||||||
|
|
||||||
def test_max_tokens_override(self) -> None:
|
|
||||||
os.environ["CODSNAP_MAX_TOKENS"] = "4000"
|
|
||||||
try:
|
|
||||||
config = Config()
|
|
||||||
result = apply_env_overrides(config)
|
|
||||||
assert result.max_tokens == 4000
|
|
||||||
finally:
|
|
||||||
del os.environ["CODSNAP_MAX_TOKENS"]
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
"""Unit tests for dependency graph module."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from codesnap.core.dependency_graph import Dependency, DependencyGraphBuilder, DependencyParser
|
|
||||||
|
|
||||||
|
|
||||||
class TestDependencyParser:
|
|
||||||
"""Tests for DependencyParser class."""
|
|
||||||
|
|
||||||
def setup_method(self) -> None:
|
|
||||||
self.parser = DependencyParser()
|
|
||||||
|
|
||||||
def test_parse_python_import(self) -> None:
|
|
||||||
code = "import os"
|
|
||||||
deps = self.parser.parse_file(Path("test.py"), code, "python")
|
|
||||||
assert len(deps) >= 1
|
|
||||||
|
|
||||||
def test_parse_python_from_import(self) -> None:
|
|
||||||
code = "from pathlib import Path"
|
|
||||||
deps = self.parser.parse_file(Path("test.py"), code, "python")
|
|
||||||
assert len(deps) >= 1
|
|
||||||
|
|
||||||
def test_parse_python_multiple_imports(self) -> None:
|
|
||||||
code = """
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from collections import defaultdict
|
|
||||||
"""
|
|
||||||
deps = self.parser.parse_file(Path("test.py"), code, "python")
|
|
||||||
assert len(deps) >= 3
|
|
||||||
|
|
||||||
def test_parse_javascript_require(self) -> None:
|
|
||||||
code = "const express = require('express');"
|
|
||||||
deps = self.parser.parse_file(Path("test.js"), code, "javascript")
|
|
||||||
assert len(deps) >= 1
|
|
||||||
|
|
||||||
def test_parse_javascript_import(self) -> None:
|
|
||||||
code = "import { useState } from 'react';"
|
|
||||||
deps = self.parser.parse_file(Path("test.js"), code, "javascript")
|
|
||||||
assert len(deps) >= 1
|
|
||||||
|
|
||||||
def test_parse_go_import(self) -> None:
|
|
||||||
code = 'import "fmt"'
|
|
||||||
deps = self.parser.parse_file(Path("test.go"), code, "go")
|
|
||||||
assert len(deps) >= 1
|
|
||||||
|
|
||||||
def test_parse_rust_use(self) -> None:
|
|
||||||
code = "use std::collections::HashMap;"
|
|
||||||
deps = self.parser.parse_file(Path("test.rs"), code, "rust")
|
|
||||||
assert len(deps) >= 1
|
|
||||||
|
|
||||||
def test_parse_java_import(self) -> None:
|
|
||||||
code = "import java.util.ArrayList;"
|
|
||||||
deps = self.parser.parse_file(Path("test.java"), code, "java")
|
|
||||||
assert len(deps) >= 1
|
|
||||||
|
|
||||||
def test_parse_unsupported_language(self) -> None:
|
|
||||||
code = "some random code"
|
|
||||||
deps = self.parser.parse_file(Path("test.xyz"), code, "unsupported")
|
|
||||||
assert len(deps) == 0
|
|
||||||
|
|
||||||
|
|
||||||
class TestDependencyGraphBuilder:
|
|
||||||
"""Tests for DependencyGraphBuilder class."""
|
|
||||||
|
|
||||||
def setup_method(self) -> None:
|
|
||||||
self.graph = DependencyGraphBuilder()
|
|
||||||
|
|
||||||
def test_add_file(self) -> None:
|
|
||||||
self.graph.add_file(Path("main.py"), "python", 100, 10, 2, 1)
|
|
||||||
assert self.graph.graph.number_of_nodes() == 1
|
|
||||||
assert Path("main.py") in self.graph.graph.nodes()
|
|
||||||
|
|
||||||
def test_add_dependency(self) -> None:
|
|
||||||
self.graph.add_file(Path("a.py"), "python", 50, 5, 1, 0)
|
|
||||||
self.graph.add_file(Path("b.py"), "python", 60, 6, 1, 0)
|
|
||||||
|
|
||||||
dep = Dependency(
|
|
||||||
source_file=Path("a.py"),
|
|
||||||
target_file=Path("b.py"),
|
|
||||||
import_statement="import b",
|
|
||||||
import_type="import"
|
|
||||||
)
|
|
||||||
self.graph.add_dependency(dep)
|
|
||||||
|
|
||||||
assert self.graph.graph.has_edge(Path("a.py"), Path("b.py"))
|
|
||||||
|
|
||||||
def test_build_from_analysis(self) -> None:
|
|
||||||
analysis_result = {
|
|
||||||
"files": [
|
|
||||||
{"path": "main.py", "language": "python", "size": 100, "lines": 10, "functions": ["main"], "classes": []},
|
|
||||||
{"path": "utils.py", "language": "python", "size": 50, "lines": 5, "functions": ["helper"], "classes": []}
|
|
||||||
],
|
|
||||||
"dependencies": [
|
|
||||||
{"source": "main.py", "target": "utils.py", "type": "import"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
self.graph.build_from_analysis(analysis_result)
|
|
||||||
|
|
||||||
assert self.graph.graph.number_of_nodes() == 2
|
|
||||||
assert self.graph.graph.has_edge(Path("main.py"), Path("utils.py"))
|
|
||||||
|
|
||||||
def test_find_cycles(self) -> None:
|
|
||||||
self.graph.add_file(Path("a.py"), "python", 50, 5, 1, 0)
|
|
||||||
self.graph.add_file(Path("b.py"), "python", 50, 5, 1, 0)
|
|
||||||
self.graph.add_file(Path("c.py"), "python", 50, 5, 1, 0)
|
|
||||||
|
|
||||||
dep1 = Dependency(Path("a.py"), Path("b.py"), "import b", "import")
|
|
||||||
dep2 = Dependency(Path("b.py"), Path("c.py"), "import c", "import")
|
|
||||||
dep3 = Dependency(Path("c.py"), Path("a.py"), "import a", "import")
|
|
||||||
|
|
||||||
self.graph.add_dependency(dep1)
|
|
||||||
self.graph.add_dependency(dep2)
|
|
||||||
self.graph.add_dependency(dep3)
|
|
||||||
|
|
||||||
cycles = self.graph.find_cycles()
|
|
||||||
assert len(cycles) >= 1
|
|
||||||
|
|
||||||
def test_find_no_cycles(self) -> None:
|
|
||||||
self.graph.add_file(Path("a.py"), "python", 50, 5, 1, 0)
|
|
||||||
self.graph.add_file(Path("b.py"), "python", 50, 5, 1, 0)
|
|
||||||
|
|
||||||
dep = Dependency(Path("a.py"), Path("b.py"), "import b", "import")
|
|
||||||
self.graph.add_dependency(dep)
|
|
||||||
|
|
||||||
cycles = self.graph.find_cycles()
|
|
||||||
assert len(cycles) == 0
|
|
||||||
|
|
||||||
def test_find_orphaned_files(self) -> None:
|
|
||||||
self.graph.add_file(Path("orphan.py"), "python", 50, 5, 1, 0)
|
|
||||||
self.graph.add_file(Path("main.py"), "python", 100, 10, 2, 1)
|
|
||||||
self.graph.add_file(Path("used.py"), "python", 50, 5, 1, 0)
|
|
||||||
|
|
||||||
dep = Dependency(Path("main.py"), Path("used.py"), "import used", "import")
|
|
||||||
self.graph.add_dependency(dep)
|
|
||||||
|
|
||||||
orphaned = self.graph.find_orphaned_files()
|
|
||||||
assert Path("orphan.py") in orphaned
|
|
||||||
assert Path("main.py") not in orphaned
|
|
||||||
assert Path("used.py") not in orphaned
|
|
||||||
|
|
||||||
def test_calculate_metrics(self) -> None:
|
|
||||||
self.graph.add_file(Path("main.py"), "python", 100, 10, 2, 1)
|
|
||||||
self.graph.add_file(Path("utils.py"), "python", 50, 5, 1, 0)
|
|
||||||
|
|
||||||
dep = Dependency(Path("main.py"), Path("utils.py"), "import utils", "import")
|
|
||||||
self.graph.add_dependency(dep)
|
|
||||||
|
|
||||||
metrics = self.graph.calculate_metrics()
|
|
||||||
|
|
||||||
assert metrics.total_files == 2
|
|
||||||
assert metrics.total_edges == 1
|
|
||||||
assert metrics.density >= 0
|
|
||||||
|
|
||||||
def test_get_transitive_closure(self) -> None:
|
|
||||||
self.graph.add_file(Path("a.py"), "python", 50, 5, 1, 0)
|
|
||||||
self.graph.add_file(Path("b.py"), "python", 50, 5, 1, 0)
|
|
||||||
self.graph.add_file(Path("c.py"), "python", 50, 5, 1, 0)
|
|
||||||
|
|
||||||
self.graph.add_dependency(Dependency(Path("a.py"), Path("b.py"), "import b", "import"))
|
|
||||||
self.graph.add_dependency(Dependency(Path("b.py"), Path("c.py"), "import c", "import"))
|
|
||||||
|
|
||||||
dependents = self.graph.get_transitive_closure(Path("c.py"))
|
|
||||||
assert len(dependents) >= 0 # May or may not find depending on graph structure
|
|
||||||
|
|
||||||
def test_get_dependencies(self) -> None:
|
|
||||||
self.graph.add_file(Path("a.py"), "python", 50, 5, 1, 0)
|
|
||||||
self.graph.add_file(Path("b.py"), "python", 50, 5, 1, 0)
|
|
||||||
self.graph.add_file(Path("c.py"), "python", 50, 5, 1, 0)
|
|
||||||
|
|
||||||
self.graph.add_dependency(Dependency(Path("a.py"), Path("b.py"), "import b", "import"))
|
|
||||||
self.graph.add_dependency(Dependency(Path("a.py"), Path("c.py"), "import c", "import"))
|
|
||||||
|
|
||||||
deps = self.graph.get_dependencies(Path("a.py"))
|
|
||||||
assert isinstance(deps, set) # Returns a set
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
from codesnap.core.extractor import FunctionExtractor
|
|
||||||
|
|
||||||
|
|
||||||
class TestFunctionExtractor:
|
|
||||||
def setup_method(self) -> None:
|
|
||||||
self.extractor = FunctionExtractor()
|
|
||||||
|
|
||||||
def test_extract_simple_function(self) -> None:
|
|
||||||
code = """
|
|
||||||
def hello():
|
|
||||||
print("Hello, World!")
|
|
||||||
"""
|
|
||||||
functions = self.extractor.extract_functions_python(code)
|
|
||||||
assert len(functions) >= 1
|
|
||||||
func = functions[0]
|
|
||||||
assert func.name == "hello"
|
|
||||||
assert len(func.parameters) == 0
|
|
||||||
|
|
||||||
def test_extract_function_with_parameters(self) -> None:
|
|
||||||
code = """
|
|
||||||
def greet(name, greeting="Hello"):
|
|
||||||
return f"{greeting}, {name}!"
|
|
||||||
"""
|
|
||||||
functions = self.extractor.extract_functions_python(code)
|
|
||||||
assert len(functions) >= 1
|
|
||||||
func = functions[0]
|
|
||||||
assert func.name == "greet"
|
|
||||||
assert "name" in func.parameters
|
|
||||||
assert "greeting" in func.parameters
|
|
||||||
|
|
||||||
def test_extract_async_function(self) -> None:
|
|
||||||
code = """
|
|
||||||
async def fetch_data(url):
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(url) as response:
|
|
||||||
return await response.json()
|
|
||||||
"""
|
|
||||||
functions = self.extractor.extract_functions_python(code)
|
|
||||||
assert len(functions) >= 1
|
|
||||||
func = functions[0]
|
|
||||||
assert func.name == "fetch_data"
|
|
||||||
assert func.is_async is True
|
|
||||||
|
|
||||||
def test_extract_function_with_return_type(self) -> None:
|
|
||||||
code = """
|
|
||||||
def add(a: int, b: int) -> int:
|
|
||||||
return a + b
|
|
||||||
"""
|
|
||||||
functions = self.extractor.extract_functions_python(code)
|
|
||||||
assert len(functions) >= 1
|
|
||||||
func = functions[0]
|
|
||||||
assert func.name == "add"
|
|
||||||
|
|
||||||
def test_extract_function_with_decorator(self) -> None:
|
|
||||||
code = """
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self._name
|
|
||||||
"""
|
|
||||||
functions = self.extractor.extract_functions_python(code)
|
|
||||||
assert len(functions) >= 1
|
|
||||||
|
|
||||||
def test_extract_classes(self) -> None:
|
|
||||||
code = """
|
|
||||||
class MyClass:
|
|
||||||
def __init__(self):
|
|
||||||
self.value = 42
|
|
||||||
|
|
||||||
def get_value(self):
|
|
||||||
return self.value
|
|
||||||
"""
|
|
||||||
classes = self.extractor.extract_classes_python(code)
|
|
||||||
assert len(classes) >= 1
|
|
||||||
cls = classes[0]
|
|
||||||
assert cls.name == "MyClass"
|
|
||||||
|
|
||||||
def test_extract_class_with_inheritance(self) -> None:
|
|
||||||
code = """
|
|
||||||
class ChildClass(ParentClass, MixinClass):
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
classes = self.extractor.extract_classes_python(code)
|
|
||||||
assert len(classes) >= 1
|
|
||||||
cls = classes[0]
|
|
||||||
assert "ParentClass" in cls.base_classes
|
|
||||||
assert "MixinClass" in cls.base_classes
|
|
||||||
|
|
||||||
def test_extract_all_python(self) -> None:
|
|
||||||
code = """
|
|
||||||
def func1():
|
|
||||||
pass
|
|
||||||
|
|
||||||
class MyClass:
|
|
||||||
def method1(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def func2():
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
functions, classes = self.extractor.extract_all(code, "python")
|
|
||||||
assert len(functions) >= 2
|
|
||||||
assert len(classes) >= 1
|
|
||||||
|
|
||||||
def test_extract_from_file(self) -> None:
|
|
||||||
code = """
|
|
||||||
def test_function(x, y):
|
|
||||||
return x + y
|
|
||||||
"""
|
|
||||||
result = self.extractor.extract_from_file("test.py", code, "python")
|
|
||||||
assert result["file"] == "test.py"
|
|
||||||
assert len(result["functions"]) >= 1
|
|
||||||
assert result["functions"][0]["name"] == "test_function"
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
from codesnap.utils.file_utils import FileUtils
|
|
||||||
|
|
||||||
|
|
||||||
class TestFileUtils:
|
|
||||||
def test_should_ignore_patterns(self) -> None:
|
|
||||||
assert FileUtils.should_ignore("test.pyc", ["*.pyc"]) is True
|
|
||||||
assert FileUtils.should_ignore("test.pyc", ["*.pyc", "*.pyo"]) is True
|
|
||||||
assert FileUtils.should_ignore("test.py", ["*.pyc"]) is False
|
|
||||||
|
|
||||||
def test_should_ignore_directory(self) -> None:
|
|
||||||
assert FileUtils.should_ignore("src/__pycache__", ["__pycache__/*"]) is True
|
|
||||||
assert FileUtils.should_ignore(".git/config", [".git/*"]) is True
|
|
||||||
|
|
||||||
def test_is_text_file(self) -> None:
|
|
||||||
assert FileUtils.is_text_file("test.py") is True
|
|
||||||
assert FileUtils.is_text_file("test.js") is True
|
|
||||||
assert FileUtils.is_text_file("test.tsx") is True
|
|
||||||
assert FileUtils.is_text_file("Dockerfile") is True
|
|
||||||
|
|
||||||
def test_is_not_binary_file(self) -> None:
|
|
||||||
assert FileUtils.is_text_file("test.png") is False
|
|
||||||
assert FileUtils.is_text_file("test.jpg") is False
|
|
||||||
assert FileUtils.is_text_file("test.so") is False
|
|
||||||
|
|
||||||
def test_read_file_content(self) -> None:
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
||||||
f.write("print('hello')")
|
|
||||||
temp_path = f.name
|
|
||||||
try:
|
|
||||||
content = FileUtils.read_file_content(temp_path)
|
|
||||||
assert content == "print('hello')"
|
|
||||||
finally:
|
|
||||||
os.unlink(temp_path)
|
|
||||||
|
|
||||||
def test_read_file_content_not_found(self) -> None:
|
|
||||||
content = FileUtils.read_file_content("/nonexistent/file.py")
|
|
||||||
assert content is None
|
|
||||||
|
|
||||||
def test_count_lines(self) -> None:
|
|
||||||
content = "line1\nline2\nline3"
|
|
||||||
assert FileUtils.count_lines(content) == 3
|
|
||||||
assert FileUtils.count_lines("") == 1
|
|
||||||
assert FileUtils.count_lines("single") == 1
|
|
||||||
|
|
||||||
def test_get_relative_path(self) -> None:
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
subdir = os.path.join(tmpdir, "subdir")
|
|
||||||
os.makedirs(subdir)
|
|
||||||
filepath = os.path.join(subdir, "test.py")
|
|
||||||
rel = FileUtils.get_relative_path(filepath, tmpdir)
|
|
||||||
assert rel == os.path.join("subdir", "test.py")
|
|
||||||
|
|
||||||
def test_walk_directory(self) -> None:
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
os.makedirs(os.path.join(tmpdir, "src"))
|
|
||||||
os.makedirs(os.path.join(tmpdir, "tests"))
|
|
||||||
with open(os.path.join(tmpdir, "main.py"), "w") as f:
|
|
||||||
f.write("print('hello')")
|
|
||||||
with open(os.path.join(tmpdir, "src", "module.py"), "w") as f:
|
|
||||||
f.write("def test(): pass")
|
|
||||||
|
|
||||||
files = FileUtils.walk_directory(tmpdir, ["*.pyc", "__pycache__/*"], 100)
|
|
||||||
assert len(files) == 2
|
|
||||||
|
|
||||||
def test_walk_directory_with_ignore(self) -> None:
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
os.makedirs(os.path.join(tmpdir, "__pycache__"))
|
|
||||||
with open(os.path.join(tmpdir, "main.py"), "w") as f:
|
|
||||||
f.write("print('hello')")
|
|
||||||
with open(os.path.join(tmpdir, "__pycache__", "cache.pyc"), "w") as f:
|
|
||||||
f.write("cached")
|
|
||||||
|
|
||||||
files = FileUtils.walk_directory(tmpdir, ["*.pyc", "__pycache__/*"], 100)
|
|
||||||
assert len(files) == 1
|
|
||||||
assert "__pycache__" not in files[0]
|
|
||||||
|
|
||||||
def test_get_directory_tree(self) -> None:
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
os.makedirs(os.path.join(tmpdir, "src"))
|
|
||||||
os.makedirs(os.path.join(tmpdir, "tests"))
|
|
||||||
with open(os.path.join(tmpdir, "main.py"), "w") as f:
|
|
||||||
f.write("")
|
|
||||||
with open(os.path.join(tmpdir, "src", "module.py"), "w") as f:
|
|
||||||
f.write("")
|
|
||||||
|
|
||||||
tree = FileUtils.get_directory_tree(tmpdir, ["*.pyc"], 3)
|
|
||||||
assert len(tree) > 0
|
|
||||||
assert any("main.py" in line for line in tree)
|
|
||||||
@@ -1,428 +0,0 @@
|
|||||||
"""Unit tests for output formatters."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from codesnap.core.analyzer import AnalysisResult, FileAnalysis
|
|
||||||
from codesnap.core.parser import ClassInfo, FunctionInfo
|
|
||||||
from codesnap.output.json_exporter import export_json, export_json_file
|
|
||||||
from codesnap.output.llm_exporter import (
|
|
||||||
estimate_tokens,
|
|
||||||
export_llm_optimized,
|
|
||||||
truncate_for_token_limit,
|
|
||||||
)
|
|
||||||
from codesnap.output.markdown_exporter import export_markdown, export_markdown_file
|
|
||||||
|
|
||||||
|
|
||||||
class TestJsonExporter:
|
|
||||||
"""Tests for JSON export functionality."""
|
|
||||||
|
|
||||||
def create_test_result(self):
|
|
||||||
"""Create a test analysis result."""
|
|
||||||
func = FunctionInfo(
|
|
||||||
name="test_function",
|
|
||||||
node_type="function_definition",
|
|
||||||
start_line=1,
|
|
||||||
end_line=10,
|
|
||||||
parameters=[{"name": "x", "type": "int"}],
|
|
||||||
return_type="str",
|
|
||||||
is_async=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
cls = ClassInfo(
|
|
||||||
name="TestClass",
|
|
||||||
start_line=1,
|
|
||||||
end_line=20,
|
|
||||||
bases=["BaseClass"],
|
|
||||||
methods=[func],
|
|
||||||
)
|
|
||||||
|
|
||||||
file_analysis = FileAnalysis(
|
|
||||||
path=Path("/test/project/main.py"),
|
|
||||||
language="python",
|
|
||||||
size=500,
|
|
||||||
lines=50,
|
|
||||||
functions=[func],
|
|
||||||
classes=[cls],
|
|
||||||
)
|
|
||||||
|
|
||||||
result = AnalysisResult()
|
|
||||||
result.summary = {
|
|
||||||
"total_files": 1,
|
|
||||||
"total_functions": 1,
|
|
||||||
"total_classes": 1,
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"languages": {"python": 1},
|
|
||||||
}
|
|
||||||
result.files = [file_analysis]
|
|
||||||
result.dependencies = []
|
|
||||||
result.metrics = {}
|
|
||||||
result.analysis_time = 0.1
|
|
||||||
result.error_count = 0
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def test_export_json_structure(self):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
|
|
||||||
json_output = export_json(result, root)
|
|
||||||
|
|
||||||
data = json.loads(json_output)
|
|
||||||
|
|
||||||
assert "metadata" in data
|
|
||||||
assert "summary" in data
|
|
||||||
assert "files" in data
|
|
||||||
assert "dependencies" in data
|
|
||||||
assert "metrics" in data
|
|
||||||
|
|
||||||
def test_export_json_metadata(self):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
|
|
||||||
json_output = export_json(result, root)
|
|
||||||
data = json.loads(json_output)
|
|
||||||
|
|
||||||
assert data["metadata"]["tool"] == "CodeSnap"
|
|
||||||
assert data["metadata"]["version"] == "0.1.0"
|
|
||||||
assert "timestamp" in data["metadata"]
|
|
||||||
assert data["metadata"]["root_path"] == "/test/project"
|
|
||||||
|
|
||||||
def test_export_json_summary(self):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
|
|
||||||
json_output = export_json(result, root)
|
|
||||||
data = json.loads(json_output)
|
|
||||||
|
|
||||||
assert data["summary"]["total_files"] == 1
|
|
||||||
assert data["summary"]["total_functions"] == 1
|
|
||||||
assert data["summary"]["total_classes"] == 1
|
|
||||||
|
|
||||||
def test_export_json_functions(self):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
|
|
||||||
json_output = export_json(result, root)
|
|
||||||
data = json.loads(json_output)
|
|
||||||
|
|
||||||
assert len(data["files"]) == 1
|
|
||||||
assert len(data["files"][0]["functions"]) == 1
|
|
||||||
assert data["files"][0]["functions"][0]["name"] == "test_function"
|
|
||||||
|
|
||||||
def test_export_json_classes(self):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
|
|
||||||
json_output = export_json(result, root)
|
|
||||||
data = json.loads(json_output)
|
|
||||||
|
|
||||||
assert len(data["files"][0]["classes"]) == 1
|
|
||||||
assert data["files"][0]["classes"][0]["name"] == "TestClass"
|
|
||||||
assert data["files"][0]["classes"][0]["bases"] == ["BaseClass"]
|
|
||||||
|
|
||||||
def test_export_json_file(self, tmp_path):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
output_file = tmp_path / "output.json"
|
|
||||||
|
|
||||||
export_json_file(result, root, output_file)
|
|
||||||
|
|
||||||
assert output_file.exists()
|
|
||||||
|
|
||||||
data = json.loads(output_file.read_text())
|
|
||||||
assert "metadata" in data
|
|
||||||
|
|
||||||
|
|
||||||
class TestMarkdownExporter:
|
|
||||||
"""Tests for Markdown export functionality."""
|
|
||||||
|
|
||||||
def create_test_result(self):
|
|
||||||
"""Create a test analysis result."""
|
|
||||||
func = FunctionInfo(
|
|
||||||
name="process_data",
|
|
||||||
node_type="function_definition",
|
|
||||||
start_line=5,
|
|
||||||
end_line=15,
|
|
||||||
parameters=[{"name": "data"}, {"name": "options"}],
|
|
||||||
is_async=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
file_analysis = FileAnalysis(
|
|
||||||
path=Path("/test/project/utils.py"),
|
|
||||||
language="python",
|
|
||||||
size=300,
|
|
||||||
lines=30,
|
|
||||||
functions=[func],
|
|
||||||
classes=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
result = AnalysisResult()
|
|
||||||
result.summary = {
|
|
||||||
"total_files": 1,
|
|
||||||
"total_functions": 1,
|
|
||||||
"total_classes": 0,
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"languages": {"python": 1},
|
|
||||||
}
|
|
||||||
result.files = [file_analysis]
|
|
||||||
result.dependencies = []
|
|
||||||
result.metrics = {}
|
|
||||||
result.analysis_time = 0.05
|
|
||||||
result.error_count = 0
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def test_export_markdown_header(self):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
|
|
||||||
md_output = export_markdown(result, root)
|
|
||||||
|
|
||||||
assert "# CodeSnap Analysis Report" in md_output
|
|
||||||
|
|
||||||
def test_export_markdown_summary(self):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
|
|
||||||
md_output = export_markdown(result, root)
|
|
||||||
|
|
||||||
assert "## Summary" in md_output
|
|
||||||
assert "Total Files" in md_output
|
|
||||||
assert "1" in md_output
|
|
||||||
|
|
||||||
def test_export_markdown_language_breakdown(self):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
|
|
||||||
md_output = export_markdown(result, root)
|
|
||||||
|
|
||||||
assert "### Language Breakdown" in md_output
|
|
||||||
assert "python" in md_output.lower()
|
|
||||||
|
|
||||||
def test_export_markdown_file_structure(self):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
|
|
||||||
md_output = export_markdown(result, root)
|
|
||||||
|
|
||||||
assert "## File Structure" in md_output
|
|
||||||
assert "```" in md_output
|
|
||||||
|
|
||||||
def test_export_markdown_functions(self):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
|
|
||||||
md_output = export_markdown(result, root)
|
|
||||||
|
|
||||||
assert "process_data" in md_output
|
|
||||||
assert "async" in md_output.lower()
|
|
||||||
|
|
||||||
def test_export_markdown_file(self, tmp_path):
|
|
||||||
result = self.create_test_result()
|
|
||||||
root = Path("/test/project")
|
|
||||||
output_file = tmp_path / "output.md"
|
|
||||||
|
|
||||||
export_markdown_file(result, root, output_file)
|
|
||||||
|
|
||||||
assert output_file.exists()
|
|
||||||
content = output_file.read_text()
|
|
||||||
assert "# CodeSnap Analysis Report" in content
|
|
||||||
|
|
||||||
def test_empty_result(self):
|
|
||||||
result = AnalysisResult()
|
|
||||||
result.summary = {}
|
|
||||||
result.files = []
|
|
||||||
result.dependencies = []
|
|
||||||
result.metrics = {}
|
|
||||||
result.analysis_time = 0
|
|
||||||
result.error_count = 0
|
|
||||||
|
|
||||||
root = Path("/test")
|
|
||||||
md_output = export_markdown(result, root)
|
|
||||||
|
|
||||||
assert "# CodeSnap Analysis Report" in md_output
|
|
||||||
|
|
||||||
|
|
||||||
class TestLLMExporter:
|
|
||||||
"""Tests for LLM-optimized export functionality."""
|
|
||||||
|
|
||||||
def test_estimate_tokens_python(self):
|
|
||||||
text = "def hello():\n print('hello')"
|
|
||||||
tokens = estimate_tokens(text, "python")
|
|
||||||
|
|
||||||
assert tokens > 0
|
|
||||||
assert tokens < len(text)
|
|
||||||
|
|
||||||
def test_estimate_tokens_markdown(self):
|
|
||||||
text = "# Heading\n\nSome content here."
|
|
||||||
tokens = estimate_tokens(text, "markdown")
|
|
||||||
|
|
||||||
assert tokens > 0
|
|
||||||
|
|
||||||
def test_truncate_under_limit(self):
|
|
||||||
text = "Short text"
|
|
||||||
result = truncate_for_token_limit(text, 100, "markdown")
|
|
||||||
|
|
||||||
assert result == text
|
|
||||||
|
|
||||||
def test_truncate_over_limit(self):
|
|
||||||
text = "A" * 1000
|
|
||||||
result = truncate_for_token_limit(text, 100, "markdown")
|
|
||||||
|
|
||||||
assert len(result) < len(text)
|
|
||||||
assert "[Output truncated due to token limit]" in result
|
|
||||||
|
|
||||||
def test_export_llm_optimized_structure(self):
|
|
||||||
func = FunctionInfo(
|
|
||||||
name="helper",
|
|
||||||
node_type="function",
|
|
||||||
start_line=1,
|
|
||||||
end_line=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
file_analysis = FileAnalysis(
|
|
||||||
path=Path("/test/main.py"),
|
|
||||||
language="python",
|
|
||||||
size=100,
|
|
||||||
lines=10,
|
|
||||||
functions=[func],
|
|
||||||
classes=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
result = AnalysisResult()
|
|
||||||
result.summary = {
|
|
||||||
"total_files": 1,
|
|
||||||
"total_functions": 1,
|
|
||||||
"total_classes": 0,
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"languages": {"python": 1},
|
|
||||||
}
|
|
||||||
result.files = [file_analysis]
|
|
||||||
result.dependencies = []
|
|
||||||
result.metrics = {}
|
|
||||||
result.analysis_time = 0.01
|
|
||||||
result.error_count = 0
|
|
||||||
|
|
||||||
root = Path("/test")
|
|
||||||
output = export_llm_optimized(result, root)
|
|
||||||
|
|
||||||
assert "## CODEBASE ANALYSIS SUMMARY" in output
|
|
||||||
assert "### STRUCTURE" in output
|
|
||||||
assert "### KEY COMPONENTS" in output
|
|
||||||
|
|
||||||
def test_export_llm_with_max_tokens(self):
|
|
||||||
func = FunctionInfo(
|
|
||||||
name="test",
|
|
||||||
node_type="function",
|
|
||||||
start_line=1,
|
|
||||||
end_line=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
file_analysis = FileAnalysis(
|
|
||||||
path=Path("/test/main.py"),
|
|
||||||
language="python",
|
|
||||||
size=100,
|
|
||||||
lines=10,
|
|
||||||
functions=[func],
|
|
||||||
classes=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
result = AnalysisResult()
|
|
||||||
result.summary = {
|
|
||||||
"total_files": 1,
|
|
||||||
"total_functions": 1,
|
|
||||||
"total_classes": 0,
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"languages": {"python": 1},
|
|
||||||
}
|
|
||||||
result.files = [file_analysis]
|
|
||||||
result.dependencies = []
|
|
||||||
result.metrics = {}
|
|
||||||
result.analysis_time = 0.01
|
|
||||||
result.error_count = 0
|
|
||||||
|
|
||||||
root = Path("/test")
|
|
||||||
output = export_llm_optimized(result, root, max_tokens=100)
|
|
||||||
|
|
||||||
tokens = estimate_tokens(output, "markdown")
|
|
||||||
assert tokens <= 100 or "[Output truncated" in output
|
|
||||||
|
|
||||||
|
|
||||||
class TestFormatterIntegration:
|
|
||||||
"""Integration tests for formatters."""
|
|
||||||
|
|
||||||
def test_json_is_valid_json(self):
|
|
||||||
func = FunctionInfo(name="test", node_type="func", start_line=1, end_line=10)
|
|
||||||
file_analysis = FileAnalysis(
|
|
||||||
path=Path("/test/main.py"),
|
|
||||||
language="python",
|
|
||||||
size=100,
|
|
||||||
lines=10,
|
|
||||||
functions=[func],
|
|
||||||
)
|
|
||||||
|
|
||||||
result = AnalysisResult()
|
|
||||||
result.summary = {"total_files": 1}
|
|
||||||
result.files = [file_analysis]
|
|
||||||
result.dependencies = []
|
|
||||||
result.metrics = {}
|
|
||||||
result.analysis_time = 0
|
|
||||||
|
|
||||||
root = Path("/test")
|
|
||||||
|
|
||||||
json_output = export_json(result, root)
|
|
||||||
|
|
||||||
data = json.loads(json_output)
|
|
||||||
assert data is not None
|
|
||||||
|
|
||||||
def test_markdown_is_readable(self):
|
|
||||||
func = FunctionInfo(name="test", node_type="func", start_line=1, end_line=10)
|
|
||||||
file_analysis = FileAnalysis(
|
|
||||||
path=Path("/test/main.py"),
|
|
||||||
language="python",
|
|
||||||
size=100,
|
|
||||||
lines=10,
|
|
||||||
functions=[func],
|
|
||||||
)
|
|
||||||
|
|
||||||
result = AnalysisResult()
|
|
||||||
result.summary = {"total_files": 1}
|
|
||||||
result.files = [file_analysis]
|
|
||||||
result.dependencies = []
|
|
||||||
result.metrics = {}
|
|
||||||
result.analysis_time = 0
|
|
||||||
|
|
||||||
root = Path("/test")
|
|
||||||
|
|
||||||
md_output = export_markdown(result, root)
|
|
||||||
|
|
||||||
assert md_output is not None
|
|
||||||
assert len(md_output) > 0
|
|
||||||
assert "#" in md_output
|
|
||||||
|
|
||||||
def test_llm_output_has_summary_first(self):
|
|
||||||
func = FunctionInfo(name="test", node_type="func", start_line=1, end_line=10)
|
|
||||||
file_analysis = FileAnalysis(
|
|
||||||
path=Path("/test/main.py"),
|
|
||||||
language="python",
|
|
||||||
size=100,
|
|
||||||
lines=10,
|
|
||||||
functions=[func],
|
|
||||||
)
|
|
||||||
|
|
||||||
result = AnalysisResult()
|
|
||||||
result.summary = {"total_files": 1}
|
|
||||||
result.files = [file_analysis]
|
|
||||||
result.dependencies = []
|
|
||||||
result.metrics = {}
|
|
||||||
result.analysis_time = 0
|
|
||||||
|
|
||||||
root = Path("/test")
|
|
||||||
|
|
||||||
output = export_llm_optimized(result, root)
|
|
||||||
|
|
||||||
summary_pos = output.find("CODEBASE ANALYSIS SUMMARY")
|
|
||||||
structure_pos = output.find("STRUCTURE")
|
|
||||||
|
|
||||||
assert summary_pos < structure_pos
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
from codesnap.output.json_formatter import JSONFormatter
|
|
||||||
|
|
||||||
|
|
||||||
class TestJSONFormatter:
|
|
||||||
def setup_method(self) -> None:
|
|
||||||
self.formatter = JSONFormatter()
|
|
||||||
|
|
||||||
def test_format_valid_result(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"file": "test.py",
|
|
||||||
"language": "python",
|
|
||||||
"lines": 50,
|
|
||||||
"functions": [{"name": "test_func", "start_line": 1, "end_line": 10}],
|
|
||||||
"classes": [],
|
|
||||||
"complexity": {"score": 5, "rating": "low"}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 0,
|
|
||||||
"cycle_details": [],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
import json
|
|
||||||
parsed = json.loads(output)
|
|
||||||
assert "schema_version" in parsed
|
|
||||||
assert "summary" in parsed
|
|
||||||
assert "files" in parsed
|
|
||||||
assert parsed["summary"]["total_files"] == 1
|
|
||||||
|
|
||||||
def test_format_empty_result(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 0,
|
|
||||||
"cycle_details": [],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
import json
|
|
||||||
parsed = json.loads(output)
|
|
||||||
assert parsed["summary"]["total_files"] == 0
|
|
||||||
|
|
||||||
def test_summary_includes_language_counts(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [
|
|
||||||
{"file": "a.py", "language": "python", "lines": 10, "functions": [], "classes": [], "complexity": {}},
|
|
||||||
{"file": "b.js", "language": "javascript", "lines": 20, "functions": [], "classes": [], "complexity": {}},
|
|
||||||
{"file": "c.py", "language": "python", "lines": 30, "functions": [], "classes": [], "complexity": {}}
|
|
||||||
],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 0,
|
|
||||||
"cycle_details": [],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
import json
|
|
||||||
parsed = json.loads(output)
|
|
||||||
assert parsed["summary"]["languages"]["python"] == 2
|
|
||||||
assert parsed["summary"]["languages"]["javascript"] == 1
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
"""Unit tests for language detection module."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from codesnap.core.language_detector import (
|
|
||||||
EXTENSION_TO_LANGUAGE,
|
|
||||||
detect_language,
|
|
||||||
detect_language_by_extension,
|
|
||||||
detect_language_by_shebang,
|
|
||||||
get_language_info,
|
|
||||||
get_supported_extensions,
|
|
||||||
get_supported_languages,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDetectLanguageByExtension:
|
|
||||||
"""Tests for extension-based language detection."""
|
|
||||||
|
|
||||||
def test_python_extension_py(self):
|
|
||||||
assert detect_language_by_extension(Path("test.py")) == "python"
|
|
||||||
|
|
||||||
def test_python_extension_pyi(self):
|
|
||||||
assert detect_language_by_extension(Path("test.pyi")) == "python"
|
|
||||||
|
|
||||||
def test_javascript_extension_js(self):
|
|
||||||
assert detect_language_by_extension(Path("test.js")) == "javascript"
|
|
||||||
|
|
||||||
def test_typescript_extension_ts(self):
|
|
||||||
assert detect_language_by_extension(Path("test.ts")) == "typescript"
|
|
||||||
|
|
||||||
def test_go_extension(self):
|
|
||||||
assert detect_language_by_extension(Path("main.go")) == "go"
|
|
||||||
|
|
||||||
def test_rust_extension(self):
|
|
||||||
assert detect_language_by_extension(Path("main.rs")) == "rust"
|
|
||||||
|
|
||||||
def test_java_extension(self):
|
|
||||||
assert detect_language_by_extension(Path("Main.java")) == "java"
|
|
||||||
|
|
||||||
def test_cpp_extension(self):
|
|
||||||
assert detect_language_by_extension(Path("test.cpp")) == "cpp"
|
|
||||||
assert detect_language_by_extension(Path("test.hpp")) == "cpp"
|
|
||||||
|
|
||||||
def test_ruby_extension(self):
|
|
||||||
assert detect_language_by_extension(Path("script.rb")) == "ruby"
|
|
||||||
|
|
||||||
def test_php_extension(self):
|
|
||||||
assert detect_language_by_extension(Path("script.php")) == "php"
|
|
||||||
|
|
||||||
def test_unknown_extension(self):
|
|
||||||
assert detect_language_by_extension(Path("test.xyz")) is None
|
|
||||||
|
|
||||||
def test_case_insensitive(self):
|
|
||||||
assert detect_language_by_extension(Path("test.PY")) == "python"
|
|
||||||
assert detect_language_by_extension(Path("test.JS")) == "javascript"
|
|
||||||
|
|
||||||
|
|
||||||
class TestDetectLanguageByShebang:
|
|
||||||
"""Tests for shebang-based language detection."""
|
|
||||||
|
|
||||||
def test_python_shebang(self):
|
|
||||||
content = "#!/usr/bin/env python3\nprint('hello')"
|
|
||||||
assert detect_language_by_shebang(content) == "python"
|
|
||||||
|
|
||||||
def test_python_shebang_alt(self):
|
|
||||||
content = "#!/usr/bin/python\nprint('hello')"
|
|
||||||
assert detect_language_by_shebang(content) == "python"
|
|
||||||
|
|
||||||
def test_node_shebang(self):
|
|
||||||
content = "#!/usr/bin/env node\nconsole.log('hello')"
|
|
||||||
assert detect_language_by_shebang(content) == "javascript"
|
|
||||||
|
|
||||||
def test_ruby_shebang(self):
|
|
||||||
content = "#!/usr/bin/env ruby\nputs 'hello'"
|
|
||||||
assert detect_language_by_shebang(content) == "ruby"
|
|
||||||
|
|
||||||
def test_php_shebang(self):
|
|
||||||
content = "#!/usr/bin/env php\necho 'hello';"
|
|
||||||
assert detect_language_by_shebang(content) == "php"
|
|
||||||
|
|
||||||
def test_no_shebang(self):
|
|
||||||
content = "print('hello')"
|
|
||||||
assert detect_language_by_shebang(content) is None
|
|
||||||
|
|
||||||
def test_empty_content(self):
|
|
||||||
assert detect_language_by_shebang("") is None
|
|
||||||
|
|
||||||
|
|
||||||
class TestDetectLanguage:
|
|
||||||
"""Tests for combined language detection."""
|
|
||||||
|
|
||||||
def test_detection_by_extension(self):
|
|
||||||
assert detect_language(Path("test.py")) == "python"
|
|
||||||
assert detect_language(Path("test.js")) == "javascript"
|
|
||||||
|
|
||||||
def test_detection_fallback_to_shebang(self):
|
|
||||||
file_path = Path("script")
|
|
||||||
assert detect_language(file_path, "#!/usr/bin/env python") == "python"
|
|
||||||
assert detect_language(file_path, "#!/usr/bin/env node") == "javascript"
|
|
||||||
|
|
||||||
def test_unknown_file_no_content(self):
|
|
||||||
assert detect_language(Path("unknown.xyz")) is None
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetLanguageInfo:
|
|
||||||
"""Tests for language info retrieval."""
|
|
||||||
|
|
||||||
def test_get_python_info(self):
|
|
||||||
info = get_language_info("python")
|
|
||||||
assert info is not None
|
|
||||||
assert info.name == "python"
|
|
||||||
assert ".py" in info.extensions
|
|
||||||
|
|
||||||
def test_get_unknown_language(self):
|
|
||||||
info = get_language_info("unknown")
|
|
||||||
assert info is None
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetSupportedExtensions:
|
|
||||||
"""Tests for supported extensions."""
|
|
||||||
|
|
||||||
def test_returns_set(self):
|
|
||||||
extensions = get_supported_extensions()
|
|
||||||
assert isinstance(extensions, set)
|
|
||||||
|
|
||||||
def test_includes_common_extensions(self):
|
|
||||||
extensions = get_supported_extensions()
|
|
||||||
assert ".py" in extensions
|
|
||||||
assert ".js" in extensions
|
|
||||||
assert ".ts" in extensions
|
|
||||||
assert ".go" in extensions
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetSupportedLanguages:
|
|
||||||
"""Tests for supported programming languages."""
|
|
||||||
|
|
||||||
def test_returns_list(self):
|
|
||||||
languages = get_supported_languages()
|
|
||||||
assert isinstance(languages, list)
|
|
||||||
|
|
||||||
def test_includes_main_languages(self):
|
|
||||||
languages = get_supported_languages()
|
|
||||||
assert "python" in languages
|
|
||||||
assert "javascript" in languages
|
|
||||||
assert "typescript" in languages
|
|
||||||
assert "go" in languages
|
|
||||||
assert "rust" in languages
|
|
||||||
assert "java" in languages
|
|
||||||
|
|
||||||
def test_excludes_config_formats(self):
|
|
||||||
languages = get_supported_languages()
|
|
||||||
assert "json" not in languages
|
|
||||||
assert "yaml" not in languages
|
|
||||||
assert "markdown" not in languages
|
|
||||||
|
|
||||||
|
|
||||||
class TestExtensionToLanguage:
|
|
||||||
"""Tests for extension to language mapping."""
|
|
||||||
|
|
||||||
def test_mapping_completeness(self):
|
|
||||||
for _ext, lang in EXTENSION_TO_LANGUAGE.items():
|
|
||||||
assert lang in ["python", "javascript", "typescript", "go", "rust",
|
|
||||||
"java", "c", "cpp", "ruby", "php", "shell",
|
|
||||||
"json", "yaml", "markdown"]
|
|
||||||
|
|
||||||
def test_no_duplicate_extensions(self):
|
|
||||||
extensions = list(EXTENSION_TO_LANGUAGE.keys())
|
|
||||||
assert len(extensions) == len(set(extensions))
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
from codesnap.output.llm_formatter import LLMFormatter
|
|
||||||
|
|
||||||
|
|
||||||
class TestLLMFormatter:
|
|
||||||
def setup_method(self) -> None:
|
|
||||||
self.formatter = LLMFormatter(max_tokens=1000)
|
|
||||||
|
|
||||||
def test_format_valid_result(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"file": "test.py",
|
|
||||||
"language": "python",
|
|
||||||
"lines": 50,
|
|
||||||
"functions": [{"name": "test_func", "start_line": 1, "end_line": 10, "parameters": [], "return_type": "str"}],
|
|
||||||
"classes": [],
|
|
||||||
"complexity": {"score": 5, "rating": "low"}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 0,
|
|
||||||
"cycle_details": [],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
assert "## Codebase Summary" in output
|
|
||||||
assert "### Key Files" in output
|
|
||||||
assert "### Classes and Functions" in output
|
|
||||||
assert "### Dependencies" in output
|
|
||||||
|
|
||||||
def test_respects_token_limit(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 0,
|
|
||||||
"cycle_details": [],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
max_chars = 1000 * 4
|
|
||||||
assert len(output) <= max_chars + 100
|
|
||||||
|
|
||||||
def test_includes_high_level_summary(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [
|
|
||||||
{"file": "a.py", "language": "python", "lines": 50, "functions": [], "classes": [], "complexity": {}},
|
|
||||||
{"file": "b.py", "language": "python", "lines": 30, "functions": [], "classes": [], "complexity": {}},
|
|
||||||
{"file": "c.js", "language": "javascript", "lines": 20, "functions": [], "classes": [], "complexity": {}}
|
|
||||||
],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 0,
|
|
||||||
"cycle_details": [],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
assert "python" in output.lower()
|
|
||||||
assert "3 files" in output or "files" in output
|
|
||||||
|
|
||||||
def test_compresses_detailed_file_list(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [
|
|
||||||
{"file": f"file{i}.py", "language": "python", "lines": 10,
|
|
||||||
"functions": [{"name": f"func{i}a"}, {"name": f"func{i}b"}, {"name": f"func{i}c"}],
|
|
||||||
"classes": [], "complexity": {}}
|
|
||||||
for i in range(10)
|
|
||||||
],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 0,
|
|
||||||
"cycle_details": [],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
assert "Detailed File List (compressed)" in output
|
|
||||||
|
|
||||||
def test_warns_about_cycles(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [
|
|
||||||
{"file": "a.py", "language": "python", "lines": 10, "functions": [], "classes": [], "complexity": {}},
|
|
||||||
{"file": "b.py", "language": "python", "lines": 10, "functions": [], "classes": [], "complexity": {}}
|
|
||||||
],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 2,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 1,
|
|
||||||
"cycle_details": [["a.py", "b.py", "a.py"]],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
assert "circular" in output.lower() or "cycle" in output.lower()
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
from codesnap.output.markdown_formatter import MarkdownFormatter
|
|
||||||
|
|
||||||
|
|
||||||
class TestMarkdownFormatter:
|
|
||||||
def setup_method(self) -> None:
|
|
||||||
self.formatter = MarkdownFormatter()
|
|
||||||
|
|
||||||
def test_format_valid_result(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"file": "test.py",
|
|
||||||
"language": "python",
|
|
||||||
"lines": 50,
|
|
||||||
"functions": [{"name": "test_func", "start_line": 1, "end_line": 10, "parameters": [], "return_type": "str"}],
|
|
||||||
"classes": [],
|
|
||||||
"complexity": {"score": 5, "rating": "low"}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 0,
|
|
||||||
"cycle_details": [],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
assert "# CodeSnap Analysis Report" in output
|
|
||||||
assert "## Overview" in output
|
|
||||||
assert "## File Structure" in output
|
|
||||||
assert "## Key Functions" in output
|
|
||||||
assert "## Dependencies" in output
|
|
||||||
assert "## Complexity Metrics" in output
|
|
||||||
|
|
||||||
def test_format_empty_result(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 0,
|
|
||||||
"cycle_details": [],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
assert "Total Files" in output
|
|
||||||
|
|
||||||
def test_includes_language_breakdown(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [
|
|
||||||
{"file": "a.py", "language": "python", "lines": 10, "functions": [], "classes": [], "complexity": {}},
|
|
||||||
{"file": "b.js", "language": "javascript", "lines": 20, "functions": [], "classes": [], "complexity": {}}
|
|
||||||
],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 0,
|
|
||||||
"cycle_details": [],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
assert "python: 1" in output or "python: 2" in output
|
|
||||||
assert "javascript:" in output
|
|
||||||
|
|
||||||
def test_shows_circular_dependencies(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [
|
|
||||||
{"file": "a.py", "language": "python", "lines": 10, "functions": [], "classes": [], "complexity": {}},
|
|
||||||
{"file": "b.py", "language": "python", "lines": 10, "functions": [], "classes": [], "complexity": {}}
|
|
||||||
],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 2,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 1,
|
|
||||||
"cycle_details": [["a.py", "b.py", "a.py"]],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [{"from": "a.py", "to": "b.py"}, {"from": "b.py", "to": "a.py"}],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
assert "Circular Dependencies Detected" in output
|
|
||||||
|
|
||||||
def test_shows_high_complexity_files(self) -> None:
|
|
||||||
result = {
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"file": "complex.py",
|
|
||||||
"language": "python",
|
|
||||||
"lines": 100,
|
|
||||||
"functions": [],
|
|
||||||
"classes": [],
|
|
||||||
"complexity": {"score": 55, "rating": "high"}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dependency_graph": {
|
|
||||||
"total_dependencies": 0,
|
|
||||||
"orphaned_files": 0,
|
|
||||||
"cycles_detected": 0,
|
|
||||||
"cycle_details": [],
|
|
||||||
"orphaned_details": [],
|
|
||||||
"edges": [],
|
|
||||||
"statistics": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output = self.formatter.format(result)
|
|
||||||
assert "High Complexity Files" in output
|
|
||||||
assert "complex.py" in output
|
|
||||||
Reference in New Issue
Block a user