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
« 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
5from pytest import fixture, raises
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)
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
30 def test_create_topology_mermaid_mmd_default(self, graph_fixture):
31 g, a, b = graph_fixture
32 mmd = create_topology_mermaid_mmd(g)
34 assert "flowchart TD" in mmd
35 assert "A --> B" in mmd
37 def test_create_topology_mermaid_mmd_custom_config(self, graph_fixture):
38 g, a, b = graph_fixture
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 )
49 mmd = create_topology_mermaid_mmd(g, config)
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
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
63 def test_export_topology_mermaid_mmd(self, graph_fixture):
64 g, _, _ = graph_fixture
65 output_path = Path("output.mmd")
67 with patch("builtins.open", mock_open()) as mock_file:
68 export_topology_mermaid_mmd(g, output_path)
70 mock_file.assert_called_with(output_path, "w+")
71 mock_file().write.assert_called()
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
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()
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()
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
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
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
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()
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()
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")
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
144 mock_script_path = MagicMock(spec=Path)
145 mock_create_script.return_value = mock_script_path
147 mock_exec.return_value = True
149 export_topology_mermaid_image(g, output_path)
151 mock_check.assert_called_once()
152 mock_exec.assert_called_with(mock_script_path)
153 assert mock_script_path.unlink.call_count == 1
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")
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
177 mock_script_path = MagicMock(spec=Path)
178 mock_create_script.return_value = mock_script_path
180 mock_exec.return_value = True
182 export_topology_mermaid_image(g, output_path)
184 mock_check.assert_called_once()
185 mock_exec.assert_called_with(mock_script_path)
186 assert mock_script_path.unlink.call_count == 1
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")
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
208 mock_script_path = MagicMock(spec=Path)
209 mock_create_script.return_value = mock_script_path
211 mock_exec.return_value = False
213 export_topology_mermaid_image(g, output_path)
215 mock_exec.assert_called_with(mock_script_path)
216 assert mock_script_path.unlink.call_count == 0
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")
226 g = Graph()
227 g.add_edge(a, b)
228 g.add_edge(b, c)
230 from graphable.views.mermaid import MermaidStylingConfig
232 config = MermaidStylingConfig(cluster_by_tag=True)
234 mmd = create_topology_mermaid_mmd(g, config)
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