Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3e72ee9a7 | |||
| d253250774 | |||
| 0f6bb5db46 | |||
| a91b3dafa0 | |||
| 3e350917d9 | |||
| 8719e3b18f | |||
| c0ca28a962 | |||
| 2391ed4fcb | |||
| b2a4110289 | |||
| 5aa13df63c | |||
| 22146c7f45 | |||
| dc50ba4765 |
@@ -11,57 +11,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
- name: Set up system dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y graphviz
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
installer-parallel: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --with dev
|
||||
|
||||
pip install -e ".[dev]"
|
||||
- name: Run tests
|
||||
run: |
|
||||
poetry run pytest tests/ -v
|
||||
|
||||
run: pytest tests/ -v
|
||||
- name: Run linting
|
||||
run: |
|
||||
poetry run ruff check src tests
|
||||
|
||||
- name: Run type checking
|
||||
run: |
|
||||
poetry run mypy src
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
installer-parallel: true
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
poetry build
|
||||
|
||||
- name: Verify build
|
||||
run: |
|
||||
poetry run pip install dist/*.whl --dry-run
|
||||
run: ruff check .
|
||||
|
||||
@@ -45,7 +45,7 @@ dev = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
codegraph = "src.cli:main"
|
||||
codegraph = "src.cli.main:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
@@ -62,6 +62,7 @@ python_version = "3.9"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
ignore_missing_imports = true
|
||||
exclude = "src/confgen"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
|
||||
@@ -123,7 +123,7 @@ class DependencyAnalyzer:
|
||||
return call_graph
|
||||
|
||||
def get_architecture_layers(self) -> dict[str, list[str]]:
|
||||
layers = {"presentation": [], "business": [], "data": [], "other": []}
|
||||
layers: dict[str, list[str]] = {"presentation": [], "business": [], "data": [], "other": []}
|
||||
|
||||
layer_keywords = {
|
||||
"presentation": ["ui", "view", "controller", "handler", "route", "api"],
|
||||
|
||||
@@ -76,16 +76,16 @@ def analyze(
|
||||
console.print(f"Graph contains {len(nodes)} nodes")
|
||||
|
||||
if format == "dot":
|
||||
exporter = DOTExporter(graph_builder)
|
||||
content = exporter.get_string()
|
||||
dot_exporter = DOTExporter(graph_builder)
|
||||
content = dot_exporter.get_string()
|
||||
if output:
|
||||
Path(output).write_text(content)
|
||||
console.print(f"Exported DOT to: {output}")
|
||||
else:
|
||||
console.print(content)
|
||||
elif format == "json":
|
||||
exporter = JSONExporter(graph_builder)
|
||||
content = exporter.get_string()
|
||||
json_exporter = JSONExporter(graph_builder)
|
||||
content = json_exporter.get_string()
|
||||
if output:
|
||||
Path(output).write_text(content)
|
||||
console.print(f"Exported JSON to: {output}")
|
||||
@@ -93,8 +93,8 @@ def analyze(
|
||||
console.print(content)
|
||||
elif format == "png":
|
||||
if output:
|
||||
exporter = PNGExporter(graph_builder)
|
||||
exporter.export(Path(output))
|
||||
png_exporter = PNGExporter(graph_builder)
|
||||
png_exporter.export(Path(output))
|
||||
console.print(f"Exported PNG to: {output}")
|
||||
else:
|
||||
console.print("[red]Error: PNG format requires output file path[/red]")
|
||||
@@ -247,14 +247,14 @@ def export(ctx: click.Context, path: str, language: str, output: str):
|
||||
graph_builder.build_from_parser_results(results)
|
||||
|
||||
if output_path.suffix == ".dot":
|
||||
exporter = DOTExporter(graph_builder)
|
||||
exporter.export(output_path)
|
||||
dot_exporter = DOTExporter(graph_builder)
|
||||
dot_exporter.export(output_path)
|
||||
elif output_path.suffix == ".json":
|
||||
exporter = JSONExporter(graph_builder)
|
||||
exporter.export(output_path)
|
||||
json_exporter = JSONExporter(graph_builder)
|
||||
json_exporter.export(output_path)
|
||||
elif output_path.suffix in [".png", ".svg"]:
|
||||
exporter = PNGExporter(graph_builder)
|
||||
exporter.export(output_path, format=output_path.suffix[1:])
|
||||
png_exporter = PNGExporter(graph_builder)
|
||||
png_exporter.export(output_path, format=output_path.suffix[1:])
|
||||
else:
|
||||
console.print(f"[red]Unsupported output format: {output_path.suffix}[/red]")
|
||||
return
|
||||
|
||||
@@ -51,11 +51,11 @@ class BaseParser(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def extract_imports(self, tree, file_path: Path) -> list[str]:
|
||||
def extract_imports(self, content: str) -> list[str]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def extract_calls(self, tree, file_path: Path) -> list[str]:
|
||||
def extract_calls(self, content: str) -> list[str]:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
import tempfile
|
||||
import json
|
||||
from pathlib import Path
|
||||
@@ -27,7 +26,7 @@ class Greeter:
|
||||
def greet(self, name):
|
||||
if self.prefix and name:
|
||||
return f"{self.prefix} {name}"
|
||||
return "Hello
|
||||
return "Hello"
|
||||
""")
|
||||
|
||||
parser = PythonParser()
|
||||
@@ -187,7 +186,23 @@ def func_b():
|
||||
|
||||
file_nodes = [n for n in nodes if n.node_type.value == "file"]
|
||||
func_nodes = [n for n in nodes if n.node_type.value == "function"]
|
||||
class_nodes = [n for n in nodes if n.node_type.value == "class"]
|
||||
|
||||
assert len(file_nodes) == 3
|
||||
assert len(func_nodes) >= 3
|
||||
|
||||
def test_error_handling_invalid_file(self):
|
||||
parser = PythonParser()
|
||||
result = parser.parse(Path("/nonexistent/file.py"), "")
|
||||
|
||||
assert result.errors is not None
|
||||
|
||||
def test_empty_code_handling(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
test_file = Path(tmpdir) / "empty.py"
|
||||
test_file.write_text("# Just a comment\n")
|
||||
|
||||
parser = PythonParser()
|
||||
content = test_file.read_text()
|
||||
result = parser.parse(test_file, content)
|
||||
|
||||
assert result.language == "python"
|
||||
|
||||
421
tests/test_chunking.py
Normal file
421
tests/test_chunking.py
Normal file
@@ -0,0 +1,421 @@
|
||||
from pathlib import Path
|
||||
|
||||
from codechunk.config import ChunkingConfig
|
||||
from codechunk.core.chunking import ChunkMetadata, ChunkPriority, CodeChunker, ParsedChunk
|
||||
|
||||
|
||||
class TestCodeChunker:
|
||||
"""Tests for CodeChunker."""
|
||||
|
||||
def test_calculate_priority_high_for_main_function(self):
|
||||
"""Test that 'main' function gets high priority."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
chunk = ParsedChunk(
|
||||
name="main",
|
||||
chunk_type="function",
|
||||
content="def main():\n pass",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
)
|
||||
)
|
||||
|
||||
result = chunker._calculate_priority(chunk)
|
||||
|
||||
assert result.priority >= 50
|
||||
|
||||
def test_calculate_priority_high_for_run_function(self):
|
||||
"""Test that 'run' function gets high priority."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
chunk = ParsedChunk(
|
||||
name="run_app",
|
||||
chunk_type="function",
|
||||
content="def run_app():\n pass",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
)
|
||||
)
|
||||
|
||||
result = chunker._calculate_priority(chunk)
|
||||
|
||||
assert result.priority >= 50
|
||||
|
||||
def test_calculate_priority_class_higher_than_function(self):
|
||||
"""Test that classes get higher priority than functions."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
func_chunk = ParsedChunk(
|
||||
name="helper",
|
||||
chunk_type="function",
|
||||
content="def helper():\n pass",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
)
|
||||
)
|
||||
|
||||
class_chunk = ParsedChunk(
|
||||
name="MyClass",
|
||||
chunk_type="class",
|
||||
content="class MyClass:\n pass",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
)
|
||||
)
|
||||
|
||||
func_priority = chunker._calculate_priority(func_chunk)
|
||||
class_priority = chunker._calculate_priority(class_chunk)
|
||||
|
||||
assert class_priority.priority > func_priority.priority
|
||||
|
||||
def test_calculate_priority_line_count_factor(self):
|
||||
"""Test that larger chunks get higher priority."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
small_chunk = ParsedChunk(
|
||||
name="small",
|
||||
chunk_type="function",
|
||||
content="def small():\n pass",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
)
|
||||
)
|
||||
|
||||
large_chunk = ParsedChunk(
|
||||
name="large",
|
||||
chunk_type="function",
|
||||
content="def large():\n " + "\n ".join(["x = 1"] * 50),
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=52,
|
||||
line_count=52
|
||||
)
|
||||
)
|
||||
|
||||
small_priority = chunker._calculate_priority(small_chunk)
|
||||
large_priority = chunker._calculate_priority(large_chunk)
|
||||
|
||||
assert large_priority.priority > small_priority.priority
|
||||
|
||||
def test_calculate_priority_complexity_factor(self):
|
||||
"""Test that complexity affects priority."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
simple_chunk = ParsedChunk(
|
||||
name="simple",
|
||||
chunk_type="function",
|
||||
content="def simple():\n return 1",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2,
|
||||
complexity_score=1
|
||||
)
|
||||
)
|
||||
|
||||
complex_chunk = ParsedChunk(
|
||||
name="complex",
|
||||
chunk_type="function",
|
||||
content="def complex():\n if True:\n for i in range(10):\n if i > 5:\n return i\n return 0",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=6,
|
||||
line_count=6,
|
||||
complexity_score=10
|
||||
)
|
||||
)
|
||||
|
||||
simple_priority = chunker._calculate_priority(simple_chunk)
|
||||
complex_priority = chunker._calculate_priority(complex_chunk)
|
||||
|
||||
assert complex_priority.priority > simple_priority.priority
|
||||
|
||||
def test_calculate_priority_decorators_factor(self):
|
||||
"""Test that decorators increase priority."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
no_decorator_chunk = ParsedChunk(
|
||||
name="no_decorator",
|
||||
chunk_type="function",
|
||||
content="def no_decorator():\n pass",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2,
|
||||
decorators=[]
|
||||
)
|
||||
)
|
||||
|
||||
with_decorator_chunk = ParsedChunk(
|
||||
name="with_decorator",
|
||||
chunk_type="function",
|
||||
content="@property\ndef with_decorator():\n pass",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=3,
|
||||
line_count=3,
|
||||
decorators=["@property"]
|
||||
)
|
||||
)
|
||||
|
||||
no_dec_priority = chunker._calculate_priority(no_decorator_chunk)
|
||||
with_dec_priority = chunker._calculate_priority(with_decorator_chunk)
|
||||
|
||||
assert with_dec_priority.priority >= no_dec_priority.priority
|
||||
|
||||
def test_remove_boilerplate_property(self):
|
||||
"""Test that boilerplate detection works for functions."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
chunk = ParsedChunk(
|
||||
name="MyClass.value",
|
||||
chunk_type="function",
|
||||
content="def value(self):\n return self._value",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
)
|
||||
)
|
||||
|
||||
result = chunker._remove_boilerplate(chunk)
|
||||
assert result.is_boilerplate is False
|
||||
|
||||
def test_remove_boilerplate_dunder_methods(self):
|
||||
"""Test that dunder methods are detected."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
chunk = ParsedChunk(
|
||||
name="MyClass.__str__",
|
||||
chunk_type="function",
|
||||
content="def __str__(self):\n return 'MyClass'",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
)
|
||||
)
|
||||
|
||||
result = chunker._remove_boilerplate(chunk)
|
||||
assert result.is_boilerplate is True
|
||||
|
||||
def test_remove_boilerplate_regular_function(self):
|
||||
"""Test that regular functions are not marked as boilerplate."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
chunk = ParsedChunk(
|
||||
name="process_data",
|
||||
chunk_type="function",
|
||||
content="def process_data(data):\n return [x for x in data if x > 0]",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
)
|
||||
)
|
||||
|
||||
result = chunker._remove_boilerplate(chunk)
|
||||
|
||||
assert result.is_boilerplate is False
|
||||
|
||||
def test_sort_by_priority(self):
|
||||
"""Test that chunks are sorted by priority."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
low_chunk = ParsedChunk(
|
||||
name="helper",
|
||||
chunk_type="function",
|
||||
content="def helper():\n pass",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
),
|
||||
priority=10
|
||||
)
|
||||
|
||||
high_chunk = ParsedChunk(
|
||||
name="main",
|
||||
chunk_type="function",
|
||||
content="def main():\n pass",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
),
|
||||
priority=100
|
||||
)
|
||||
|
||||
chunks = [low_chunk, high_chunk]
|
||||
sorted_chunks = chunker._sort_by_priority(chunks)
|
||||
|
||||
assert sorted_chunks[0].name == "main"
|
||||
assert sorted_chunks[1].name == "helper"
|
||||
|
||||
def test_chunk_all_processes_all_chunks(self):
|
||||
"""Test that chunk_all processes all chunks correctly."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
chunks = [
|
||||
ParsedChunk(
|
||||
name="helper",
|
||||
chunk_type="function",
|
||||
content="def helper():\n pass",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
)
|
||||
),
|
||||
ParsedChunk(
|
||||
name="main",
|
||||
chunk_type="function",
|
||||
content="def main():\n pass",
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=4,
|
||||
end_line=5,
|
||||
line_count=2
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
result = chunker.chunk_all(chunks)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0].priority > result[1].priority
|
||||
|
||||
def test_split_large_chunk(self, tmp_path):
|
||||
"""Test splitting a large chunk into smaller pieces."""
|
||||
config = ChunkingConfig()
|
||||
config.max_chunk_size = 10
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
large_content = "\n".join([f"line {i}" for i in range(30)])
|
||||
chunk = ParsedChunk(
|
||||
name="large_function",
|
||||
chunk_type="function",
|
||||
content=large_content,
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=30,
|
||||
line_count=30
|
||||
)
|
||||
)
|
||||
|
||||
parts = chunker.split_large_chunk(chunk)
|
||||
|
||||
assert len(parts) > 1
|
||||
for part in parts:
|
||||
assert part.metadata.line_count <= config.max_chunk_size
|
||||
|
||||
def test_split_small_chunk(self, tmp_path):
|
||||
"""Test that small chunks are not split."""
|
||||
config = ChunkingConfig()
|
||||
chunker = CodeChunker(config)
|
||||
|
||||
small_content = "def small():\n pass"
|
||||
chunk = ParsedChunk(
|
||||
name="small",
|
||||
chunk_type="function",
|
||||
content=small_content,
|
||||
metadata=ChunkMetadata(
|
||||
file_path=Path("test.py"),
|
||||
file_name="test.py",
|
||||
language="python",
|
||||
start_line=1,
|
||||
end_line=2,
|
||||
line_count=2
|
||||
)
|
||||
)
|
||||
|
||||
parts = chunker.split_large_chunk(chunk)
|
||||
|
||||
assert len(parts) == 1
|
||||
assert parts[0].content == small_content
|
||||
|
||||
|
||||
class TestChunkPriority:
|
||||
"""Tests for ChunkPriority constants."""
|
||||
|
||||
def test_priority_values(self):
|
||||
"""Test that priority constants have expected values."""
|
||||
assert ChunkPriority.CRITICAL == 100
|
||||
assert ChunkPriority.HIGH == 75
|
||||
assert ChunkPriority.MEDIUM == 50
|
||||
assert ChunkPriority.LOW == 25
|
||||
assert ChunkPriority.MINIMAL == 10
|
||||
@@ -1,8 +1,7 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from src.analyzers.complexity import ComplexityCalculator
|
||||
from src.analyzers.dependencies import DependencyAnalyzer
|
||||
from src.graph.builder import GraphBuilder, GraphType, GraphNode, NodeType, GraphEdge
|
||||
from src.graph.builder import GraphBuilder, GraphType, GraphNode, NodeType
|
||||
from src.parsers.base import Entity, EntityType
|
||||
|
||||
|
||||
@@ -137,6 +136,21 @@ class TestComplexityCalculator:
|
||||
assert report["total_cyclomatic_complexity"] >= 2
|
||||
assert "complexity_distribution" in report
|
||||
|
||||
def test_complexity_distribution(self):
|
||||
entities = [
|
||||
Entity(
|
||||
name=f"func{i}",
|
||||
entity_type=EntityType.FUNCTION,
|
||||
file_path=Path("/test.py"),
|
||||
start_line=1,
|
||||
end_line=5,
|
||||
code="def func():\n pass",
|
||||
)
|
||||
for i in range(10)
|
||||
]
|
||||
report = self.calculator.calculate_project_complexity(entities)
|
||||
assert report["complexity_distribution"]["low"] == 10
|
||||
|
||||
|
||||
class TestDependencyAnalyzer:
|
||||
def setup_method(self):
|
||||
@@ -172,3 +186,38 @@ class TestDependencyAnalyzer:
|
||||
self.builder.add_node(file_node)
|
||||
layers = self.analyzer.get_architecture_layers()
|
||||
assert "presentation" in layers or "other" in layers
|
||||
|
||||
def test_get_file_dependencies(self):
|
||||
file_node = GraphNode(
|
||||
node_id="file_test.py",
|
||||
node_type=NodeType.FILE,
|
||||
name="test.py",
|
||||
file_path=Path("/test.py"),
|
||||
)
|
||||
self.builder.add_node(file_node)
|
||||
deps = self.analyzer.get_file_dependencies(Path("/test.py"))
|
||||
assert isinstance(deps, list)
|
||||
|
||||
|
||||
class TestComplexityReport:
|
||||
def test_report_creation(self):
|
||||
from src.analyzers.complexity import ComplexityReport
|
||||
report = ComplexityReport(file_path=Path("/test.py"))
|
||||
assert report.file_path == Path("/test.py")
|
||||
assert report.functions == []
|
||||
assert report.classes == []
|
||||
assert report.total_cyclomatic_complexity == 0
|
||||
|
||||
def test_average_complexity_calculation(self):
|
||||
from src.analyzers.complexity import ComplexityReport
|
||||
report = ComplexityReport(file_path=Path("/test.py"))
|
||||
report.functions = [
|
||||
{"complexity_score": 5},
|
||||
{"complexity_score": 10},
|
||||
{"complexity_score": 15},
|
||||
]
|
||||
expected_avg = (5 + 10 + 15) / 3
|
||||
actual_avg = sum(f["complexity_score"] for f in report.functions) / len(
|
||||
report.functions
|
||||
)
|
||||
assert actual_avg == expected_avg
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import patch, MagicMock
|
||||
from pathlib import Path
|
||||
from src.cli.main import cli, analyze, visualize, complexity, deps, export
|
||||
from src.graph.builder import GraphBuilder, GraphType
|
||||
from src.cli.main import cli, visualize, complexity, deps, export
|
||||
|
||||
|
||||
class TestCLI:
|
||||
@@ -104,6 +101,69 @@ class TestCLI:
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
@patch("src.cli.main._parse_files")
|
||||
@patch("src.cli.main._get_parser")
|
||||
def test_export_command(self, mock_get_parser, mock_parse_files):
|
||||
mock_parser = MagicMock()
|
||||
mock_get_parser.return_value = mock_parser
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.file_path = Path("/test.py")
|
||||
mock_result.entities = []
|
||||
mock_result.imports = []
|
||||
mock_parse_files.return_value = [mock_result]
|
||||
|
||||
mock_graph_builder = MagicMock()
|
||||
mock_graph_builder.get_nodes.return_value = []
|
||||
mock_graph_builder.edges = []
|
||||
|
||||
with patch("src.cli.main.GraphBuilder") as MockGraphBuilder:
|
||||
MockGraphBuilder.return_value = mock_graph_builder
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
|
||||
output_path = f.name
|
||||
|
||||
try:
|
||||
result = self.runner.invoke(export, ["/test", "-o", output_path])
|
||||
assert result.exit_code == 0 or "Error" in result.output
|
||||
finally:
|
||||
Path(output_path).unlink(missing_ok=True)
|
||||
|
||||
def test_verbose_flag(self):
|
||||
result = self.runner.invoke(cli, ["--verbose", "--help"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
class TestHelperFunctions:
|
||||
def test_get_parser_auto_python(self):
|
||||
from src.cli.main import _get_parser
|
||||
with patch("pathlib.Path.glob") as mock_glob:
|
||||
mock_glob.return_value = [Path("/test.py")]
|
||||
parser = _get_parser("auto", Path("/test"))
|
||||
assert parser is not None
|
||||
|
||||
def test_parse_files_returns_empty_for_no_matches(self):
|
||||
from src.cli.main import _parse_files
|
||||
mock_parser = MagicMock()
|
||||
mock_result = MagicMock()
|
||||
mock_result.entities = []
|
||||
mock_result.errors = []
|
||||
mock_parser.SUPPORTED_EXTENSIONS = [".py"]
|
||||
mock_parser.parse.return_value = mock_result
|
||||
|
||||
with patch("pathlib.Path.rglob") as mock_rglob:
|
||||
mock_rglob.return_value = []
|
||||
|
||||
results = _parse_files(Path("/test"), mock_parser, False)
|
||||
|
||||
assert results == []
|
||||
|
||||
def test_display_summary(self):
|
||||
from src.cli.main import _display_summary
|
||||
|
||||
mock_graph_builder = MagicMock()
|
||||
mock_graph_builder.get_nodes.return_value = []
|
||||
|
||||
mock_results = []
|
||||
|
||||
_display_summary(mock_graph_builder, mock_results, False)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from src.graph.builder import GraphBuilder, GraphType, GraphNode, NodeType, GraphEdge
|
||||
from src.parsers.base import Entity, EntityType
|
||||
|
||||
|
||||
class TestGraphBuilder:
|
||||
@@ -123,6 +121,19 @@ class TestGraphNode:
|
||||
assert node.style == "filled"
|
||||
assert node.shape == "ellipse"
|
||||
|
||||
def test_class_node_shape(self):
|
||||
node = GraphNode(node_id="test", node_type=NodeType.CLASS, name="TestClass")
|
||||
assert node.shape == "ellipse"
|
||||
|
||||
def test_class_node_shape_in_builder(self):
|
||||
from src.graph.builder import GraphBuilder, GraphType
|
||||
builder = GraphBuilder(GraphType.DIRECTED)
|
||||
node = GraphNode(node_id="test", node_type=NodeType.CLASS, name="TestClass")
|
||||
node.shape = "diamond"
|
||||
builder.add_node(node)
|
||||
added_node = builder.get_node_by_id("test")
|
||||
assert added_node.shape == "diamond"
|
||||
|
||||
|
||||
class TestGraphEdge:
|
||||
def test_default_edge_type(self):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from src.parsers.base import BaseParser, Entity, EntityType, ParserResult
|
||||
from src.parsers.python import PythonParser
|
||||
|
||||
Reference in New Issue
Block a user