Coverage for tests / unit / views / test_graphviz.py: 100%
111 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.graphviz import (
10 GraphvizStylingConfig,
11 _check_dot_on_path,
12 create_topology_graphviz_dot,
13 export_topology_graphviz_dot,
14 export_topology_graphviz_image,
15)
18class TestGraphviz:
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_graphviz_dot_default(self, graph_fixture):
28 g, a, b = graph_fixture
29 dot = create_topology_graphviz_dot(g)
31 assert "digraph G {" in dot
32 assert '"A" [label="A"]' in dot
33 assert '"B" [label="B"]' in dot
34 assert '"A" -> "B"' in dot
36 def test_create_topology_graphviz_dot_custom_config(self, graph_fixture):
37 g, a, b = graph_fixture
39 config = GraphvizStylingConfig(
40 node_label_fnc=lambda n: f"Node:{n.reference}",
41 node_attr_fnc=lambda n: {"style": "filled", "fillcolor": "red"}
42 if n.reference == "A"
43 else {},
44 edge_attr_fnc=lambda n, sn: {"label": "depends"},
45 graph_attr={"rankdir": "LR"},
46 node_attr_default={"shape": "box"},
47 edge_attr_default={"color": "blue"},
48 )
50 dot = create_topology_graphviz_dot(g, config)
52 assert 'rankdir="LR";' in dot
53 assert 'node [shape="box"];' in dot
54 assert 'edge [color="blue"];' in dot
55 assert '"A" [label="Node:A", style="filled", fillcolor="red"]' in dot
56 assert '"A" -> "B" [label="depends"]' in dot
58 def test_export_topology_graphviz_dot(self, graph_fixture):
59 g, _, _ = graph_fixture
60 output_path = Path("output.dot")
62 with patch("builtins.open", mock_open()) as mock_file:
63 export_topology_graphviz_dot(g, output_path)
65 mock_file.assert_called_with(output_path, "w+")
66 mock_file().write.assert_called()
68 @patch("graphable.views.graphviz.which")
69 def test_check_dot_on_path_success(self, mock_which):
70 mock_which.return_value = "/usr/bin/dot"
71 _check_dot_on_path() # Should not raise
73 @patch("graphable.views.graphviz.which")
74 def test_check_dot_on_path_failure(self, mock_which):
75 mock_which.return_value = None
76 with raises(FileNotFoundError):
77 _check_dot_on_path()
79 @patch("graphable.views.graphviz.run")
80 @patch("graphable.views.graphviz._check_dot_on_path")
81 def test_export_topology_graphviz_image_svg(
82 self, mock_check, mock_run, graph_fixture
83 ):
84 g, _, _ = graph_fixture
85 output_path = Path("output.svg")
87 mock_run.return_value = MagicMock()
89 export_topology_graphviz_image(g, output_path)
91 mock_check.assert_called_once()
92 mock_run.assert_called_once()
93 args, kwargs = mock_run.call_args
94 assert "dot" in args[0]
95 assert "-Tsvg" in args[0]
96 assert str(output_path) in args[0]
98 @patch("graphable.views.graphviz.run")
99 @patch("graphable.views.graphviz._check_dot_on_path")
100 def test_export_topology_graphviz_image_png(
101 self, mock_check, mock_run, graph_fixture
102 ):
103 g, _, _ = graph_fixture
104 output_path = Path("output.png")
106 mock_run.return_value = MagicMock()
108 export_topology_graphviz_image(g, output_path)
110 mock_check.assert_called_once()
111 mock_run.assert_called_once()
112 args, kwargs = mock_run.call_args
113 assert "dot" in args[0]
114 assert "-Tpng" in args[0]
115 assert str(output_path) in args[0]
117 @patch("graphable.views.graphviz.run")
118 @patch("graphable.views.graphviz._check_dot_on_path")
119 def test_export_topology_graphviz_image_failure(
120 self, mock_check, mock_run, graph_fixture
121 ):
122 g, _, _ = graph_fixture
123 output_path = Path("output.svg")
125 mock_run.side_effect = CalledProcessError(1, "dot", stderr="error")
127 with raises(CalledProcessError):
128 export_topology_graphviz_image(g, output_path)
130 @patch("graphable.views.graphviz.run")
131 @patch("graphable.views.graphviz._check_dot_on_path")
132 def test_export_topology_graphviz_image_generic_exception(
133 self, mock_check, mock_run, graph_fixture
134 ):
135 g, _, _ = graph_fixture
136 output_path = Path("output.svg")
138 mock_run.side_effect = Exception("generic error")
140 with raises(Exception):
141 export_topology_graphviz_image(g, output_path)
143 def test_create_topology_graphviz_dot_clustering(self):
144 a = Graphable("A")
145 a.add_tag("group1")
146 b = Graphable("B")
147 b.add_tag("group1")
148 c = Graphable("C")
149 c.add_tag("group2")
151 g = Graph()
152 g.add_edge(a, b)
153 g.add_edge(b, c)
155 from graphable.views.graphviz import GraphvizStylingConfig
157 config = GraphvizStylingConfig(cluster_by_tag=True)
159 dot = create_topology_graphviz_dot(g, config)
161 assert 'subgraph "cluster_group1"' in dot
162 assert 'label="group1";' in dot
163 assert 'subgraph "cluster_group2"' in dot
164 assert 'label="group2";' in dot
165 assert '"A" [label="A"];' in dot
166 assert '"B" [label="B"];' in dot
167 assert '"C" [label="C"];' in dot
168 assert '"A" -> "B"' in dot
169 assert '"B" -> "C"' in dot