Coverage for tests / unit / views / test_d2.py: 100%
127 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.d2 import (
10 D2StylingConfig,
11 _check_d2_on_path,
12 create_topology_d2,
13 export_topology_d2,
14 export_topology_d2_image,
15)
18class TestD2:
19 @fixture
20 def graph_fixture(self):
21 a = Graphable("A")
22 b = Graphable("B")
23 g = Graph()
24 g.add_edge(a, b)
25 return g, a, b
27 def test_create_topology_d2_default(self, graph_fixture):
28 g, a, b = graph_fixture
29 d2 = create_topology_d2(g)
31 assert "A: A" in d2
32 assert "B: B" in d2
33 assert "A -> B" in d2
35 def test_create_topology_d2_custom_config(self, graph_fixture):
36 g, a, b = graph_fixture
38 config = D2StylingConfig(
39 node_label_fnc=lambda n: f"Node:{n.reference}",
40 node_style_fnc=lambda n: {"fill": "red"} if n.reference == "A" else {},
41 edge_style_fnc=lambda n, sn: {"stroke": "blue"},
42 layout="dagre",
43 theme="200",
44 )
46 d2 = create_topology_d2(g, config)
48 assert "layout-engine: dagre" in d2
49 assert "A: Node:A" in d2
50 assert "fill: red" in d2
51 assert "A -> B: {" in d2
52 assert "stroke: blue" in d2
54 def test_create_topology_d2_edge_style_empty(self, graph_fixture):
55 g, a, b = graph_fixture
56 config = D2StylingConfig(edge_style_fnc=lambda n, sn: {})
57 d2 = create_topology_d2(g, config)
58 assert "A -> B" in d2
59 assert "{" not in d2
61 def test_export_topology_d2(self, graph_fixture):
62 g, _, _ = graph_fixture
63 output_path = Path("output.d2")
65 with patch("builtins.open", mock_open()) as mock_file:
66 export_topology_d2(g, output_path)
68 mock_file.assert_called_with(output_path, "w+")
69 mock_file().write.assert_called()
71 @patch("graphable.views.d2.which")
72 def test_check_d2_on_path_success(self, mock_which):
73 mock_which.return_value = "/usr/bin/d2"
74 _check_d2_on_path() # Should not raise
76 @patch("graphable.views.d2.which")
77 def test_check_d2_on_path_failure(self, mock_which):
78 mock_which.return_value = None
79 with raises(FileNotFoundError):
80 _check_d2_on_path()
82 @patch("graphable.views.d2.run")
83 @patch("graphable.views.d2._check_d2_on_path")
84 def test_export_topology_d2_image_svg_success(
85 self, mock_check, mock_run, graph_fixture
86 ):
87 g, _, _ = graph_fixture
88 output_path = Path("output.svg")
90 mock_run.return_value = MagicMock()
92 export_topology_d2_image(g, output_path)
94 mock_check.assert_called_once()
95 mock_run.assert_called_once()
96 args, kwargs = mock_run.call_args
97 assert "d2" == args[0][0]
98 assert str(output_path) in args[0]
100 @patch("graphable.views.d2.run")
101 @patch("graphable.views.d2._check_d2_on_path")
102 def test_export_topology_d2_image_png_success(
103 self, mock_check, mock_run, graph_fixture
104 ):
105 g, _, _ = graph_fixture
106 output_path = Path("output.png")
108 mock_run.return_value = MagicMock()
110 export_topology_d2_image(g, output_path)
112 mock_check.assert_called_once()
113 mock_run.assert_called_once()
114 args, kwargs = mock_run.call_args
115 assert "d2" == args[0][0]
116 assert str(output_path) in args[0]
118 @patch("graphable.views.d2.run")
119 @patch("graphable.views.d2._check_d2_on_path")
120 def test_export_topology_d2_image_with_config(
121 self, mock_check, mock_run, graph_fixture
122 ):
123 g, _, _ = graph_fixture
124 output_path = Path("output.svg")
125 config = D2StylingConfig(theme="200", layout="elk")
127 mock_run.return_value = MagicMock()
129 export_topology_d2_image(g, output_path, config)
131 mock_run.assert_called_once()
132 args, kwargs = mock_run.call_args
133 cmd = args[0]
134 assert "--theme" in cmd
135 assert "200" in cmd
136 assert "--layout" in cmd
137 assert "elk" in cmd
139 @patch("graphable.views.d2.run")
140 @patch("graphable.views.d2._check_d2_on_path")
141 def test_export_topology_d2_image_failure(
142 self, mock_check, mock_run, graph_fixture
143 ):
144 g, _, _ = graph_fixture
145 output_path = Path("output.svg")
147 mock_run.side_effect = CalledProcessError(1, "d2", stderr="error")
149 with raises(CalledProcessError):
150 export_topology_d2_image(g, output_path)
152 @patch("graphable.views.d2.run")
153 @patch("graphable.views.d2._check_d2_on_path")
154 def test_export_topology_d2_image_generic_exception(
155 self, mock_check, mock_run, graph_fixture
156 ):
157 g, _, _ = graph_fixture
158 output_path = Path("output.svg")
160 mock_run.side_effect = Exception("generic error")
162 with raises(Exception):
163 export_topology_d2_image(g, output_path)
165 def test_create_topology_d2_clustering(self):
166 a = Graphable("A")
167 a.add_tag("group1")
168 b = Graphable("B")
169 b.add_tag("group1")
170 c = Graphable("C")
171 c.add_tag("group2")
173 g = Graph()
174 g.add_edge(a, b)
175 g.add_edge(b, c)
177 from graphable.views.d2 import D2StylingConfig
179 config = D2StylingConfig(cluster_by_tag=True)
181 d2 = create_topology_d2(g, config)
183 assert "group1: {" in d2
184 assert "group2: {" in d2
185 assert "A: A" in d2
186 assert "B: B" in d2
187 assert "C: C" in d2
188 assert "A -> B" in d2
189 assert "B -> C" in d2