"""Test utils for ansible-lint.""" from __future__ import annotations import os import shutil import subprocess import sys import tempfile from pathlib import Path from typing import TYPE_CHECKING, Any from ansiblelint.app import get_app if TYPE_CHECKING: # https://github.com/PyCQA/pylint/issues/3240 # pylint: disable=unsubscriptable-object CompletedProcess = subprocess.CompletedProcess[Any] from ansiblelint.errors import MatchError from ansiblelint.rules import RulesCollection else: CompletedProcess = subprocess.CompletedProcess # pylint: disable=wrong-import-position from ansiblelint.runner import Runner class RunFromText: """Use Runner on temp files created from testing text snippets.""" app = None def __init__(self, collection: RulesCollection) -> None: """Initialize a RunFromText instance with rules collection.""" # Emulate command line execution initialization as without it Ansible module # would be loaded with incomplete module/role/collection list. if not self.app: # pragma: no cover self.app = get_app(offline=True) self.collection = collection def _call_runner(self, path: Path) -> list[MatchError]: runner = Runner(path, rules=self.collection) return runner.run() def run(self, filename: Path) -> list[MatchError]: """Lints received filename.""" return self._call_runner(filename) def run_playbook( self, playbook_text: str, prefix: str = "playbook", ) -> list[MatchError]: """Lints received text as a playbook.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".yml", prefix=prefix) as fh: fh.write(playbook_text) fh.flush() results = self._call_runner(Path(fh.name)) return results def run_role_tasks_main( self, tasks_main_text: str, tmp_path: Path, ) -> list[MatchError]: """Lints received text as tasks.""" role_path = tmp_path tasks_path = role_path / "tasks" tasks_path.mkdir(parents=True, exist_ok=True) with (tasks_path / "main.yml").open("w", encoding="utf-8") as fh: fh.write(tasks_main_text) fh.flush() results = self._call_runner(role_path) shutil.rmtree(role_path) return results def run_role_meta_main( self, meta_main_text: str, temp_path: Path, ) -> list[MatchError]: """Lints received text as meta.""" role_path = temp_path meta_path = role_path / "meta" meta_path.mkdir(parents=True, exist_ok=True) with (meta_path / "main.yml").open("w", encoding="utf-8") as fh: fh.write(meta_main_text) fh.flush() results = self._call_runner(role_path) shutil.rmtree(role_path) return results def run_role_defaults_main( self, defaults_main_text: str, tmp_path: Path, ) -> list[MatchError]: """Lints received text as vars file in defaults.""" role_path = tmp_path defaults_path = role_path / "defaults" defaults_path.mkdir(parents=True, exist_ok=True) with (defaults_path / "main.yml").open("w", encoding="utf-8") as fh: fh.write(defaults_main_text) fh.flush() results = self._call_runner(role_path) shutil.rmtree(role_path) return results def run_ansible_lint( *argv: str | Path, cwd: Path | None = None, executable: str | None = None, env: dict[str, str] | None = None, offline: bool = True, ) -> CompletedProcess: """Run ansible-lint on a given path and returns its output.""" args = [str(item) for item in argv] if offline: # pragma: no cover args.insert(0, "--offline") if not executable: executable = sys.executable args = [sys.executable, "-m", "ansiblelint", *args] else: args = [executable, *args] # It is not safe to pass entire env for testing as other tests would # pollute the env, causing weird behaviors, so we pass only a safe list of # vars. safe_list = [ "COVERAGE_FILE", "COVERAGE_PROCESS_START", "HOME", "LANG", "LC_ALL", "LC_CTYPE", "NO_COLOR", "PATH", "PYTHONIOENCODING", "PYTHONPATH", "TERM", "VIRTUAL_ENV", ] _env = {} if env is None else env for v in safe_list: if v in os.environ and v not in _env: _env[v] = os.environ[v] return subprocess.run( args, capture_output=True, shell=False, # needed when command is a list check=False, cwd=cwd, env=_env, text=True, )