ansible-playbook-grapher/tests/test_parser.py
2023-05-13 18:21:39 +02:00

377 lines
13 KiB
Python

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"
# third include_role
include_role_3 = tasks[4]
assert tasks[4].when == "[when: x is not defined]"
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"
@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}'"