Coverage for tests / unit / cli / test_commands.py: 100%

127 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-16 21:32 +0000

1from json import loads 

2from pathlib import Path 

3from unittest.mock import MagicMock, patch 

4 

5from pytest import raises 

6 

7from graphable.cli.commands.core import ( 

8 check_command, 

9 checksum_command, 

10 convert_command, 

11 diff_command, 

12 diff_visual_command, 

13 info_command, 

14 reduce_command, 

15 render_command, 

16 verify_command, 

17 write_checksum_command, 

18) 

19from graphable.enums import Engine 

20 

21 

22def test_info_command(tmp_path): 

23 graph_file = tmp_path / "test.json" 

24 graph_file.write_text( 

25 '{"nodes": [{"id": "A"}, {"id": "B"}], "edges": [{"source": "A", "target": "B"}]}' 

26 ) 

27 

28 data = info_command(graph_file) 

29 assert data["nodes"] == 2 

30 assert data["edges"] == 1 

31 assert "A" in data["sources"] 

32 assert "B" in data["sinks"] 

33 

34 

35def test_check_command(tmp_path): 

36 graph_file = tmp_path / "test.json" 

37 graph_file.write_text('{"nodes": [{"id": "A"}], "edges": []}') 

38 

39 data = check_command(graph_file) 

40 assert data["valid"] is True 

41 

42 # Cyclic graph 

43 cyclic_file = tmp_path / "cyclic.json" 

44 cyclic_file.write_text( 

45 '{"nodes": [{"id": "A"}, {"id": "B"}], "edges": [{"source": "A", "target": "B"}, {"source": "B", "target": "A"}]}' 

46 ) 

47 data = check_command(cyclic_file) 

48 assert data["valid"] is False 

49 assert "Cycle detected" in data["error"] or "would create a cycle" in data["error"] 

50 

51 

52def test_convert_command(tmp_path): 

53 input_file = tmp_path / "test.json" 

54 input_file.write_text('{"nodes": [{"id": "A"}], "edges": []}') 

55 output_file = tmp_path / "test.yaml" 

56 

57 convert_command(input_file, output_file) 

58 assert output_file.exists() 

59 assert "id: A" in output_file.read_text() 

60 

61 

62def test_convert_command_error_on_images(tmp_path): 

63 input_file = tmp_path / "test.json" 

64 input_file.write_text('{"nodes": [{"id": "A"}], "edges": []}') 

65 

66 with raises(ValueError, match="is for images. Use the 'render' command instead"): 

67 convert_command(input_file, Path("output.png")) 

68 

69 with raises(ValueError, match="is for images. Use the 'render' command instead"): 

70 convert_command(input_file, Path("output.svg")) 

71 

72 

73def test_reduce_command(tmp_path): 

74 input_file = tmp_path / "test.json" 

75 # A -> B -> C, A -> C 

76 input_file.write_text( 

77 '{"nodes": [{"id": "A"}, {"id": "B"}, {"id": "C"}], "edges": [{"source": "A", "target": "B"}, {"source": "B", "target": "C"}, {"source": "A", "target": "C"}]}' 

78 ) 

79 output_file = tmp_path / "reduced.json" 

80 

81 reduce_command(input_file, output_file) 

82 assert output_file.exists() 

83 

84 data = loads(output_file.read_text()) 

85 # Should have 2 edges (A->B, B->C) instead of 3 

86 assert len(data["edges"]) == 2 

87 

88 

89def test_checksum_command(tmp_path): 

90 graph_file = tmp_path / "test.json" 

91 graph_file.write_text('{"nodes": [{"id": "A"}], "edges": []}') 

92 

93 from graphable.graph import Graph 

94 

95 g = Graph.from_json(graph_file) 

96 expected = g.checksum() 

97 

98 assert checksum_command(graph_file) == expected 

99 

100 

101def test_write_checksum_command(tmp_path): 

102 graph_file = tmp_path / "test.json" 

103 graph_file.write_text('{"nodes": [{"id": "A"}], "edges": []}') 

104 checksum_file = tmp_path / "test.blake2b" 

105 

106 write_checksum_command(graph_file, checksum_file) 

107 assert checksum_file.exists() 

108 

109 from graphable.graph import Graph 

110 

111 g = Graph.from_json(graph_file) 

112 assert checksum_file.read_text() == g.checksum() 

113 

114 

115def test_verify_command(tmp_path): 

116 graph_file = tmp_path / "test.json" 

