feat: Add support multiple playbooks in one graph (#118)

This commit is contained in:
Mohamed El Mouctar Haidara 2022-08-14 22:59:46 +02:00 committed by GitHub
parent 89534c1fd3
commit af67fd5f60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 205 additions and 168 deletions

View file

@ -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"

View file

@ -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":

View file

@ -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);

View file

@ -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)

View file

@ -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 systems
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,

View file

@ -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

View file

@ -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", "")

View file

@ -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(

View file

@ -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()

View file

@ -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"],
)

View file

@ -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)