summaryrefslogtreecommitdiffstats
path: root/test/test_transformer.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--test/test_transformer.py562
1 files changed, 511 insertions, 51 deletions
diff --git a/test/test_transformer.py b/test/test_transformer.py
index 78dd121..51e97d5 100644
--- a/test/test_transformer.py
+++ b/test/test_transformer.py
@@ -1,125 +1,247 @@
+# cspell:ignore classinfo
"""Tests for Transformer."""
+
from __future__ import annotations
+import builtins
import os
import shutil
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
+from unittest import mock
import pytest
+import ansiblelint.__main__ as main
+from ansiblelint.app import App
+from ansiblelint.file_utils import Lintable
+from ansiblelint.rules import TransformMixin
+
# noinspection PyProtectedMember
-from ansiblelint.runner import LintResult, _get_matches
+from ansiblelint.runner import LintResult, get_matches
from ansiblelint.transformer import Transformer
if TYPE_CHECKING:
- from argparse import Namespace
- from collections.abc import Iterator
-
from ansiblelint.config import Options
+ from ansiblelint.errors import MatchError
from ansiblelint.rules import RulesCollection
-@pytest.fixture(name="copy_examples_dir")
-def fixture_copy_examples_dir(
- tmp_path: Path,
- config_options: Namespace,
-) -> Iterator[tuple[Path, Path]]:
- """Fixture that copies the examples/ dir into a tmpdir."""
- examples_dir = Path("examples")
-
- shutil.copytree(examples_dir, tmp_path / "examples")
- old_cwd = Path.cwd()
- try:
- os.chdir(tmp_path)
- config_options.cwd = tmp_path
- yield old_cwd, tmp_path
- finally:
- os.chdir(old_cwd)
-
-
@pytest.fixture(name="runner_result")
def fixture_runner_result(
config_options: Options,
default_rules_collection: RulesCollection,
- playbook: str,
+ playbook_str: str,
+ monkeypatch: pytest.MonkeyPatch,
) -> LintResult:
"""Fixture that runs the Runner to populate a LintResult for a given file."""
- config_options.lintables = [playbook]
- result = _get_matches(rules=default_rules_collection, options=config_options)
+ # needed for testing transformer when roles/modules are missing:
+ monkeypatch.setenv("ANSIBLE_LINT_NODEPS", "1")
+ config_options.lintables = [playbook_str]
+ result = get_matches(rules=default_rules_collection, options=config_options)
return result
@pytest.mark.parametrize(
- ("playbook", "matches_count", "transformed"),
+ ("playbook_str", "matches_count", "transformed", "is_owned_by_ansible"),
(
# reuse TestRunner::test_runner test cases to ensure transformer does not mangle matches
pytest.param(
"examples/playbooks/nomatchestest.yml",
0,
False,
+ True,
id="nomatchestest",
),
- pytest.param("examples/playbooks/unicode.yml", 1, False, id="unicode"),
+ pytest.param("examples/playbooks/unicode.yml", 1, False, True, id="unicode"),
pytest.param(
"examples/playbooks/lots_of_warnings.yml",
- 992,
+ 993,
False,
+ True,
id="lots_of_warnings",
),
- pytest.param("examples/playbooks/become.yml", 0, False, id="become"),
+ pytest.param("examples/playbooks/become.yml", 0, False, True, id="become"),
pytest.param(
"examples/playbooks/contains_secrets.yml",
0,
False,
+ True,
id="contains_secrets",
),
pytest.param(
"examples/playbooks/vars/empty_vars.yml",
0,
False,
+ True,
id="empty_vars",
),
- pytest.param("examples/playbooks/vars/strings.yml", 0, True, id="strings"),
- pytest.param("examples/playbooks/vars/empty.yml", 1, False, id="empty"),
- pytest.param("examples/playbooks/name-case.yml", 1, True, id="name_case"),
- pytest.param("examples/playbooks/fqcn.yml", 3, True, id="fqcn"),
+ pytest.param(
+ "examples/playbooks/vars/strings.yml",
+ 0,
+ True,
+ True,
+ id="strings",
+ ),
+ pytest.param("examples/playbooks/vars/empty.yml", 1, False, True, id="empty"),
+ pytest.param("examples/playbooks/fqcn.yml", 3, True, True, id="fqcn"),
+ pytest.param(
+ "examples/playbooks/multi_yaml_doc.yml",
+ 1,
+ False,
+ True,
+ id="multi_yaml_doc",
+ ),
+ pytest.param(
+ "examples/playbooks/transform_command_instead_of_shell.yml",
+ 3,
+ True,
+ True,
+ id="cmd_instead_of_shell",
+ ),
+ pytest.param(
+ "examples/playbooks/transform-deprecated-local-action.yml",
+ 1,
+ True,
+ True,
+ id="dep_local_action",
+ ),
+ pytest.param(
+ "examples/playbooks/transform-block-indentation-indicator.yml",
+ 0,
+ True,
+ True,
+ id="multiline_msg_with_indent_indicator",
+ ),
+ pytest.param(
+ "examples/playbooks/transform-jinja.yml",
+ 7,
+ True,
+ True,
+ id="jinja_spacing",
+ ),
+ pytest.param(
+ "examples/playbooks/transform-no-jinja-when.yml",
+ 3,
+ True,
+ True,
+ id="no_jinja_when",
+ ),
+ pytest.param(
+ "examples/playbooks/vars/transform_nested_data.yml",
+ 3,
+ True,
+ True,
+ id="nested",
+ ),
+ pytest.param(
+ "examples/playbooks/transform-key-order.yml",
+ 6,
+ True,
+ True,
+ id="key_order_transform",
+ ),
+ pytest.param(
+ "examples/playbooks/transform-no-free-form.yml",
+ 5,
+ True,
+ True,
+ id="no_free_form_transform",
+ ),
+ pytest.param(
+ "examples/playbooks/transform-partial-become.yml",
+ 4,
+ True,
+ True,
+ id="partial_become",
+ ),
+ pytest.param(
+ "examples/playbooks/transform-key-order-play.yml",
+ 1,
+ True,
+ True,
+ id="key_order_play_transform",
+ ),
+ pytest.param(
+ "examples/playbooks/transform-key-order-block.yml",
+ 1,
+ True,
+ True,
+ id="key_order_block_transform",
+ ),
+ pytest.param(
+ "examples/.github/workflows/sample.yml",
+ 0,
+ False,
+ False,
+ id="github-workflow",
+ ),
+ pytest.param(
+ "examples/playbooks/invalid-transform.yml",
+ 1,
+ False,
+ True,
+ id="invalid_transform",
+ ),
+ pytest.param(
+ "examples/roles/name_prefix/tasks/test.yml",
+ 1,
+ True,
+ True,
+ id="name_casing_prefix",
+ ),
+ pytest.param(
+ "examples/roles/name_casing/tasks/main.yml",
+ 2,
+ True,
+ True,
+ id="name_case_roles",
+ ),
+ pytest.param(
+ "examples/playbooks/4114/transform-with-missing-role-and-modules.yml",
+ 1,
+ True,
+ True,
+ id="4114",
+ ),
),
)
-def test_transformer( # pylint: disable=too-many-arguments, too-many-locals
+@mock.patch.dict(os.environ, {"ANSIBLE_LINT_WRITE_TMP": "1"}, clear=True)
+def test_transformer( # pylint: disable=too-many-arguments
config_options: Options,
- copy_examples_dir: tuple[Path, Path],
- playbook: str,
+ playbook_str: str,
runner_result: LintResult,
transformed: bool,
+ is_owned_by_ansible: bool,
matches_count: int,
) -> None:
"""Test that transformer can go through any corner cases.
Based on TestRunner::test_runner
"""
+ # test ability to detect is_owned_by_ansible
+ assert Lintable(playbook_str).is_owned_by_ansible() == is_owned_by_ansible
+ playbook = Path(playbook_str)
config_options.write_list = ["all"]
- transformer = Transformer(result=runner_result, options=config_options)
- transformer.run()
matches = runner_result.matches
assert len(matches) == matches_count
- orig_dir, tmp_dir = copy_examples_dir
- orig_playbook = orig_dir / playbook
- expected_playbook = orig_dir / playbook.replace(".yml", ".transformed.yml")
- transformed_playbook = tmp_dir / playbook
-
- orig_playbook_content = orig_playbook.read_text()
- expected_playbook_content = expected_playbook.read_text()
- transformed_playbook_content = transformed_playbook.read_text()
+ transformer = Transformer(result=runner_result, options=config_options)
+ transformer.run()
+ orig_content = playbook.read_text(encoding="utf-8")
if transformed:
- assert orig_playbook_content != transformed_playbook_content
- else:
- assert orig_playbook_content == transformed_playbook_content
+ expected_content = playbook.with_suffix(
+ f".transformed{playbook.suffix}",
+ ).read_text(encoding="utf-8")
+ transformed_content = playbook.with_suffix(f".tmp{playbook.suffix}").read_text(
+ encoding="utf-8",
+ )
- assert transformed_playbook_content == expected_playbook_content
+ assert orig_content != transformed_content
+ assert expected_content == transformed_content
+ playbook.with_suffix(f".tmp{playbook.suffix}").unlink()
@pytest.mark.parametrize(
@@ -173,3 +295,341 @@ def test_effective_write_set(write_list: list[str], expected: set[str]) -> None:
"""Make sure effective_write_set handles all/none keywords correctly."""
actual = Transformer.effective_write_set(write_list)
assert actual == expected
+
+
+def test_pruned_err_after_fix(monkeypatch: pytest.MonkeyPatch, tmpdir: Path) -> None:
+ """Test that pruned errors are not reported after fixing.
+
+ :param monkeypatch: Monkeypatch
+ :param tmpdir: Temporary directory
+ """
+ file = Path("examples/playbooks/transform-jinja.yml")
+ source = Path.cwd() / file
+ dest = tmpdir / source.name
+ shutil.copyfile(source, dest)
+
+ monkeypatch.setattr("sys.argv", ["ansible-lint", str(dest), "--fix=all"])
+
+ fix_called = False
+ orig_fix = main.fix
+
+ def test_fix(
+ runtime_options: Options,
+ result: LintResult,
+ rules: RulesCollection,
+ ) -> None:
+ """Wrap main.fix to check if it was called and match count is correct.
+
+ :param runtime_options: Runtime options
+ :param result: Lint result
+ :param rules: Rules collection
+ """
+ nonlocal fix_called
+ fix_called = True
+ assert len(result.matches) == 7
+ orig_fix(runtime_options, result, rules)
+
+ report_called = False
+
+ class TestApp(App):
+ """Wrap App to check if it was called and match count is correct."""
+
+ def report_outcome(
+ self: TestApp,
+ result: LintResult,
+ *,
+ mark_as_success: bool = False,
+ ) -> int:
+ """Wrap App.report_outcome to check if it was called and match count is correct.
+
+ :param result: Lint result
+ :param mark_as_success: Mark as success
+ :returns: Exit code
+ """
+ nonlocal report_called
+ report_called = True
+ assert len(result.matches) == 1
+ return super().report_outcome(result, mark_as_success=mark_as_success)
+
+ monkeypatch.setattr("ansiblelint.__main__.fix", test_fix)
+ monkeypatch.setattr("ansiblelint.app.App", TestApp)
+
+ main.main()
+ assert fix_called
+ assert report_called
+
+
+class TransformTests:
+ """A carrier for some common test constants."""
+
+ FILE_NAME = "examples/playbooks/transform-no-free-form.yml"
+ FILE_TYPE = "playbook"
+ LINENO = 5
+ ID = "no-free-form"
+ MATCH_TYPE = "task"
+ VERSION_PART = "version=(1, 1)"
+
+ @classmethod
+ def match_id(cls) -> str:
+ """Generate a match id.
+
+ :returns: Match id string
+ """
+ return f"{cls.ID}/{cls.MATCH_TYPE} {cls.FILE_NAME}:{cls.LINENO}"
+
+ @classmethod
+ def rewrite_part(cls) -> str:
+ """Generate a rewrite part.
+
+ :returns: Rewrite part string
+ """
+ return f"{cls.FILE_NAME} ({cls.FILE_TYPE}), {cls.VERSION_PART}"
+
+
+@pytest.fixture(name="test_result")
+def fixture_test_result(
+ config_options: Options,
+ default_rules_collection: RulesCollection,
+) -> tuple[LintResult, Options]:
+ """Fixture that runs the Runner to populate a LintResult for a given file.
+
+ The results are confirmed and a limited to a single match.
+
+ :param config_options: Configuration options
+ :param default_rules_collection: Default rules collection
+ :returns: Tuple of LintResult and Options
+ """
+ config_options.write_list = [TransformTests.ID]
+ config_options.lintables = [TransformTests.FILE_NAME]
+
+ result = get_matches(rules=default_rules_collection, options=config_options)
+ match = result.matches[0]
+
+ def write(*_args: Any, **_kwargs: Any) -> None:
+ """Don't rewrite the test fixture.
+
+ :param _args: Arguments
+ :param _kwargs: Keyword arguments
+ """
+
+ setattr(match.lintable, "write", write) # noqa: B010
+
+ assert match.rule.id == TransformTests.ID
+ assert match.filename == TransformTests.FILE_NAME
+ assert match.lineno == TransformTests.LINENO
+ assert match.match_type == TransformTests.MATCH_TYPE
+ result.matches = [match]
+
+ return result, config_options
+
+
+def test_transform_na(
+ caplog: pytest.LogCaptureFixture,
+ monkeypatch: pytest.MonkeyPatch,
+ test_result: tuple[LintResult, Options],
+) -> None:
+ """Test the transformer is not available.
+
+ :param caplog: Log capture fixture
+ :param monkeypatch: Monkeypatch
+ :param test_result: Test result fixture
+ """
+ result = test_result[0]
+ options = test_result[1]
+
+ _isinstance = builtins.isinstance
+ called = False
+
+ def mp_isinstance(t_object: Any, classinfo: type) -> bool:
+ if classinfo is TransformMixin:
+ nonlocal called
+ called = True
+ return False
+ return _isinstance(t_object, classinfo)
+
+ monkeypatch.setattr(builtins, "isinstance", mp_isinstance)
+
+ transformer = Transformer(result=result, options=options)
+ with caplog.at_level(10):
+ transformer.run()
+
+ assert called
+ logs = [record for record in caplog.records if record.module == "transformer"]
+ assert len(logs) == 2
+
+ log_0 = f"{transformer.FIX_NA_MSG} {TransformTests.match_id()}"
+ assert logs[0].message == log_0
+ assert logs[0].levelname == "DEBUG"
+
+ log_1 = f"{transformer.DUMP_MSG} {TransformTests.rewrite_part()}"
+ assert logs[1].message == log_1
+ assert logs[1].levelname == "DEBUG"
+
+
+def test_transform_no_tb(
+ caplog: pytest.LogCaptureFixture,
+ test_result: tuple[LintResult, Options],
+) -> None:
+ """Test the transformer does not traceback.
+
+ :param caplog: Log capture fixture
+ :param test_result: Test result fixture
+ :raises RuntimeError: If the rule is not a TransformMixin
+ """
+ result = test_result[0]
+ options = test_result[1]
+ exception_msg = "FixFailure"
+
+ def transform(*_args: Any, **_kwargs: Any) -> None:
+ """Raise an exception for the transform call.
+
+ :raises RuntimeError: Always
+ """
+ raise RuntimeError(exception_msg)
+
+ if isinstance(result.matches[0].rule, TransformMixin):
+ setattr(result.matches[0].rule, "transform", transform) # noqa: B010
+ else:
+ err = "Rule is not a TransformMixin"
+ raise TypeError(err)
+
+ transformer = Transformer(result=result, options=options)
+ with caplog.at_level(10):
+ transformer.run()
+
+ logs = [record for record in caplog.records if record.module == "transformer"]
+ assert len(logs) == 5
+
+ log_0 = f"{transformer.FIX_APPLY_MSG} {TransformTests.match_id()}"
+ assert logs[0].message == log_0
+ assert logs[0].levelname == "DEBUG"
+
+ log_1 = f"{transformer.FIX_FAILED_MSG} {TransformTests.match_id()}"
+ assert logs[1].message == log_1
+ assert logs[1].levelname == "ERROR"
+
+ log_2 = exception_msg
+ assert logs[2].message == log_2
+ assert logs[2].levelname == "ERROR"
+
+ log_3 = f"{transformer.FIX_ISSUE_MSG}"
+ assert logs[3].message == log_3
+ assert logs[3].levelname == "ERROR"
+
+ log_4 = f"{transformer.DUMP_MSG} {TransformTests.rewrite_part()}"
+ assert logs[4].message == log_4
+ assert logs[4].levelname == "DEBUG"
+
+
+def test_transform_applied(
+ caplog: pytest.LogCaptureFixture,
+ test_result: tuple[LintResult, Options],
+) -> None:
+ """Test the transformer is applied.
+
+ :param caplog: Log capture fixture
+ :param test_result: Test result fixture
+ """
+ result = test_result[0]
+ options = test_result[1]
+
+ transformer = Transformer(result=result, options=options)
+ with caplog.at_level(10):
+ transformer.run()
+
+ logs = [record for record in caplog.records if record.module == "transformer"]
+ assert len(logs) == 3
+
+ log_0 = f"{transformer.FIX_APPLY_MSG} {TransformTests.match_id()}"
+ assert logs[0].message == log_0
+ assert logs[0].levelname == "DEBUG"
+
+ log_1 = f"{transformer.FIX_APPLIED_MSG} {TransformTests.match_id()}"
+ assert logs[1].message == log_1
+ assert logs[1].levelname == "DEBUG"
+
+ log_2 = f"{transformer.DUMP_MSG} {TransformTests.rewrite_part()}"
+ assert logs[2].message == log_2
+ assert logs[2].levelname == "DEBUG"
+
+
+def test_transform_not_enabled(
+ caplog: pytest.LogCaptureFixture,
+ test_result: tuple[LintResult, Options],
+) -> None:
+ """Test the transformer is not enabled.
+
+ :param caplog: Log capture fixture
+ :param test_result: Test result fixture
+ """
+ result = test_result[0]
+ options = test_result[1]
+ options.write_list = []
+
+ transformer = Transformer(result=result, options=options)
+ with caplog.at_level(10):
+ transformer.run()
+
+ logs = [record for record in caplog.records if record.module == "transformer"]
+ assert len(logs) == 2
+
+ log_0 = f"{transformer.FIX_NE_MSG} {TransformTests.match_id()}"
+ assert logs[0].message == log_0
+ assert logs[0].levelname == "DEBUG"
+
+ log_1 = f"{transformer.DUMP_MSG} {TransformTests.rewrite_part()}"
+ assert logs[1].message == log_1
+ assert logs[1].levelname == "DEBUG"
+
+
+def test_transform_not_applied(
+ caplog: pytest.LogCaptureFixture,
+ test_result: tuple[LintResult, Options],
+) -> None:
+ """Test the transformer is not applied.
+
+ :param caplog: Log capture fixture
+ :param test_result: Test result fixture
+ :raises RuntimeError: If the rule is not a TransformMixin
+ """
+ result = test_result[0]
+ options = test_result[1]
+
+ called = False
+
+ def transform(match: MatchError, *_args: Any, **_kwargs: Any) -> None:
+ """Do not apply the transform.
+
+ :param match: Match object
+ :param _args: Arguments
+ :param _kwargs: Keyword arguments
+ """
+ nonlocal called
+ called = True
+ match.fixed = False
+
+ if isinstance(result.matches[0].rule, TransformMixin):
+ setattr(result.matches[0].rule, "transform", transform) # noqa: B010
+ else:
+ err = "Rule is not a TransformMixin"
+ raise TypeError(err)
+
+ transformer = Transformer(result=result, options=options)
+ with caplog.at_level(10):
+ transformer.run()
+
+ assert called
+ logs = [record for record in caplog.records if record.module == "transformer"]
+ assert len(logs) == 3
+
+ log_0 = f"{transformer.FIX_APPLY_MSG} {TransformTests.match_id()}"
+ assert logs[0].message == log_0
+ assert logs[0].levelname == "DEBUG"
+
+ log_1 = f"{transformer.FIX_NOT_APPLIED_MSG} {TransformTests.match_id()}"
+ assert logs[1].message == log_1
+ assert logs[1].levelname == "ERROR"
+
+ log_2 = f"{transformer.DUMP_MSG} {TransformTests.rewrite_part()}"
+ assert logs[2].message == log_2
+ assert logs[2].levelname == "DEBUG"