Coverage for tests / test_circular_deps / test_core.py: 100%
112 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 15:04 -0800
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 15:04 -0800
1from __future__ import annotations
3import os
4import sys
5from pathlib import Path
7sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
9from circular_deps import find_cycles, format_json, format_text
10from circular_deps.models import Cycle, ImportInfo
11from circular_deps.parsers.python import PythonImportParser
14def test_python_parser_extract_import():
15 parser = PythonImportParser()
16 source = """
17from foo import bar
18import os
19from .. import baz
20"""
21 imports = parser.extract_imports(source, "test.py")
22 assert len(imports) == 3
23 assert imports[0].raw_module == "foo"
24 assert imports[1].raw_module == "os"
25 assert imports[2].import_type == "relative"
28def test_cycle_detection_simple():
29 """Test simple 2-file circular dependency."""
30 import shutil
31 import tempfile
33 tmpdir = Path(tempfile.mkdtemp())
34 try:
35 a_py = tmpdir / "a.py"
36 b_py = tmpdir / "b.py"
38 a_py.write_text("from b import foo\ndef bar():\n return 'bar'\n")
39 b_py.write_text("from a import bar\ndef foo():\n return 'foo'\n")
41 files = [a_py, b_py]
42 cycles = find_cycles(files)
44 assert len(cycles) == 1
45 assert cycles[0].depth == 2
46 assert cycles[0].severity == "warning"
47 finally:
48 shutil.rmtree(tmpdir)
51def test_format_text():
52 cycle = Cycle(
53 files=["a.py", "b.py", "a.py"],
54 edges=[("a.py", "b.py", 3), ("b.py", "a.py", 5)],
55 depth=2,
56 )
57 output = format_text([cycle])
59 assert "Warning: cycle detected" in output
60 assert "a.py:3" in output or "a.py" in output
61 assert "b.py" in output
64def test_format_json():
65 cycles = [
66 Cycle(
67 files=["a.py", "b.py", "a.py"],
68 edges=[("a.py", "b.py", 3), ("b.py", "a.py", 5)],
69 depth=2,
70 )
71 ]
73 output = format_json(cycles)
74 assert '"severity": "warning"' in output
75 assert '"depth": 2' in output
76 assert '"total_cycles": 1' in output
79def test_no_cycles_linear_chain():
80 """Test linear import chain with no circular dependencies."""
81 import shutil
82 import tempfile
84 tmpdir = Path(tempfile.mkdtemp())
85 try:
86 a_py = tmpdir / "a.py"
87 b_py = tmpdir / "b.py"
88 c_py = tmpdir / "c.py"
90 a_py.write_text("import b\n")
91 b_py.write_text("import sys\n")
92 c_py.write_text("import os\n")
94 files = [a_py, b_py, c_py]
95 cycles = find_cycles(files)
97 assert len(cycles) == 0
98 finally:
99 shutil.rmtree(tmpdir)
102def test_transitive_cycle_three_files():
103 """Test transitive cycle with 3 files."""
104 import shutil
105 import tempfile
107 tmpdir = Path(tempfile.mkdtemp())
108 try:
109 a_py = tmpdir / "a.py"
110 b_py = tmpdir / "b.py"
111 c_py = tmpdir / "c.py"
113 a_py.write_text("import b\n")
114 b_py.write_text("import c\n")
115 c_py.write_text("import a\n")
117 files = [a_py, b_py, c_py]
118 cycles = find_cycles(files)
120 assert len(cycles) == 1
121 assert cycles[0].depth == 3
122 finally:
123 shutil.rmtree(tmpdir)
126def test_cycle_detection_in_src_subdirectory():
127 """Test circular dependency detection with files in ./src subdirectory."""
128 import shutil
129 import tempfile
131 tmpdir = Path(tempfile.mkdtemp())
132 try:
133 src_dir = tmpdir / "src"
134 src_dir.mkdir(parents=True, exist_ok=True)
136 afet_dir = src_dir / "afet"
137 bfet_dir = src_dir / "bfet"
138 afet_dir.mkdir(parents=True, exist_ok=True)
139 bfet_dir.mkdir(parents=True, exist_ok=True)
141 (afet_dir / "__init__.py").write_text("")
142 (bfet_dir / "__init__.py").write_text("")
144 a_py = afet_dir / "a.py"
145 b_py = bfet_dir / "b.py"
147 a_py.write_text("from bfet.b import foo\n")
148 b_py.write_text("from afet.a import bar\n")
150 files = [a_py, b_py]
151 cycles = find_cycles(files)
153 assert len(cycles) == 1
154 finally:
155 shutil.rmtree(tmpdir)
158def test_cycle_nested_to_root():
159 """Test circular dependency with nested module importing parent."""
160 import shutil
161 import tempfile
163 tmpdir = Path(tempfile.mkdtemp())
164 try:
165 analysis_dir = tmpdir / "analysis"
166 analysis_dir.mkdir(parents=True, exist_ok=True)
167 (analysis_dir / "__init__.py").write_text("")
169 verticality_py = analysis_dir / "verticality.py"
170 index_py = tmpdir / "index.py"
172 verticality_py.write_text("from index import foo\n")
173 index_py.write_text("from analysis.verticality import bar\n")
175 files = [verticality_py, index_py]
176 cycles = find_cycles(files)
178 assert len(cycles) == 1
179 assert cycles[0].depth == 2
180 finally:
181 shutil.rmtree(tmpdir)