Solved run_after dependency bug

This commit is contained in:
2025-07-09 06:47:10 +02:00
parent 39d2e6c0fa
commit a69b2c9cb2
6 changed files with 428 additions and 257 deletions

View File

@@ -0,0 +1,36 @@
import os
import unittest
# import the functions from your CLI script
from cli.generate_playbook import build_dependency_graph, find_cycle
class TestCircularDependencies(unittest.TestCase):
"""
Integration test: ensure there are no circular 'run_after' dependencies
among the roles in the roles/ directory.
"""
@classmethod
def setUpClass(cls):
# Determine the path to the repo root and the roles directory
here = os.path.abspath(os.path.dirname(__file__))
repo_root = os.path.abspath(os.path.join(here, '..', '..'))
cls.roles_dir = os.path.join(repo_root, 'roles')
def test_no_circular_dependencies(self):
# Build the dependency graph using the real roles/
graph, in_degree, roles = build_dependency_graph(self.roles_dir)
# Attempt to find a cycle in the run_after mapping
cycle = find_cycle(roles)
if cycle:
# Format cycle as "A -> B -> C -> A"
cycle_str = " -> ".join(cycle)
self.fail(f"Circular dependency detected among roles: {cycle_str}")
# If no cycle, this assertion will pass
self.assertIsNone(cycle, "Expected no circular dependencies")
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,49 @@
import os
import unittest
import yaml
class TestRunAfterReferences(unittest.TestCase):
"""
Integration test: ensure that every name listed under
galaxy_info.run_after in each role's meta/main.yml
corresponds to an existing role directory.
"""
@classmethod
def setUpClass(cls):
here = os.path.abspath(os.path.dirname(__file__))
repo_root = os.path.abspath(os.path.join(here, '..', '..'))
cls.roles_dir = os.path.join(repo_root, 'roles')
# collect all role names (folder names) in roles/
cls.existing_roles = {
name for name in os.listdir(cls.roles_dir)
if os.path.isdir(os.path.join(cls.roles_dir, name))
}
def test_run_after_points_to_existing_roles(self):
errors = []
for role in sorted(self.existing_roles):
meta_path = os.path.join(self.roles_dir, role, 'meta', 'main.yml')
if not os.path.isfile(meta_path):
# skip roles without a meta/main.yml
continue
with open(meta_path, 'r') as f:
data = yaml.safe_load(f) or {}
run_after = data.get('galaxy_info', {}).get('run_after', [])
for dep in run_after:
if dep not in self.existing_roles:
errors.append(
f"Role '{role}' declares run_after: '{dep}', "
f"but '{dep}' is not a directory under roles/"
)
if errors:
self.fail(
"Some run_after references are invalid:\n " +
"\n ".join(errors)
)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,35 @@
import os
import unittest
import yaml
class TestSelfDependency(unittest.TestCase):
"""
Integration test: ensure no role lists itself in its own 'run_after'
in meta/main.yml.
"""
@classmethod
def setUpClass(cls):
here = os.path.abspath(os.path.dirname(__file__))
repo_root = os.path.abspath(os.path.join(here, '..', '..'))
cls.roles_dir = os.path.join(repo_root, 'roles')
def test_no_self_in_run_after(self):
for entry in os.listdir(self.roles_dir):
role_path = os.path.join(self.roles_dir, entry)
meta_file = os.path.join(role_path, 'meta', 'main.yml')
if not os.path.isdir(role_path) or not os.path.isfile(meta_file):
continue
with open(meta_file, 'r') as f:
data = yaml.safe_load(f) or {}
run_after = data.get('galaxy_info', {}).get('run_after', [])
if entry in run_after:
self.fail(
f"Role '{entry}' has a self-dependency in its run_after list "
f"in {meta_file}"
)
if __name__ == '__main__':
unittest.main()