From 29f50da2269b50b0c8babb721eff20dd3f4aeb5a Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 18 Aug 2025 11:27:26 +0200 Subject: [PATCH] Add custom Ansible filter plugin get_category_entries This commit introduces a new Ansible filter plugin named 'get_category_entries', which returns all role names under the roles/ directory that start with a given prefix. Additionally, unit tests (unittest framework) have been added under tests/unit/filterplugins/ to ensure correct behavior, including: - Returns empty list when roles/ directory is missing - Correctly filters and sorts by prefix - Ignores non-directory entries - Supports custom roles_path argument - Returns all roles when prefix is empty Reference: https://chatgpt.com/share/68a2f1ab-1fe8-800f-b22a-28c1c95802c2 --- filter_plugins/get_category_entries.py | 31 +++++++ .../test_get_category_entries.py | 85 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 filter_plugins/get_category_entries.py create mode 100644 tests/unit/filter_plugins/test_get_category_entries.py diff --git a/filter_plugins/get_category_entries.py b/filter_plugins/get_category_entries.py new file mode 100644 index 00000000..77f9cd55 --- /dev/null +++ b/filter_plugins/get_category_entries.py @@ -0,0 +1,31 @@ +# Custom Ansible filter to get all role names under "roles/" with a given prefix. + +import os + +def get_category_entries(prefix, roles_path="roles"): + """ + Returns a list of role names under the given roles_path + that start with the specified prefix. + + :param prefix: String prefix to match role names. + :param roles_path: Path to the roles directory (default: 'roles'). + :return: List of matching role names. + """ + if not os.path.isdir(roles_path): + return [] + + roles = [] + for entry in os.listdir(roles_path): + full_path = os.path.join(roles_path, entry) + if os.path.isdir(full_path) and entry.startswith(prefix): + roles.append(entry) + + return sorted(roles) + +class FilterModule(object): + """ Custom filters for Ansible """ + + def filters(self): + return { + "get_category_entries": get_category_entries + } diff --git a/tests/unit/filter_plugins/test_get_category_entries.py b/tests/unit/filter_plugins/test_get_category_entries.py new file mode 100644 index 00000000..98236682 --- /dev/null +++ b/tests/unit/filter_plugins/test_get_category_entries.py @@ -0,0 +1,85 @@ +# Unit tests for the get_category_entries Ansible filter plugin (unittest version). + +import os +import unittest +import tempfile +import shutil +from pathlib import Path + +from filter_plugins.get_category_entries import get_category_entries + + +class TestGetCategoryEntries(unittest.TestCase): + def setUp(self): + # Create an isolated temp directory for each test + self._tmpdir = tempfile.TemporaryDirectory() + self.tmp = Path(self._tmpdir.name) + + def tearDown(self): + # Clean up the temp directory + self._tmpdir.cleanup() + + def test_returns_empty_when_roles_dir_missing(self): + """If the roles directory does not exist, the filter must return an empty list.""" + missing_dir = self.tmp / "no_such_roles_dir" + self.assertFalse(missing_dir.exists()) + self.assertEqual(get_category_entries("docker-", roles_path=str(missing_dir)), []) + + def test_matches_prefix_and_sorts(self): + """ + The filter should return only directory names starting with the prefix, + and the result must be sorted. + """ + roles_dir = self.tmp / "roles" + roles_dir.mkdir() + + # Create role directories + (roles_dir / "docker-nginx").mkdir() + (roles_dir / "docker-postgres").mkdir() + (roles_dir / "web-app-keycloak").mkdir() + (roles_dir / "docker-redis").mkdir() + + # A file that should be ignored + (roles_dir / "docker-file").write_text("not a directory") + + result = get_category_entries("docker-", roles_path=str(roles_dir)) + self.assertEqual(result, ["docker-nginx", "docker-postgres", "docker-redis"]) + + def test_ignores_non_directories(self): + """Non-directory entries under roles/ must be ignored.""" + roles_dir = self.tmp / "roles" + roles_dir.mkdir() + + (roles_dir / "docker-engine").mkdir() + (roles_dir / "docker-engine.txt").write_text("file, should be ignored") + + result = get_category_entries("docker-", roles_path=str(roles_dir)) + self.assertEqual(result, ["docker-engine"]) + + def test_respects_custom_roles_path(self): + """When roles_path is provided, the filter should use it instead of 'roles'.""" + custom_roles = self.tmp / "custom" / "rolesdir" + custom_roles.mkdir(parents=True) + + (custom_roles / "docker-a").mkdir() + (custom_roles / "docker-b").mkdir() + (custom_roles / "other-c").mkdir() + + result = get_category_entries("docker-", roles_path=str(custom_roles)) + self.assertEqual(result, ["docker-a", "docker-b"]) + + def test_empty_prefix_returns_all_roles_sorted(self): + """If an empty prefix is passed, the filter should return all role directories (sorted).""" + roles_dir = self.tmp / "roles" + roles_dir.mkdir() + + (roles_dir / "a-role").mkdir() + (roles_dir / "c-role").mkdir() + (roles_dir / "b-role").mkdir() + + result = get_category_entries("", roles_path=str(roles_dir)) + self.assertEqual(result, ["a-role", "b-role", "c-role"]) + + +if __name__ == "__main__": + unittest.main()