import re import unittest from pathlib import Path class TestDockerComposeTemplates(unittest.TestCase): # Search for all roles/*/templates/compose.yml.j2 PROJECT_ROOT = Path(__file__).resolve().parents[2] TEMPLATE_PATTERN = "roles/*/templates/compose.yml.j2" # Allowed lines before BASE_INCLUDE ALLOWED_BEFORE_BASE = [ re.compile(r"^\s*$"), # empty line re.compile(r"^\s*version:.*$"), # version: ... re.compile(r"^\s*#.*$"), # YAML comment re.compile(r"^\s*\{\#.*\#\}\s*$"), # Jinja comment {# ... #} ] BASE_INCLUDE = "{% include 'roles/sys-svc-compose/templates/base.yml.j2' %}" NET_INCLUDE = "{% include 'roles/sys-svc-compose/templates/networks.yml.j2' %}" HOST_MODE = 'network_mode: "host"' def test_docker_compose_includes(self): """ Verifies for each found compose.yml.j2: 1. BASE_INCLUDE is present exactly once 2. If no host‑mode is set, NET_INCLUDE must appear exactly once 3. BASE_INCLUDE appears before NET_INCLUDE when both are required 4. Only allowed lines appear before BASE_INCLUDE (invalid lines issue warnings) """ template_paths = sorted(self.PROJECT_ROOT.glob(self.TEMPLATE_PATTERN)) self.assertTrue( template_paths, f"No templates found for pattern {self.TEMPLATE_PATTERN}" ) for template_path in template_paths: with self.subTest(template=template_path): content = template_path.read_text(encoding="utf-8") lines = content.splitlines() # BASE_INCLUDE must always occur exactly once count_base = lines.count(self.BASE_INCLUDE) self.assertEqual( count_base, 1, f"{template_path}: '{self.BASE_INCLUDE}' occurs {count_base} times, expected once", ) # Determine if host‑mode is in use host_mode = self.HOST_MODE in content # If not host‑mode, NET_INCLUDE must occur exactly once count_net = lines.count(self.NET_INCLUDE) if host_mode: # No network include needed for host mode self.assertEqual( count_net, 0, f"{template_path}: '{self.NET_INCLUDE}' should be omitted when using host networking", ) else: # Must include networks.yml exactly once self.assertEqual( count_net, 1, f"{template_path}: '{self.NET_INCLUDE}' occurs {count_net} times, expected once", ) # If both includes are present, check order if count_base and count_net: idx_base = lines.index(self.BASE_INCLUDE) idx_net = lines.index(self.NET_INCLUDE) self.assertLess( idx_base, idx_net, f"{template_path}: '{self.BASE_INCLUDE}' must come before '{self.NET_INCLUDE}'", ) if __name__ == "__main__": unittest.main()