Coverage for tests / unit / views / test_mermaid.py: 100%

153 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-16 21:32 +0000

1from pathlib import Path 

2from subprocess import CalledProcessError 

3from unittest.mock import MagicMock, mock_open, patch 

4 

5from pytest import fixture, raises 

6 

7from graphable.graph import Graph 

8from graphable.graphable import Graphable 

9from graphable.views.mermaid import ( 

10 MermaidStylingConfig, 

11 _check_mmdc_on_path, 

12 _cleanup_on_exit, 

13 _create_mmdc_script, 

14 _execute_build_script, 

15 create_topology_mermaid_mmd, 

16 export_topology_mermaid_image, 

17 export_topology_mermaid_mmd, 

18) 

19 

20 

21class TestMermaid: 

22 @fixture 

23 def graph_fixture(self): 

24 a = Graphable("A") 

25 b = Graphable("B") 

26 g = Graph() 

27 g.add_edge(a, b) 

28 return g, a, b 

29 

30 def test_create_topology_mermaid_mmd_default(self, graph_fixture): 

31 g, a, b = graph_fixture 

32 mmd = create_topology_mermaid_mmd(g) 

33 

34 assert "flowchart TD" in mmd 

35 assert "A --> B" in mmd 

36 

37 def test_create_topology_mermaid_mmd_custom_config(self, graph_fixture): 

38 g, a, b = graph_fixture 

39 

40 config = MermaidStylingConfig( 

41 node_text_fnc=lambda n: f"Node:{n.reference}", 

42 link_text_fnc=lambda n, sn: "--link-->", 

43 node_style_fnc=lambda n: "fill:#f9f,stroke:#333" 

44 if n.reference == "A" 

45 else None, 

46 link_style_default="stroke-width:2px,fill:none,stroke:red", 

47 ) 

48 

49 mmd = create_topology_mermaid_mmd(g, config) 

50 

51 assert "A --link--> B" in mmd 

52 assert "A[Node:A]" in mmd 

53 assert "B[Node:B]" in mmd 

54 assert "style A fill:#f9f,stroke:#333" in mmd 

55 assert "linkStyle default stroke-width:2px,fill:none,stroke:red" in mmd 

56 

57 def test_create_topology_mermaid_mmd_link_style(self, graph_fixture): 

58 g, a, b = graph_fixture 

59 config = MermaidStylingConfig(link_style_fnc=lambda n, sn: "stroke:blue") 

60 mmd = create_topology_mermaid_mmd(g, config) 

61 assert "linkStyle 0 stroke:blue" in mmd 

62 

63 def test_export_topology_mermaid_mmd(self, graph_fixture): 

64 g, _, _ = graph_fixture 

65 output_path = Path("output.mmd") 

66 

67 with patch("builtins.open", mock_open()) as mock_file: 

68 export_topology_mermaid_mmd(g, output_path) 

69 

70 mock_file.assert_called_with(output_path, "w+") 

71 mock_file().write.assert_called() 

72 

73 @patch("graphable.views.mermaid.which") 

74 def test_check_mmdc_on_path_success(self, mock_which): 

75 mock_which.return_value = "/usr/bin/mmdc" 

76 _check_mmdc_on_path() # Should not raise 

77 

78 @patch("graphable.views.mermaid.which") 

79 def test_check_mmdc_on_path_failure(self, mock_which): 

80 mock_which.return_value = None 

81 with raises(FileNotFoundError): 

82 _check_mmdc_on_path() 

83 

84 def test_create_mmdc_script(self): 

85 content = "test script content" 

86 script_path = _create_mmdc_script(content) 

87 try: 

88 assert script_path.exists() 

89 assert script_path.read_text() == content 

90 finally: 

91 script_path.unlink() 

92 

93 @patch("graphable.views.mermaid.run") 

94 def test_execute_build_script_success(self, mock_run): 

95 mock_run.return_value = MagicMock() 

96 assert _execute_build_script(Path("dummy.sh")) is True 

97 

98 @patch("graphable.views.mermaid.run") 

99 def test_execute_build_script_failure(self, mock_run): 

100 mock_run.side_effect = CalledProcessError(1, "cmd", stderr="error message") 

101 assert _execute_build_script(Path("dummy.sh")) is False 

102 

103 @patch("graphable.views.mermaid.run") 

104 def test_execute_build_script_not_found(self, mock_run): 

105 mock_run.side_effect = FileNotFoundError() 

106 assert _execute_build_script(Path("dummy.sh")) is False 

107 

108 @patch("graphable.views.mermaid.Path.unlink") 

109 def test_cleanup_on_exit_exists(self, mock_unlink): 

110 mock_path = MagicMock(spec=Path) 

111 mock_path.exists.return_value = True 

112 _cleanup_on_exit(mock_path) 

113 mock_path.unlink.assert_called_once() 

114 

115 @patch("graphable.views.mermaid.Path.unlink") 

