ansible-playbook-grapher/tests/test_parser.py

378 lines
13 KiB
Python
Raw Normal View History

import os
from typing import List
import pytest
from ansible.utils.display import Display
from ansibleplaybookgrapher import PlaybookParser
from ansibleplaybookgrapher.cli import PlaybookGrapherCLI
from ansibleplaybookgrapher.graph_model import (
TaskNode,
BlockNode,
RoleNode,
Node,
CompositeNode,
)
from tests import FIXTURES_DIR
# This file directory abspath
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
# Fixtures abspath
FIXTURES_PATH = os.path.join(DIR_PATH, FIXTURES_DIR)
def get_all_tasks(nodes: List[Node]) -> List[TaskNode]:
"""
Recursively Get all tasks from a list of nodes
:param nodes:
:return:
"""
tasks = []
for n in nodes:
if isinstance(n, CompositeNode):
tasks.extend(n.get_all_tasks())
else:
tasks.append(n)
return tasks
@pytest.mark.parametrize("grapher_cli", [["example.yml"]], indirect=True)
def test_example_parsing(grapher_cli: PlaybookGrapherCLI, display: Display):
"""
Test the parsing of example.yml
:param grapher_cli:
:param display:
:return:
"""
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")
assert playbook_node.line == 1
assert playbook_node.column == 1
assert (
playbook_node.index is None
), "The index of the playbook should be None (it has no parent)"
play_node = playbook_node.plays[0]
assert play_node.path == os.path.join(FIXTURES_PATH, "example.yml")
assert play_node.line == 2
assert play_node.index == 1
pre_tasks = play_node.pre_tasks
assert len(pre_tasks) == 2
assert pre_tasks[0].index == 1, "The index of the first pre_task should be 1"
assert pre_tasks[1].index == 2, "The index of the second pre_task should be 2"
tasks = play_node.tasks
assert len(tasks) == 4
for task_counter, task in enumerate(tasks):
assert (
task.index == task_counter + len(pre_tasks) + 1
), "The index of the task should start after the pre_tasks"
post_tasks = play_node.post_tasks
assert len(post_tasks) == 2
for post_task_counter, task in enumerate(post_tasks):
assert (
task.index == post_task_counter + len(pre_tasks) + len(tasks) + 1
), "The index of the post task should start after the pre_tasks and tasks"
@pytest.mark.parametrize("grapher_cli", [["with_roles.yml"]], indirect=True)
def test_with_roles_parsing(grapher_cli: PlaybookGrapherCLI):
"""
Test the parsing of with_roles.yml
:param grapher_cli:
:return:
"""
parser = PlaybookParser(grapher_cli.options.playbook_filenames[0])
playbook_node = parser.parse()
assert len(playbook_node.plays) == 1
play_node = playbook_node.plays[0]
assert play_node.index == 1
assert len(play_node.roles) == 2
fake_role = play_node.roles[0]
assert isinstance(fake_role, RoleNode)
assert not fake_role.include_role
assert fake_role.path == os.path.join(FIXTURES_PATH, "roles", "fake_role")
assert fake_role.line is None
assert fake_role.column is None
assert fake_role.index == 3
for task_counter, task in enumerate(fake_role.tasks):
assert (
task.index == task_counter + 1
), "The index of the task in the role should start at 1"
display_some_facts = play_node.roles[1]
for task_counter, task in enumerate(display_some_facts.tasks):
assert (
task.index == task_counter + 1
), "The index of the task in the role the should start at 1"
@pytest.mark.parametrize("grapher_cli", [["include_role.yml"]], indirect=True)
def test_include_role_parsing(grapher_cli: PlaybookGrapherCLI, capsys):
"""
Test parsing of include_role
:param grapher_cli:
:return:
"""
parser = PlaybookParser(
grapher_cli.options.playbook_filenames[0], include_role_tasks=True
)
playbook_node = parser.parse()
assert len(playbook_node.plays) == 1
play_node = playbook_node.plays[0]
tasks = play_node.tasks
assert len(tasks) == 6
# Since we use some loops inside the playbook, a warning should be displayed
assert (
"Looping on tasks or roles are not supported for the moment"
in capsys.readouterr().err
), "A warning should be displayed regarding loop being not supported"
# first include_role using a block
block_include_role = tasks[0]
assert isinstance(block_include_role, BlockNode)
include_role_1 = block_include_role.tasks[0]
assert isinstance(include_role_1, RoleNode)
assert include_role_1.include_role
assert include_role_1.path == os.path.join(FIXTURES_PATH, "include_role.yml")
assert include_role_1.line == 10, "The first include role should be at line 9"
assert (
len(include_role_1.tasks) == 0
), "We don't support adding tasks from include_role with loop"
assert include_role_1.has_loop(), "The first include role has a loop"
# first task
assert tasks[1].name == "(1) Debug"
assert tasks[1].when == '[when: ansible_os == "ubuntu"]'
# second include_role
include_role_2 = tasks[2]
assert isinstance(include_role_2, RoleNode)
assert include_role_2.include_role
assert len(include_role_2.tasks) == 3
assert not include_role_2.has_loop(), "The second include role doesn't have a loop"
# second task
assert tasks[3].name == "(3) Debug 2"
2022-01-09 19:24:52 +00:00
# third include_role
include_role_3 = tasks[4]
assert tasks[4].when == "[when: x is not defined]"
2022-01-09 19:24:52 +00:00
assert isinstance(include_role_3, RoleNode)
assert include_role_3.include_role
assert len(include_role_3.tasks) == 3
assert not include_role_3.has_loop(), "The second include role doesn't have a loop"
# fourth include_role
include_role_4 = tasks[5]
assert isinstance(include_role_4, RoleNode)
assert include_role_4.include_role
assert (
len(include_role_4.tasks) == 0
), "We don't support adding tasks from include_role with loop"
assert include_role_4.has_loop(), "The third include role has a loop"
2022-01-09 19:24:52 +00:00
@pytest.mark.parametrize("grapher_cli", [["with_block.yml"]], indirect=True)
def test_block_parsing(grapher_cli: PlaybookGrapherCLI):
"""
The parsing of a playbook with blocks
:param grapher_cli:
:return:
"""
parser = PlaybookParser(
grapher_cli.options.playbook_filenames[0], include_role_tasks=True
)
playbook_node = parser.parse()
assert len(playbook_node.plays) == 1
play_node = playbook_node.plays[0]
pre_tasks = play_node.pre_tasks
tasks = play_node.tasks
post_tasks = play_node.post_tasks
total_pre_tasks = get_all_tasks(pre_tasks)
total_tasks = get_all_tasks(tasks)
total_post_tasks = get_all_tasks(post_tasks)
assert (
len(total_pre_tasks) == 4
), f"The play should contain 4 pre tasks but we found {len(total_pre_tasks)} pre task(s)"
assert (
len(total_tasks) == 7
), f"The play should contain 3 tasks but we found {len(total_tasks)} task(s)"
assert (
len(total_post_tasks) == 2
), f"The play should contain 2 post tasks but we found {len(total_post_tasks)} post task(s)"
# Check pre tasks
assert isinstance(
pre_tasks[0], RoleNode
), "The first edge should have a RoleNode as destination"
pre_task_block = pre_tasks[1]
assert isinstance(
pre_task_block, BlockNode
), "The second edge should have a BlockNode as destination"
assert pre_task_block.path == os.path.join(FIXTURES_PATH, "with_block.yml")
assert pre_task_block.line == 7
# Check tasks
task_1 = tasks[0]
assert isinstance(task_1, TaskNode)
assert task_1.name == "Install tree"
# Check the second task: the first block
first_block = tasks[1]
assert isinstance(first_block, BlockNode)
assert first_block.name == "Install Apache"
assert len(first_block.tasks) == 4
assert first_block.index == 4
for task_counter, task in enumerate(first_block.tasks):
assert (
task.index == task_counter + 1
), "The index of the task in the block should start at 1"
assert first_block.tasks[0].name == "Install some packages"
assert first_block.tasks[0].has_loop(), "The task has a 'with_items'"
# Check the second block (nested block)
nested_block = first_block.tasks[2]
assert isinstance(nested_block, BlockNode)
assert len(nested_block.tasks) == 2
assert nested_block.tasks[0].name == "get_url"
assert nested_block.tasks[1].name == "command"
assert nested_block.index == 3
for task_counter, task in enumerate(nested_block.tasks):
assert (
task.index == task_counter + 1
), "The index of the task in the block should start at 1"
# Check the post task
assert post_tasks[0].name == "Debug"
assert post_tasks[0].index == 6
@pytest.mark.parametrize("grapher_cli", [["multi-plays.yml"]], indirect=True)
@pytest.mark.parametrize(
[
"group_roles_by_name",
"roles_number",
"nb_fake_role",
"nb_display_some_facts",
"nb_nested_include_role",
],
[(False, 8, 1, 1, 1), (True, 3, 3, 3, 1)],
ids=["no_group", "group"],
)
def test_roles_usage_multi_plays(
grapher_cli: PlaybookGrapherCLI,
roles_number: int,
group_roles_by_name: bool,
nb_fake_role: int,
nb_display_some_facts: int,
nb_nested_include_role: int,
):
"""
Test the role_usages method for multiple plays referencing the same roles
:param grapher_cli:
:param roles_number: The number of uniq roles in the graph
:param group_roles_by_name: flag to enable grouping roles or not
:param nb_fake_role: number of usages for the role fake_role
:param nb_display_some_facts: number of usages for the role display_some_facts
:param nb_nested_include_role: number of usages for the role nested_include_role
:return:
"""
parser = PlaybookParser(
grapher_cli.options.playbook_filenames[0],
include_role_tasks=True,
group_roles_by_name=group_roles_by_name,
)
playbook_node = parser.parse()
roles_usage = playbook_node.roles_usage()
expectation = {
"fake_role": nb_fake_role,
"display_some_facts": nb_display_some_facts,
"nested_include_role": nb_nested_include_role,
}
assert roles_number == len(
roles_usage
), "The number of unique roles should be equal to the number of usages"
for role, plays in roles_usage.items():
assert all(
map(lambda node: node.id.startswith("play_"), plays)
), "All nodes IDs should be play"
nb_plays_for_the_role = len(plays)
assert (
expectation.get(role.name) == nb_plays_for_the_role
), f"The role '{role.name}' is used {nb_plays_for_the_role} times but we expect {expectation.get(role.name)}"
@pytest.mark.parametrize("grapher_cli", [["group-roles-by-name.yml"]], indirect=True)
@pytest.mark.parametrize(
[
"group_roles_by_name",
],
[(False,), (True,)],
ids=["no_group", "group"],
)
def test_roles_usage_single_play(
grapher_cli: PlaybookGrapherCLI, group_roles_by_name: bool
):
"""
Test the role_usages method for a single play using the same roles multiple times.
The role usage should always be one regardless of the number of usages
:return:
"""
parser = PlaybookParser(
grapher_cli.options.playbook_filenames[0],
include_role_tasks=True,
group_roles_by_name=group_roles_by_name,
)
playbook_node = parser.parse()
roles_usage = playbook_node.roles_usage()
for role, plays in roles_usage.items():
assert len(plays) == 1, "The number of plays should be equal to 1"
@pytest.mark.parametrize("grapher_cli", [["roles_dependencies.yml"]], indirect=True)
def test_roles_dependencies(grapher_cli: PlaybookGrapherCLI):
"""
Test if the role dependencies in meta/main.yml are included in the graph
:return:
"""
parser = PlaybookParser(
grapher_cli.options.playbook_filenames[0], include_role_tasks=True
)
playbook_node = parser.parse()
roles = playbook_node.plays[0].roles
assert len(roles) == 1, "Only one explicit role is called inside the playbook"
role_with_dependencies = roles[0]
tasks = role_with_dependencies.tasks
expected_tasks = 5
dependant_role_name = "fake_role"
assert (
len(tasks) == expected_tasks
), f"There should be {expected_tasks} tasks in the graph"
# The first 3 tasks are coming from the dependency
for task_from_dependency in tasks[:3]:
assert (
dependant_role_name in task_from_dependency.name
), f"The task name should include the dependant role name '{dependant_role_name}'"