test_dependency_matrix.py 5.5 KB

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