Created IAC reverse generator
This commit is contained in:
405
tests/unit/test_variable_extractor.py
Normal file
405
tests/unit/test_variable_extractor.py
Normal file
@@ -0,0 +1,405 @@
|
||||
"""Unit tests for the VariableExtractor."""
|
||||
|
||||
import pytest
|
||||
|
||||
from iac_reverse.models import (
|
||||
CpuArchitecture,
|
||||
DiscoveredResource,
|
||||
ExtractedVariable,
|
||||
PlatformCategory,
|
||||
ProviderType,
|
||||
)
|
||||
from iac_reverse.generator import VariableExtractor
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def make_resource(
|
||||
resource_type: str = "kubernetes_deployment",
|
||||
unique_id: str = "default/deployments/nginx",
|
||||
name: str = "nginx",
|
||||
provider: ProviderType = ProviderType.KUBERNETES,
|
||||
platform_category: PlatformCategory = PlatformCategory.CONTAINER_ORCHESTRATION,
|
||||
architecture: CpuArchitecture = CpuArchitecture.AARCH64,
|
||||
attributes: dict | None = None,
|
||||
) -> DiscoveredResource:
|
||||
"""Create a sample DiscoveredResource for testing."""
|
||||
return DiscoveredResource(
|
||||
resource_type=resource_type,
|
||||
unique_id=unique_id,
|
||||
name=name,
|
||||
provider=provider,
|
||||
platform_category=platform_category,
|
||||
architecture=architecture,
|
||||
endpoint="https://k8s-api.local:6443",
|
||||
attributes=attributes or {},
|
||||
raw_references=[],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: No shared values produces no variables
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestNoSharedValues:
|
||||
"""Tests that no variables are produced when values are not shared."""
|
||||
|
||||
def test_empty_resources_produces_no_variables(self):
|
||||
"""An empty resource list produces no variables."""
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([])
|
||||
assert result == []
|
||||
|
||||
def test_single_resource_produces_no_variables(self):
|
||||
"""A single resource cannot have shared values."""
|
||||
resource = make_resource(attributes={"namespace": "default", "replicas": 3})
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([resource])
|
||||
assert result == []
|
||||
|
||||
def test_two_resources_no_common_values_produces_no_variables(self):
|
||||
"""Two resources with completely different attribute values produce no variables."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1",
|
||||
name="app-a",
|
||||
attributes={"namespace": "alpha", "replicas": 1},
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2",
|
||||
name="app-b",
|
||||
attributes={"namespace": "beta", "replicas": 2},
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2])
|
||||
assert result == []
|
||||
|
||||
def test_two_resources_same_key_different_values_no_variable(self):
|
||||
"""Two resources with the same key but different values produce no variable."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1",
|
||||
name="app-a",
|
||||
attributes={"environment": "staging"},
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2",
|
||||
name="app-b",
|
||||
attributes={"environment": "production"},
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2])
|
||||
assert result == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: Value appearing in 2 resources produces a variable
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSharedValueExtraction:
|
||||
"""Tests that shared values are correctly extracted as variables."""
|
||||
|
||||
def test_same_value_in_two_resources_produces_variable(self):
|
||||
"""A value appearing in 2 resources for the same key produces a variable."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1",
|
||||
name="app-a",
|
||||
attributes={"namespace": "production"},
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2",
|
||||
name="app-b",
|
||||
attributes={"namespace": "production"},
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "var_namespace"
|
||||
|
||||
def test_same_value_in_three_resources_produces_one_variable(self):
|
||||
"""A value appearing in 3 resources still produces exactly one variable."""
|
||||
resources = [
|
||||
make_resource(
|
||||
unique_id=f"r{i}",
|
||||
name=f"app-{i}",
|
||||
attributes={"region": "us-east-1"},
|
||||
)
|
||||
for i in range(3)
|
||||
]
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables(resources)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "var_region"
|
||||
|
||||
def test_multiple_shared_keys_produce_multiple_variables(self):
|
||||
"""Multiple shared attribute keys each produce their own variable."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1",
|
||||
name="app-a",
|
||||
attributes={"namespace": "default", "environment": "prod"},
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2",
|
||||
name="app-b",
|
||||
attributes={"namespace": "default", "environment": "prod"},
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2])
|
||||
|
||||
var_names = {v.name for v in result}
|
||||
assert "var_namespace" in var_names
|
||||
assert "var_environment" in var_names
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: Default is set to most common value
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDefaultValue:
|
||||
"""Tests that the default value is set to the most common value."""
|
||||
|
||||
def test_default_is_most_common_value(self):
|
||||
"""When only one value is shared (2+ resources), default is that value."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1", name="app-a", attributes={"namespace": "production"}
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2", name="app-b", attributes={"namespace": "production"}
|
||||
)
|
||||
r3 = make_resource(
|
||||
unique_id="r3", name="app-c", attributes={"namespace": "staging"}
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2, r3])
|
||||
|
||||
# "production" appears in 2 resources, "staging" in 1 (not shared)
|
||||
# The variable for "production" should have default = "production"
|
||||
assert len(result) == 1
|
||||
assert result[0].default_value == '"production"'
|
||||
|
||||
def test_default_with_equal_counts_picks_one(self):
|
||||
"""When values have equal counts, each variable gets its own value as default."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1", name="app-a", attributes={"namespace": "alpha"}
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2", name="app-b", attributes={"namespace": "alpha"}
|
||||
)
|
||||
r3 = make_resource(
|
||||
unique_id="r3", name="app-c", attributes={"namespace": "beta"}
|
||||
)
|
||||
r4 = make_resource(
|
||||
unique_id="r4", name="app-d", attributes={"namespace": "beta"}
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2, r3, r4])
|
||||
|
||||
# Both "alpha" and "beta" appear in 2 resources each
|
||||
# Both should produce variables; each with its own value as default
|
||||
assert len(result) == 2
|
||||
defaults = {v.default_value for v in result}
|
||||
assert '"alpha"' in defaults
|
||||
assert '"beta"' in defaults
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: Type expression matches value type
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestTypeExpression:
|
||||
"""Tests that type expressions match the Python value type."""
|
||||
|
||||
def test_string_value_has_string_type(self):
|
||||
"""A string attribute value produces type = string."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1", name="app-a", attributes={"namespace": "default"}
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2", name="app-b", attributes={"namespace": "default"}
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2])
|
||||
|
||||
assert result[0].type_expr == "string"
|
||||
|
||||
def test_integer_value_has_number_type(self):
|
||||
"""An integer attribute value produces type = number."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1", name="app-a", attributes={"replicas": 3}
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2", name="app-b", attributes={"replicas": 3}
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2])
|
||||
|
||||
assert result[0].type_expr == "number"
|
||||
|
||||
def test_boolean_value_has_bool_type(self):
|
||||
"""A boolean attribute value produces type = bool."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1", name="app-a", attributes={"enabled": True}
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2", name="app-b", attributes={"enabled": True}
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2])
|
||||
|
||||
assert result[0].type_expr == "bool"
|
||||
|
||||
def test_number_default_format(self):
|
||||
"""A number default is formatted without quotes."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1", name="app-a", attributes={"replicas": 3}
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2", name="app-b", attributes={"replicas": 3}
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2])
|
||||
|
||||
assert result[0].default_value == "3"
|
||||
|
||||
def test_bool_default_format(self):
|
||||
"""A boolean default is formatted as true/false."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1", name="app-a", attributes={"enabled": False}
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2", name="app-b", attributes={"enabled": False}
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2])
|
||||
|
||||
assert result[0].default_value == "false"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: used_by lists all resources using the variable
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestUsedBy:
|
||||
"""Tests that used_by correctly lists all resources using the variable."""
|
||||
|
||||
def test_used_by_contains_both_resource_ids(self):
|
||||
"""used_by lists both resource unique_ids that share the value."""
|
||||
r1 = make_resource(
|
||||
unique_id="ns/deployments/app-a",
|
||||
name="app-a",
|
||||
attributes={"namespace": "production"},
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="ns/deployments/app-b",
|
||||
name="app-b",
|
||||
attributes={"namespace": "production"},
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2])
|
||||
|
||||
assert len(result) == 1
|
||||
assert "ns/deployments/app-a" in result[0].used_by
|
||||
assert "ns/deployments/app-b" in result[0].used_by
|
||||
|
||||
def test_used_by_contains_all_three_resource_ids(self):
|
||||
"""used_by lists all three resource unique_ids when value is shared by 3."""
|
||||
resources = [
|
||||
make_resource(
|
||||
unique_id=f"ns/deployments/app-{i}",
|
||||
name=f"app-{i}",
|
||||
attributes={"environment": "prod"},
|
||||
)
|
||||
for i in range(3)
|
||||
]
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables(resources)
|
||||
|
||||
assert len(result[0].used_by) == 3
|
||||
for i in range(3):
|
||||
assert f"ns/deployments/app-{i}" in result[0].used_by
|
||||
|
||||
def test_used_by_excludes_resources_with_different_value(self):
|
||||
"""used_by does not include resources that have a different value for the key."""
|
||||
r1 = make_resource(
|
||||
unique_id="r1", name="app-a", attributes={"namespace": "production"}
|
||||
)
|
||||
r2 = make_resource(
|
||||
unique_id="r2", name="app-b", attributes={"namespace": "production"}
|
||||
)
|
||||
r3 = make_resource(
|
||||
unique_id="r3", name="app-c", attributes={"namespace": "staging"}
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.extract_variables([r1, r2, r3])
|
||||
|
||||
# Only the "production" variable should exist (2 resources)
|
||||
assert len(result) == 1
|
||||
assert "r1" in result[0].used_by
|
||||
assert "r2" in result[0].used_by
|
||||
assert "r3" not in result[0].used_by
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: generate_variables_tf output
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGenerateVariablesTf:
|
||||
"""Tests for the variables.tf file content generation."""
|
||||
|
||||
def test_empty_variables_produces_empty_string(self):
|
||||
"""No variables produces an empty string."""
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.generate_variables_tf([])
|
||||
assert result == ""
|
||||
|
||||
def test_single_variable_produces_valid_block(self):
|
||||
"""A single variable produces a valid Terraform variable block."""
|
||||
var = ExtractedVariable(
|
||||
name="var_namespace",
|
||||
type_expr="string",
|
||||
default_value='"production"',
|
||||
description="Shared namespace value extracted from 2 resources",
|
||||
used_by=["r1", "r2"],
|
||||
)
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.generate_variables_tf([var])
|
||||
|
||||
assert 'variable "var_namespace"' in result
|
||||
assert "type = string" in result
|
||||
assert 'description = "Shared namespace value extracted from 2 resources"' in result
|
||||
assert 'default = "production"' in result
|
||||
|
||||
def test_multiple_variables_separated_by_blank_line(self):
|
||||
"""Multiple variables are separated by blank lines."""
|
||||
vars_list = [
|
||||
ExtractedVariable(
|
||||
name="var_namespace",
|
||||
type_expr="string",
|
||||
default_value='"default"',
|
||||
description="Shared namespace",
|
||||
used_by=["r1", "r2"],
|
||||
),
|
||||
ExtractedVariable(
|
||||
name="var_replicas",
|
||||
type_expr="number",
|
||||
default_value="3",
|
||||
description="Shared replicas",
|
||||
used_by=["r1", "r2"],
|
||||
),
|
||||
]
|
||||
extractor = VariableExtractor()
|
||||
result = extractor.generate_variables_tf(vars_list)
|
||||
|
||||
assert 'variable "var_namespace"' in result
|
||||
assert 'variable "var_replicas"' in result
|
||||
# Two blocks separated by a blank line
|
||||
assert "\n\n" in result
|
||||
Reference in New Issue
Block a user