test_dependency_matrix.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. from __future__ import annotations
  2. import json
  3. from pathlib import Path
  4. from cli.core.template import Template
  5. from cli.core.validation import (
  6. AnsibleValidator,
  7. DependencyMatrixBuilder,
  8. KindValidationResult,
  9. MatrixOptions,
  10. ValidationRunner,
  11. )
  12. def _write_template(tmp_path: Path, manifest: dict, files: dict[str, str]) -> Template:
  13. template_dir = tmp_path / "sample-compose"
  14. files_dir = template_dir / "files"
  15. files_dir.mkdir(parents=True)
  16. (template_dir / "template.json").write_text(json.dumps(manifest), encoding="utf-8")
  17. for relative_path, content in files.items():
  18. output_path = files_dir / relative_path
  19. output_path.parent.mkdir(parents=True, exist_ok=True)
  20. output_path.write_text(content, encoding="utf-8")
  21. return Template(template_dir, library_name="test", library_type="static")
  22. def test_dependency_matrix_covers_bool_and_enum_branches(tmp_path: Path) -> None:
  23. template = _write_template(
  24. tmp_path,
  25. {
  26. "kind": "compose",
  27. "slug": "sample-compose",
  28. "metadata": {
  29. "name": "Sample",
  30. "description": "Sample",
  31. "author": "test",
  32. "date": "2026-01-01",
  33. },
  34. "variables": [
  35. {
  36. "name": "general",
  37. "title": "General",
  38. "items": [
  39. {"name": "service_name", "type": "str", "default": "app"},
  40. {
  41. "name": "network_mode",
  42. "type": "enum",
  43. "default": "bridge",
  44. "config": {"options": ["bridge", "host", "macvlan"]},
  45. },
  46. ],
  47. },
  48. {
  49. "name": "traefik",
  50. "title": "Traefik",
  51. "toggle": "traefik_enabled",
  52. "needs": "network_mode=bridge,macvlan",
  53. "items": [
  54. {"name": "traefik_enabled", "type": "bool", "default": False},
  55. {"name": "traefik_host", "type": "str", "default": "app.example.com"},
  56. ],
  57. },
  58. ],
  59. },
  60. {
  61. "compose.yaml": """
  62. services:
  63. << service_name >>:
  64. image: nginx:1.25.3
  65. <% if traefik_enabled %>
  66. labels:
  67. - traefik.http.routers.<< service_name >>.rule=Host(`<< traefik_host >>`)
  68. <% endif %>
  69. """,
  70. },
  71. )
  72. cases = DependencyMatrixBuilder(template, MatrixOptions(max_combinations=20)).build()
  73. rendered_values = [case.variables.get_satisfied_values() for case in cases]
  74. assert any(values.get("network_mode") == "bridge" for values in rendered_values)
  75. assert any(values.get("network_mode") == "macvlan" for values in rendered_values)
  76. assert any(values.get("traefik_enabled") is True for values in rendered_values)
  77. assert any(case.overrides.get("traefik_enabled") is False for case in cases)
  78. def test_validation_runner_reports_semantic_failure_for_matrix_case(tmp_path: Path) -> None:
  79. template = _write_template(
  80. tmp_path,
  81. {
  82. "kind": "compose",
  83. "slug": "broken-compose",
  84. "metadata": {
  85. "name": "Broken",
  86. "description": "Broken",
  87. "author": "test",
  88. "date": "2026-01-01",
  89. },
  90. "variables": [
  91. {
  92. "name": "general",
  93. "title": "General",
  94. "items": [
  95. {"name": "service_name", "type": "str", "default": "app"},
  96. {"name": "invalid_enabled", "type": "bool", "default": False},
  97. ],
  98. }
  99. ],
  100. },
  101. {
  102. "compose.yaml": """
  103. <% if invalid_enabled %>
  104. services: []
  105. <% else %>
  106. services:
  107. << service_name >>:
  108. image: nginx:1.25.3
  109. <% endif %>
  110. """,
  111. },
  112. )
  113. cases = DependencyMatrixBuilder(template, MatrixOptions(max_combinations=10)).build()
  114. summary = ValidationRunner(template, cases, semantic=True).run()
  115. assert not summary.ok
  116. assert any(failure.stage == "sem" and failure.file_path == "compose.yaml" for failure in summary.failures)
  117. def test_validation_runner_treats_unavailable_kind_validator_as_skip(tmp_path: Path) -> None:
  118. template = _write_template(
  119. tmp_path,
  120. {
  121. "kind": "custom",
  122. "slug": "custom-template",
  123. "metadata": {
  124. "name": "Custom",
  125. "description": "Custom",
  126. "author": "test",
  127. "date": "2026-01-01",
  128. },
  129. "variables": [
  130. {
  131. "name": "general",
  132. "title": "General",
  133. "items": [{"name": "service_name", "type": "str", "default": "app"}],
  134. }
  135. ],
  136. },
  137. {"config.yaml": "name: << service_name >>\n"},
  138. )
  139. def unavailable_validator(_rendered_files: dict[str, str], _case_name: str) -> KindValidationResult:
  140. return KindValidationResult(
  141. validator="missing-tool",
  142. available=False,
  143. details=["Required command is unavailable: missing-tool"],
  144. )
  145. cases = DependencyMatrixBuilder(template, MatrixOptions(max_combinations=10)).build()
  146. summary = ValidationRunner(template, cases, semantic=True, kind_validator=unavailable_validator).run()
  147. assert summary.ok
  148. assert summary.kind_available is False
  149. assert summary.kind_skipped_cases == {"defaults"}
  150. assert summary.failures == []
  151. def test_ansible_validator_detects_main_yml_playbook_by_hosts_key(tmp_path: Path) -> None:
  152. playbook = tmp_path / "main.yml"
  153. playbook.write_text("- name: Configure host\n hosts: all\n tasks: []\n", encoding="utf-8")
  154. assert AnsibleValidator._find_playbooks(tmp_path) == [playbook]
  155. def test_ansible_validator_classifies_missing_collection_as_dependency_resolution_failure() -> None:
  156. assert AnsibleValidator._is_dependency_resolution_failure("ERROR! the role 'vendor.role' was not found")
  157. assert AnsibleValidator._is_dependency_resolution_failure("ERROR! couldn't resolve module/action 'vendor.module'")
  158. assert not AnsibleValidator._is_dependency_resolution_failure("ERROR! Syntax Error while loading YAML")