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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up system dependencies
|
||||||
- name: Set up Python
|
run: |
|
||||||
uses: actions/setup-python@v5
|
apt-get update
|
||||||
|
apt-get install -y graphviz
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
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
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
poetry install --with dev
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: pytest tests/ -v
|
||||||
poetry run pytest tests/ -v
|
|
||||||
|
|
||||||
- name: Run linting
|
- name: Run linting
|
||||||
run: |
|
run: ruff check .
|
||||||
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
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
codegraph = "src.cli:main"
|
codegraph = "src.cli.main:main"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
@@ -62,6 +62,7 @@ python_version = "3.9"
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unused_configs = true
|
warn_unused_configs = true
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
exclude = "src/confgen"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class DependencyAnalyzer:
|
|||||||
return call_graph
|
return call_graph
|
||||||
|
|
||||||
def get_architecture_layers(self) -> dict[str, list[str]]:
|
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 = {
|
layer_keywords = {
|
||||||
"presentation": ["ui", "view", "controller", "handler", "route", "api"],
|
"presentation": ["ui", "view", "controller", "handler", "route", "api"],
|
||||||
|
|||||||
@@ -76,16 +76,16 @@ def analyze(
|
|||||||
console.print(f"Graph contains {len(nodes)} nodes")
|
console.print(f"Graph contains {len(nodes)} nodes")
|
||||||
|
|
||||||
if format == "dot":
|
if format == "dot":
|
||||||
exporter = DOTExporter(graph_builder)
|
dot_exporter = DOTExporter(graph_builder)
|
||||||
content = exporter.get_string()
|
content = dot_exporter.get_string()
|
||||||
if output:
|
if output:
|
||||||
Path(output).write_text(content)
|
Path(output).write_text(content)
|
||||||
console.print(f"Exported DOT to: {output}")
|
console.print(f"Exported DOT to: {output}")
|
||||||
else:
|
else:
|
||||||
console.print(content)
|
console.print(content)
|
||||||
elif format == "json":
|
elif format == "json":
|
||||||
exporter = JSONExporter(graph_builder)
|
json_exporter = JSONExporter(graph_builder)
|
||||||
content = exporter.get_string()
|
content = json_exporter.get_string()
|
||||||
if output:
|
if output:
|
||||||
Path(output).write_text(content)
|
Path(output).write_text(content)
|
||||||
console.print(f"Exported JSON to: {output}")
|
console.print(f"Exported JSON to: {output}")
|
||||||
@@ -93,8 +93,8 @@ def analyze(
|
|||||||
console.print(content)
|
console.print(content)
|
||||||
elif format == "png":
|
elif format == "png":
|
||||||
if output:
|
if output:
|
||||||
exporter = PNGExporter(graph_builder)
|
png_exporter = PNGExporter(graph_builder)
|
||||||
exporter.export(Path(output))
|
png_exporter.export(Path(output))
|
||||||
console.print(f"Exported PNG to: {output}")
|
console.print(f"Exported PNG to: {output}")
|
||||||
else:
|
else:
|
||||||
console.print("[red]Error: PNG format requires output file path[/red]")
|
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)
|
graph_builder.build_from_parser_results(results)
|
||||||
|
|
||||||
if output_path.suffix == ".dot":
|
if output_path.suffix == ".dot":
|
||||||
exporter = DOTExporter(graph_builder)
|
dot_exporter = DOTExporter(graph_builder)
|
||||||
exporter.export(output_path)
|
dot_exporter.export(output_path)
|
||||||
elif output_path.suffix == ".json":
|
elif output_path.suffix == ".json":
|
||||||
exporter = JSONExporter(graph_builder)
|
json_exporter = JSONExporter(graph_builder)
|
||||||
exporter.export(output_path)
|
json_exporter.export(output_path)
|
||||||
elif output_path.suffix in [".png", ".svg"]:
|
elif output_path.suffix in [".png", ".svg"]:
|
||||||
exporter = PNGExporter(graph_builder)
|
png_exporter = PNGExporter(graph_builder)
|
||||||
exporter.export(output_path, format=output_path.suffix[1:])
|
png_exporter.export(output_path, format=output_path.suffix[1:])
|
||||||
else:
|
else:
|
||||||
console.print(f"[red]Unsupported output format: {output_path.suffix}[/red]")
|
console.print(f"[red]Unsupported output format: {output_path.suffix}[/red]")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ class BaseParser(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def extract_imports(self, tree, file_path: Path) -> list[str]:
|
def extract_imports(self, content: str) -> list[str]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def extract_calls(self, tree, file_path: Path) -> list[str]:
|
def extract_calls(self, content: str) -> list[str]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import pytest
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -27,7 +26,7 @@ class Greeter:
|
|||||||
def greet(self, name):
|
def greet(self, name):
|
||||||
if self.prefix and name:
|
if self.prefix and name:
|
||||||
return f"{self.prefix} {name}"
|
return f"{self.prefix} {name}"
|
||||||
return "Hello
|
return "Hello"
|
||||||
""")
|
""")
|
||||||
|
|
||||||
parser = PythonParser()
|
parser = PythonParser()
|
||||||
@@ -187,7 +186,23 @@ def func_b():
|
|||||||
|
|
||||||
file_nodes = [n for n in nodes if n.node_type.value == "file"]
|
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"]
|
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(file_nodes) == 3
|
||||||
assert len(func_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 pathlib import Path
|
||||||
from src.analyzers.complexity import ComplexityCalculator
|
from src.analyzers.complexity import ComplexityCalculator
|
||||||
from src.analyzers.dependencies import DependencyAnalyzer
|
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
|
from src.parsers.base import Entity, EntityType
|
||||||
|
|
||||||
|
|
||||||
@@ -137,6 +136,21 @@ class TestComplexityCalculator:
|
|||||||
assert report["total_cyclomatic_complexity"] >= 2
|
assert report["total_cyclomatic_complexity"] >= 2
|
||||||
assert "complexity_distribution" in report
|
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:
|
class TestDependencyAnalyzer:
|
||||||
def setup_method(self):
|
def setup_method(self):
|
||||||
@@ -172,3 +186,38 @@ class TestDependencyAnalyzer:
|
|||||||
self.builder.add_node(file_node)
|
self.builder.add_node(file_node)
|
||||||
layers = self.analyzer.get_architecture_layers()
|
layers = self.analyzer.get_architecture_layers()
|
||||||
assert "presentation" in layers or "other" in 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 tempfile
|
||||||
import os
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from src.cli.main import cli, analyze, visualize, complexity, deps, export
|
from src.cli.main import cli, visualize, complexity, deps, export
|
||||||
from src.graph.builder import GraphBuilder, GraphType
|
|
||||||
|
|
||||||
|
|
||||||
class TestCLI:
|
class TestCLI:
|
||||||
@@ -104,6 +101,69 @@ class TestCLI:
|
|||||||
|
|
||||||
assert result.exit_code == 0
|
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):
|
def test_verbose_flag(self):
|
||||||
result = self.runner.invoke(cli, ["--verbose", "--help"])
|
result = self.runner.invoke(cli, ["--verbose", "--help"])
|
||||||
assert result.exit_code == 0
|
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 pathlib import Path
|
||||||
from src.graph.builder import GraphBuilder, GraphType, GraphNode, NodeType, GraphEdge
|
from src.graph.builder import GraphBuilder, GraphType, GraphNode, NodeType, GraphEdge
|
||||||
from src.parsers.base import Entity, EntityType
|
|
||||||
|
|
||||||
|
|
||||||
class TestGraphBuilder:
|
class TestGraphBuilder:
|
||||||
@@ -123,6 +121,19 @@ class TestGraphNode:
|
|||||||
assert node.style == "filled"
|
assert node.style == "filled"
|
||||||
assert node.shape == "ellipse"
|
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:
|
class TestGraphEdge:
|
||||||
def test_default_edge_type(self):
|
def test_default_edge_type(self):
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import pytest
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from src.parsers.base import BaseParser, Entity, EntityType, ParserResult
|
from src.parsers.base import BaseParser, Entity, EntityType, ParserResult
|
||||||
from src.parsers.python import PythonParser
|
from src.parsers.python import PythonParser
|
||||||
|
|||||||
Reference in New Issue
Block a user