117 graph_file.write_text('{"nodes": [{"id": "A"}], "edges": []}') 

118 

119 from graphable.graph import Graph 

120 

121 g = Graph.from_json(graph_file) 

122 digest = g.checksum() 

123 

124 # Explicit expected 

125 assert verify_command(graph_file, digest)["valid"] is True 

126 assert verify_command(graph_file, "wrong")["valid"] is False 

127 

128 # Embedded checksum 

129 embedded_file = tmp_path / "embedded.yaml" 

130 g.write(embedded_file, embed_checksum=True) 

131 

132 data = verify_command(embedded_file) 

133 assert data["valid"] is True 

134 assert data["actual"] == digest 

135 assert data["expected"] == digest 

136 

137 

138def test_diff_command(tmp_path): 

139 f1 = tmp_path / "v1.json" 

140 f1.write_text('{"nodes": [{"id": "A"}], "edges": []}') 

141 f2 = tmp_path / "v2.json" 

142 f2.write_text('{"nodes": [{"id": "A"}, {"id": "B"}], "edges": []}') 

143 

144 diff = diff_command(f1, f2) 

145 assert "B" in diff["added_nodes"] 

146 

147 

148def test_diff_visual_command(tmp_path): 

149 f1 = tmp_path / "v1.json" 

150 f1.write_text('{"nodes": [{"id": "A"}], "edges": []}') 

151 f2 = tmp_path / "v2.json" 

152 f2.write_text('{"nodes": [{"id": "A"}, {"id": "B"}], "edges": []}') 

153 output = tmp_path / "diff.json" 

154 

155 diff_visual_command(f1, f2, output) 

156 assert output.exists() 

157 assert "diff:added" in output.read_text() 

158 

159 

160@patch("graphable.cli.commands.core.load_graph") 

161def test_render_command_explicit_engine(mock_load_graph): 

162 """Verify render_command calls the correct engine when explicitly provided.""" 

163 mock_g = MagicMock() 

164 mock_load_graph.return_value = mock_g 

165 

166 input_path = Path("input.json") 

167 output_path = Path("output.png") 

168 

169 # Mermaid 

170 with patch("graphable.views.mermaid.export_topology_mermaid_image") as mock_mermaid: 

171 render_command(input_path, output_path, engine=Engine.MERMAID) 

172 mock_mermaid.assert_called_once_with(mock_g, output_path) 

173 

174 # Graphviz 

175 with patch( 

176 "graphable.views.graphviz.export_topology_graphviz_image" 

177 ) as mock_graphviz: 

178 render_command(input_path, output_path, engine=Engine.GRAPHVIZ) 

179 mock_graphviz.assert_called_once_with(mock_g, output_path) 

180 

181 # D2 

182 with patch("graphable.views.d2.export_topology_d2_image") as mock_d2: 

183 render_command(input_path, output_path, engine=Engine.D2) 

184 mock_d2.assert_called_once_with(mock_g, output_path) 

185 

186 # PlantUML 

187 with patch("graphable.views.plantuml.export_topology_plantuml_image") as mock_puml: 

188 render_command(input_path, output_path, engine=Engine.PLANTUML) 

189 mock_puml.assert_called_once_with(mock_g, output_path) 

190 

191 

192@patch("graphable.cli.commands.core.load_graph") 

193@patch("graphable.views.utils.detect_engine") 

194def test_render_command_auto_detection(mock_detect_engine, mock_load_graph): 

195 """Verify render_command auto-detects engine when not provided.""" 

196 mock_g = MagicMock() 

197 mock_load_graph.return_value = mock_g 

198 mock_detect_engine.return_value = "graphviz" 

199 

200 input_path = Path("input.json") 

201 output_path = Path("output.png") 

202 

203 with patch( 

204 "graphable.views.graphviz.export_topology_graphviz_image" 

205 ) as mock_graphviz: 

206 render_command(input_path, output_path) 

207 mock_detect_engine.assert_called_once() 

208 mock_graphviz.assert_called_once_with(mock_g, output_path) 

209 

210 

211@patch("graphable.cli.commands.core.load_graph") 

212def test_render_command_invalid_engine(mock_load_graph): 

213 """Verify render_command raises ValueError for unknown engine.""" 

214 mock_load_graph.return_value = MagicMock() 

215 with raises(ValueError, match="Unknown rendering engine: unknown"): 

216 render_command(Path("i.json"), Path("o.png"), engine="unknown")