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

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.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) 

16 

17 

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 

26 

27 def test_create_topology_graphviz_dot_default(self, graph_fixture): 

28 g, a, b = graph_fixture 

29 dot = create_topology_graphviz_dot(g) 

30 

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 

35 

36 def test_create_topology_graphviz_dot_custom_config(self, graph_fixture): 

37 g, a, b = graph_fixture 

38 

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 ) 

49 

50 dot = create_topology_graphviz_dot(g, config) 

51 

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 

57 

58 def test_export_topology_graphviz_dot(self, graph_fixture): 

59 g, _, _ = graph_fixture 

60 output_path = Path("output.dot") 

61 

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

63 export_topology_graphviz_dot(g, output_path) 

64 

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

66 mock_file().write.assert_called() 

67 

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 

72 

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() 

78 

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") 

86 

87 mock_run.return_value = MagicMock() 

88 

89 export_topology_graphviz_image(g, output_path) 

90 

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] 

97 

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") 

105 

106 mock_run.return_value = MagicMock() 

107 

108 export_topology_graphviz_image(g, output_path) 

109 

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] 

116 

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") 

124 

125 mock_run.side_effect = CalledProcessError(1, "dot", stderr="error") 

126 

127 with raises(CalledProcessError): 

128 export_topology_graphviz_image(g, output_path) 

129 

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") 

137 

138 mock_run.side_effect = Exception("generic error") 

139 

140 with raises(Exception): 

141 export_topology_graphviz_image(g, output_path) 

142 

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") 

150 

151 g = Graph() 

152 g.add_edge(a, b) 

153 g.add_edge(b, c) 

154 

155 from graphable.views.graphviz import GraphvizStylingConfig 

156 

157 config = GraphvizStylingConfig(cluster_by_tag=True) 

158 

159 dot = create_topology_graphviz_dot(g, config) 

160 

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