mirror of
https://github.com/haidaraM/ansible-playbook-grapher
synced 2024-11-10 06:04:15 +00:00
feat: Add support multiple playbooks in one graph (#118)
This commit is contained in:
parent
89534c1fd3
commit
af67fd5f60
11 changed files with 205 additions and 168 deletions
|
@ -15,7 +15,7 @@
|
|||
|
||||
from .parser import PlaybookParser
|
||||
from .postprocessor import GraphVizPostProcessor
|
||||
from .renderer import GraphvizRenderer
|
||||
from .graphbuilder import GraphvizGraphBuilder
|
||||
|
||||
__version__ = "1.1.3"
|
||||
__prog__ = "ansible-playbook-grapher"
|
||||
|
|
|
@ -23,11 +23,15 @@ from ansible.cli.arguments import option_helpers
|
|||
from ansible.errors import AnsibleOptionsError
|
||||
from ansible.release import __version__ as ansible_version
|
||||
from ansible.utils.display import Display, initialize_locale
|
||||
from graphviz import Digraph
|
||||
|
||||
from ansibleplaybookgrapher import __prog__, __version__
|
||||
from ansibleplaybookgrapher.graphbuilder import (
|
||||
GraphvizGraphBuilder,
|
||||
OPEN_PROTOCOL_HANDLERS,
|
||||
)
|
||||
from ansibleplaybookgrapher.parser import PlaybookParser
|
||||
from ansibleplaybookgrapher.postprocessor import GraphVizPostProcessor
|
||||
from ansibleplaybookgrapher.renderer import GraphvizRenderer, OPEN_PROTOCOL_HANDLERS
|
||||
|
||||
# The display is a singleton. This instruction will NOT return a new instance.
|
||||
# We explicitly set the verbosity after the init.
|
||||
|
@ -54,27 +58,53 @@ class GrapherCLI(CLI, ABC):
|
|||
# Required to fix the warning "ansible.utils.display.initialize_locale has not been called..."
|
||||
initialize_locale()
|
||||
display.verbosity = self.options.verbosity
|
||||
playbook_nodes = []
|
||||
|
||||
parser = PlaybookParser(
|
||||
tags=self.options.tags,
|
||||
skip_tags=self.options.skip_tags,
|
||||
playbook_filename=self.options.playbook_filename,
|
||||
include_role_tasks=self.options.include_role_tasks,
|
||||
digraph = Digraph(
|
||||
format="svg",
|
||||
graph_attr=GraphvizGraphBuilder.DEFAULT_GRAPH_ATTR,
|
||||
edge_attr=GraphvizGraphBuilder.DEFAULT_EDGE_ATTR,
|
||||
)
|
||||
|
||||
playbook_node = parser.parse()
|
||||
renderer = GraphvizRenderer(
|
||||
playbook_node,
|
||||
open_protocol_handler=self.options.open_protocol_handler,
|
||||
open_protocol_custom_formats=self.options.open_protocol_custom_formats,
|
||||
)
|
||||
svg_path = renderer.render(
|
||||
self.options.output_filename, self.options.save_dot_file, self.options.view
|
||||
# parsing playbooks and build the graph
|
||||
for playbook_file in self.options.playbook_filenames:
|
||||
parser = PlaybookParser(
|
||||
tags=self.options.tags,
|
||||
skip_tags=self.options.skip_tags,
|
||||
playbook_filename=playbook_file,
|
||||
include_role_tasks=self.options.include_role_tasks,
|
||||
)
|
||||
|
||||
display.display(f"Parsing playbook {playbook_file}")
|
||||
|
||||
playbook_node = parser.parse()
|
||||
playbook_nodes.append(playbook_node)
|
||||
|
||||
GraphvizGraphBuilder(
|
||||
playbook_node,
|
||||
digraph=digraph,
|
||||
open_protocol_handler=self.options.open_protocol_handler,
|
||||
open_protocol_custom_formats=self.options.open_protocol_custom_formats,
|
||||
).build_graphviz_graph()
|
||||
|
||||
display.display("Rendering the graph...")
|
||||
|
||||
svg_path = digraph.render(
|
||||
cleanup=not self.options.save_dot_file,
|
||||
format="svg",
|
||||
filename=self.options.output_filename,
|
||||
view=self.options.view,
|
||||
)
|
||||
|
||||
if self.options.save_dot_file:
|
||||
# add .dot extension. The render doesn't add an extension
|
||||
final_name = self.options.output_filename + ".dot"
|
||||
os.rename(self.options.output_filename, final_name)
|
||||
display.display(f"Graphviz dot file has been exported to {final_name}")
|
||||
|
||||
post_processor = GraphVizPostProcessor(svg_path=svg_path)
|
||||
display.v("Post processing the SVG...")
|
||||
post_processor.post_process(playbook_node=playbook_node)
|
||||
post_processor.post_process(playbook_nodes)
|
||||
post_processor.write()
|
||||
|
||||
display.display(f"The graph has been exported to {svg_path}", color="green")
|
||||
|
@ -176,7 +206,10 @@ class PlaybookGrapherCLI(GrapherCLI):
|
|||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
"playbook_filename", help="Playbook to graph", metavar="playbook"
|
||||
"playbook_filenames",
|
||||
help="Playbook(s) to graph",
|
||||
metavar="playbooks",
|
||||
nargs="+",
|
||||
)
|
||||
|
||||
# Use ansible helper to add some default options also
|
||||
|
@ -200,9 +233,9 @@ class PlaybookGrapherCLI(GrapherCLI):
|
|||
self.options = options
|
||||
|
||||
if self.options.output_filename is None:
|
||||
# use the playbook name (without the extension) as output filename
|
||||
# use the first playbook name (without the extension) as output filename
|
||||
self.options.output_filename = os.path.splitext(
|
||||
ntpath.basename(self.options.playbook_filename)
|
||||
ntpath.basename(self.options.playbook_filenames[0])
|
||||
)[0]
|
||||
|
||||
if self.options.open_protocol_handler == "custom":
|
||||
|
|
|
@ -140,6 +140,8 @@ $("#svg").ready(function () {
|
|||
playbook.click(clickOnElement);
|
||||
playbook.dblclick(dblClickElement);
|
||||
|
||||
// TODO: add hover on playbooks
|
||||
|
||||
// Set hover and click events on the plays
|
||||
plays.hover(hoverMouseEnter, hoverMouseLeave);
|
||||
plays.click(clickOnElement);
|
||||
|
|
|
@ -219,8 +219,8 @@ class PlaybookNode(CompositeNode):
|
|||
def roles_usage(self) -> Dict["RoleNode", List[str]]:
|
||||
"""
|
||||
For each role in the graph, return the plays that reference the role
|
||||
# FIXME: Review this implementation. It may not be the most efficient way, but it's ok for the moment
|
||||
:return: A dict with key as role ID and value the list of plays
|
||||
FIXME: Review this implementation. It may not be the most efficient way, but it's ok for the moment
|
||||
:return: A dict with key as role node and value the list of plays
|
||||
"""
|
||||
|
||||
usages = defaultdict(list)
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import os
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from ansible.utils.display import Display
|
||||
|
@ -41,9 +40,9 @@ OPEN_PROTOCOL_HANDLERS = {
|
|||
}
|
||||
|
||||
|
||||
class GraphvizRenderer:
|
||||
class GraphvizGraphBuilder:
|
||||
"""
|
||||
Render the graph with graphviz
|
||||
Build the graphviz graph
|
||||
"""
|
||||
|
||||
DEFAULT_EDGE_ATTR = {"sep": "10", "esep": "5"}
|
||||
|
@ -58,24 +57,21 @@ class GraphvizRenderer:
|
|||
self,
|
||||
playbook_node: "PlaybookNode",
|
||||
open_protocol_handler: str,
|
||||
digraph: Digraph,
|
||||
open_protocol_custom_formats: Dict[str, str] = None,
|
||||
graph_format: str = "svg",
|
||||
graph_attr: Dict = None,
|
||||
edge_attr: Dict = None,
|
||||
):
|
||||
"""
|
||||
|
||||
:param playbook_node: Playbook parsed node
|
||||
:param open_protocol_handler: The protocol handler name to use
|
||||
:param digraph: Graphviz graph into which build the graph
|
||||
:param open_protocol_custom_formats: The custom formats to use when the protocol handler is set to custom
|
||||
:param graph_format: the graph format to render. See https://graphviz.org/docs/outputs/
|
||||
:param graph_attr: Default graph attributes
|
||||
:param edge_attr: Default edge attributes
|
||||
"""
|
||||
self.playbook_node = playbook_node
|
||||
self._play_colors = self._init_play_colors()
|
||||
# A map containing the roles that have been rendered so far
|
||||
self._rendered_roles = {}
|
||||
# FIXME: should be merged for all playbooks
|
||||
self.roles_usage = playbook_node.roles_usage()
|
||||
|
||||
self.open_protocol_handler = open_protocol_handler
|
||||
|
@ -83,11 +79,7 @@ class GraphvizRenderer:
|
|||
formats = {**OPEN_PROTOCOL_HANDLERS, **{"custom": open_protocol_custom_formats}}
|
||||
self.open_protocol_formats = formats[self.open_protocol_handler]
|
||||
|
||||
self.digraph = Digraph(
|
||||
format=graph_format,
|
||||
graph_attr=graph_attr or GraphvizRenderer.DEFAULT_GRAPH_ATTR,
|
||||
edge_attr=edge_attr or GraphvizRenderer.DEFAULT_EDGE_ATTR,
|
||||
)
|
||||
self.digraph = digraph
|
||||
|
||||
def _init_play_colors(self) -> Dict[str, Tuple[str, str]]:
|
||||
"""
|
||||
|
@ -102,31 +94,7 @@ class GraphvizRenderer:
|
|||
# https://stackoverflow.com/questions/9018016/how-to-compare-two-colors-for-similarity-difference
|
||||
return colors
|
||||
|
||||
def render(self, output_filename: str, save_dot_file=False, view=False) -> str:
|
||||
"""
|
||||
Render the graph
|
||||
:param output_filename: Output file name without '.svg' extension.
|
||||
:param save_dot_file: If true, the dot file will be saved when rendering the graph.
|
||||
:param view: If true, will automatically open the resulting (PDF, PNG, SVG, etc.) file with your system’s
|
||||
default viewer application for the file type
|
||||
:return: The rendered file path (output_filename.svg)
|
||||
"""
|
||||
self._convert_to_graphviz()
|
||||
|
||||
display.display("Rendering the graph...")
|
||||
rendered_file_path = self.digraph.render(
|
||||
cleanup=not save_dot_file, format="svg", filename=output_filename, view=view
|
||||
)
|
||||
|
||||
if save_dot_file:
|
||||
# add .dot extension. The render doesn't add an extension
|
||||
final_name = output_filename + ".dot"
|
||||
os.rename(output_filename, final_name)
|
||||
display.display(f"Graphviz dot file has been exported to {final_name}")
|
||||
|
||||
return rendered_file_path
|
||||
|
||||
def render_node(
|
||||
def build_node(
|
||||
self,
|
||||
graph: Digraph,
|
||||
counter: int,
|
||||
|
@ -150,7 +118,7 @@ class GraphvizRenderer:
|
|||
node_label_prefix = kwargs.get("node_label_prefix", "")
|
||||
|
||||
if isinstance(destination, BlockNode):
|
||||
self.render_block(
|
||||
self.build_block(
|
||||
graph,
|
||||
counter,
|
||||
source=source,
|
||||
|
@ -158,7 +126,7 @@ class GraphvizRenderer:
|
|||
color=color,
|
||||
)
|
||||
elif isinstance(destination, RoleNode):
|
||||
self.render_role(
|
||||
self.build_role(
|
||||
graph,
|
||||
counter,
|
||||
source=source,
|
||||
|
@ -190,7 +158,7 @@ class GraphvizRenderer:
|
|||
labeltooltip=edge_label,
|
||||
)
|
||||
|
||||
def render_block(
|
||||
def build_block(
|
||||
self,
|
||||
graph: Digraph,
|
||||
counter: int,
|
||||
|
@ -241,7 +209,7 @@ class GraphvizRenderer:
|
|||
# The reverse here is a little hack due to how graphviz render nodes inside a cluster by reversing them.
|
||||
# Don't really know why for the moment neither if there is an attribute to change that.
|
||||
for b_counter, task in enumerate(reversed(destination.tasks)):
|
||||
self.render_node(
|
||||
self.build_node(
|
||||
cluster_block_subgraph,
|
||||
source=destination,
|
||||
destination=task,
|
||||
|
@ -249,7 +217,7 @@ class GraphvizRenderer:
|
|||
color=color,
|
||||
)
|
||||
|
||||
def render_role(
|
||||
def build_role(
|
||||
self,
|
||||
graph: Digraph,
|
||||
counter: int,
|
||||
|
@ -310,7 +278,7 @@ class GraphvizRenderer:
|
|||
)
|
||||
# role tasks
|
||||
for role_task_counter, role_task in enumerate(destination.tasks, 1):
|
||||
self.render_node(
|
||||
self.build_node(
|
||||
role_subgraph,
|
||||
source=destination,
|
||||
destination=role_task,
|
||||
|
@ -318,7 +286,7 @@ class GraphvizRenderer:
|
|||
color=role_color,
|
||||
)
|
||||
|
||||
def _convert_to_graphviz(self):
|
||||
def build_graphviz_graph(self):
|
||||
"""
|
||||
Convert the PlaybookNode to the graphviz dot format
|
||||
:return:
|
||||
|
@ -367,7 +335,7 @@ class GraphvizRenderer:
|
|||
|
||||
# pre_tasks
|
||||
for pre_task_counter, pre_task in enumerate(play.pre_tasks, 1):
|
||||
self.render_node(
|
||||
self.build_node(
|
||||
play_subgraph,
|
||||
counter=pre_task_counter,
|
||||
source=play,
|
||||
|
@ -378,7 +346,7 @@ class GraphvizRenderer:
|
|||
|
||||
# roles
|
||||
for role_counter, role in enumerate(play.roles, 1):
|
||||
self.render_role(
|
||||
self.build_role(
|
||||
play_subgraph,
|
||||
source=play,
|
||||
destination=role,
|
||||
|
@ -388,7 +356,7 @@ class GraphvizRenderer:
|
|||
|
||||
# tasks
|
||||
for task_counter, task in enumerate(play.tasks, 1):
|
||||
self.render_node(
|
||||
self.build_node(
|
||||
play_subgraph,
|
||||
source=play,
|
||||
destination=task,
|
||||
|
@ -399,7 +367,7 @@ class GraphvizRenderer:
|
|||
|
||||
# post_tasks
|
||||
for post_task_counter, post_task in enumerate(play.post_tasks, 1):
|
||||
self.render_node(
|
||||
self.build_node(
|
||||
play_subgraph,
|
||||
source=play,
|
||||
destination=post_task,
|
|
@ -190,7 +190,7 @@ class PlaybookParser(BaseParser):
|
|||
play_name = f"Play: {clean_name(play.get_name())} ({len(play_hosts)})"
|
||||
play_name = self.template(play_name, play_vars)
|
||||
|
||||
display.banner("Parsing " + play_name)
|
||||
display.v(f"Parsing {play_name}")
|
||||
|
||||
play_node = PlayNode(play_name, hosts=play_hosts, raw_object=play)
|
||||
playbook_root_node.add_node("plays", play_node)
|
||||
|
@ -267,14 +267,10 @@ class PlaybookParser(BaseParser):
|
|||
node_type="post_task",
|
||||
)
|
||||
# Summary
|
||||
display.display("") # just an empty line
|
||||
display.v(f"{len(play_node.pre_tasks)} pre_task(s) added to the graph.")
|
||||
display.v(f"{len(play_node.roles)} role(s) added to the play")
|
||||
display.v(f"{len(play_node.tasks)} task(s) added to the play")
|
||||
display.v(f"{len(play_node.post_tasks)} post_task(s) added to the play")
|
||||
|
||||
display.banner(f"Done parsing {play_name}")
|
||||
display.display("") # just an empty line
|
||||
# moving to the next play
|
||||
|
||||
return playbook_root_node
|
||||
|
@ -433,10 +429,10 @@ class PlaybookParser(BaseParser):
|
|||
parent_nodes.pop()
|
||||
else: # It's here that we add the task in the graph
|
||||
if (
|
||||
len(parent_nodes) > 1
|
||||
and not has_role_parent(task_or_block) # 1
|
||||
and parent_nodes[-1].raw_object != task_or_block._parent # 2
|
||||
): # 3
|
||||
len(parent_nodes) > 1 # 1
|
||||
and not has_role_parent(task_or_block) # 2
|
||||
and parent_nodes[-1].raw_object != task_or_block._parent # 3
|
||||
):
|
||||
# We remove a parent node :
|
||||
# 1. When have at least two parents. Every node (except the playbook) should have a parent node
|
||||
# AND
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import os
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
|
||||
from ansible.utils.display import Display
|
||||
from lxml import etree
|
||||
|
@ -79,10 +79,10 @@ class GraphVizPostProcessor:
|
|||
|
||||
self.root.insert(index, element)
|
||||
|
||||
def post_process(self, playbook_node: PlaybookNode = None, *args, **kwargs):
|
||||
def post_process(self, playbook_nodes: List[PlaybookNode] = None, *args, **kwargs):
|
||||
"""
|
||||
|
||||
:param playbook_node:
|
||||
:param playbook_nodes:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
|
@ -113,9 +113,10 @@ class GraphVizPostProcessor:
|
|||
# Curve the text on the edges
|
||||
self._curve_text_on_edges()
|
||||
|
||||
if playbook_node:
|
||||
playbook_nodes = playbook_nodes or []
|
||||
for p_node in playbook_nodes:
|
||||
# Insert the graph representation for the links between the nodes
|
||||
self._insert_graph_representation(playbook_node)
|
||||
self._insert_links(p_node)
|
||||
|
||||
def write(self, output_filename: str = None):
|
||||
"""
|
||||
|
@ -128,11 +129,14 @@ class GraphVizPostProcessor:
|
|||
|
||||
self.tree.write(output_filename, xml_declaration=True, encoding="UTF-8")
|
||||
|
||||
def _insert_graph_representation(self, graph_representation: PlaybookNode):
|
||||
def _insert_links(self, playbook_node: PlaybookNode):
|
||||
"""
|
||||
Insert graph in the SVG
|
||||
Insert the links between nodes in the SVG file.
|
||||
:param playbook_node: one of the playbook in the svg
|
||||
"""
|
||||
links_structure = graph_representation.links_structure()
|
||||
display.vv(f"Inserting links structure for the playbook '{playbook_node.name}'")
|
||||
links_structure = playbook_node.links_structure()
|
||||
|
||||
for node_id, node_links in links_structure.items():
|
||||
# Find the group g with the specified id
|
||||
xpath_result = self.root.xpath(
|
||||
|
@ -216,7 +220,7 @@ class GraphVizPostProcessor:
|
|||
text_element.append(text_path)
|
||||
|
||||
# The more paths we have, the more we move the text from the path
|
||||
dy = -0.2 - (len(path_elements) - 1) * 0.4
|
||||
dy = -0.2 - (len(path_elements) - 1) * 0.3
|
||||
text_element.set("dy", f"{dy}%")
|
||||
# Remove unnecessary attributes and clear the text
|
||||
text_element.attrib.pop("x", "")
|
||||
|
|
|
@ -189,9 +189,9 @@ def test_cli_multiple_playbooks():
|
|||
args = [__prog__, "playbook1.yml", "playbook2.yml"]
|
||||
|
||||
cli = get_cli_class()(args)
|
||||
cli.parse()
|
||||
|
||||
with pytest.raises((AnsibleOptionsError, SystemExit)) as exception_info:
|
||||
cli.parse()
|
||||
assert cli.options.playbook_filenames == ["playbook1.yml", "playbook2.yml"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
@ -46,7 +46,7 @@ def test_example_parsing(grapher_cli: PlaybookGrapherCLI, display: Display):
|
|||
:param display:
|
||||
:return:
|
||||
"""
|
||||
parser = PlaybookParser(grapher_cli.options.playbook_filename)
|
||||
parser = PlaybookParser(grapher_cli.options.playbook_filenames[0])
|
||||
playbook_node = parser.parse()
|
||||
assert len(playbook_node.plays) == 1
|
||||
assert playbook_node.path == os.path.join(FIXTURES_PATH, "example.yml")
|
||||
|
@ -72,7 +72,7 @@ def test_with_roles_parsing(grapher_cli: PlaybookGrapherCLI):
|
|||
:param grapher_cli:
|
||||
:return:
|
||||
"""
|
||||
parser = PlaybookParser(grapher_cli.options.playbook_filename)
|
||||
parser = PlaybookParser(grapher_cli.options.playbook_filenames[0])
|
||||
playbook_node = parser.parse()
|
||||
assert len(playbook_node.plays) == 1
|
||||
play_node = playbook_node.plays[0]
|
||||
|
@ -94,7 +94,7 @@ def test_include_role_parsing(grapher_cli: PlaybookGrapherCLI, capsys):
|
|||
:return:
|
||||
"""
|
||||
parser = PlaybookParser(
|
||||
grapher_cli.options.playbook_filename, include_role_tasks=True
|
||||
grapher_cli.options.playbook_filenames[0], include_role_tasks=True
|
||||
)
|
||||
playbook_node = parser.parse()
|
||||
assert len(playbook_node.plays) == 1
|
||||
|
@ -155,7 +155,7 @@ def test_block_parsing(grapher_cli: PlaybookGrapherCLI):
|
|||
:return:
|
||||
"""
|
||||
parser = PlaybookParser(
|
||||
grapher_cli.options.playbook_filename, include_role_tasks=True
|
||||
grapher_cli.options.playbook_filenames[0], include_role_tasks=True
|
||||
)
|
||||
playbook_node = parser.parse()
|
||||
assert len(playbook_node.plays) == 1
|
||||
|
@ -219,7 +219,7 @@ def test_roles_usage(grapher_cli: PlaybookGrapherCLI):
|
|||
:return:
|
||||
"""
|
||||
parser = PlaybookParser(
|
||||
grapher_cli.options.playbook_filename, include_role_tasks=True
|
||||
grapher_cli.options.playbook_filenames[0], include_role_tasks=True
|
||||
)
|
||||
playbook_node = parser.parse()
|
||||
roles_usage = playbook_node.roles_usage()
|
||||
|
|
|
@ -14,13 +14,15 @@ DIR_PATH = os.path.dirname(os.path.realpath(__file__))
|
|||
|
||||
|
||||
def run_grapher(
|
||||
playbook_file: str, output_filename: str = None, additional_args: List[str] = None
|
||||
) -> Tuple[str, str]:
|
||||
playbook_files: List[str],
|
||||
output_filename: str = None,
|
||||
additional_args: List[str] = None,
|
||||
) -> Tuple[str, List[str]]:
|
||||
"""
|
||||
Utility function to run the grapher
|
||||
:param output_filename:
|
||||
:param additional_args:
|
||||
:param playbook_file:
|
||||
:param playbook_files:
|
||||
:return: SVG path and playbook absolute path
|
||||
"""
|
||||
additional_args = additional_args or []
|
||||
|
@ -34,7 +36,7 @@ def run_grapher(
|
|||
additional_args.insert(0, "--open-protocol-handler")
|
||||
additional_args.insert(1, "vscode")
|
||||
|
||||
playbook_path = os.path.join(FIXTURES_DIR, playbook_file)
|
||||
playbook_paths = [os.path.join(FIXTURES_DIR, p_file) for p_file in playbook_files]
|
||||
args = [__prog__]
|
||||
|
||||
if output_filename: # the default filename is the playbook file name minus .yml
|
||||
|
@ -44,16 +46,17 @@ def run_grapher(
|
|||
|
||||
args.extend(additional_args)
|
||||
|
||||
args.append(playbook_path)
|
||||
args.extend(playbook_paths)
|
||||
|
||||
cli = get_cli_class()(args)
|
||||
|
||||
return cli.run(), playbook_path
|
||||
return cli.run(), playbook_paths
|
||||
|
||||
|
||||
def _common_tests(
|
||||
svg_path: str,
|
||||
playbook_path: str,
|
||||
playbook_paths: List[str],
|
||||
playbooks_number: int = 1,
|
||||
plays_number: int = 0,
|
||||
tasks_number: int = 0,
|
||||
post_tasks_number: int = 0,
|
||||
|
@ -79,8 +82,8 @@ def _common_tests(
|
|||
|
||||
# test if the file exist. It will exist only if we write in it.
|
||||
assert os.path.isfile(svg_path), "The svg file should exist"
|
||||
assert pq("g[id^=playbook_] text").text() == playbook_path
|
||||
|
||||
playbooks = pq("g[id^='playbook_']")
|
||||
plays = pq("g[id^='play_']")
|
||||
tasks = pq("g[id^='task_']")
|
||||
post_tasks = pq("g[id^='post_task_']")
|
||||
|
@ -88,29 +91,38 @@ def _common_tests(
|
|||
blocks = pq("g[id^='block_']")
|
||||
roles = pq("g[id^='role_']")
|
||||
|
||||
playbooks_file_names = [e.text for e in playbooks.find("text")]
|
||||
assert (
|
||||
playbooks_file_names == playbook_paths
|
||||
), "The playbook file names should be in the svg file"
|
||||
|
||||
assert (
|
||||
len(playbooks) == playbooks_number
|
||||
), f"The graph '{svg_path}' should contains {playbooks_number} play(s) but we found {len(playbooks)} play(s)"
|
||||
|
||||
assert (
|
||||
len(plays) == plays_number
|
||||
), f"The graph '{playbook_path}' should contains {plays_number} play(s) but we found {len(plays)} play(s)"
|
||||
), f"The graph '{svg_path}' should contains {plays_number} play(s) but we found {len(plays)} play(s)"
|
||||
|
||||
assert (
|
||||
len(pre_tasks) == pre_tasks_number
|
||||
), f"The graph '{playbook_path}' should contains {pre_tasks_number} pre tasks(s) but we found {len(pre_tasks)} pre tasks"
|
||||
), f"The graph '{svg_path}' should contains {pre_tasks_number} pre tasks(s) but we found {len(pre_tasks)} pre tasks"
|
||||
|
||||
assert (
|
||||
len(roles) == roles_number
|
||||
), f"The playbook '{playbook_path}' should contains {roles_number} role(s) but we found {len(roles)} role(s)"
|
||||
), f"The playbook '{svg_path}' should contains {roles_number} role(s) but we found {len(roles)} role(s)"
|
||||
|
||||
assert (
|
||||
len(tasks) == tasks_number
|
||||
), f"The graph '{playbook_path}' should contains {tasks_number} tasks(s) but we found {len(tasks)} tasks"
|
||||
), f"The graph '{svg_path}' should contains {tasks_number} tasks(s) but we found {len(tasks)} tasks"
|
||||
|
||||
assert (
|
||||
len(post_tasks) == post_tasks_number
|
||||
), f"The graph '{playbook_path}' should contains {post_tasks_number} post tasks(s) but we found {len(post_tasks)} post tasks"
|
||||
), f"The graph '{svg_path}' should contains {post_tasks_number} post tasks(s) but we found {len(post_tasks)} post tasks"
|
||||
|
||||
assert (
|
||||
len(blocks) == blocks_number
|
||||
), f"The graph '{playbook_path}' should contains {blocks_number} blocks(s) but we found {len(blocks)} blocks "
|
||||
), f"The graph '{svg_path}' should contains {blocks_number} blocks(s) but we found {len(blocks)} blocks "
|
||||
|
||||
return {
|
||||
"tasks": tasks,
|
||||
|
@ -125,15 +137,15 @@ def test_simple_playbook(request):
|
|||
"""
|
||||
Test simple_playbook.yml
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"simple_playbook.yml",
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["simple_playbook.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=["-i", os.path.join(FIXTURES_DIR, "inventory")],
|
||||
)
|
||||
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=1,
|
||||
post_tasks_number=2,
|
||||
)
|
||||
|
@ -143,13 +155,13 @@ def test_example(request):
|
|||
"""
|
||||
Test example.yml
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"example.yml", output_filename=request.node.name
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["example.yml"], output_filename=request.node.name
|
||||
)
|
||||
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=1,
|
||||
tasks_number=4,
|
||||
post_tasks_number=2,
|
||||
|
@ -161,12 +173,12 @@ def test_include_tasks(request):
|
|||
"""
|
||||
Test include_tasks.yml, an example with some included tasks
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"include_tasks.yml", output_filename=request.node.name
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["include_tasks.yml"], output_filename=request.node.name
|
||||
)
|
||||
|
||||
_common_tests(
|
||||
svg_path=svg_path, playbook_path=playbook_path, plays_number=1, tasks_number=7
|
||||
svg_path=svg_path, playbook_paths=playbook_paths, plays_number=1, tasks_number=7
|
||||
)
|
||||
|
||||
|
||||
|
@ -174,12 +186,12 @@ def test_import_tasks(request):
|
|||
"""
|
||||
Test import_tasks.yml, an example with some imported tasks
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"import_tasks.yml", output_filename=request.node.name
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["import_tasks.yml"], output_filename=request.node.name
|
||||
)
|
||||
|
||||
_common_tests(
|
||||
svg_path=svg_path, playbook_path=playbook_path, plays_number=1, tasks_number=5
|
||||
svg_path=svg_path, playbook_paths=playbook_paths, plays_number=1, tasks_number=5
|
||||
)
|
||||
|
||||
|
||||
|
@ -193,20 +205,20 @@ def test_with_roles(request, include_role_tasks_option, expected_tasks_number):
|
|||
Test with_roles.yml, an example with roles
|
||||
"""
|
||||
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"with_roles.yml",
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["with_roles.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=[include_role_tasks_option],
|
||||
)
|
||||
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=1,
|
||||
tasks_number=expected_tasks_number,
|
||||
post_tasks_number=2,
|
||||
pre_tasks_number=2,
|
||||
roles_number=2,
|
||||
pre_tasks_number=2,
|
||||
)
|
||||
|
||||
|
||||
|
@ -219,15 +231,15 @@ def test_include_role(request, include_role_tasks_option, expected_tasks_number)
|
|||
"""
|
||||
Test include_role.yml, an example with include_role
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"include_role.yml",
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["include_role.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=[include_role_tasks_option],
|
||||
)
|
||||
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=1,
|
||||
tasks_number=expected_tasks_number,
|
||||
roles_number=3,
|
||||
|
@ -238,21 +250,21 @@ def test_with_block(request):
|
|||
"""
|
||||
Test with_block.yml, an example with roles
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"with_block.yml",
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["with_block.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=["--include-role-tasks", "--save-dot-file"],
|
||||
)
|
||||
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=1,
|
||||
tasks_number=7,
|
||||
post_tasks_number=2,
|
||||
roles_number=1,
|
||||
pre_tasks_number=4,
|
||||
blocks_number=4,
|
||||
roles_number=1,
|
||||
)
|
||||
|
||||
|
||||
|
@ -260,12 +272,12 @@ def test_nested_include_tasks(request):
|
|||
"""
|
||||
Test nested_include.yml, an example with an include_tasks that include another tasks
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"nested_include_tasks.yml", output_filename=request.node.name
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["nested_include_tasks.yml"], output_filename=request.node.name
|
||||
)
|
||||
|
||||
_common_tests(
|
||||
svg_path=svg_path, playbook_path=playbook_path, plays_number=1, tasks_number=3
|
||||
svg_path=svg_path, playbook_paths=playbook_paths, plays_number=1, tasks_number=3
|
||||
)
|
||||
|
||||
|
||||
|
@ -279,15 +291,15 @@ def test_import_role(request, include_role_tasks_option, expected_tasks_number):
|
|||
Test import_role.yml, an example with import role.
|
||||
Import role is special because the tasks imported from role are treated as "normal tasks" when the playbook is parsed.
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"import_role.yml",
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["import_role.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=[include_role_tasks_option],
|
||||
)
|
||||
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=1,
|
||||
tasks_number=expected_tasks_number,
|
||||
roles_number=1,
|
||||
|
@ -299,16 +311,16 @@ def test_import_playbook(request):
|
|||
Test import_playbook
|
||||
"""
|
||||
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"import_playbook.yml", output_filename=request.node.name
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["import_playbook.yml"], output_filename=request.node.name
|
||||
)
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=1,
|
||||
pre_tasks_number=2,
|
||||
tasks_number=4,
|
||||
post_tasks_number=2,
|
||||
pre_tasks_number=2,
|
||||
)
|
||||
|
||||
|
||||
|
@ -323,14 +335,14 @@ def test_nested_import_playbook(
|
|||
"""
|
||||
Test nested import playbook with an import_role and include_tasks
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"nested_import_playbook.yml",
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["nested_import_playbook.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=[include_role_tasks_option],
|
||||
)
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=2,
|
||||
tasks_number=expected_tasks_number,
|
||||
)
|
||||
|
@ -340,11 +352,11 @@ def test_relative_var_files(request):
|
|||
"""
|
||||
Test a playbook with a relative var file
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"relative_var_files.yml", output_filename=request.node.name
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["relative_var_files.yml"], output_filename=request.node.name
|
||||
)
|
||||
res = _common_tests(
|
||||
svg_path=svg_path, playbook_path=playbook_path, plays_number=1, tasks_number=2
|
||||
svg_path=svg_path, playbook_paths=playbook_paths, plays_number=1, tasks_number=2
|
||||
)
|
||||
|
||||
# check if the plays title contains the interpolated variables
|
||||
|
@ -360,14 +372,14 @@ def test_tags(request):
|
|||
"""
|
||||
Test a playbook by only graphing a specific tasks based on the given tags
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"tags.yml",
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["tags.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=["-t", "pre_task_tag_1"],
|
||||
)
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=1,
|
||||
pre_tasks_number=1,
|
||||
)
|
||||
|
@ -377,18 +389,18 @@ def test_skip_tags(request):
|
|||
"""
|
||||
Test a playbook by only graphing a specific tasks based on the given tags
|
||||
"""
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"tags.yml",
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["tags.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=["--skip-tags", "pre_task_tag_1", "--include-role-tasks"],
|
||||
)
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=1,
|
||||
pre_tasks_number=1,
|
||||
roles_number=1,
|
||||
tasks_number=3,
|
||||
roles_number=1,
|
||||
pre_tasks_number=1,
|
||||
)
|
||||
|
||||
|
||||
|
@ -397,19 +409,41 @@ def test_multi_plays(request):
|
|||
Test with multiple plays, include_role and roles
|
||||
"""
|
||||
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"multi-plays.yml",
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["multi-plays.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=["--include-role-tasks"],
|
||||
)
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=3,
|
||||
roles_number=3,
|
||||
pre_tasks_number=2,
|
||||
tasks_number=10,
|
||||
post_tasks_number=2,
|
||||
roles_number=3,
|
||||
pre_tasks_number=2,
|
||||
)
|
||||
|
||||
|
||||
def test_multiple_playbooks(request):
|
||||
"""
|
||||
Test with multiple playbooks
|
||||
"""
|
||||
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["multi-plays.yml", "relative_var_files.yml", "with_roles.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=["--include-role-tasks"],
|
||||
)
|
||||
_common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_paths=playbook_paths,
|
||||
playbooks_number=3,
|
||||
plays_number=3 + 1 + 1,
|
||||
pre_tasks_number=2 + 0 + 2,
|
||||
roles_number=3,
|
||||
tasks_number=10 + 2 + 8,
|
||||
post_tasks_number=2 + 0 + 2,
|
||||
)
|
||||
|
||||
|
||||
|
@ -418,8 +452,8 @@ def test_with_roles_with_custom_protocol_handlers(request):
|
|||
Test with_roles.yml with a custom protocol handlers
|
||||
"""
|
||||
formats_str = '{"file": "vscode://file/{path}:{line}", "folder": "{path}"}'
|
||||
svg_path, playbook_path = run_grapher(
|
||||
"with_roles.yml",
|
||||
svg_path, playbook_paths = run_grapher(
|
||||
["with_roles.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=[
|
||||
"--open-protocol-handler",
|
||||
|
@ -431,12 +465,12 @@ def test_with_roles_with_custom_protocol_handlers(request):
|
|||
|
||||
res = _common_tests(
|
||||
svg_path=svg_path,
|
||||
playbook_path=playbook_path,
|
||||
playbook_paths=playbook_paths,
|
||||
plays_number=1,
|
||||
tasks_number=2,
|
||||
post_tasks_number=2,
|
||||
pre_tasks_number=2,
|
||||
roles_number=2,
|
||||
pre_tasks_number=2,
|
||||
)
|
||||
|
||||
xlink_ref_selector = "{http://www.w3.org/1999/xlink}href"
|
||||
|
@ -457,7 +491,7 @@ def test_community_download_roles_and_collection(request):
|
|||
:return:
|
||||
"""
|
||||
run_grapher(
|
||||
"docker-mysql-galaxy.yml",
|
||||
["docker-mysql-galaxy.yml"],
|
||||
output_filename=request.node.name,
|
||||
additional_args=["--include-role-tasks"],
|
||||
)
|
||||
|
|
|
@ -108,7 +108,7 @@ def test_post_processor_with_graph_representation(
|
|||
play.add_node("tasks", task_1)
|
||||
play.add_node("tasks", task_2)
|
||||
|
||||
post_processor.post_process(playbook_node)
|
||||
post_processor.post_process([playbook_node])
|
||||
|
||||
post_processor.write(output_filename=svg_post_processed_path.strpath)
|
||||
|
||||
|
|
Loading…
Reference in a new issue