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

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.d2 import ( 

10 D2StylingConfig, 

11 _check_d2_on_path, 

12 create_topology_d2, 

13 export_topology_d2, 

14 export_topology_d2_image, 

15) 

16 

17 

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 

26 

27 def test_create_topology_d2_default(self, graph_fixture): 

28 g, a, b = graph_fixture 

29 d2 = create_topology_d2(g) 

30 

31 assert "A: A" in d2 

32 assert "B: B" in d2 

33 assert "A -> B" in d2 

34 

35 def test_create_topology_d2_custom_config(self, graph_fixture): 

36 g, a, b = graph_fixture 

37 

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 ) 

45 

46 d2 = create_topology_d2(g, config) 

47 

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 

53 

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 

60 

61 def test_export_topology_d2(self, graph_fixture): 

62 g, _, _ = graph_fixture 

63 output_path = Path("output.d2") 

64 

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

66 export_topology_d2(g, output_path) 

67 

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

69 mock_file().write.assert_called() 

70 

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 

75 

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

81 

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

89 

90 mock_run.return_value = MagicMock() 

91 

92 export_topology_d2_image(g, output_path) 

93 

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] 

99 

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

107 

108 mock_run.return_value = MagicMock() 

109 

110 export_topology_d2_image(g, output_path) 

111 

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] 

117 

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

126 

127 mock_run.return_value = MagicMock() 

128 

129 export_topology_d2_image(g, output_path, config) 

130 

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 

138 

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

146 

147 mock_run.side_effect = CalledProcessError(1, "d2", stderr="error") 

148 

149 with raises(CalledProcessError): 

150 export_topology_d2_image(g, output_path) 

151 

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

159 

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

161 

162 with raises(Exception): 

163 export_topology_d2_image(g, output_path) 

164 

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

172 

173 g = Graph() 

174 g.add_edge(a, b) 

175 g.add_edge(b, c) 

176 

177 from graphable.views.d2 import D2StylingConfig 

178 

179 config = D2StylingConfig(cluster_by_tag=True) 

180 

181 d2 = create_topology_d2(g, config) 

182 

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