Selaa lähdekoodia

fix(validation): preserve default validate behavior

xcad 3 päivää sitten
vanhempi
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
 - Static template kind for technology-agnostic file and directory boilerplates (#1786)
 - 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)
 - 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)

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

@@ -18,6 +18,7 @@ from ..exceptions import (
 from ..input import InputManager
 from ..template import Template
 from ..validation import DependencyMatrixBuilder, MatrixOptions, ValidationRunner
+from ..validators import get_validator_registry
 from .generation_destination import (
     GenerationDestination,
     format_remote_destination,
@@ -61,7 +62,8 @@ class ValidationConfig:
     """Configuration for template validation."""
 
     verbose: bool
-    semantic: bool = False
+    semantic: bool = True
+    matrix: bool = False
     kind: bool = False
     all_templates: bool = False
     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")
         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)
 
     if template:
@@ -654,13 +652,17 @@ def _validate_single_template(
         if not config.quiet_success:
             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)
             return
 
+        # Semantic validation for the default rendered output.
+        if config.semantic:
+            _run_semantic_validation(module_instance, template, config.verbose)
+
         # Verbose output
         if config.verbose:
-            _display_validation_details(module_instance, template)
+            _display_validation_details(module_instance, template, config.semantic)
 
     except TemplateRenderError as e:
         module_instance.display.error(str(e), context=f"template '{template_id}'")
@@ -784,10 +786,41 @@ def _matrix_stage_status(
     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."""
     module_instance.display.info(f"\nTemplate path: {template.template_dir}")
     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:
@@ -800,7 +833,14 @@ def _validate_all_templates(module_instance, config: ValidationConfig) -> None:
 
     all_templates = module_instance._load_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:
         try:

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

@@ -255,7 +255,7 @@ class Module(ABC):
         self,
         template_id: Annotated[
             str | None,
-            Argument(help="Template ID to validate"),
+            Argument(help="Template ID to validate (omit to validate all templates)"),
         ] = None,
         *,
         path: Annotated[
@@ -264,14 +264,21 @@ class Module(ABC):
         ] = None,
         all_templates: Annotated[
             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,
         verbose: Annotated[bool, Option("--verbose", "-v", help="Show detailed validation information")] = False,
         semantic: Annotated[
             bool,
             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,
         kind: Annotated[
@@ -282,17 +289,17 @@ class Module(ABC):
             ),
         ] = False,
     ) -> None:
-        """Validate templates for syntax, rendered semantics, and optional kind-specific checks.
+        """Validate templates for syntax, rendered semantics, and optional dependency matrix checks.
 
         Examples:
             # Validate specific template
             cli terraform validate cloudflare-dns-record
 
             # Validate all templates
-            cli terraform validate --all
+            cli terraform validate
 
             # 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(
             self,
@@ -301,6 +308,7 @@ class Module(ABC):
             ValidationConfig(
                 verbose=verbose,
                 semantic=semantic,
+                matrix=matrix,
                 kind=kind,
                 all_templates=all_templates,
                 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,
         template_id: Annotated[
             str | None,
-            Argument(help="Template ID to validate"),
+            Argument(help="Template ID to validate (omit to validate all templates)"),
         ] = None,
         *,
         path: Annotated[
@@ -33,14 +33,21 @@ class ComposeModule(Module):
         ] = None,
         all_templates: Annotated[
             bool,
-            Option("--all", help="Validate all Compose templates"),
+            Option("--all", help="Validate all Compose templates (default when no template ID is provided)"),
         ] = False,
         verbose: Annotated[bool, Option("--verbose", "-v", help="Show detailed validation information")] = False,
         semantic: Annotated[
             bool,
             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,
         kind: Annotated[
@@ -50,9 +57,25 @@ class ComposeModule(Module):
                 help="Enable dependency-matrix Docker Compose validation",
             ),
         ] = 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:
         """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(
             self,
             template_id,
@@ -60,7 +83,8 @@ class ComposeModule(Module):
             ValidationConfig(
                 verbose=verbose,
                 semantic=semantic,
-                kind=kind,
+                matrix=matrix_enabled,
+                kind=kind_enabled,
                 all_templates=all_templates,
                 kind_validator=kind_validator,
             ),

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

@@ -24,7 +24,7 @@ class SwarmModule(Module):
         self,
         template_id: Annotated[
             str | None,
-            Argument(help="Template ID to validate"),
+            Argument(help="Template ID to validate (omit to validate all templates)"),
         ] = None,
         *,
         path: Annotated[
@@ -33,14 +33,21 @@ class SwarmModule(Module):
         ] = None,
         all_templates: Annotated[
             bool,
-            Option("--all", help="Validate all Swarm templates"),
+            Option("--all", help="Validate all Swarm templates (default when no template ID is provided)"),
         ] = False,
         verbose: Annotated[bool, Option("--verbose", "-v", help="Show detailed validation information")] = False,
         semantic: Annotated[
             bool,
             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,
         kind: Annotated[
@@ -50,9 +57,25 @@ class SwarmModule(Module):
                 help="Enable dependency-matrix Docker Compose validation",
             ),
         ] = 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:
         """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(
             self,
             template_id,
@@ -60,7 +83,8 @@ class SwarmModule(Module):
             ValidationConfig(
                 verbose=verbose,
                 semantic=semantic,
-                kind=kind,
+                matrix=matrix_enabled,
+                kind=kind_enabled,
                 all_templates=all_templates,
                 kind_validator=kind_validator,
             ),

+ 75 - 1
tests/test_base_commands.py

@@ -4,7 +4,15 @@ from __future__ import annotations
 
 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:
@@ -53,6 +61,16 @@ class _DisplayCapture:
         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:
     """Raw listing should emit one tab-separated row per template."""
     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("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)