From 34d5c415bb84f6bb8382a85f471dbe4c79dc2000 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sat, 12 Jul 2025 11:56:20 +0200 Subject: [PATCH] Added invokable test --- .../test_categories_invokable_parents.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/integration/test_categories_invokable_parents.py diff --git a/tests/integration/test_categories_invokable_parents.py b/tests/integration/test_categories_invokable_parents.py new file mode 100644 index 00000000..38f8ccba --- /dev/null +++ b/tests/integration/test_categories_invokable_parents.py @@ -0,0 +1,56 @@ +import os +import unittest +import yaml + +class TestCategoriesInvokableExclusion(unittest.TestCase): + def test_no_child_invokable_if_any_parent_is_invokable(self): + """ + Verify that if any ancestor in the hierarchy is invokable, + none of its descendants may be invokable. + """ + # locate roles/categories.yml + base_dir = os.path.dirname(__file__) + yaml_path = os.path.abspath( + os.path.join(base_dir, '..', '..', 'roles', 'categories.yml') + ) + + with open(yaml_path, 'r', encoding='utf-8') as f: + data = yaml.safe_load(f) + + violations = [] + + def recurse(node: dict, path: list[str], ancestor_invokable: bool): + for key, value in node.items(): + if not isinstance(value, dict): + continue + + is_invokable = value.get('invokable', False) + current_path = path + [key] + + # Violation: a descendant is invokable despite an invokable ancestor + if ancestor_invokable and is_invokable: + ancestor_name = '.'.join(path) if path else '' + violations.append( + f"{'.'.join(current_path)} is invokable, " + f"but its ancestor ({ancestor_name}) is also invokable." + ) + + # Any_invokable = True if this node or any ancestor is invokable + new_ancestor_flag = ancestor_invokable or is_invokable + + # Recurse into subcategories + for subkey, subval in value.items(): + if isinstance(subval, dict): + recurse({subkey: subval}, current_path, new_ancestor_flag) + + # start at top-level roles, with no invokable ancestor + recurse(data.get('roles', {}), [], False) + + if violations: + self.fail( + "Found invokable descendants under invokable parents:\n" + + "\n".join(violations) + ) + +if __name__ == '__main__': + unittest.main()