"""Tests for dependency graph generation in diagrams module."""
import pytest
from local_deepwiki.generators.diagrams import (
_find_circular_dependencies,
_module_to_wiki_path,
_parse_import_line,
_path_to_module,
generate_dependency_graph,
generate_module_overview,
)
from local_deepwiki.models import ChunkType, CodeChunk, FileInfo, IndexStatus, Language
class TestGenerateDependencyGraph:
"""Tests for generate_dependency_graph function."""
def test_generates_flowchart(self):
"""Test basic flowchart generation."""
chunks = [
CodeChunk(
id="1",
file_path="src/local_deepwiki/core/parser.py",
content="from local_deepwiki.models import ChunkType",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
CodeChunk(
id="2",
file_path="src/local_deepwiki/models.py",
content="# models",
chunk_type=ChunkType.MODULE,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "local_deepwiki")
assert diagram is None or "flowchart" in diagram
def test_returns_none_for_no_imports(self):
"""Test returns None when no imports."""
chunks = [
CodeChunk(
id="1",
file_path="test.py",
content="def func(): pass",
chunk_type=ChunkType.FUNCTION,
language=Language.PYTHON,
start_line=1,
end_line=1,
)
]
diagram = generate_dependency_graph(chunks, "project")
assert diagram is None
class TestFindCircularDependencies:
"""Tests for _find_circular_dependencies function."""
def test_finds_direct_cycle(self):
"""Test detection of A -> B -> A cycle."""
deps = {
"a": {"b"},
"b": {"a"},
}
circular = _find_circular_dependencies(deps)
assert len(circular) > 0
assert ("a", "b") in circular or ("b", "a") in circular
def test_finds_longer_cycle(self):
"""Test detection of A -> B -> C -> A cycle."""
deps = {
"a": {"b"},
"b": {"c"},
"c": {"a"},
}
circular = _find_circular_dependencies(deps)
assert len(circular) > 0
def test_no_cycle(self):
"""Test no false positives for acyclic graph."""
deps = {
"a": {"b"},
"b": {"c"},
"c": set(),
}
circular = _find_circular_dependencies(deps)
assert len(circular) == 0
class TestPathToModule:
"""Tests for _path_to_module function."""
def test_converts_simple_path(self):
"""Test basic path conversion."""
result = _path_to_module("src/mypackage/core/parser.py")
assert result is not None
assert "parser" in result
def test_skips_init_files(self):
"""Test __init__.py files return None."""
result = _path_to_module("src/pkg/__init__.py")
assert result is None
def test_skips_non_python(self):
"""Test non-Python files return None."""
result = _path_to_module("src/pkg/script.js")
assert result is None
class TestParseImportLine:
"""Tests for _parse_import_line function."""
def test_parses_from_import(self):
"""Test from X import Y parsing."""
result = _parse_import_line("from myproject.core import parser", "myproject")
assert result is not None
assert "core" in result
def test_ignores_external(self):
"""Test external imports return None."""
result = _parse_import_line("from pathlib import Path", "myproject")
assert result is None
def test_parses_import_statement(self):
"""Test import X parsing."""
result = _parse_import_line("import myproject.core.parser", "myproject")
assert result is not None
class TestGenerateModuleOverview:
"""Tests for generate_module_overview function."""
def test_generates_diagram(self):
"""Test module overview generation."""
status = IndexStatus(
repo_path="/test",
indexed_at=1234567890.0,
total_files=3,
total_chunks=10,
languages={"python": 3},
files=[
FileInfo(
path="src/core/parser.py",
language="python",
hash="a",
chunk_count=5,
size_bytes=100,
last_modified=1234567890.0,
),
FileInfo(
path="src/core/chunker.py",
language="python",
hash="b",
chunk_count=3,
size_bytes=100,
last_modified=1234567890.0,
),
FileInfo(
path="src/utils/helpers.py",
language="python",
hash="c",
chunk_count=2,
size_bytes=100,
last_modified=1234567890.0,
),
],
)
diagram = generate_module_overview(status)
assert diagram is not None
assert "graph TB" in diagram
def test_returns_none_for_empty(self):
"""Test returns None when no files."""
status = IndexStatus(
repo_path="/test",
indexed_at=1234567890.0,
total_files=0,
total_chunks=0,
languages={},
files=[],
)
diagram = generate_module_overview(status)
assert diagram is None
class TestEnhancedDependencyGraph:
"""Tests for enhanced dependency graph features."""
def test_subgraph_grouping(self):
"""Test modules are grouped by directory in subgraphs."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from myproject.core.chunker import chunk",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
CodeChunk(
id="2",
file_path="src/myproject/core/chunker.py",
content="from myproject.generators.wiki import WikiGen",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
CodeChunk(
id="3",
file_path="src/myproject/generators/wiki.py",
content="# wiki generator",
chunk_type=ChunkType.MODULE,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "myproject")
assert diagram is not None
assert "subgraph" in diagram
assert "core" in diagram.lower() or "Core" in diagram
def test_clickable_links(self):
"""Test click handlers are added when wiki_base_path provided."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from myproject.core.chunker import chunk",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
CodeChunk(
id="2",
file_path="src/myproject/core/chunker.py",
content="# chunker",
chunk_type=ChunkType.MODULE,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(
chunks, "myproject", wiki_base_path="files/"
)
assert diagram is not None
assert "click" in diagram
assert "files/" in diagram
def test_no_clickable_links_without_base_path(self):
"""Test click handlers are not added when wiki_base_path is empty."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from myproject.core.chunker import chunk",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
CodeChunk(
id="2",
file_path="src/myproject/core/chunker.py",
content="# chunker",
chunk_type=ChunkType.MODULE,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "myproject", wiki_base_path="")
assert diagram is not None
assert "click" not in diagram
def test_external_dependencies_shown(self):
"""Test external deps shown with different styling when enabled."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from pathlib import Path\nimport os\nfrom pydantic import BaseModel",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=3,
),
]
diagram = generate_dependency_graph(chunks, "myproject", show_external=True)
if diagram:
assert "external" in diagram.lower() or "External" in diagram
assert "stroke-dasharray" in diagram
def test_external_dependencies_hidden(self):
"""Test external deps hidden when show_external=False."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from pathlib import Path\nimport os",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=2,
),
]
diagram = generate_dependency_graph(chunks, "myproject", show_external=False)
if diagram:
assert "External Dependencies" not in diagram
def test_max_external_limit(self):
"""Test max_external limits number of external deps shown."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="import os\nimport sys\nimport json\nimport re\nimport pathlib",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=5,
),
]
diagram = generate_dependency_graph(
chunks, "myproject", show_external=True, max_external=2
)
if diagram and "External" in diagram:
import re as regex
ext_nodes = regex.findall(r"E\d+\(\[", diagram)
assert len(ext_nodes) <= 2
class TestDependencyCollectionEdgeCases:
"""Tests for dependency collection edge cases."""
def test_handles_search_result_wrapper(self):
"""Test unwrapping SearchResult objects (line 370)."""
class MockSearchResult:
def __init__(self, chunk):
self.chunk = chunk
inner_chunk = CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from myproject.core.chunker import chunk",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
)
wrapped = MockSearchResult(inner_chunk)
chunks = [wrapped]
diagram = generate_dependency_graph(chunks, "myproject")
def test_skips_empty_module_path(self):
"""Test skipping chunks with non-convertible paths (line 377)."""
chunks = [
CodeChunk(
id="1",
file_path="__init__.py",
content="from myproject.core import parser",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "myproject")
def test_excludes_test_modules(self):
"""Test test modules are excluded (lines 380, 396)."""
chunks = [
CodeChunk(
id="1",
file_path="tests/test_parser.py",
content="from myproject.core import parser",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
CodeChunk(
id="2",
file_path="src/myproject/core/parser.py",
content="from myproject.utils import helpers",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "myproject", exclude_tests=True)
if diagram:
assert "test_parser" not in diagram
def test_includes_test_modules_when_not_excluded(self):
"""Test test modules included when exclude_tests=False."""
chunks = [
CodeChunk(
id="1",
file_path="tests/test_parser.py",
content="from myproject.core import parser",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "myproject", exclude_tests=False)
def test_skips_empty_lines(self):
"""Test empty lines in import content are skipped (line 391)."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from myproject.utils import helpers\n\n\nfrom myproject.core import chunker",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=4,
),
]
diagram = generate_dependency_graph(chunks, "myproject")
class TestExternalDependencySubgraph:
"""Tests for external dependency subgraph generation."""
def test_empty_external_deps(self):
"""Test no external subgraph when empty (line 506)."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from myproject.core.chunker import chunk",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
CodeChunk(
id="2",
file_path="src/myproject/core/chunker.py",
content="# module",
chunk_type=ChunkType.MODULE,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "myproject", show_external=True)
if diagram:
pass
class TestEdgesAndCircularDependencies:
"""Tests for edge generation and circular dependency handling."""
def test_skips_missing_from_id(self):
"""Test edges skipped when from_id not found (line 536)."""
def test_circular_dependency_styling(self):
"""Test circular dependencies get special styling (lines 540-541, 563-574)."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/a.py",
content="from myproject.core.b import B",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
CodeChunk(
id="2",
file_path="src/myproject/core/b.py",
content="from myproject.core.a import A",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "myproject", detect_circular=True)
if diagram:
assert "circular" in diagram or "stroke:#f00" in diagram
class TestExternalDependencyEdges:
"""Tests for external dependency edge generation."""
def test_external_dep_edges(self):
"""Test external dependency edges are added (line 641)."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from myproject.utils import helpers\nimport pathlib\nimport os",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=3,
),
CodeChunk(
id="2",
file_path="src/myproject/utils/helpers.py",
content="# helpers",
chunk_type=ChunkType.MODULE,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "myproject", show_external=True)
if diagram:
assert "-.-> " in diagram or "External" in diagram
class TestFindCircularDependenciesEdgeCases:
"""Tests for circular dependency detection edge cases."""
def test_visited_node_skipped(self):
"""Test already-visited nodes are skipped (line 727)."""
deps = {
"a": {"b", "c"},
"b": {"c"},
"c": {"d"},
"d": set(),
}
circular = _find_circular_dependencies(deps)
assert len(circular) == 0
class TestModuleToWikiPath:
"""Tests for _module_to_wiki_path function."""
def test_simple_module(self):
"""Test simple module path conversion."""
result = _module_to_wiki_path("core.parser", "local_deepwiki")
assert result == "src/local_deepwiki/core/parser.md"
def test_nested_module(self):
"""Test nested module path conversion."""
result = _module_to_wiki_path("providers.llm.ollama", "local_deepwiki")
assert result == "src/local_deepwiki/providers/llm/ollama.md"
def test_single_level_module(self):
"""Test single-level module path conversion."""
result = _module_to_wiki_path("models", "local_deepwiki")
assert result == "src/local_deepwiki/models.md"
class TestExcludeTestImports:
"""Tests for excluding test module imports (line 396)."""
def test_excludes_test_module_imports(self):
"""Test that imports of test_ modules are excluded when exclude_tests=True."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from myproject.test_helpers import mock\nfrom myproject.core.chunker import chunk",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=2,
),
CodeChunk(
id="2",
file_path="src/myproject/core/chunker.py",
content="# chunker module",
chunk_type=ChunkType.MODULE,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "myproject", exclude_tests=True)
if diagram:
assert "test_helpers" not in diagram
class TestCircularDependencyStylingComplete:
"""Complete tests for circular dependency styling (lines 536, 568, 572-574)."""
def test_full_circular_styling_with_multiple_modules(self):
"""Test complete circular styling with modules that have node_ids."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/module_a.py",
content="from myproject.core.module_b import B",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
CodeChunk(
id="2",
file_path="src/myproject/core/module_b.py",
content="from myproject.core.module_a import A",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "myproject", detect_circular=True)
if diagram:
assert "circular" in diagram or "linkStyle" in diagram
class TestExternalDependencyEdgesComplete:
"""Complete tests for external dependency edges (line 641)."""
def test_external_edges_with_valid_from_id(self):
"""Test external dependency edges when from_id exists."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from myproject.core.chunker import chunk\nimport pathlib\nimport json",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=3,
),
CodeChunk(
id="2",
file_path="src/myproject/core/chunker.py",
content="# chunker",
chunk_type=ChunkType.MODULE,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
diagram = generate_dependency_graph(chunks, "myproject", show_external=True)
if diagram:
assert "-.-> " in diagram or "E0" in diagram or "E1" in diagram
class TestDependencyGraphEdges:
"""Verify dependency graph produces edges with real import chunks."""
def test_internal_import_produces_edge(self):
"""An IMPORT chunk referencing another internal module should produce an edge."""
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from myproject.models import Schema",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
CodeChunk(
id="2",
file_path="src/myproject/models.py",
content="class Schema: pass",
chunk_type=ChunkType.CLASS,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
CodeChunk(
id="3",
file_path="src/myproject/core/parser.py",
content="def parse(): pass",
chunk_type=ChunkType.FUNCTION,
language=Language.PYTHON,
start_line=3,
end_line=5,
),
]
diagram = generate_dependency_graph(chunks, "myproject")
assert diagram is not None, "Expected a diagram but got None"
assert "flowchart" in diagram
assert "-->" in diagram
class TestDependencyGraphExternalEdgeSkip:
"""Test that external edges are properly skipped when from_id is missing."""
def test_full_dependency_graph_with_orphan_external(self):
"""Test dependency graph where external dep module has no from_id."""
pass
class TestInternalFunctionsDirectly:
"""Direct tests for internal functions to ensure coverage."""
def test_add_edges_with_missing_node(self):
"""Test _add_edges skips modules not in node_ids."""
from local_deepwiki.generators.diagrams import _add_edges
lines = []
internal_deps = {"unknown_module": {"other"}}
node_ids = {}
circular_edges = set()
_add_edges(lines, internal_deps, node_ids, circular_edges)
assert len(lines) == 0
def test_add_circular_styling_with_missing_node(self):
"""Test _add_circular_styling skips modules not in node_ids."""
from local_deepwiki.generators.diagrams import _add_circular_styling
lines = []
internal_deps = {"unknown_module": {"other"}}
node_ids = {}
circular_edges = {("unknown_module", "other")}
_add_circular_styling(lines, internal_deps, node_ids, circular_edges)
assert "linkStyle default" in lines[0]
def test_collect_dependencies_with_test_import(self):
"""Test _collect_dependencies excludes test_ imports."""
from local_deepwiki.generators.diagrams import _collect_dependencies
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/core/parser.py",
content="from myproject.test_utils import helper\nfrom myproject.core.other import func",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=2,
),
]
data = _collect_dependencies(
chunks, "myproject", show_external=False, exclude_tests=True
)
parser_deps = data.dependencies.get("core.parser", set())
assert "test_utils" not in parser_deps
class TestExternalEdgeMissingFromId:
"""Test for external dependency edges with missing from_id (line 641)."""
def test_external_edge_skipped_when_no_from_id(self):
"""Test that external edges are skipped when from_id doesn't exist."""
from local_deepwiki.generators.diagrams import (
_collect_dependencies,
_build_node_ids,
)
chunks = [
CodeChunk(
id="1",
file_path="src/myproject/orphan.py",
content="import pathlib",
chunk_type=ChunkType.IMPORT,
language=Language.PYTHON,
start_line=1,
end_line=1,
),
]
data = _collect_dependencies(
chunks, "myproject", show_external=True, exclude_tests=False
)
node_ids = {}
class TestExternalEdgeFromIdMissing:
"""Direct test of external edge logic when from_id is missing (line 641)."""
def test_direct_external_edge_skip(self):
"""Test external edge skipping by manipulating internal data."""
from local_deepwiki.generators.diagrams import _DependencyData
lines = []
node_ids = {"module_a": "M0"}
module_external_deps = {
"module_a": {"pathlib"},
"module_b": {"os"},
}
ext_node_ids = {"pathlib": "E0", "os": "E1"}
for module, ext_imports in sorted(module_external_deps.items()):
from_id = node_ids.get(module)
if not from_id:
continue
for ext in sorted(ext_imports):
target_ext_id = ext_node_ids.get(ext)
if target_ext_id:
lines.append(f" {from_id} -.-> {target_ext_id}")
assert len(lines) == 1
assert "M0" in lines[0]
assert "E0" in lines[0]