test_base_commands.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. """Focused regression tests for module base commands."""
  2. from __future__ import annotations
  3. from types import SimpleNamespace
  4. from cli.core.module.base_commands import (
  5. GenerationConfig,
  6. ValidationConfig,
  7. _validate_all_templates,
  8. apply_output_name,
  9. generate_template,
  10. list_templates,
  11. validate_templates,
  12. )
  13. def _noop(*_args, **_kwargs) -> None:
  14. return None
  15. def _raise_destination_prompt(slug: str):
  16. raise AssertionError(f"prompt_generation_destination called for {slug}")
  17. def _raise_output_check(*_args, **_kwargs):
  18. raise AssertionError("check_output_directory should not run")
  19. class _DisplayCapture:
  20. def __init__(self) -> None:
  21. self.lines: list[str] = []
  22. self.templates = SimpleNamespace(
  23. render_template_header=_noop,
  24. render_file_tree=_noop,
  25. )
  26. self.variables = SimpleNamespace(render_variables_table=_noop)
  27. def text(self, value: str, style: str | None = None) -> None:
  28. del style
  29. self.lines.append(value)
  30. def success(self, value: str, *args, **kwargs) -> None:
  31. del args, kwargs
  32. self.lines.append(value)
  33. def warning(self, value: str, *args, **kwargs) -> None:
  34. del args, kwargs
  35. self.lines.append(value)
  36. def error(self, value: str, *args, **kwargs) -> None:
  37. del args, kwargs
  38. self.lines.append(value)
  39. def data_table(self, *args, **kwargs) -> None:
  40. del args, kwargs
  41. raise AssertionError("data_table should not be used for raw output")
  42. def info(self, *args, **kwargs) -> None:
  43. del args, kwargs
  44. raise AssertionError("info should not be used when templates exist")
  45. class _ValidationDisplayCapture(_DisplayCapture):
  46. def data_table(self, *args, **kwargs) -> None:
  47. del args, kwargs
  48. self.lines.append("data_table")
  49. def info(self, value: str = "", *args, **kwargs) -> None:
  50. del args, kwargs
  51. self.lines.append(value)
  52. def test_list_templates_raw_outputs_tab_separated_rows() -> None:
  53. """Raw listing should emit one tab-separated row per template."""
  54. template = SimpleNamespace(
  55. id="whoami",
  56. metadata=SimpleNamespace(
  57. name="Whoami",
  58. tags=["docker", "test"],
  59. version=SimpleNamespace(name="1.0.0"),
  60. library="default",
  61. library_type="git",
  62. ),
  63. )
  64. display = _DisplayCapture()
  65. module_instance = SimpleNamespace(
  66. name="compose",
  67. display=display,
  68. _load_all_templates=lambda: [template],
  69. )
  70. returned_templates = list_templates(module_instance, raw=True)
  71. assert returned_templates == [template]
  72. assert display.lines == ["whoami\tWhoami\tdocker,test\t1.0.0\tdefault"]
  73. def test_apply_output_name_renames_top_level_paths_only() -> None:
  74. """Named generation should rename top-level outputs while preserving nested names."""
  75. rendered_files = {
  76. "files/test.txt": "nested",
  77. "main.tf": "main",
  78. "dns.tf": "dns",
  79. }
  80. assert apply_output_name(rendered_files, "servertest1") == {
  81. "servertest1_files/test.txt": "nested",
  82. "servertest1.tf": "main",
  83. "servertest1_dns.tf": "dns",
  84. }
  85. def test_generate_template_applies_output_name_before_writing(monkeypatch, tmp_path) -> None:
  86. """Generate should write renamed paths when --name is provided."""
  87. display = _DisplayCapture()
  88. template = SimpleNamespace(id="terraform", slug="terraform")
  89. module_instance = SimpleNamespace(name="terraform", display=display)
  90. written: dict[str, object] = {}
  91. monkeypatch.setattr("cli.core.module.base_commands._prepare_template", lambda *_args, **_kwargs: template)
  92. monkeypatch.setattr(
  93. "cli.core.module.base_commands._render_template",
  94. lambda *_args, **_kwargs: ({"files/test.txt": "nested", "main.tf": "main", "dns.tf": "dns"}, {}),
  95. )
  96. monkeypatch.setattr("cli.core.module.base_commands.check_output_directory", lambda *_args, **_kwargs: [])
  97. def capture_write(output_dir, rendered_files):
  98. written["output_dir"] = output_dir
  99. written["rendered_files"] = rendered_files
  100. monkeypatch.setattr("cli.core.module.base_commands.write_rendered_files", capture_write)
  101. generate_template(
  102. module_instance,
  103. GenerationConfig(
  104. id="terraform",
  105. output=str(tmp_path),
  106. interactive=False,
  107. name="servertest1",
  108. ),
  109. )
  110. assert written["output_dir"] == tmp_path
  111. assert written["rendered_files"] == {
  112. "servertest1_files/test.txt": "nested",
  113. "servertest1.tf": "main",
  114. "servertest1_dns.tf": "dns",
  115. }
  116. def test_generate_template_dry_run_skips_destination_prompt_and_overwrite_check(
  117. monkeypatch,
  118. ) -> None:
  119. """Dry runs without explicit destinations should not ask where to write or confirm overwrites."""
  120. display = _DisplayCapture()
  121. template = SimpleNamespace(id="whoami", slug="whoami")
  122. module_instance = SimpleNamespace(name="compose", display=display)
  123. monkeypatch.setattr("cli.core.module.base_commands._prepare_template", lambda *_args, **_kwargs: template)
  124. monkeypatch.setattr(
  125. "cli.core.module.base_commands._render_template",
  126. lambda *_args, **_kwargs: ({"compose.yaml": "services:\n"}, {}),
  127. )
  128. monkeypatch.setattr("cli.core.module.base_commands.prompt_generation_destination", _raise_destination_prompt)
  129. monkeypatch.setattr(
  130. "cli.core.module.base_commands.check_output_directory",
  131. _raise_output_check,
  132. )
  133. generate_template(
  134. module_instance,
  135. GenerationConfig(
  136. id="whoami",
  137. interactive=True,
  138. dry_run=True,
  139. ),
  140. )
  141. assert any("boilerplate rendered successfully" in line for line in display.lines)
  142. assert any("preview only" in line for line in display.lines)
  143. def test_validate_templates_without_id_validates_all_templates(monkeypatch) -> None:
  144. """Omitting the template ID should keep the legacy validate-all behavior."""
  145. display = _ValidationDisplayCapture()
  146. module_instance = SimpleNamespace(name="compose", display=display)
  147. called = {}
  148. def capture_validate_all(received_module, received_config):
  149. called["module"] = received_module
  150. called["config"] = received_config
  151. monkeypatch.setattr("cli.core.module.base_commands._validate_all_templates", capture_validate_all)
  152. validate_templates(module_instance, None, None, ValidationConfig(verbose=False))
  153. assert called["module"] is module_instance
  154. assert called["config"].semantic is True
  155. def test_validate_single_template_runs_default_semantic_validation(monkeypatch) -> None:
  156. """Single-template validation should still run semantic validation by default."""
  157. display = _ValidationDisplayCapture()
  158. template = SimpleNamespace(id="whoami", used_variables=[], variables=SimpleNamespace())
  159. module_instance = SimpleNamespace(name="compose", display=display)
  160. called = {}
  161. monkeypatch.setattr("cli.core.module.base_commands._load_template_for_validation", lambda *_args: template)
  162. def capture_semantic(received_module, received_template, verbose):
  163. called["module"] = received_module
  164. called["template"] = received_template
  165. called["verbose"] = verbose
  166. monkeypatch.setattr("cli.core.module.base_commands._run_semantic_validation", capture_semantic)
  167. validate_templates(module_instance, "whoami", None, ValidationConfig(verbose=False))
  168. assert called == {"module": module_instance, "template": template, "verbose": False}
  169. def test_validate_all_templates_keeps_basic_validation_by_default(monkeypatch) -> None:
  170. """Validate-all should not start rendering every template semantically unless matrix/kind is requested."""
  171. display = _ValidationDisplayCapture()
  172. template = SimpleNamespace(id="whoami", used_variables=[], variables=SimpleNamespace())
  173. module_instance = SimpleNamespace(
  174. name="compose",
  175. display=display,
  176. _load_all_templates=lambda: [template],
  177. )
  178. monkeypatch.setattr("cli.core.module.base_commands._run_semantic_validation", _raise_output_check)
  179. _validate_all_templates(module_instance, ValidationConfig(verbose=False))
  180. assert any("All templates are valid" in line for line in display.lines)