| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- from __future__ import annotations
- import json
- from pathlib import Path
- from cli.core.template import Template
- from cli.core.validation import (
- AnsibleValidator,
- DependencyMatrixBuilder,
- KindValidationResult,
- MatrixOptions,
- ValidationRunner,
- )
- def _write_template(tmp_path: Path, manifest: dict, files: dict[str, str]) -> Template:
- template_dir = tmp_path / "sample-compose"
- files_dir = template_dir / "files"
- files_dir.mkdir(parents=True)
- (template_dir / "template.json").write_text(json.dumps(manifest), encoding="utf-8")
- for relative_path, content in files.items():
- output_path = files_dir / relative_path
- output_path.parent.mkdir(parents=True, exist_ok=True)
- output_path.write_text(content, encoding="utf-8")
- return Template(template_dir, library_name="test", library_type="static")
- def test_dependency_matrix_covers_bool_and_enum_branches(tmp_path: Path) -> None:
- template = _write_template(
- tmp_path,
- {
- "kind": "compose",
- "slug": "sample-compose",
- "metadata": {
- "name": "Sample",
- "description": "Sample",
- "author": "test",
- "date": "2026-01-01",
- },
- "variables": [
- {
- "name": "general",
- "title": "General",
- "items": [
- {"name": "service_name", "type": "str", "default": "app"},
- {
- "name": "network_mode",
- "type": "enum",
- "default": "bridge",
- "config": {"options": ["bridge", "host", "macvlan"]},
- },
- ],
- },
- {
- "name": "traefik",
- "title": "Traefik",
- "toggle": "traefik_enabled",
- "needs": "network_mode=bridge,macvlan",
- "items": [
- {"name": "traefik_enabled", "type": "bool", "default": False},
- {"name": "traefik_host", "type": "str", "default": "app.example.com"},
- ],
- },
- ],
- },
- {
- "compose.yaml": """
- services:
- << service_name >>:
- image: nginx:1.25.3
- <% if traefik_enabled %>
- labels:
- - traefik.http.routers.<< service_name >>.rule=Host(`<< traefik_host >>`)
- <% endif %>
- """,
- },
- )
- cases = DependencyMatrixBuilder(template, MatrixOptions(max_combinations=20)).build()
- rendered_values = [case.variables.get_satisfied_values() for case in cases]
- assert any(values.get("network_mode") == "bridge" for values in rendered_values)
- assert any(values.get("network_mode") == "macvlan" for values in rendered_values)
- assert any(values.get("traefik_enabled") is True for values in rendered_values)
- assert any(case.overrides.get("traefik_enabled") is False for case in cases)
- def test_validation_runner_reports_semantic_failure_for_matrix_case(tmp_path: Path) -> None:
- template = _write_template(
- tmp_path,
- {
- "kind": "compose",
- "slug": "broken-compose",
- "metadata": {
- "name": "Broken",
- "description": "Broken",
- "author": "test",
- "date": "2026-01-01",
- },
- "variables": [
- {
- "name": "general",
- "title": "General",
- "items": [
- {"name": "service_name", "type": "str", "default": "app"},
- {"name": "invalid_enabled", "type": "bool", "default": False},
- ],
- }
- ],
- },
- {
- "compose.yaml": """
- <% if invalid_enabled %>
- services: []
- <% else %>
- services:
- << service_name >>:
- image: nginx:1.25.3
- <% endif %>
- """,
- },
- )
- cases = DependencyMatrixBuilder(template, MatrixOptions(max_combinations=10)).build()
- summary = ValidationRunner(template, cases, semantic=True).run()
- assert not summary.ok
- assert any(failure.stage == "sem" and failure.file_path == "compose.yaml" for failure in summary.failures)
- def test_validation_runner_treats_unavailable_kind_validator_as_skip(tmp_path: Path) -> None:
- template = _write_template(
- tmp_path,
- {
- "kind": "custom",
- "slug": "custom-template",
- "metadata": {
- "name": "Custom",
- "description": "Custom",
- "author": "test",
- "date": "2026-01-01",
- },
- "variables": [
- {
- "name": "general",
- "title": "General",
- "items": [{"name": "service_name", "type": "str", "default": "app"}],
- }
- ],
- },
- {"config.yaml": "name: << service_name >>\n"},
- )
- def unavailable_validator(_rendered_files: dict[str, str], _case_name: str) -> KindValidationResult:
- return KindValidationResult(
- validator="missing-tool",
- available=False,
- details=["Required command is unavailable: missing-tool"],
- )
- cases = DependencyMatrixBuilder(template, MatrixOptions(max_combinations=10)).build()
- summary = ValidationRunner(template, cases, semantic=True, kind_validator=unavailable_validator).run()
- assert summary.ok
- assert summary.kind_available is False
- assert summary.kind_skipped_cases == {"defaults"}
- assert summary.failures == []
- def test_ansible_validator_detects_main_yml_playbook_by_hosts_key(tmp_path: Path) -> None:
- playbook = tmp_path / "main.yml"
- playbook.write_text("- name: Configure host\n hosts: all\n tasks: []\n", encoding="utf-8")
- assert AnsibleValidator._find_playbooks(tmp_path) == [playbook]
- def test_ansible_validator_classifies_missing_collection_as_dependency_resolution_failure() -> None:
- assert AnsibleValidator._is_dependency_resolution_failure("ERROR! the role 'vendor.role' was not found")
- assert AnsibleValidator._is_dependency_resolution_failure("ERROR! couldn't resolve module/action 'vendor.module'")
- assert not AnsibleValidator._is_dependency_resolution_failure("ERROR! Syntax Error while loading YAML")
|