Coverage for tests/test_circular_deps/test_core.py: 100%

1 from __future__ import annotations

2

3 import os

4 import sys

5 from pathlib import Path

6

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

8

9 from circular_deps import find_cycles, format_json, format_text

10 from circular_deps.models import Cycle, ImportInfo

11 from circular_deps.parsers.python import PythonImportParser

12

13

14 def test_python_parser_extract_import():

15 parser = PythonImportParser()

16 source = """

17 from foo import bar

18 import os

19 from .. 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

28 def 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

51 def 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

64 def 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

79 def 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

102 def 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

126 def 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

158 def 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)