summaryrefslogtreecommitdiffstats
path: root/test/test_yaml_utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/test_yaml_utils.py')
-rw-r--r--test/test_yaml_utils.py905
1 files changed, 905 insertions, 0 deletions
diff --git a/test/test_yaml_utils.py b/test/test_yaml_utils.py
new file mode 100644
index 0000000..8af486b
--- /dev/null
+++ b/test/test_yaml_utils.py
@@ -0,0 +1,905 @@
+"""Tests for yaml-related utility functions."""
+from __future__ import annotations
+
+from io import StringIO
+from pathlib import Path
+from typing import Any
+
+import pytest
+from ruamel.yaml.comments import CommentedMap, CommentedSeq
+from ruamel.yaml.emitter import Emitter
+from ruamel.yaml.main import YAML
+from yamllint.linter import run as run_yamllint
+
+import ansiblelint.yaml_utils
+from ansiblelint.file_utils import Lintable
+
+fixtures_dir = Path(__file__).parent / "fixtures"
+formatting_before_fixtures_dir = fixtures_dir / "formatting-before"
+formatting_prettier_fixtures_dir = fixtures_dir / "formatting-prettier"
+formatting_after_fixtures_dir = fixtures_dir / "formatting-after"
+
+
+@pytest.fixture(name="empty_lintable")
+def fixture_empty_lintable() -> Lintable:
+ """Return a Lintable with no contents."""
+ lintable = Lintable("__empty_file__.yaml", content="")
+ return lintable
+
+
+def test_iter_tasks_in_file_with_empty_file(empty_lintable: Lintable) -> None:
+ """Make sure that iter_tasks_in_file returns early when files are empty."""
+ res = list(ansiblelint.yaml_utils.iter_tasks_in_file(empty_lintable))
+ assert not res
+
+
+def test_nested_items_path() -> None:
+ """Verify correct function of nested_items_path()."""
+ data = {
+ "foo": "text",
+ "bar": {"some": "text2"},
+ "fruits": ["apple", "orange"],
+ "answer": [{"forty-two": ["life", "universe", "everything"]}],
+ }
+
+ items = [
+ ("foo", "text", []),
+ ("bar", {"some": "text2"}, []),
+ ("some", "text2", ["bar"]),
+ ("fruits", ["apple", "orange"], []),
+ (0, "apple", ["fruits"]),
+ (1, "orange", ["fruits"]),
+ ("answer", [{"forty-two": ["life", "universe", "everything"]}], []),
+ (0, {"forty-two": ["life", "universe", "everything"]}, ["answer"]),
+ ("forty-two", ["life", "universe", "everything"], ["answer", 0]),
+ (0, "life", ["answer", 0, "forty-two"]),
+ (1, "universe", ["answer", 0, "forty-two"]),
+ (2, "everything", ["answer", 0, "forty-two"]),
+ ]
+ assert list(ansiblelint.yaml_utils.nested_items_path(data)) == items
+
+
+@pytest.mark.parametrize(
+ "invalid_data_input",
+ (
+ "string",
+ 42,
+ 1.234,
+ ("tuple",),
+ {"set"},
+ # NoneType is no longer include, as we assume we have to ignore it
+ ),
+)
+def test_nested_items_path_raises_typeerror(invalid_data_input: Any) -> None:
+ """Verify non-dict/non-list types make nested_items_path() raises TypeError."""
+ with pytest.raises(TypeError, match=r"Expected a dict or a list.*"):
+ list(ansiblelint.yaml_utils.nested_items_path(invalid_data_input))
+
+
+_input_playbook = [
+ {
+ "name": "It's a playbook", # unambiguous; no quotes needed
+ "tasks": [
+ {
+ "name": '"fun" task', # should be a single-quoted string
+ "debug": {
+ # ruamel.yaml default to single-quotes
+ # our Emitter defaults to double-quotes
+ "msg": "{{ msg }}",
+ },
+ }
+ ],
+ }
+]
+_SINGLE_QUOTE_WITHOUT_INDENTS = """\
+---
+- name: It's a playbook
+ tasks:
+ - name: '"fun" task'
+ debug:
+ msg: '{{ msg }}'
+"""
+_SINGLE_QUOTE_WITH_INDENTS = """\
+---
+ - name: It's a playbook
+ tasks:
+ - name: '"fun" task'
+ debug:
+ msg: '{{ msg }}'
+"""
+_DOUBLE_QUOTE_WITHOUT_INDENTS = """\
+---
+- name: It's a playbook
+ tasks:
+ - name: '"fun" task'
+ debug:
+ msg: "{{ msg }}"
+"""
+_DOUBLE_QUOTE_WITH_INDENTS_EXCEPT_ROOT_LEVEL = """\
+---
+- name: It's a playbook
+ tasks:
+ - name: '"fun" task'
+ debug:
+ msg: "{{ msg }}"
+"""
+
+
+@pytest.mark.parametrize(
+ (
+ "map_indent",
+ "sequence_indent",
+ "sequence_dash_offset",
+ "alternate_emitter",
+ "expected_output",
+ ),
+ (
+ pytest.param(
+ 2,
+ 2,
+ 0,
+ None,
+ _SINGLE_QUOTE_WITHOUT_INDENTS,
+ id="single_quote_without_indents",
+ ),
+ pytest.param(
+ 2,
+ 4,
+ 2,
+ None,
+ _SINGLE_QUOTE_WITH_INDENTS,
+ id="single_quote_with_indents",
+ ),
+ pytest.param(
+ 2,
+ 2,
+ 0,
+ ansiblelint.yaml_utils.FormattedEmitter,
+ _DOUBLE_QUOTE_WITHOUT_INDENTS,
+ id="double_quote_without_indents",
+ ),
+ pytest.param(
+ 2,
+ 4,
+ 2,
+ ansiblelint.yaml_utils.FormattedEmitter,
+ _DOUBLE_QUOTE_WITH_INDENTS_EXCEPT_ROOT_LEVEL,
+ id="double_quote_with_indents_except_root_level",
+ ),
+ ),
+)
+def test_custom_ruamel_yaml_emitter(
+ map_indent: int,
+ sequence_indent: int,
+ sequence_dash_offset: int,
+ alternate_emitter: Emitter | None,
+ expected_output: str,
+) -> None:
+ """Test ``ruamel.yaml.YAML.dump()`` sequence formatting and quotes."""
+ yaml = YAML(typ="rt")
+ # NB: ruamel.yaml does not have typehints, so mypy complains about everything here.
+ yaml.explicit_start = True # type: ignore[assignment]
+ yaml.map_indent = map_indent # type: ignore[assignment]
+ yaml.sequence_indent = sequence_indent # type: ignore[assignment]
+ yaml.sequence_dash_offset = sequence_dash_offset
+ if alternate_emitter is not None:
+ yaml.Emitter = alternate_emitter
+ # ruamel.yaml only writes to a stream (there is no `dumps` function)
+ with StringIO() as output_stream:
+ yaml.dump(_input_playbook, output_stream)
+ output = output_stream.getvalue()
+ assert output == expected_output
+
+
+@pytest.fixture(name="yaml_formatting_fixtures")
+def fixture_yaml_formatting_fixtures(fixture_filename: str) -> tuple[str, str, str]:
+ """Get the contents for the formatting fixture files.
+
+ To regenerate these fixtures, please run ``pytest --regenerate-formatting-fixtures``.
+
+ Ideally, prettier should not have to change any ``formatting-after`` fixtures.
+ """
+ before_path = formatting_before_fixtures_dir / fixture_filename
+ prettier_path = formatting_prettier_fixtures_dir / fixture_filename
+ after_path = formatting_after_fixtures_dir / fixture_filename
+ before_content = before_path.read_text()
+ prettier_content = prettier_path.read_text()
+ formatted_content = after_path.read_text()
+ return before_content, prettier_content, formatted_content
+
+
+@pytest.mark.parametrize(
+ "fixture_filename",
+ (
+ "fmt-1.yml",
+ "fmt-2.yml",
+ "fmt-3.yml",
+ ),
+)
+def test_formatted_yaml_loader_dumper(
+ yaml_formatting_fixtures: tuple[str, str, str],
+ fixture_filename: str,
+) -> None:
+ """Ensure that FormattedYAML loads/dumps formatting fixtures consistently."""
+ # pylint: disable=unused-argument
+ before_content, prettier_content, after_content = yaml_formatting_fixtures
+ assert before_content != prettier_content
+ assert before_content != after_content
+
+ yaml = ansiblelint.yaml_utils.FormattedYAML()
+
+ data_before = yaml.loads(before_content)
+ dump_from_before = yaml.dumps(data_before)
+ data_prettier = yaml.loads(prettier_content)
+ dump_from_prettier = yaml.dumps(data_prettier)
+ data_after = yaml.loads(after_content)
+ dump_from_after = yaml.dumps(data_after)
+
+ # comparing data does not work because the Comment objects
+ # have different IDs even if contents do not match.
+
+ assert dump_from_before == after_content
+ assert dump_from_prettier == after_content
+ assert dump_from_after == after_content
+
+ # We can't do this because FormattedYAML is stricter in some cases:
+ # assert prettier_content == after_content
+ #
+ # Instead, `pytest --regenerate-formatting-fixtures` will fail if prettier would
+ # change any files in test/fixtures/formatting-after
+
+ # Running our files through yamllint, after we reformatted them,
+ # should not yield any problems.
+ config = ansiblelint.yaml_utils.load_yamllint_config()
+ assert not list(run_yamllint(after_content, config))
+
+
+@pytest.fixture(name="lintable")
+def fixture_lintable(file_path: str) -> Lintable:
+ """Return a playbook Lintable for use in ``get_path_to_*`` tests."""
+ return Lintable(file_path)
+
+
+@pytest.fixture(name="ruamel_data")
+def fixture_ruamel_data(lintable: Lintable) -> CommentedMap | CommentedSeq:
+ """Return the loaded YAML data for the Lintable."""
+ yaml = ansiblelint.yaml_utils.FormattedYAML()
+ data: CommentedMap | CommentedSeq = yaml.loads(lintable.content)
+ return data
+
+
+@pytest.mark.parametrize(
+ ("file_path", "line_number", "expected_path"),
+ (
+ # ignored lintables
+ pytest.param(
+ "examples/playbooks/tasks/passing_task.yml", 2, [], id="ignore_tasks_file"
+ ),
+ pytest.param(
+ "examples/roles/more_complex/handlers/main.yml",
+ 2,
+ [],
+ id="ignore_handlers_file",
+ ),
+ pytest.param("examples/playbooks/vars/other.yml", 2, [], id="ignore_vars_file"),
+ pytest.param(
+ "examples/host_vars/localhost.yml", 2, [], id="ignore_host_vars_file"
+ ),
+ pytest.param("examples/group_vars/all.yml", 2, [], id="ignore_group_vars_file"),
+ pytest.param(
+ "examples/inventory/inventory.yml", 2, [], id="ignore_inventory_file"
+ ),
+ pytest.param(
+ "examples/roles/dependency_in_meta/meta/main.yml",
+ 2,
+ [],
+ id="ignore_meta_file",
+ ),
+ pytest.param(
+ "examples/reqs_v1/requirements.yml", 2, [], id="ignore_requirements_v1_file"
+ ),
+ pytest.param(
+ "examples/reqs_v2/requirements.yml", 2, [], id="ignore_requirements_v2_file"
+ ),
+ # we don't have any release notes examples. Oh well.
+ # pytest.param("examples/", 2, [], id="ignore_release_notes_file"),
+ pytest.param(
+ ".pre-commit-config.yaml", 2, [], id="ignore_unrecognized_yaml_file"
+ ),
+ # playbook lintables
+ pytest.param(
+ "examples/playbooks/become.yml",
+ 1,
+ [],
+ id="1_play_playbook-line_before_play",
+ ),
+ pytest.param(
+ "examples/playbooks/become.yml",
+ 2,
+ [0],
+ id="1_play_playbook-first_line_in_play",
+ ),
+ pytest.param(
+ "examples/playbooks/become.yml",
+ 10,
+ [0],
+ id="1_play_playbook-middle_line_in_play",
+ ),
+ pytest.param(
+ "examples/playbooks/become.yml",
+ 100,
+ [0],
+ id="1_play_playbook-line_after_eof",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 1,
+ [],
+ id="4_play_playbook-line_before_play_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 2,
+ [0],
+ id="4_play_playbook-first_line_in_play_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 5,
+ [0],
+ id="4_play_playbook-middle_line_in_play_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 9,
+ [0],
+ id="4_play_playbook-last_line_in_play_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 10,
+ [1],
+ id="4_play_playbook-first_line_in_play_2",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 14,
+ [1],
+ id="4_play_playbook-middle_line_in_play_2",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 18,
+ [1],
+ id="4_play_playbook-last_line_in_play_2",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 19,
+ [2],
+ id="4_play_playbook-first_line_in_play_3",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 23,
+ [2],
+ id="4_play_playbook-middle_line_in_play_3",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 27,
+ [2],
+ id="4_play_playbook-last_line_in_play_3",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 28,
+ [3],
+ id="4_play_playbook-first_line_in_play_4",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 31,
+ [3],
+ id="4_play_playbook-middle_line_in_play_4",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 35,
+ [3],
+ id="4_play_playbook-last_line_in_play_4",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 100,
+ [3],
+ id="4_play_playbook-line_after_eof",
+ ),
+ pytest.param(
+ "examples/playbooks/playbook-parent.yml",
+ 1,
+ [],
+ id="import_playbook-line_before_play_1",
+ ),
+ pytest.param(
+ "examples/playbooks/playbook-parent.yml",
+ 2,
+ [0],
+ id="import_playbook-first_line_in_play_1",
+ ),
+ pytest.param(
+ "examples/playbooks/playbook-parent.yml",
+ 3,
+ [0],
+ id="import_playbook-middle_line_in_play_1",
+ ),
+ pytest.param(
+ "examples/playbooks/playbook-parent.yml",
+ 4,
+ [0],
+ id="import_playbook-last_line_in_play_1",
+ ),
+ pytest.param(
+ "examples/playbooks/playbook-parent.yml",
+ 5,
+ [1],
+ id="import_playbook-first_line_in_play_2",
+ ),
+ pytest.param(
+ "examples/playbooks/playbook-parent.yml",
+ 6,
+ [1],
+ id="import_playbook-middle_line_in_play_2",
+ ),
+ pytest.param(
+ "examples/playbooks/playbook-parent.yml",
+ 7,
+ [1],
+ id="import_playbook-last_line_in_play_2",
+ ),
+ pytest.param(
+ "examples/playbooks/playbook-parent.yml",
+ 8,
+ [2],
+ id="import_playbook-first_line_in_play_3",
+ ),
+ pytest.param(
+ "examples/playbooks/playbook-parent.yml",
+ 9,
+ [2],
+ id="import_playbook-last_line_in_play_3",
+ ),
+ pytest.param(
+ "examples/playbooks/playbook-parent.yml",
+ 15,
+ [2],
+ id="import_playbook-line_after_eof",
+ ),
+ ),
+)
+def test_get_path_to_play(
+ lintable: Lintable,
+ line_number: int,
+ ruamel_data: CommentedMap | CommentedSeq,
+ expected_path: list[int | str],
+) -> None:
+ """Ensure ``get_path_to_play`` returns the expected path given a file + line."""
+ path_to_play = ansiblelint.yaml_utils.get_path_to_play(
+ lintable, line_number, ruamel_data
+ )
+ assert path_to_play == expected_path
+
+
+@pytest.mark.parametrize(
+ ("file_path", "line_number", "expected_path"),
+ (
+ # ignored lintables
+ pytest.param("examples/playbooks/vars/other.yml", 2, [], id="ignore_vars_file"),
+ pytest.param(
+ "examples/host_vars/localhost.yml", 2, [], id="ignore_host_vars_file"
+ ),
+ pytest.param("examples/group_vars/all.yml", 2, [], id="ignore_group_vars_file"),
+ pytest.param(
+ "examples/inventory/inventory.yml", 2, [], id="ignore_inventory_file"
+ ),
+ pytest.param(
+ "examples/roles/dependency_in_meta/meta/main.yml",
+ 2,
+ [],
+ id="ignore_meta_file",
+ ),
+ pytest.param(
+ "examples/reqs_v1/requirements.yml", 2, [], id="ignore_requirements_v1_file"
+ ),
+ pytest.param(
+ "examples/reqs_v2/requirements.yml", 2, [], id="ignore_requirements_v2_file"
+ ),
+ # we don't have any release notes examples. Oh well.
+ # pytest.param("examples/", 2, [], id="ignore_release_notes_file"),
+ pytest.param(
+ ".pre-commit-config.yaml", 2, [], id="ignore_unrecognized_yaml_file"
+ ),
+ # tasks-containing lintables
+ pytest.param(
+ "examples/playbooks/become.yml",
+ 4,
+ [],
+ id="1_task_playbook-line_before_tasks",
+ ),
+ pytest.param(
+ "examples/playbooks/become.yml",
+ 5,
+ [0, "tasks", 0],
+ id="1_task_playbook-first_line_in_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/become.yml",
+ 10,
+ [0, "tasks", 0],
+ id="1_task_playbook-middle_line_in_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/become.yml",
+ 15,
+ [0, "tasks", 0],
+ id="1_task_playbook-last_line_in_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/become.yml",
+ 100,
+ [0, "tasks", 0],
+ id="1_task_playbook-line_after_eof_without_anything_after_task",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 1,
+ [],
+ id="4_play_playbook-play_1_line_before_tasks",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 7,
+ [0, "tasks", 0],
+ id="4_play_playbook-play_1_first_line_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 9,
+ [0, "tasks", 0],
+ id="4_play_playbook-play_1_last_line_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 10,
+ [],
+ id="4_play_playbook-play_2_line_before_tasks",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 12,
+ [],
+ id="4_play_playbook-play_2_line_before_tasks",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 13,
+ [1, "tasks", 0],
+ id="4_play_playbook-play_2_first_line_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 18,
+ [1, "tasks", 0],
+ id="4_play_playbook-play_2_middle_line_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 18,
+ [1, "tasks", 0],
+ id="4_play_playbook-play_2_last_line_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 19,
+ [],
+ id="4_play_playbook-play_3_line_before_tasks",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 22,
+ [],
+ id="4_play_playbook-play_3_line_before_tasks",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 23,
+ [2, "tasks", 0],
+ id="4_play_playbook-play_3_first_line_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 25,
+ [2, "tasks", 0],
+ id="4_play_playbook-play_3_middle_line_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 27,
+ [2, "tasks", 0],
+ id="4_play_playbook-play_3_last_line_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 28,
+ [],
+ id="4_play_playbook-play_4_line_before_tasks",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 31,
+ [],
+ id="4_play_playbook-play_4_line_before_tasks",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 32,
+ [3, "tasks", 0],
+ id="4_play_playbook-play_4_first_line_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 33,
+ [3, "tasks", 0],
+ id="4_play_playbook-play_4_middle_line_task_1",
+ ),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 35,
+ [3, "tasks", 0],
+ id="4_play_playbook-play_4_last_line_task_1",
+ ),
+ # playbook with multiple tasks + tasks blocks in a play
+ pytest.param(
+ # must have at least one key after one of the tasks blocks
+ "examples/playbooks/include.yml",
+ 6,
+ [0, "pre_tasks", 0],
+ id="playbook-multi_tasks_blocks-pre_tasks_last_task_before_roles",
+ ),
+ pytest.param(
+ "examples/playbooks/include.yml",
+ 7,
+ [],
+ id="playbook-multi_tasks_blocks-roles_after_pre_tasks",
+ ),
+ pytest.param(
+ "examples/playbooks/include.yml",
+ 10,
+ [],
+ id="playbook-multi_tasks_blocks-roles_before_tasks",
+ ),
+ pytest.param(
+ "examples/playbooks/include.yml",
+ 12,
+ [0, "tasks", 0],
+ id="playbook-multi_tasks_blocks-tasks_first_task",
+ ),
+ pytest.param(
+ "examples/playbooks/include.yml",
+ 14,
+ [0, "tasks", 1],
+ id="playbook-multi_tasks_blocks-tasks_last_task_before_handlers",
+ ),
+ pytest.param(
+ "examples/playbooks/include.yml",
+ 16,
+ [0, "handlers", 0],
+ id="playbook-multi_tasks_blocks-handlers_task",
+ ),
+ # playbook with subtasks blocks
+ pytest.param(
+ "examples/playbooks/blockincludes.yml",
+ 14,
+ [0, "tasks", 0, "block", 1, "block", 0],
+ id="playbook-deeply_nested_task",
+ ),
+ pytest.param(
+ "examples/playbooks/block.yml",
+ 12,
+ [0, "tasks", 0, "block", 1],
+ id="playbook-subtasks-block_task_2",
+ ),
+ pytest.param(
+ "examples/playbooks/block.yml",
+ 22,
+ [0, "tasks", 0, "rescue", 2],
+ id="playbook-subtasks-rescue_task_3",
+ ),
+ pytest.param(
+ "examples/playbooks/block.yml",
+ 25,
+ [0, "tasks", 0, "always", 0],
+ id="playbook-subtasks-always_task_3",
+ ),
+ # tasks files
+ pytest.param("examples/playbooks/tasks/x.yml", 2, [0], id="tasks-null_task"),
+ pytest.param(
+ "examples/playbooks/tasks/x.yml", 6, [1], id="tasks-null_task_next"
+ ),
+ pytest.param(
+ "examples/playbooks/tasks/empty_blocks.yml",
+ 7,
+ [0], # this IS part of the first task and "rescue" does not have subtasks.
+ id="tasks-null_rescue",
+ ),
+ pytest.param(
+ "examples/playbooks/tasks/empty_blocks.yml",
+ 8,
+ [0], # this IS part of the first task and "always" does not have subtasks.
+ id="tasks-empty_always",
+ ),
+ pytest.param(
+ "examples/playbooks/tasks/empty_blocks.yml",
+ 16,
+ [1, "always", 0],
+ id="tasks-task_beyond_empty_blocks",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/tasks/main.yml",
+ 1,
+ [],
+ id="tasks-line_before_tasks",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/tasks/main.yml",
+ 2,
+ [0],
+ id="tasks-first_line_in_task_1",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/tasks/main.yml",
+ 3,
+ [0],
+ id="tasks-middle_line_in_task_1",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/tasks/main.yml",
+ 4,
+ [0],
+ id="tasks-last_line_in_task_1",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/tasks/main.yml",
+ 5,
+ [1],
+ id="tasks-first_line_in_task_2",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/tasks/main.yml",
+ 6,
+ [1],
+ id="tasks-middle_line_in_task_2",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/tasks/main.yml",
+ 7,
+ [1],
+ id="tasks-last_line_in_task_2",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/tasks/main.yml",
+ 8,
+ [2],
+ id="tasks-first_line_in_task_3",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/tasks/main.yml",
+ 9,
+ [2],
+ id="tasks-last_line_in_task_3",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/tasks/main.yml",
+ 100,
+ [2],
+ id="tasks-line_after_eof",
+ ),
+ # handlers
+ pytest.param(
+ "examples/roles/more_complex/handlers/main.yml",
+ 1,
+ [],
+ id="handlers-line_before_tasks",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/handlers/main.yml",
+ 2,
+ [0],
+ id="handlers-first_line_in_task_1",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/handlers/main.yml",
+ 3,
+ [0],
+ id="handlers-last_line_in_task_1",
+ ),
+ pytest.param(
+ "examples/roles/more_complex/handlers/main.yml",
+ 100,
+ [0],
+ id="handlers-line_after_eof",
+ ),
+ ),
+)
+def test_get_path_to_task(
+ lintable: Lintable,
+ line_number: int,
+ ruamel_data: CommentedMap | CommentedSeq,
+ expected_path: list[int | str],
+) -> None:
+ """Ensure ``get_task_to_play`` returns the expected path given a file + line."""
+ path_to_task = ansiblelint.yaml_utils.get_path_to_task(
+ lintable, line_number, ruamel_data
+ )
+ assert path_to_task == expected_path
+
+
+@pytest.mark.parametrize(
+ ("file_path", "line_number"),
+ (
+ pytest.param("examples/playbooks/become.yml", 0, id="1_play_playbook"),
+ pytest.param(
+ "examples/playbooks/rule-partial-become-without-become-pass.yml",
+ 0,
+ id="4_play_playbook",
+ ),
+ pytest.param("examples/playbooks/playbook-parent.yml", 0, id="import_playbook"),
+ pytest.param("examples/playbooks/become.yml", 0, id="1_task_playbook"),
+ ),
+)
+def test_get_path_to_play_raises_value_error_for_bad_line_number(
+ lintable: Lintable,
+ line_number: int,
+ ruamel_data: CommentedMap | CommentedSeq,
+) -> None:
+ """Ensure ``get_path_to_play`` raises ValueError for line_number < 1."""
+ with pytest.raises(
+ ValueError, match=f"expected line_number >= 1, got {line_number}"
+ ):
+ ansiblelint.yaml_utils.get_path_to_play(lintable, line_number, ruamel_data)
+
+
+@pytest.mark.parametrize(
+ ("file_path", "line_number"),
+ (pytest.param("examples/roles/more_complex/tasks/main.yml", 0, id="tasks"),),
+)
+def test_get_path_to_task_raises_value_error_for_bad_line_number(
+ lintable: Lintable,
+ line_number: int,
+ ruamel_data: CommentedMap | CommentedSeq,
+) -> None:
+ """Ensure ``get_task_to_play`` raises ValueError for line_number < 1."""
+ with pytest.raises(
+ ValueError, match=f"expected line_number >= 1, got {line_number}"
+ ):
+ ansiblelint.yaml_utils.get_path_to_task(lintable, line_number, ruamel_data)
+
+
+@pytest.mark.parametrize(
+ ("before", "after"),
+ (
+ pytest.param(None, None, id="1"),
+ pytest.param(1, 1, id="2"),
+ pytest.param({}, {}, id="3"),
+ pytest.param({"__file__": 1}, {}, id="simple"),
+ pytest.param({"foo": {"__file__": 1}}, {"foo": {}}, id="nested"),
+ pytest.param([{"foo": {"__file__": 1}}], [{"foo": {}}], id="nested-in-lint"),
+ pytest.param({"foo": [{"__file__": 1}]}, {"foo": [{}]}, id="nested-in-lint"),
+ ),
+)
+def test_deannotate(
+ before: Any,
+ after: Any,
+) -> None:
+ """Ensure deannotate works as intended."""
+ assert ansiblelint.yaml_utils.deannotate(before) == after