Ver Fonte

fix(validation): preserve default validate behavior

xcad há 3 dias atrás
pai
commit
4475a1d7d8

+ 1 - 0
CHANGELOG.md

@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 ### Added
 - Static template kind for technology-agnostic file and directory boilerplates (#1786)
 - Static template kind for technology-agnostic file and directory boilerplates (#1786)
 - Named generation output paths via `generate --name` / `-n` (#1783)
 - Named generation output paths via `generate --name` / `-n` (#1783)
+- Exhaustive dependency matrix validation for rendering reachable template states, with optional kind-specific validators (#1780)
 - `template.json` runtime support with required `files/` directories, custom `<< >>` / `<% %>` / `<# #>` delimiters, and legacy-format rejection for `0.2.0` (#1768)
 - `template.json` runtime support with required `files/` directories, custom `<< >>` / `<% %>` / `<# #>` delimiters, and legacy-format rejection for `0.2.0` (#1768)
 - Remote generation destinations via `generate --remote` and `--remote-path`, including SSH host discovery and SCP upload flow (#1765)
 - Remote generation destinations via `generate --remote` and `--remote-path`, including SSH host discovery and SCP upload flow (#1765)
 - Initial dedicated `swarm` module and validation flow as groundwork for the `compose` / `swarm` split in `0.2.0` (#1766)
 - Initial dedicated `swarm` module and validation flow as groundwork for the `compose` / `swarm` split in `0.2.0` (#1766)

+ 49 - 9
cli/core/module/base_commands.py

@@ -18,6 +18,7 @@ from ..exceptions import (
 from ..input import InputManager
 from ..input import InputManager
 from ..template import Template
 from ..template import Template
 from ..validation import DependencyMatrixBuilder, MatrixOptions, ValidationRunner
 from ..validation import DependencyMatrixBuilder, MatrixOptions, ValidationRunner
+from ..validators import get_validator_registry
 from .generation_destination import (
 from .generation_destination import (
     GenerationDestination,
     GenerationDestination,
     format_remote_destination,
     format_remote_destination,
@@ -61,7 +62,8 @@ class ValidationConfig:
     """Configuration for template validation."""
     """Configuration for template validation."""
 
 
     verbose: bool
     verbose: bool
-    semantic: bool = False
+    semantic: bool = True
+    matrix: bool = False
     kind: bool = False
     kind: bool = False
     all_templates: bool = False
     all_templates: bool = False
     matrix_max_combinations: int = 100
     matrix_max_combinations: int = 100
@@ -598,10 +600,6 @@ def validate_templates(
         module_instance.display.error("--all cannot be combined with a template ID or --path")
         module_instance.display.error("--all cannot be combined with a template ID or --path")
         raise Exit(code=1) from None
         raise Exit(code=1) from None
 
 
-    if not config.all_templates and not template_id and not path:
-        module_instance.display.error("Provide a template ID, --path, or --all")
-        raise Exit(code=1) from None
-
     template = _load_template_for_validation(module_instance, template_id, path)
     template = _load_template_for_validation(module_instance, template_id, path)
 
 
     if template:
     if template:
@@ -654,13 +652,17 @@ def _validate_single_template(
         if not config.quiet_success:
         if not config.quiet_success:
             module_instance.display.success("Jinja2 validation passed")
             module_instance.display.success("Jinja2 validation passed")
 
 
-        if config.semantic or config.kind:
+        if config.matrix or config.kind:
             _run_matrix_validation(module_instance, template, config)
             _run_matrix_validation(module_instance, template, config)
             return
             return
 
 
+        # Semantic validation for the default rendered output.
+        if config.semantic:
+            _run_semantic_validation(module_instance, template, config.verbose)
+
         # Verbose output
         # Verbose output
         if config.verbose:
         if config.verbose:
-            _display_validation_details(module_instance, template)
+            _display_validation_details(module_instance, template, config.semantic)
 
 
     except TemplateRenderError as e:
     except TemplateRenderError as e:
         module_instance.display.error(str(e), context=f"template '{template_id}'")
         module_instance.display.error(str(e), context=f"template '{template_id}'")
@@ -784,10 +786,41 @@ def _matrix_stage_status(
     return "pass"
     return "pass"
 
 
 
 
-def _display_validation_details(module_instance, template) -> None:
+def _run_semantic_validation(module_instance, template, verbose: bool) -> None:
+    """Run semantic validation on the default rendered template files."""
+    module_instance.display.info("")
+    module_instance.display.info("Running semantic validation...")
+
+    registry = get_validator_registry()
+    debug_mode = logger.isEnabledFor(logging.DEBUG)
+    rendered_files, _ = template.render(template.variables, debug=debug_mode)
+
+    has_semantic_errors = False
+    for file_path, content in rendered_files.items():
+        result = registry.validate_file(content, file_path)
+
+        if result.errors or result.warnings or (verbose and result.info):
+            module_instance.display.info(f"\nFile: {file_path}")
+            result.display(f"{file_path}")
+
+            if result.errors:
+                has_semantic_errors = True
+
+    if has_semantic_errors:
+        module_instance.display.error("Semantic validation found errors")
+        raise Exit(code=1) from None
+
+    module_instance.display.success("Semantic validation passed")
+
+
+def _display_validation_details(module_instance, template, semantic: bool) -> None:
     """Display verbose validation details."""
     """Display verbose validation details."""
     module_instance.display.info(f"\nTemplate path: {template.template_dir}")
     module_instance.display.info(f"\nTemplate path: {template.template_dir}")
     module_instance.display.info(f"Found {len(template.used_variables)} variables")
     module_instance.display.info(f"Found {len(template.used_variables)} variables")
+    if semantic:
+        debug_mode = logger.isEnabledFor(logging.DEBUG)
+        rendered_files, _ = template.render(template.variables, debug=debug_mode)
+        module_instance.display.info(f"Generated {len(rendered_files)} files")
 
 
 
 
 def _validate_all_templates(module_instance, config: ValidationConfig) -> None:
 def _validate_all_templates(module_instance, config: ValidationConfig) -> None:
@@ -800,7 +833,14 @@ def _validate_all_templates(module_instance, config: ValidationConfig) -> None:
 
 
     all_templates = module_instance._load_all_templates()
     all_templates = module_instance._load_all_templates()
     total = len(all_templates)
     total = len(all_templates)
-    child_config = replace(config, quiet_success=not config.verbose)
+    # Preserve historical all-template validation behavior: by default this
+    # checks template syntax/variables only. Matrix or kind validation can be
+    # explicitly enabled for all templates with --matrix/--kind.
+    child_config = replace(
+        config,
+        semantic=config.semantic if config.matrix or config.kind else False,
+        quiet_success=not config.verbose,
+    )
 
 
     for template in all_templates:
     for template in all_templates:
         try:
         try:

+ 15 - 7
cli/core/module/base_module.py

@@ -255,7 +255,7 @@ class Module(ABC):
         self,
         self,
         template_id: Annotated[
         template_id: Annotated[
             str | None,
             str | None,
-            Argument(help="Template ID to validate"),
+            Argument(help="Template ID to validate (omit to validate all templates)"),
         ] = None,
         ] = None,
         *,
         *,
         path: Annotated[
         path: Annotated[
@@ -264,14 +264,21 @@ class Module(ABC):
         ] = None,
         ] = None,
         all_templates: Annotated[
         all_templates: Annotated[
             bool,
             bool,
-            Option("--all", help="Validate all templates in this module"),
+            Option("--all", help="Validate all templates in this module (default when no template ID is provided)"),
         ] = False,
         ] = False,
         verbose: Annotated[bool, Option("--verbose", "-v", help="Show detailed validation information")] = False,
         verbose: Annotated[bool, Option("--verbose", "-v", help="Show detailed validation information")] = False,
         semantic: Annotated[
         semantic: Annotated[
             bool,
             bool,
             Option(
             Option(
-                "--semantic",
-                help="Enable dependency-matrix semantic validation",
+                "--semantic/--no-semantic",
+                help="Enable semantic validation for rendered files",
+            ),
+        ] = True,
+        matrix: Annotated[
+            bool,
+            Option(
+                "--matrix",
+                help="Validate all reachable dependency states for a single template",
             ),
             ),
         ] = False,
         ] = False,
         kind: Annotated[
         kind: Annotated[
@@ -282,17 +289,17 @@ class Module(ABC):
             ),
             ),
         ] = False,
         ] = False,
     ) -> None:
     ) -> None:
-        """Validate templates for syntax, rendered semantics, and optional kind-specific checks.
+        """Validate templates for syntax, rendered semantics, and optional dependency matrix checks.
 
 
         Examples:
         Examples:
             # Validate specific template
             # Validate specific template
             cli terraform validate cloudflare-dns-record
             cli terraform validate cloudflare-dns-record
 
 
             # Validate all templates
             # Validate all templates
-            cli terraform validate --all
+            cli terraform validate
 
 
             # Validate rendered semantic and kind-specific matrix cases
             # Validate rendered semantic and kind-specific matrix cases
-            cli terraform validate cloudflare-dns-record --semantic --kind
+            cli terraform validate cloudflare-dns-record --matrix --kind
         """
         """
         return validate_templates(
         return validate_templates(
             self,
             self,
@@ -301,6 +308,7 @@ class Module(ABC):
             ValidationConfig(
             ValidationConfig(
                 verbose=verbose,
                 verbose=verbose,
                 semantic=semantic,
                 semantic=semantic,
+                matrix=matrix,
                 kind=kind,
                 kind=kind,
                 all_templates=all_templates,
                 all_templates=all_templates,
                 kind_validator=self.kind_validator_class(verbose).validate_rendered_files
                 kind_validator=self.kind_validator_class(verbose).validate_rendered_files

+ 30 - 6
cli/modules/compose/__init__.py

@@ -24,7 +24,7 @@ class ComposeModule(Module):
         self,
         self,
         template_id: Annotated[
         template_id: Annotated[
             str | None,
             str | None,
-            Argument(help="Template ID to validate"),
+            Argument(help="Template ID to validate (omit to validate all templates)"),
         ] = None,
         ] = None,
         *,
         *,
         path: Annotated[
         path: Annotated[
@@ -33,14 +33,21 @@ class ComposeModule(Module):
         ] = None,
         ] = None,
         all_templates: Annotated[
         all_templates: Annotated[
             bool,
             bool,
-            Option("--all", help="Validate all Compose templates"),
+            Option("--all", help="Validate all Compose templates (default when no template ID is provided)"),
         ] = False,
         ] = False,
         verbose: Annotated[bool, Option("--verbose", "-v", help="Show detailed validation information")] = False,
         verbose: Annotated[bool, Option("--verbose", "-v", help="Show detailed validation information")] = False,
         semantic: Annotated[
         semantic: Annotated[
             bool,
             bool,
             Option(
             Option(
-                "--semantic",
-                help="Enable dependency-matrix semantic validation",
+                "--semantic/--no-semantic",
+                help="Enable semantic validation for rendered files",
+            ),
+        ] = True,
+        matrix: Annotated[
+            bool,
+            Option(
+                "--matrix",
+                help="Validate all reachable dependency states for a single template",
             ),
             ),
         ] = False,
         ] = False,
         kind: Annotated[
         kind: Annotated[
@@ -50,9 +57,25 @@ class ComposeModule(Module):
                 help="Enable dependency-matrix Docker Compose validation",
                 help="Enable dependency-matrix Docker Compose validation",
             ),
             ),
         ] = False,
         ] = False,
+        docker: Annotated[
+            bool,
+            Option(
+                "--docker/--no-docker",
+                help="Alias for --kind Docker Compose validation",
+            ),
+        ] = False,
+        docker_test_all: Annotated[
+            bool,
+            Option(
+                "--docker-test-all",
+                help="Alias for --matrix --kind Docker Compose validation. Requires --docker.",
+            ),
+        ] = False,
     ) -> None:
     ) -> None:
         """Validate Compose templates."""
         """Validate Compose templates."""
-        kind_validator = self.kind_validator_class(verbose).validate_rendered_files if kind else None
+        kind_enabled = kind or docker or docker_test_all
+        matrix_enabled = matrix or docker_test_all
+        kind_validator = self.kind_validator_class(verbose).validate_rendered_files if kind_enabled else None
         validate_templates(
         validate_templates(
             self,
             self,
             template_id,
             template_id,
@@ -60,7 +83,8 @@ class ComposeModule(Module):
             ValidationConfig(
             ValidationConfig(
                 verbose=verbose,
                 verbose=verbose,
                 semantic=semantic,
                 semantic=semantic,
-                kind=kind,
+                matrix=matrix_enabled,
+                kind=kind_enabled,
                 all_templates=all_templates,
                 all_templates=all_templates,
                 kind_validator=kind_validator,
                 kind_validator=kind_validator,
             ),
             ),

+ 30 - 6
cli/modules/swarm/__init__.py

@@ -24,7 +24,7 @@ class SwarmModule(Module):
         self,
         self,
         template_id: Annotated[
         template_id: Annotated[
             str | None,
             str | None,
-            Argument(help="Template ID to validate"),
+            Argument(help="Template ID to validate (omit to validate all templates)"),
         ] = None,
         ] = None,
         *,
         *,
         path: Annotated[
         path: Annotated[
@@ -33,14 +33,21 @@ class SwarmModule(Module):
         ] = None,
         ] = None,
         all_templates: Annotated[
         all_templates: Annotated[
             bool,
             bool,
-            Option("--all", help="Validate all Swarm templates"),
+            Option("--all", help="Validate all Swarm templates (default when no template ID is provided)"),
         ] = False,
         ] = False,
         verbose: Annotated[bool, Option("--verbose", "-v", help="Show detailed validation information")] = False,
         verbose: Annotated[bool, Option("--verbose", "-v", help="Show detailed validation information")] = False,
         semantic: Annotated[
         semantic: Annotated[
             bool,
             bool,
             Option(
             Option(
-                "--semantic",
-                help="Enable dependency-matrix semantic validation",
+                "--semantic/--no-semantic",
+                help="Enable semantic validation for rendered files",
+            ),
+        ] = True,
+        matrix: Annotated[
+            bool,
+            Option(
+                "--matrix",
+                help="Validate all reachable dependency states for a single template",
             ),
             ),
         ] = False,
         ] = False,
         kind: Annotated[
         kind: Annotated[
@@ -50,9 +57,25 @@ class SwarmModule(Module):
                 help="Enable dependency-matrix Docker Compose validation",
                 help="Enable dependency-matrix Docker Compose validation",
             ),
             ),
         ] = False,
         ] = False,
+        docker: Annotated[
+            bool,
+            Option(
+                "--docker/--no-docker",
+                help="Alias for --kind Docker Compose validation",
+            ),
+        ] = False,
+        docker_test_all: Annotated[
+            bool,
+            Option(
+                "--docker-test-all",
+                help="Alias for --matrix --kind Docker Compose validation. Requires --docker.",
+            ),
+        ] = False,
     ) -> None:
     ) -> None:
         """Validate Swarm templates."""
         """Validate Swarm templates."""
-        kind_validator = self.kind_validator_class(verbose).validate_rendered_files if kind else None
+        kind_enabled = kind or docker or docker_test_all
+        matrix_enabled = matrix or docker_test_all
+        kind_validator = self.kind_validator_class(verbose).validate_rendered_files if kind_enabled else None
         validate_templates(
         validate_templates(
             self,
             self,
             template_id,
             template_id,
@@ -60,7 +83,8 @@ class SwarmModule(Module):
             ValidationConfig(
             ValidationConfig(
                 verbose=verbose,
                 verbose=verbose,
                 semantic=semantic,
                 semantic=semantic,
-                kind=kind,
+                matrix=matrix_enabled,
+                kind=kind_enabled,
                 all_templates=all_templates,
                 all_templates=all_templates,
                 kind_validator=kind_validator,
                 kind_validator=kind_validator,
             ),
             ),

+ 75 - 1
tests/test_base_commands.py

@@ -4,7 +4,15 @@ from __future__ import annotations
 
 
 from types import SimpleNamespace
 from types import SimpleNamespace
 
 
-from cli.core.module.base_commands import GenerationConfig, apply_output_name, generate_template, list_templates
+from cli.core.module.base_commands import (
+    GenerationConfig,
+    ValidationConfig,
+    _validate_all_templates,
+    apply_output_name,
+    generate_template,
+    list_templates,
+    validate_templates,
+)
 
 
 
 
 def _noop(*_args, **_kwargs) -> None:
 def _noop(*_args, **_kwargs) -> None:
@@ -53,6 +61,16 @@ class _DisplayCapture:
         raise AssertionError("info should not be used when templates exist")
         raise AssertionError("info should not be used when templates exist")
 
 
 
 
+class _ValidationDisplayCapture(_DisplayCapture):
+    def data_table(self, *args, **kwargs) -> None:
+        del args, kwargs
+        self.lines.append("data_table")
+
+    def info(self, value: str = "", *args, **kwargs) -> None:
+        del args, kwargs
+        self.lines.append(value)
+
+
 def test_list_templates_raw_outputs_tab_separated_rows() -> None:
 def test_list_templates_raw_outputs_tab_separated_rows() -> None:
     """Raw listing should emit one tab-separated row per template."""
     """Raw listing should emit one tab-separated row per template."""
     template = SimpleNamespace(
     template = SimpleNamespace(
@@ -161,3 +179,59 @@ def test_generate_template_dry_run_skips_destination_prompt_and_overwrite_check(
 
 
     assert any("boilerplate rendered successfully" in line for line in display.lines)
     assert any("boilerplate rendered successfully" in line for line in display.lines)
     assert any("preview only" in line for line in display.lines)
     assert any("preview only" in line for line in display.lines)
+
+
+def test_validate_templates_without_id_validates_all_templates(monkeypatch) -> None:
+    """Omitting the template ID should keep the legacy validate-all behavior."""
+    display = _ValidationDisplayCapture()
+    module_instance = SimpleNamespace(name="compose", display=display)
+    called = {}
+
+    def capture_validate_all(received_module, received_config):
+        called["module"] = received_module
+        called["config"] = received_config
+
+    monkeypatch.setattr("cli.core.module.base_commands._validate_all_templates", capture_validate_all)
+
+    validate_templates(module_instance, None, None, ValidationConfig(verbose=False))
+
+    assert called["module"] is module_instance
+    assert called["config"].semantic is True
+
+
+def test_validate_single_template_runs_default_semantic_validation(monkeypatch) -> None:
+    """Single-template validation should still run semantic validation by default."""
+    display = _ValidationDisplayCapture()
+    template = SimpleNamespace(id="whoami", used_variables=[], variables=SimpleNamespace())
+    module_instance = SimpleNamespace(name="compose", display=display)
+    called = {}
+
+    monkeypatch.setattr("cli.core.module.base_commands._load_template_for_validation", lambda *_args: template)
+
+    def capture_semantic(received_module, received_template, verbose):
+        called["module"] = received_module
+        called["template"] = received_template
+        called["verbose"] = verbose
+
+    monkeypatch.setattr("cli.core.module.base_commands._run_semantic_validation", capture_semantic)
+
+    validate_templates(module_instance, "whoami", None, ValidationConfig(verbose=False))
+
+    assert called == {"module": module_instance, "template": template, "verbose": False}
+
+
+def test_validate_all_templates_keeps_basic_validation_by_default(monkeypatch) -> None:
+    """Validate-all should not start rendering every template semantically unless matrix/kind is requested."""
+    display = _ValidationDisplayCapture()
+    template = SimpleNamespace(id="whoami", used_variables=[], variables=SimpleNamespace())
+    module_instance = SimpleNamespace(
+        name="compose",
+        display=display,
+        _load_all_templates=lambda: [template],
+    )
+
+    monkeypatch.setattr("cli.core.module.base_commands._run_semantic_validation", _raise_output_check)
+
+    _validate_all_templates(module_instance, ValidationConfig(verbose=False))
+
+    assert any("All templates are valid" in line for line in display.lines)