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

1from __future__ import annotations 

2 

3import os 

4import sys 

5from pathlib import Path 

6 

7sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 

8 

9from circular_deps import find_cycles, format_json, format_text 

10from circular_deps.models import Cycle, ImportInfo 

11from circular_deps.parsers.python import PythonImportParser 

12 

13 

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" 

26 

27 

28def test_cycle_detection_simple(): 

29 """Test simple 2-file circular dependency.""" 

30 import shutil 

31 import tempfile 

32 

33 tmpdir = Path(tempfile.mkdtemp()) 

34 try: 

35 a_py = tmpdir / "a.py" 

36 b_py = tmpdir / "b.py" 

37 

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") 

40 

41 files = [a_py, b_py] 

42 cycles = find_cycles(files) 

43 

44 assert len(cycles) == 1 

45 assert cycles[0].depth == 2 

46 assert cycles[0].severity == "warning" 

47 finally: 

48 shutil.rmtree(tmpdir) 

49 

50 

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]) 

58 

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 

62 

63 

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 ] 

72 

73 output = format_json(cycles) 

74 assert '"severity": "warning"' in output 

75 assert '"depth": 2' in output 

76 assert '"total_cycles": 1' in output 

77 

78 

79def test_no_cycles_linear_chain(): 

80 """Test linear import chain with no circular dependencies.""" 

81 import shutil 

82 import tempfile 

83 

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" 

89 

90 a_py.write_text("import b\n") 

91 b_py.write_text("import sys\n") 

92 c_py.write_text("import os\n") 

93 

94 files = [a_py, b_py, c_py] 

95 cycles = find_cycles(files) 

96 

97 assert len(cycles) == 0 

98 finally: 

99 shutil.rmtree(tmpdir) 

100 

101 

102def test_transitive_cycle_three_files(): 

103 """Test transitive cycle with 3 files.""" 

104 import shutil 

105 import tempfile 

106 

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" 

112 

113 a_py.write_text("import b\n") 

114 b_py.write_text("import c\n") 

115 c_py.write_text("import a\n") 

116 

117 files = [a_py, b_py, c_py] 

118 cycles = find_cycles(files) 

119 

120 assert len(cycles) == 1 

121 assert cycles[0].depth == 3 

122 finally: 

123 shutil.rmtree(tmpdir) 

124 

125 

126def test_cycle_detection_in_src_subdirectory(): 

127 """Test circular dependency detection with files in ./src subdirectory.""" 

128 import shutil 

129 import tempfile 

130 

131 tmpdir = Path(tempfile.mkdtemp()) 

132 try: 

133 src_dir = tmpdir / "src" 

134 src_dir.mkdir(parents=True, exist_ok=True) 

135 

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) 

140 

141 (afet_dir / "__init__.py").write_text("") 

142 (bfet_dir / "__init__.py").write_text("") 

143 

144 a_py = afet_dir / "a.py" 

145 b_py = bfet_dir / "b.py" 

146 

147 a_py.write_text("from bfet.b import foo\n") 

148 b_py.write_text("from afet.a import bar\n") 

149 

150 files = [a_py, b_py] 

151 cycles = find_cycles(files) 

152 

153 assert len(cycles) == 1 

154 finally: 

155 shutil.rmtree(tmpdir) 

156 

157 

158def test_cycle_nested_to_root(): 

159 """Test circular dependency with nested module importing parent.""" 

160 import shutil 

161 import tempfile 

162 

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("") 

168 

169 verticality_py = analysis_dir / "verticality.py" 

170 index_py = tmpdir / "index.py" 

171 

172 verticality_py.write_text("from index import foo\n") 

173 index_py.write_text("from analysis.verticality import bar\n") 

174 

175 files = [verticality_py, index_py] 

176 cycles = find_cycles(files) 

177 

178 assert len(cycles) == 1 

179 assert cycles[0].depth == 2 

180 finally: 

181 shutil.rmtree(tmpdir)