import json import os import unittest from unittest.mock import patch, MagicMock import networkx as nx from pydantic import ValidationError from python_workflow_definition.plot import plot from python_workflow_definition.shared import ( NODES_LABEL, EDGES_LABEL, SOURCE_LABEL, TARGET_LABEL, SOURCE_PORT_LABEL, TARGET_PORT_LABEL, ) class TestPlot(unittest.TestCase): def setUp(self): self.test_file = "test_workflow.json" self.workflow_data = { "version": "0.0.1", NODES_LABEL: [ {"id": 1, "name": "Node 1", "type": "function", "value": "a.b"}, {"id": 2, "name": "Node 2", "type": "function", "value": "c.d"}, {"id": 3, "name": "Node 3", "type": "function", "value": "e.f"}, ], EDGES_LABEL: [ { SOURCE_LABEL: 1, TARGET_LABEL: 2, SOURCE_PORT_LABEL: "out1", TARGET_PORT_LABEL: "in1", }, { SOURCE_LABEL: 2, TARGET_LABEL: 3, SOURCE_PORT_LABEL: "out2", TARGET_PORT_LABEL: "in2", }, { SOURCE_LABEL: 1, TARGET_LABEL: 3, SOURCE_PORT_LABEL: None, TARGET_PORT_LABEL: "in3", }, ], } with open(self.test_file, "w") as f: json.dump(self.workflow_data, f) def tearDown(self): if os.path.exists(self.test_file): os.remove(self.test_file) @patch("python_workflow_definition.plot.display") @patch("python_workflow_definition.plot.SVG") @patch("networkx.nx_agraph.to_agraph") def test_plot(self, mock_to_agraph, mock_svg, mock_display): mock_agraph = MagicMock() mock_to_agraph.return_value = mock_agraph mock_agraph.draw.return_value = "" plot(self.test_file) self.assertEqual(1, mock_to_agraph.call_count) graph = mock_to_agraph.call_args[0][0] self.assertIsInstance(graph, nx.DiGraph) self.assertCountEqual(["1", "2", "3"], graph.nodes) self.assertEqual("a.b", graph.nodes["1"]["name"]) self.assertEqual("c.d", graph.nodes["2"]["name"]) self.assertEqual("e.f", graph.nodes["3"]["name"]) self.assertCountEqual([("1", "2"), ("2", "3"), ("1", "3")], graph.edges) edge_n1_n2_data = graph.get_edge_data("1", "2") self.assertIn("label", edge_n1_n2_data) self.assertEqual("in1=result[out1]", edge_n1_n2_data["label"]) edge_n1_n3_data = graph.get_edge_data("1", "3") self.assertIn("label", edge_n1_n3_data) self.assertEqual("in3", edge_n1_n3_data["label"]) mock_svg.assert_called_once_with("") mock_display.assert_called_once() @patch("python_workflow_definition.plot.display") @patch("python_workflow_definition.plot.SVG") @patch("networkx.nx_agraph.to_agraph") def test_plot_multiple_edges_same_source(self, mock_to_agraph, mock_svg, mock_display): self.workflow_data[EDGES_LABEL].append( { SOURCE_LABEL: 1, TARGET_LABEL: 2, SOURCE_PORT_LABEL: "out2", TARGET_PORT_LABEL: "in2", } ) with open(self.test_file, "w") as f: json.dump(self.workflow_data, f) mock_agraph = MagicMock() mock_to_agraph.return_value = mock_agraph mock_agraph.draw.return_value = "" plot(self.test_file) self.assertEqual(1, mock_to_agraph.call_count) graph = mock_to_agraph.call_args[0][0] self.assertIsInstance(graph, nx.DiGraph) # This assertion is correct due to the logic in `plot.py`. The function # groups all connections between a single source node and a single target # node. If it finds more than one connection (e.g., from different # source ports to different target ports), it creates a single, # unlabeled edge in the graph to represent the multiple connections. edge_n1_n2_data = graph.get_edge_data("1", "2") self.assertNotIn("label", edge_n1_n2_data) def test_plot_file_not_found(self): with self.assertRaises(FileNotFoundError): plot("non_existent_file.json") def test_plot_invalid_json(self): with open(self.test_file, "w") as f: f.write("{'invalid': 'json'") with self.assertRaises(ValidationError): plot(self.test_file) def test_plot_missing_keys(self): invalid_data = {"version": "0.0.1", "edges": []} with open(self.test_file, "w") as f: json.dump(invalid_data, f) with self.assertRaises(ValidationError): plot(self.test_file) if __name__ == "__main__": unittest.main()