import re import unittest from pathlib import Path class TestJinjaIncludePaths(unittest.TestCase): """ Verifies that in all .j2 files in the project (root + subfolders): - Every {% include 'string/path' %} or {% include "string/path" %} refers to an existing file. - Any include using a variable or concatenation is ignored. - Includes are resolved relative to project root, template directory, or file's parent. """ PROJECT_ROOT = Path(__file__).resolve().parents[2] INCLUDE_STMT_RE = re.compile(r"{%\s*include\s+(.+?)\s*%}") LITERAL_PATH_RE = re.compile(r"^[\'\"]([^\'\"]+)[\'\"]$") def test_all_jinja_includes_exist(self): template_paths = list(self.PROJECT_ROOT.glob("**/*.j2")) self.assertTrue( template_paths, "No .j2 templates found anywhere in the project" ) missing = [] for tpl in template_paths: text = tpl.read_text(encoding="utf-8") for stmt in self.INCLUDE_STMT_RE.finditer(text): expr = stmt.group(1).strip() m = self.LITERAL_PATH_RE.match(expr) if not m: continue # ignore variable-based includes include_path = m.group(1) # check absolute project-relative path abs_target = self.PROJECT_ROOT / include_path # check path relative to the template file rel_target = tpl.parent / include_path # check path relative to role's templates directory role_templates_root = None for parent in tpl.parents: if parent.name == 'templates': role_templates_root = parent break role_target = None if role_templates_root: role_target = role_templates_root / include_path if not ( abs_target.exists() or rel_target.exists() or (role_target and role_target.exists()) ): rel_tpl = tpl.relative_to(self.PROJECT_ROOT) missing.append( f"{rel_tpl}: included file '{include_path}' not found " f"(checked in project root, {tpl.parent.relative_to(self.PROJECT_ROOT)} or templates folder)" ) if missing: self.fail("Broken {% include %} references:\n" + "\n".join(missing)) if __name__ == "__main__": unittest.main()