116 def test_cleanup_on_exit_not_exists(self, mock_unlink): 

117 mock_path = MagicMock(spec=Path) 

118 mock_path.exists.return_value = False 

119 _cleanup_on_exit(mock_path) 

120 mock_path.unlink.assert_not_called() 

121 

122 @patch("graphable.views.mermaid.Path.unlink") 

123 @patch("graphable.views.mermaid._execute_build_script") 

124 @patch("graphable.views.mermaid._create_mmdc_script") 

125 @patch("graphable.views.mermaid.NamedTemporaryFile") 

126 @patch("graphable.views.mermaid._check_mmdc_on_path") 

127 def test_export_topology_mermaid_image_svg( 

128 self, 

129 mock_check, 

130 mock_temp, 

131 mock_create_script, 

132 mock_exec, 

133 mock_unlink, 

134 graph_fixture, 

135 ): 

136 g, _, _ = graph_fixture 

137 output_path = Path("output.svg") 

138 

139 # Setup mocks 

140 mock_temp_file = MagicMock() 

141 mock_temp_file.name = "temp.mmd" 

142 mock_temp.return_value.__enter__.return_value = mock_temp_file 

143 

144 mock_script_path = MagicMock(spec=Path) 

145 mock_create_script.return_value = mock_script_path 

146 

147 mock_exec.return_value = True 

148 

149 export_topology_mermaid_image(g, output_path) 

150 

151 mock_check.assert_called_once() 

152 mock_exec.assert_called_with(mock_script_path) 

153 assert mock_script_path.unlink.call_count == 1 

154 

155 @patch("graphable.views.mermaid.Path.unlink") 

156 @patch("graphable.views.mermaid._execute_build_script") 

157 @patch("graphable.views.mermaid._create_mmdc_script") 

158 @patch("graphable.views.mermaid.NamedTemporaryFile") 

159 @patch("graphable.views.mermaid._check_mmdc_on_path") 

160 def test_export_topology_mermaid_image_png( 

161 self, 

162 mock_check, 

163 mock_temp, 

164 mock_create_script, 

165 mock_exec, 

166 mock_unlink, 

167 graph_fixture, 

168 ): 

169 g, _, _ = graph_fixture 

170 output_path = Path("output.png") 

171 

172 # Setup mocks 

173 mock_temp_file = MagicMock() 

174 mock_temp_file.name = "temp.mmd" 

175 mock_temp.return_value.__enter__.return_value = mock_temp_file 

176 

177 mock_script_path = MagicMock(spec=Path) 

178 mock_create_script.return_value = mock_script_path 

179 

180 mock_exec.return_value = True 

181 

182 export_topology_mermaid_image(g, output_path) 

183 

184 mock_check.assert_called_once() 

185 mock_exec.assert_called_with(mock_script_path) 

186 assert mock_script_path.unlink.call_count == 1 

187 

188 @patch("graphable.views.mermaid._execute_build_script") 

189 @patch("graphable.views.mermaid._create_mmdc_script") 

190 @patch("graphable.views.mermaid.NamedTemporaryFile") 

191 @patch("graphable.views.mermaid._check_mmdc_on_path") 

192 def test_export_topology_mermaid_image_failure( 

193 self, 

194 mock_check, 

195 mock_temp, 

196 mock_create_script, 

197 mock_exec, 

198 graph_fixture, 

199 ): 

200 g, _, _ = graph_fixture 

201 output_path = Path("output.svg") 

202 

203 # Setup mocks 

204 mock_temp_file = MagicMock() 

205 mock_temp_file.name = "temp.mmd" 

206 mock_temp.return_value.__enter__.return_value = mock_temp_file 

207 

208 mock_script_path = MagicMock(spec=Path) 

209 mock_create_script.return_value = mock_script_path 

210 

211 mock_exec.return_value = False 

212 

213 export_topology_mermaid_image(g, output_path) 

214 

215 mock_exec.assert_called_with(mock_script_path) 

216 assert mock_script_path.unlink.call_count == 0 

217 

218 def test_create_topology_mermaid_mmd_clustering(self): 

219 a = Graphable("A") 

220 a.add_tag("group1") 

221 b = Graphable("B") 

222 b.add_tag("group1") 

223 c = Graphable("C") 

224 c.add_tag("group2") 

225 

226 g = Graph() 

227 g.add_edge(a, b) 

228 g.add_edge(b, c) 

229 

230 from graphable.views.mermaid import MermaidStylingConfig 

231 

232 config = MermaidStylingConfig(cluster_by_tag=True) 

233 

234 mmd = create_topology_mermaid_mmd(g, config) 

235 

236 assert "subgraph group1" in mmd 

237 assert "subgraph group2" in mmd 

238 assert "A[A]" in mmd 

239 assert "B[B]" in mmd 

240 assert "C[C]" in mmd 

241 # Ensure connections still exist 

242 assert "A --> B" in mmd 

243 assert "B --> C" in mmd