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
« 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
5from pytest import raises
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
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 )
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"]
35def test_check_command(tmp_path):
36 graph_file = tmp_path / "test.json"
37 graph_file.write_text('{"nodes": [{"id": "A"}], "edges": []}')
39 data = check_command(graph_file)
40 assert data["valid"] is True
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"]
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"
57 convert_command(input_file, output_file)
58 assert output_file.exists()
59 assert "id: A" in output_file.read_text()
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": []}')
66 with raises(ValueError, match="is for images. Use the 'render' command instead"):
67 convert_command(input_file, Path("output.png"))
69 with raises(ValueError, match="is for images. Use the 'render' command instead"):
70 convert_command(input_file, Path("output.svg"))
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"
81 reduce_command(input_file, output_file)
82 assert output_file.exists()
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
89def test_checksum_command(tmp_path):
90 graph_file = tmp_path / "test.json"
91 graph_file.write_text('{"nodes": [{"id": "A"}], "edges": []}')
93 from graphable.graph import Graph
95 g = Graph.from_json(graph_file)
96 expected = g.checksum()
98 assert checksum_command(graph_file) == expected
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"
106 write_checksum_command(graph_file, checksum_file)
107 assert checksum_file.exists()
109 from graphable.graph import Graph
111 g = Graph.from_json(graph_file)
112 assert checksum_file.read_text() == g.checksum()
115def test_verify_command(tmp_path):
116 graph_file = tmp_path / "test.json"
117 graph_file.write_text('{"nodes": [{"id": "A"}], "edges": []}')
119 from graphable.graph import Graph
121 g = Graph.from_json(graph_file)
122 digest = g.checksum()
124 # Explicit expected
125 assert verify_command(graph_file, digest)["valid"] is True
126 assert verify_command(graph_file, "wrong")["valid"] is False
128 # Embedded checksum
129 embedded_file = tmp_path / "embedded.yaml"
130 g.write(embedded_file, embed_checksum=True)
132 data = verify_command(embedded_file)
133 assert data["valid"] is True
134 assert data["actual"] == digest
135 assert data["expected"] == digest
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": []}')
144 diff = diff_command(f1, f2)
145 assert "B" in diff["added_nodes"]
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"
155 diff_visual_command(f1, f2, output)
156 assert output.exists()
157 assert "diff:added" in output.read_text()
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
166 input_path = Path("input.json")
167 output_path = Path("output.png")
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)
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)
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)
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)
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"
200 input_path = Path("input.json")
201 output_path = Path("output.png")
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)
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")