diff options
Diffstat (limited to 'test/test_yaml_utils.py')
-rw-r--r-- | test/test_yaml_utils.py | 955 |
1 files changed, 955 insertions, 0 deletions
diff --git a/test/test_yaml_utils.py b/test/test_yaml_utils.py new file mode 100644 index 0000000..5546e58 --- /dev/null +++ b/test/test_yaml_utils.py @@ -0,0 +1,955 @@ +"""Tests for yaml-related utility functions.""" +from __future__ import annotations + +from io import StringIO +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import pytest +from ruamel.yaml.main import YAML +from yamllint.linter import run as run_yamllint + +import ansiblelint.yaml_utils +from ansiblelint.file_utils import Lintable +from ansiblelint.utils import task_in_list + +if TYPE_CHECKING: + from ruamel.yaml.comments import CommentedMap, CommentedSeq + from ruamel.yaml.emitter import Emitter + +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_tasks_in_list_empty_file(empty_lintable: Lintable) -> None: + """Make sure that task_in_list returns early when files are empty.""" + assert empty_lintable.kind + assert empty_lintable.path + res = list( + task_in_list( + data=empty_lintable, + file=empty_lintable, + kind=empty_lintable.kind, + ), + ) + 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 + yaml.map_indent = map_indent + yaml.sequence_indent = sequence_indent + 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, # noqa: ARG001 +) -> 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: + # + # 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", "lineno", "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( + ".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, + lineno: 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, + lineno, + ruamel_data, + ) + assert path_to_play == expected_path + + +@pytest.mark.parametrize( + ("file_path", "lineno", "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( + ".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, + lineno: 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, + lineno, + ruamel_data, + ) + assert path_to_task == expected_path + + +@pytest.mark.parametrize( + ("file_path", "lineno"), + ( + 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_lineno( + lintable: Lintable, + lineno: int, + ruamel_data: CommentedMap | CommentedSeq, +) -> None: + """Ensure ``get_path_to_play`` raises ValueError for lineno < 1.""" + with pytest.raises( + ValueError, + match=f"expected lineno >= 1, got {lineno}", + ): + ansiblelint.yaml_utils.get_path_to_play(lintable, lineno, ruamel_data) + + +@pytest.mark.parametrize( + ("file_path", "lineno"), + (pytest.param("examples/roles/more_complex/tasks/main.yml", 0, id="tasks"),), +) +def test_get_path_to_task_raises_value_error_for_bad_lineno( + lintable: Lintable, + lineno: int, + ruamel_data: CommentedMap | CommentedSeq, +) -> None: + """Ensure ``get_task_to_play`` raises ValueError for lineno < 1.""" + with pytest.raises( + ValueError, + match=f"expected lineno >= 1, got {lineno}", + ): + ansiblelint.yaml_utils.get_path_to_task(lintable, lineno, 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 |