From 655db5e96282bbafc4dae54462a01823b6b464cb Mon Sep 17 00:00:00 2001 From: Ivan Kliuk Date: Mon, 12 Oct 2015 21:51:03 +0300 Subject: [PATCH] Add transitive reduction filter for task graphs Remove 'pydot_ng' that doesn't support transitive reduction filtering. Add '--tred' option for CLI. Usage example: fuel graph --render graph.gv --tred fuel graph --render graph.gv --dir ./output/dir/ --tred Change-Id: Ibfd7112dc0751df9bcadd0ab46e29f5159cbb3a5 Closes-bug: #1443446 --- fuelclient/cli/actions/graph.py | 53 ++++++---------- fuelclient/cli/arguments.py | 4 ++ fuelclient/tests/unit/v1/test_graph_action.py | 61 +++++++++++++++++-- 3 files changed, 79 insertions(+), 39 deletions(-) diff --git a/fuelclient/cli/actions/graph.py b/fuelclient/cli/actions/graph.py index 5fe83bf7..3197144e 100644 --- a/fuelclient/cli/actions/graph.py +++ b/fuelclient/cli/actions/graph.py @@ -53,6 +53,7 @@ class GraphAction(base.Action): Args.get_graph_startpoint(), Args.get_remove_type_arg(self.task_types), Args.get_parents_arg(), + Args.get_tred_arg("Apply transitive reduction filter for graph."), ) self.flag_func_map = ( ('render', self.render), @@ -68,8 +69,8 @@ class GraphAction(base.Action): fuel graph --env 1 --download --skip X Y --end pre_deployment fuel graph --env 1 --download --skip X Y --start post_deployment - Sepcify output: - fuel graph --env 1 --download > outpup/dir/file.gv + Specify output: + fuel graph --env 1 --download > output/dir/file.gv Get parents only for task A: @@ -105,6 +106,11 @@ class GraphAction(base.Action): fuel graph --render graph.gv fuel graph --render graph.gv --dir ./output/dir/ + To apply transitive reduction filter on rendered graph: + + fuel graph --render graph.gv --tred + fuel graph --render graph.gv --dir ./output/dir/ --tred + Read graph from stdin some_process | fuel graph --render - """ @@ -132,49 +138,26 @@ class GraphAction(base.Action): if not os.access(os.path.dirname(target_file), os.W_OK): raise error.ActionException( 'Path {0} is not writable'.format(target_file)) - - render_graph(dot_data, target_file) + render_graph(dot_data, target_file, params.tred) print('Graph saved in "{0}"'.format(target_file)) -def render_graph(input_data, output_path): - """Renders DOT graph using pydot or pygraphviz depending on their presence. - - If none of the libraries is available and are fully functional it is not - possible to render graphs. +def render_graph(input_data, output_path, tred=False): + """Renders DOT graph using pygraphviz. :param input_data: DOT graph representation :param output_path: path to the rendered graph + :param tred: applies transitive reduction of graph """ try: - _render_with_pydot(input_data, output_path) + import pygraphviz except ImportError: - try: - _render_with_pygraphiz(input_data, output_path) - except ImportError: - raise error.WrongEnvironmentError( - "This action require Graphviz installed toghether with " - "'pydot_ng' or 'pygraphviz' Python library") - - -def _render_with_pydot(input_data, output_path): - """Renders graph using pydot_ng library.""" - import pydot_ng as pydot - - graph = pydot.graph_from_dot_data(input_data) - if not graph: - raise error.BadDataException( - "Passed data does not contain graph in DOT format") - try: - graph.write_png(output_path) - except pydot.InvocationException as e: raise error.WrongEnvironmentError( - "There was an error with rendering graph:\n{0}".format(e)) + "This action requires Graphviz installed " + "together with 'pygraphviz' Python library") + graph = pygraphviz.AGraph(string=input_data) + if tred: + graph = graph.tred() -def _render_with_pygraphiz(input_data, output_path): - """Renders graph using pygraphviz library.""" - import pygraphviz as pgv - - graph = pgv.AGraph(string=input_data) graph.draw(output_path, prog='dot', format='png') diff --git a/fuelclient/cli/arguments.py b/fuelclient/cli/arguments.py index 8493bc59..9170999b 100644 --- a/fuelclient/cli/arguments.py +++ b/fuelclient/cli/arguments.py @@ -488,6 +488,10 @@ def get_render_arg(help_msg): help=help_msg) +def get_tred_arg(help_msg): + return get_boolean_arg("tred", help=help_msg) + + def get_node_arg(help_msg): default_kwargs = { "action": NodeAction, diff --git a/fuelclient/tests/unit/v1/test_graph_action.py b/fuelclient/tests/unit/v1/test_graph_action.py index 8246605a..e3eb2450 100644 --- a/fuelclient/tests/unit/v1/test_graph_action.py +++ b/fuelclient/tests/unit/v1/test_graph_action.py @@ -151,7 +151,25 @@ class TestGraphAction(base.UnitTestCase): ) m_open.assert_called_with('graph.gv', 'r') - m_render.assert_called_once_with(graph_data, '/path/graph.gv.png') + m_render.assert_called_once_with( + graph_data, '/path/graph.gv.png', False) + + @mock.patch('fuelclient.cli.actions.graph.open', create=True) + @mock.patch('fuelclient.cli.actions.graph.render_graph') + @mock.patch('fuelclient.cli.actions.graph.os.access') + @mock.patch('fuelclient.cli.actions.graph.os.path.exists') + def test_render_with_tred(self, m_exists, m_access, m_render, m_open): + graph_data = 'some-dot-data' + m_exists.return_value = True + m_open().__enter__().read.return_value = graph_data + + self.execute( + ['fuel', 'graph', '--render', 'graph.gv', '--tred'] + ) + + m_open.assert_called_with('graph.gv', 'r') + m_render.assert_called_once_with( + graph_data, '/path/graph.gv.png', True) @mock.patch('fuelclient.cli.actions.graph.os.path.exists') def test_render_no_file(self, m_exists): @@ -177,8 +195,29 @@ class TestGraphAction(base.UnitTestCase): ) self.m_full_path.assert_called_once_with(output_dir, '') - m_render.assert_called_once_with(graph_data, - '/output/dir/graph.gv.png') + m_render.assert_called_once_with( + graph_data, '/output/dir/graph.gv.png', False) + + @mock.patch('fuelclient.cli.actions.graph.open', create=True) + @mock.patch('fuelclient.cli.actions.graph.render_graph') + @mock.patch('fuelclient.cli.actions.graph.os.access') + @mock.patch('fuelclient.cli.actions.graph.os.path.exists') + def test_render_with_output_path_with_tred( + self, m_exists, m_access, m_render, m_open): + output_dir = '/output/dir' + graph_data = 'some-dot-data' + m_exists.return_value = True + m_open().__enter__().read.return_value = graph_data + self.m_full_path.return_value = output_dir + + self.execute( + ['fuel', 'graph', '--render', 'graph.gv', '--dir', output_dir, + '--tred'] + ) + + self.m_full_path.assert_called_once_with(output_dir, '') + m_render.assert_called_once_with( + graph_data, '/output/dir/graph.gv.png', True) @mock.patch('fuelclient.cli.actions.graph.os.access') @mock.patch('fuelclient.cli.actions.graph.render_graph') @@ -190,7 +229,21 @@ class TestGraphAction(base.UnitTestCase): ['fuel', 'graph', '--render', '-', ] ) - m_render.assert_called_once_with(graph_data, '/path/graph.gv.png') + m_render.assert_called_once_with( + graph_data, '/path/graph.gv.png', False) + + @mock.patch('fuelclient.cli.actions.graph.os.access') + @mock.patch('fuelclient.cli.actions.graph.render_graph') + def test_render_from_stdin_with_tred(self, m_render, m_access): + graph_data = u'graph data' + + with mock.patch('sys.stdin', new=io.StringIO(graph_data)): + self.execute( + ['fuel', 'graph', '--render', '-', '--tred'] + ) + + m_render.assert_called_once_with( + graph_data, '/path/graph.gv.png', True) @mock.patch('fuelclient.cli.actions.graph.open', create=True) @mock.patch('fuelclient.cli.actions.graph.os.path.exists')