Public Access
DEVX-155: refactor: extract wait_for_checks, consolidate ansible_checks, deprecate ci/discover_runners
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user