Files
devx/tests/unit/test_ansible_checks_shared.py
T

232 lines
9.4 KiB
Python

"""Unit tests for devx.tools.ansible_checks._shared."""
from pathlib import Path
import pytest
from devx.tools.ansible_checks._shared import (
DEFAULT_ANSIBLE_DIRS,
AnsibleFileFinder,
AnsibleYAMLParser,
ViolationReporter,
)
class TestAnsibleFileFinder:
def test_find_task_files_single_yaml(self, tmp_path: Path) -> None:
f = tmp_path / "test.yml"
f.write_text("tasks: []")
assert AnsibleFileFinder.find_task_files(f) == [f]
def test_find_task_files_single_non_yaml(self, tmp_path: Path) -> None:
f = tmp_path / "test.txt"
f.write_text("hello")
assert AnsibleFileFinder.find_task_files(f) == []
def test_find_task_files_dir(self, tmp_path: Path) -> None:
(tmp_path / "a.yml").write_text("tasks: []")
(tmp_path / "b.yaml").write_text("tasks: []")
(tmp_path / "c.txt").write_text("hello")
result = AnsibleFileFinder.find_task_files(tmp_path)
assert len(result) == 2
assert all(f.suffix in (".yml", ".yaml") for f in result)
def test_find_task_files_skip_molecule(self, tmp_path: Path) -> None:
(tmp_path / "a.yml").write_text("tasks: []")
mol = tmp_path / "molecule" / "default"
mol.mkdir(parents=True)
(mol / "main.yml").write_text("tasks: []")
result = AnsibleFileFinder.find_task_files(tmp_path, skip_molecule=True)
assert len(result) == 1
assert "molecule" not in result[0].parts
def test_find_task_files_include_molecule(self, tmp_path: Path) -> None:
(tmp_path / "a.yml").write_text("tasks: []")
mol = tmp_path / "molecule" / "default"
mol.mkdir(parents=True)
(mol / "main.yml").write_text("tasks: []")
result = AnsibleFileFinder.find_task_files(tmp_path, skip_molecule=False)
assert len(result) == 2
def test_find_task_files_nonexistent(self, tmp_path: Path) -> None:
assert AnsibleFileFinder.find_task_files(tmp_path / "nonexistent") == []
def test_find_yaml_files_single_file(self, tmp_path: Path) -> None:
f = tmp_path / "test.txt"
f.write_text("hello")
# find_yaml_files accepts any single file (no suffix check)
assert AnsibleFileFinder.find_yaml_files(f) == [f]
def test_find_yaml_files_dir(self, tmp_path: Path) -> None:
(tmp_path / "a.yml").write_text("tasks: []")
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "b.yaml").write_text("tasks: []")
result = AnsibleFileFinder.find_yaml_files(tmp_path)
assert len(result) == 2
def test_find_yaml_files_skip_molecule(self, tmp_path: Path) -> None:
(tmp_path / "a.yml").write_text("tasks: []")
mol = tmp_path / "molecule" / "default"
mol.mkdir(parents=True)
(mol / "main.yml").write_text("tasks: []")
result = AnsibleFileFinder.find_yaml_files(tmp_path, skip_molecule=True)
assert len(result) == 1
def test_find_yaml_files_include_molecule(self, tmp_path: Path) -> None:
(tmp_path / "a.yml").write_text("tasks: []")
mol = tmp_path / "molecule" / "default"
mol.mkdir(parents=True)
(mol / "main.yml").write_text("tasks: []")
result = AnsibleFileFinder.find_yaml_files(tmp_path, skip_molecule=False)
assert len(result) == 2
def test_find_task_and_playbook_files(self, tmp_path: Path) -> None:
role = tmp_path / "roles" / "myrole"
(role / "tasks").mkdir(parents=True)
(role / "tasks" / "main.yml").write_text("tasks: []")
pb = tmp_path / "playbooks"
pb.mkdir()
(pb / "deploy.yml").write_text("tasks: []")
(tmp_path / "random.yml").write_text("tasks: []")
result = AnsibleFileFinder.find_task_and_playbook_files(tmp_path)
# Should find tasks/main.yml and playbooks/deploy.yml, not random.yml
names = [f.name for f in result]
assert "main.yml" in names
assert "deploy.yml" in names
assert "random.yml" not in names
def test_find_task_and_playbook_files_skip_molecule(self, tmp_path: Path) -> None:
role = tmp_path / "roles" / "myrole"
(role / "tasks").mkdir(parents=True)
(role / "tasks" / "main.yml").write_text("tasks: []")
mol = role / "molecule" / "default" / "tasks"
mol.mkdir(parents=True)
(mol / "main.yml").write_text("tasks: []")
result = AnsibleFileFinder.find_task_and_playbook_files(tmp_path, skip_molecule=True)
assert len(result) == 1
assert "molecule" not in result[0].parts
class TestAnsibleYAMLParser:
def test_parse_file_valid(self) -> None:
content = "---\n- name: test\n shell: echo hi\n"
docs = AnsibleYAMLParser.parse_file(content)
assert len(docs) == 1
assert isinstance(docs[0], list)
def test_parse_file_multi_doc(self) -> None:
content = "---\n- a\n---\n- b\n"
docs = AnsibleYAMLParser.parse_file(content)
assert len(docs) == 2
def test_parse_file_empty_docs_filtered(self) -> None:
content = "---\n- a\n---\n\n"
docs = AnsibleYAMLParser.parse_file(content)
assert len(docs) == 1
def test_parse_file_yaml_error(self) -> None:
content = "{{ invalid: ["
docs = AnsibleYAMLParser.parse_file(content)
assert docs == []
def test_iter_tasks_bare_list(self) -> None:
doc = [{"name": "task1", "shell": "echo hi"}, {"name": "task2", "shell": "echo bye"}]
tasks = list(AnsibleYAMLParser.iter_tasks(doc))
assert len(tasks) == 2
assert tasks[0][0]["name"] == "task1"
assert tasks[0][1] == 1
assert tasks[1][1] == 2
def test_iter_tasks_play_dict(self) -> None:
doc = {"hosts": "all", "tasks": [{"name": "task1", "shell": "echo hi"}]}
tasks = list(AnsibleYAMLParser.iter_tasks(doc))
assert len(tasks) == 1
assert tasks[0][0]["name"] == "task1"
def test_iter_tasks_play_with_pre_post_handlers(self) -> None:
doc = {
"hosts": "all",
"pre_tasks": [{"name": "pre", "shell": "echo pre"}],
"tasks": [{"name": "main", "shell": "echo main"}],
"post_tasks": [{"name": "post", "shell": "echo post"}],
"handlers": [{"name": "handler", "shell": "echo handler"}],
}
tasks = list(AnsibleYAMLParser.iter_tasks(doc))
assert len(tasks) == 4
names = [t[0]["name"] for t in tasks]
# Order: tasks, pre_tasks, post_tasks, handlers (as defined in _iter_play_sections)
assert names == ["main", "pre", "post", "handler"]
def test_iter_tasks_block(self) -> None:
doc = [{"name": "outer", "block": [{"name": "inner", "shell": "echo hi"}]}]
tasks = list(AnsibleYAMLParser.iter_tasks(doc))
# outer is not a play (no task sections) → yielded as bare task
# inner is yielded from block
assert len(tasks) == 2
assert tasks[0][0]["name"] == "outer"
assert tasks[1][0]["name"] == "inner"
def test_iter_tasks_block_in_play_section(self) -> None:
"""Block tasks within a play's tasks section are yielded."""
doc = {
"hosts": "all",
"tasks": [
{"name": "outer", "block": [{"name": "inner", "shell": "echo hi"}]},
],
}
tasks = list(AnsibleYAMLParser.iter_tasks(doc))
assert len(tasks) == 2
assert tasks[0][0]["name"] == "outer"
assert tasks[1][0]["name"] == "inner"
def test_iter_tasks_play_list(self) -> None:
doc = [{"hosts": "all", "tasks": [{"name": "task1", "shell": "echo hi"}]}]
tasks = list(AnsibleYAMLParser.iter_tasks(doc))
assert len(tasks) == 1
assert tasks[0][0]["name"] == "task1"
def test_iter_tasks_non_dict_items_skipped(self) -> None:
doc = ["string", 42, {"name": "task1", "shell": "echo hi"}]
tasks = list(AnsibleYAMLParser.iter_tasks(doc))
assert len(tasks) == 1
class TestViolationReporter:
def test_format_violation_with_line(self, tmp_path: Path) -> None:
result = ViolationReporter.format_violation(tmp_path / "foo.yml", tmp_path, 42, "bad")
assert result == "foo.yml:42 — bad"
def test_format_violation_without_line(self, tmp_path: Path) -> None:
result = ViolationReporter.format_violation(tmp_path / "foo.yml", tmp_path, None, "bad")
assert result == "foo.yml — bad"
def test_format_violation_not_relative(self, tmp_path: Path) -> None:
other = Path("/other/path")
result = ViolationReporter.format_violation(other, tmp_path, 1, "bad")
assert str(other) in result
assert "bad" in result
def test_report_no_violations(self, capsys: pytest.CaptureFixture[str]) -> None:
ViolationReporter.report([], "test-tool")
captured = capsys.readouterr()
assert "OK" in captured.out
assert "test-tool" in captured.out
def test_report_with_violations(self, capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as exc_info:
ViolationReporter.report(["v1", "v2"], "test-tool")
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert "FAIL" in captured.out
assert "v1" in captured.out
assert "v2" in captured.out
class TestDefaultAnsibleDirs:
def test_is_tuple(self) -> None:
assert isinstance(DEFAULT_ANSIBLE_DIRS, tuple)
def test_contains_expected(self) -> None:
assert "ansible/roles" in DEFAULT_ANSIBLE_DIRS
assert "ansible/playbooks" in DEFAULT_ANSIBLE_DIRS