add new tests

ast-hclwrite-migration
Daniel Schmidt 2 months ago
parent 4824e7c9c0
commit 4051eacfd3
No known key found for this signature in database
GPG Key ID: 377C3A4D62FBBBE2

@ -7,6 +7,7 @@ import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/hashicorp/terraform/internal/providers"
"github.com/zclconf/go-cty/cty"
@ -79,6 +80,11 @@ func (b *Block) Type() string {
return b.block.Type()
}
// SetType changes the type name of the block.
func (b *Block) SetType(typeName string) {
b.block.SetType(typeName)
}
// BlockAtPath navigates nested blocks using a cty.Path, where each
// cty.GetAttrStep matches a child block by type name. Returns nil if any
// step along the path is not found or uses an unsupported step type.
@ -151,6 +157,17 @@ func (b *Block) AddBlock(blockType string) *Block {
return &Block{block: nb}
}
// NestedBlocks returns all immediate child blocks matching blockType.
func (b *Block) NestedBlocks(blockType string) []*Block {
var result []*Block
for _, child := range b.block.Body().Blocks() {
if child.Type() == blockType {
result = append(result, &Block{block: child})
}
}
return result
}
// traversalToNames converts an hcl.Traversal to the []string format
// expected by hclwrite.Expression.RenameVariablePrefix.
func traversalToNames(t hcl.Traversal) []string {
@ -218,3 +235,11 @@ func (f *File) AddBlock(blockType string, labels []string) *Block {
func (f *File) RenameReferencePrefix(old, new hcl.Traversal) {
renameReferencesInBody(f.file.Body(), traversalToNames(old), traversalToNames(new))
}
// AppendComment appends a line comment to the end of the file body.
func (f *File) AppendComment(text string) {
f.file.Body().AppendUnstructuredTokens(hclwrite.Tokens{
{Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}},
{Type: hclsyntax.TokenComment, Bytes: []byte("# " + text + "\n")},
})
}

@ -1022,6 +1022,120 @@ resource "aws_instance" "web" {
}
}
func TestBlock_SetType(t *testing.T) {
input := `resource "aws_instance" "example" {
ami = "abc-123"
log {
enabled = true
}
metric {
enabled = false
}
}
`
f, err := ParseFile([]byte(input), "main.tf", nil)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
blocks := f.FindBlocks("resource", "aws_instance")
if len(blocks) != 1 {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
// Change nested block type from "log" to "enabled_log"
for _, nb := range blocks[0].NestedBlocks("log") {
nb.SetType("enabled_log")
}
output := string(f.Bytes())
if !strings.Contains(output, "enabled_log") {
t.Errorf("output missing enabled_log\n%s", output)
}
if strings.Contains(output, "\n log {") {
t.Errorf("output should not contain old block type 'log'\n%s", output)
}
// metric block should be unchanged
if !strings.Contains(output, "metric") {
t.Errorf("output missing unchanged metric block\n%s", output)
}
}
func TestBlock_NestedBlocks(t *testing.T) {
input := `resource "aws_instance" "example" {
ami = "abc-123"
log {
category = "audit"
}
metric {
enabled = true
}
log {
category = "request"
}
}
`
f, err := ParseFile([]byte(input), "main.tf", nil)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
blocks := f.FindBlocks("resource", "aws_instance")
if len(blocks) != 1 {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
logs := blocks[0].NestedBlocks("log")
if len(logs) != 2 {
t.Fatalf("expected 2 log blocks, got %d", len(logs))
}
for _, l := range logs {
if l.Type() != "log" {
t.Errorf("expected type 'log', got %q", l.Type())
}
}
metrics := blocks[0].NestedBlocks("metric")
if len(metrics) != 1 {
t.Fatalf("expected 1 metric block, got %d", len(metrics))
}
none := blocks[0].NestedBlocks("nonexistent")
if len(none) != 0 {
t.Errorf("expected 0 blocks for nonexistent type, got %d", len(none))
}
}
func TestFile_AppendComment(t *testing.T) {
input := `resource "aws_instance" "example" {
ami = "abc-123"
}
`
f, err := ParseFile([]byte(input), "main.tf", nil)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
f.AppendComment("FIXME: aws_instance.example is deprecated. Migrate manually.")
output := string(f.Bytes())
if !strings.Contains(output, "# FIXME: aws_instance.example is deprecated. Migrate manually.") {
t.Errorf("output missing expected comment\n%s", output)
}
// Original content should still be present
if !strings.Contains(output, `ami = "abc-123"`) {
t.Errorf("output missing original content\n%s", output)
}
}
func TestFile_RenameReferencePrefix(t *testing.T) {
tests := map[string]struct {
input string

@ -0,0 +1,292 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package ast
import (
"testing"
)
// TestMigrationPatterns_NestedBlockRename exercises realistic provider migration
// scenarios where a nested block type is renamed. Each test case represents a
// category of breaking change observed across real Terraform provider major
// version upgrades:
//
// - AzureRM provider v3→v4 log {} → enabled_log {}, metric {} → enabled_metric {}
// in azurerm_monitor_diagnostic_setting
//
// Each test has multiple files and resource instances with varying expression
// complexity: literals, variable/local references, ternary conditionals, and
// string interpolation mixing literals with resource references. All HCL
// comment styles (#, //, /* */) are used to verify comment preservation.
func TestMigrationPatterns_NestedBlockRename(t *testing.T) {
tests := []struct {
name string
files map[string]string
mutate func(t *testing.T, mod *Module)
want map[string]string
}{
// ── Category: Nested Block Type Rename ────────────────────────
// Real: AzureRM v4 log {} → enabled_log {}, metric {} → enabled_metric {}
// Behavior: Nested block type name changes; all content preserved
{
name: "rename_nested_block_type",
files: map[string]string{
"main.tf": `# Primary diagnostic setting with literal values
resource "test_diagnostic_setting" "example" {
name = "diag-primary" # the diagnostic name
target_resource_id = "/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/sa"
log {
category = "AuditEvent"
enabled = true // always on
}
# Second log category
log {
category = "RequestResponse"
enabled = false
}
metric {
category = "AllMetrics"
enabled = true
}
}
/* Another diagnostic setting with only log blocks */
resource "test_diagnostic_setting" "logs_only" {
name = "diag-logs" // logs only
target_resource_id = "/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm"
log {
category = "Administrative"
enabled = true # always audited
}
}
output "diag_id" {
value = test_diagnostic_setting.example.id
}
`,
"expressions.tf": `# Diagnostic setting with variable references
resource "test_diagnostic_setting" "from_ref" {
name = var.diag_name
target_resource_id = var.target_resource_id
log {
category = var.log_category // from variable
enabled = var.log_enabled
}
metric {
category = var.metric_category
enabled = var.env == "prod" ? true : false # conditional
}
}
/* Diagnostic setting with interpolated name and conditional */
resource "test_diagnostic_setting" "interpolated" {
name = "${var.project}-diag-${var.env}"
target_resource_id = "${var.resource_id_prefix}/providers/Microsoft.Storage/storageAccounts/${var.storage_name}"
log {
category = var.env == "prod" ? "AuditEvent" : "RequestResponse" // env-based
enabled = var.audit_enabled
}
log {
category = "Policy"
enabled = var.policy_enabled
}
metric {
category = "AllMetrics"
enabled = var.metrics_enabled
}
}
`,
"complex.tf": `# Diagnostic setting with map index and nested object access
resource "test_diagnostic_setting" "map_lookup" {
name = var.diag_configs[var.env].name
target_resource_id = var.resources[var.region].id
log {
category = var.log_categories[var.env]
enabled = var.diag_config.logging.enabled
}
metric {
category = var.metric_config[var.tier].category
enabled = try(var.diag_config.metrics.enabled, true)
}
}
// Diagnostic setting with try, for expression, and nested object access
resource "test_diagnostic_setting" "complex_expr" {
name = try(var.diag_name_override, "${var.project}-diag")
target_resource_id = try(var.resource_overrides[var.region], var.default_resource_id)
log {
category = try(var.log_category_overrides[var.env], "AuditEvent")
enabled = alltrue([var.logging_enabled, var.compliance_mode])
}
/* Metrics block with for-derived value */
metric {
category = [for c in var.metric_categories : c.name if c.primary][0]
enabled = !var.metrics_disabled
}
}
`,
},
mutate: func(t *testing.T, mod *Module) {
for _, r := range mod.FindBlocks("resource", "test_diagnostic_setting") {
for _, log := range r.Block.NestedBlocks("log") {
log.SetType("enabled_log")
}
for _, metric := range r.Block.NestedBlocks("metric") {
metric.SetType("enabled_metric")
}
}
},
want: map[string]string{
"main.tf": `# Primary diagnostic setting with literal values
resource "test_diagnostic_setting" "example" {
name = "diag-primary" # the diagnostic name
target_resource_id = "/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/sa"
enabled_log {
category = "AuditEvent"
enabled = true // always on
}
# Second log category
enabled_log {
category = "RequestResponse"
enabled = false
}
enabled_metric {
category = "AllMetrics"
enabled = true
}
}
/* Another diagnostic setting with only log blocks */
resource "test_diagnostic_setting" "logs_only" {
name = "diag-logs" // logs only
target_resource_id = "/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm"
enabled_log {
category = "Administrative"
enabled = true # always audited
}
}
output "diag_id" {
value = test_diagnostic_setting.example.id
}
`,
"expressions.tf": `# Diagnostic setting with variable references
resource "test_diagnostic_setting" "from_ref" {
name = var.diag_name
target_resource_id = var.target_resource_id
enabled_log {
category = var.log_category // from variable
enabled = var.log_enabled
}
enabled_metric {
category = var.metric_category
enabled = var.env == "prod" ? true : false # conditional
}
}
/* Diagnostic setting with interpolated name and conditional */
resource "test_diagnostic_setting" "interpolated" {
name = "${var.project}-diag-${var.env}"
target_resource_id = "${var.resource_id_prefix}/providers/Microsoft.Storage/storageAccounts/${var.storage_name}"
enabled_log {
category = var.env == "prod" ? "AuditEvent" : "RequestResponse" // env-based
enabled = var.audit_enabled
}
enabled_log {
category = "Policy"
enabled = var.policy_enabled
}
enabled_metric {
category = "AllMetrics"
enabled = var.metrics_enabled
}
}
`,
"complex.tf": `# Diagnostic setting with map index and nested object access
resource "test_diagnostic_setting" "map_lookup" {
name = var.diag_configs[var.env].name
target_resource_id = var.resources[var.region].id
enabled_log {
category = var.log_categories[var.env]
enabled = var.diag_config.logging.enabled
}
enabled_metric {
category = var.metric_config[var.tier].category
enabled = try(var.diag_config.metrics.enabled, true)
}
}
// Diagnostic setting with try, for expression, and nested object access
resource "test_diagnostic_setting" "complex_expr" {
name = try(var.diag_name_override, "${var.project}-diag")
target_resource_id = try(var.resource_overrides[var.region], var.default_resource_id)
enabled_log {
category = try(var.log_category_overrides[var.env], "AuditEvent")
enabled = alltrue([var.logging_enabled, var.compliance_mode])
}
/* Metrics block with for-derived value */
enabled_metric {
category = [for c in var.metric_categories : c.name if c.primary][0]
enabled = !var.metrics_disabled
}
}
`,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var files []*File
for name, content := range tc.files {
f, err := ParseFile([]byte(content), name, nil)
if err != nil {
t.Fatalf("parsing %s: %s", name, err)
}
files = append(files, f)
}
mod := NewModule(files, "", true, nil)
tc.mutate(t, mod)
got := mod.Bytes()
for name, wantContent := range tc.want {
gotContent, ok := got[name]
if !ok {
t.Errorf("missing output file %s", name)
continue
}
if string(gotContent) != wantContent {
t.Errorf("file %s mismatch\n--- want ---\n%s\n--- got ---\n%s", name, wantContent, string(gotContent))
}
}
})
}
}

@ -0,0 +1,182 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package ast
import (
"fmt"
"testing"
)
// TestMigrationPatterns_DeprecatedResource exercises the deprecated_resource_add_fixme
// migration category: resources that cannot be auto-migrated are left in place and
// a FIXME comment is appended to the file to signal that manual intervention is required.
//
// Real-world examples:
// - GCP Cloud Source Repos deprecated → Secure Source Manager (no automated path)
// - AWS EC2-Classic resources removed in favour of VPC-only equivalents
func TestMigrationPatterns_DeprecatedResource(t *testing.T) {
tests := []struct {
name string
files map[string]string
mutate func(t *testing.T, mod *Module)
want map[string]string
}{
// ── Category: Deprecated Resource Add FIXME ───────────────────
// Real: GCP Cloud Source Repos deprecated → Secure Source Manager (manual),
// AWS EC2-Classic resources removed
// Behavior: resource is left unchanged; a # FIXME: comment is appended to
// the end of each file that contains at least one matching resource block.
{
name: "deprecated_resource_add_fixme",
files: map[string]string{
"main.tf": `# Source repository with literal project and name
resource "test_source_repo" "example" {
project = "my-project" # the GCP project
name = "my-repo"
}
// Second repo in the same project
resource "test_source_repo" "from_local" {
project = "my-project"
name = local.secondary_repo_name
}
/* Reference the repos to prove surrounding code is preserved */
output "repo_id" {
value = test_source_repo.example.id
}
`,
"expressions.tf": `# Repo with conditional project
resource "test_source_repo" "conditional" {
project = var.env == "prod" ? var.prod_project : var.dev_project // env-based
name = "shared-repo"
}
/* Repo with string interpolation mixing literal and resource reference */
resource "test_source_repo" "interpolated" {
project = "${var.org}-${var.env}"
name = "${test_random_id.suffix.hex}-repo"
}
// Repo driven entirely by variable references
resource "test_source_repo" "from_ref" {
project = var.project
name = var.repo_name
}
`,
"complex.tf": `# Repo with map index and nested object access
resource "test_source_repo" "map_lookup" {
project = var.projects[var.env]
name = var.repo_config[var.env].name
}
// Repo with try, for expression, and nested object access
resource "test_source_repo" "complex_expr" {
project = try(var.project_overrides[var.region], var.default_project)
name = coalesce(var.repo_name_override, var.config.source.repo_name)
labels = { for k, v in var.base_labels : k => v if v != "" }
}
`,
},
mutate: func(t *testing.T, mod *Module) {
for _, r := range mod.FindBlocks("resource", "test_source_repo") {
labels := r.Block.Labels()
r.File.AppendComment(fmt.Sprintf("FIXME: %s.%s is deprecated. Migrate to test_secure_source manually.", labels[0], labels[1]))
}
},
want: map[string]string{
"main.tf": `# Source repository with literal project and name
resource "test_source_repo" "example" {
project = "my-project" # the GCP project
name = "my-repo"
}
// Second repo in the same project
resource "test_source_repo" "from_local" {
project = "my-project"
name = local.secondary_repo_name
}
/* Reference the repos to prove surrounding code is preserved */
output "repo_id" {
value = test_source_repo.example.id
}
# FIXME: test_source_repo.example is deprecated. Migrate to test_secure_source manually.
# FIXME: test_source_repo.from_local is deprecated. Migrate to test_secure_source manually.
`,
"expressions.tf": `# Repo with conditional project
resource "test_source_repo" "conditional" {
project = var.env == "prod" ? var.prod_project : var.dev_project // env-based
name = "shared-repo"
}
/* Repo with string interpolation mixing literal and resource reference */
resource "test_source_repo" "interpolated" {
project = "${var.org}-${var.env}"
name = "${test_random_id.suffix.hex}-repo"
}
// Repo driven entirely by variable references
resource "test_source_repo" "from_ref" {
project = var.project
name = var.repo_name
}
# FIXME: test_source_repo.conditional is deprecated. Migrate to test_secure_source manually.
# FIXME: test_source_repo.interpolated is deprecated. Migrate to test_secure_source manually.
# FIXME: test_source_repo.from_ref is deprecated. Migrate to test_secure_source manually.
`,
"complex.tf": `# Repo with map index and nested object access
resource "test_source_repo" "map_lookup" {
project = var.projects[var.env]
name = var.repo_config[var.env].name
}
// Repo with try, for expression, and nested object access
resource "test_source_repo" "complex_expr" {
project = try(var.project_overrides[var.region], var.default_project)
name = coalesce(var.repo_name_override, var.config.source.repo_name)
labels = { for k, v in var.base_labels : k => v if v != "" }
}
# FIXME: test_source_repo.map_lookup is deprecated. Migrate to test_secure_source manually.
# FIXME: test_source_repo.complex_expr is deprecated. Migrate to test_secure_source manually.
`,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var files []*File
for name, content := range tc.files {
f, err := ParseFile([]byte(content), name, nil)
if err != nil {
t.Fatalf("parsing %s: %s", name, err)
}
files = append(files, f)
}
mod := NewModule(files, "", true, nil)
tc.mutate(t, mod)
got := mod.Bytes()
for name, wantContent := range tc.want {
gotContent, ok := got[name]
if !ok {
t.Errorf("missing output file %s", name)
continue
}
if string(gotContent) != wantContent {
t.Errorf("file %s mismatch\n--- want ---\n%s\n--- got ---\n%s", name, wantContent, string(gotContent))
}
}
})
}
}

@ -0,0 +1,167 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package ast
import (
"testing"
"github.com/zclconf/go-cty/cty"
)
// TestMigrationPatterns_MergeAttributes exercises the merge_attributes_into_one
// migration pattern. This covers the Azure SQL provider upgrade where the
// separate edition and requested_service_objective_name attributes were
// consolidated into a single sku_name attribute (e.g. "S0", "P1", "Basic").
func TestMigrationPatterns_MergeAttributes(t *testing.T) {
tests := []struct {
name string
files map[string]string
mutate func(t *testing.T, mod *Module)
want map[string]string
}{
// ── Category: Merge Attributes Into One ───────────────────────
// Real: Azure SQL edition + requested_service_objective_name → sku_name
// Behavior: Remove multiple attributes, add one consolidated attribute
{
name: "merge_attributes_into_one",
files: map[string]string{
"main.tf": `# Primary SQL database with literal values
resource "test_sql_database" "primary" {
name = "primary-db"
server_name = "sql-server-01" # the target server
edition = "Standard"
requested_service_objective_name = "S0" // service tier
}
/* Secondary database on the same server */
resource "test_sql_database" "secondary" {
name = "secondary-db"
server_name = "sql-server-01"
edition = "Premium" # premium edition
requested_service_objective_name = "P1"
}
output "primary_id" {
value = test_sql_database.primary.id
}
`,
"expressions.tf": `# SQL database with conditional edition
resource "test_sql_database" "conditional" {
name = var.env == "prod" ? "prod-db" : "dev-db" // env-based name
server_name = var.server_name
edition = var.env == "prod" ? "Premium" : "Standard"
requested_service_objective_name = var.env == "prod" ? "P1" : "S0"
}
/* SQL database with string interpolation */
resource "test_sql_database" "interpolated" {
name = "${var.project}-db-${var.suffix}"
server_name = "${var.project}-server"
edition = var.db_edition
requested_service_objective_name = var.db_objective # from variable
}
`,
"complex.tf": `# SQL database with map index and nested object access
resource "test_sql_database" "map_lookup" {
name = var.databases[var.env].name
server_name = var.servers[var.env].hostname
edition = var.db_config[var.env].edition
requested_service_objective_name = var.db_config[var.env].objective
}
// SQL database with try, for expression, and nested object access
resource "test_sql_database" "complex_expr" {
name = try(var.db_overrides[var.region], "${var.project}-db")
server_name = coalesce(var.server_override, var.config.sql.server)
edition = try(var.edition_map[var.tier], "Standard")
requested_service_objective_name = [for o in var.objectives : o.name if o.tier == var.tier][0]
}
`,
},
mutate: func(t *testing.T, mod *Module) {
for _, r := range mod.FindBlocks("resource", "test_sql_database") {
r.Block.RemoveAttribute("edition")
r.Block.RemoveAttribute("requested_service_objective_name")
r.Block.SetAttributeValue("sku_name", cty.StringVal("S0"))
}
},
want: map[string]string{
"main.tf": `# Primary SQL database with literal values
resource "test_sql_database" "primary" {
name = "primary-db"
server_name = "sql-server-01" # the target server
sku_name = "S0"
}
/* Secondary database on the same server */
resource "test_sql_database" "secondary" {
name = "secondary-db"
server_name = "sql-server-01"
sku_name = "S0"
}
output "primary_id" {
value = test_sql_database.primary.id
}
`,
"expressions.tf": `# SQL database with conditional edition
resource "test_sql_database" "conditional" {
name = var.env == "prod" ? "prod-db" : "dev-db" // env-based name
server_name = var.server_name
sku_name = "S0"
}
/* SQL database with string interpolation */
resource "test_sql_database" "interpolated" {
name = "${var.project}-db-${var.suffix}"
server_name = "${var.project}-server"
sku_name = "S0"
}
`,
"complex.tf": `# SQL database with map index and nested object access
resource "test_sql_database" "map_lookup" {
name = var.databases[var.env].name
server_name = var.servers[var.env].hostname
sku_name = "S0"
}
// SQL database with try, for expression, and nested object access
resource "test_sql_database" "complex_expr" {
name = try(var.db_overrides[var.region], "${var.project}-db")
server_name = coalesce(var.server_override, var.config.sql.server)
sku_name = "S0"
}
`,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var files []*File
for name, content := range tc.files {
f, err := ParseFile([]byte(content), name, nil)
if err != nil {
t.Fatalf("parsing %s: %s", name, err)
}
files = append(files, f)
}
mod := NewModule(files, "", true, nil)
tc.mutate(t, mod)
got := mod.Bytes()
for name, wantContent := range tc.want {
gotContent, ok := got[name]
if !ok {
t.Errorf("missing output file %s", name)
continue
}
if string(gotContent) != wantContent {
t.Errorf("file %s mismatch\n--- want ---\n%s\n--- got ---\n%s", name, wantContent, string(gotContent))
}
}
})
}
}

@ -0,0 +1,183 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package ast
import (
"testing"
"github.com/zclconf/go-cty/cty"
)
// TestMigrationPatterns_TypeChanges exercises realistic provider migration
// scenarios where an attribute changes type. Each test case represents a
// category of breaking change observed across real Terraform provider major
// version upgrades:
//
// - AzureRM provider v3→v4 address_prefix (string) → address_prefixes (list)
//
// Each test has multiple files and resource instances with varying expression
// complexity: literals, variable/local references, ternary conditionals, and
// string interpolation mixing literals with resource references. All HCL
// comment styles (#, //, /* */) are used to verify comment preservation.
func TestMigrationPatterns_TypeChanges(t *testing.T) {
tests := []struct {
name string
files map[string]string
mutate func(t *testing.T, mod *Module)
want map[string]string
}{
// ── Category: Scalar Attribute to List ────────────────────────
// Real: AzureRM v4 address_prefix (string) → address_prefixes ([]string)
// Behavior: Remove scalar attr, add list attr wrapping the value
{
name: "scalar_attribute_to_list",
files: map[string]string{
"main.tf": `# Primary subnet with literal CIDR
resource "test_subnet" "example" {
name = "subnet-a" # the subnet name
address_prefix = "10.0.1.0/24"
}
// Subnet referencing locals
resource "test_subnet" "from_local" {
name = local.subnet_name
address_prefix = local.subnet_cidr // local ref
}
/* Reference the subnet to prove surrounding code is preserved */
output "subnet_id" {
value = test_subnet.example.id
}
`,
"expressions.tf": `# Subnet with conditional CIDR expression
resource "test_subnet" "conditional" {
name = var.subnet_name
address_prefix = var.env == "prod" ? var.prod_cidr : var.dev_cidr // conditional
}
/* Subnet mixing string literal with resource reference */
resource "test_subnet" "interpolated" {
name = "${var.project}-subnet"
address_prefix = "10.${var.octet}.1.0/24"
}
// Subnet with variable reference
resource "test_subnet" "from_var" {
name = var.name
address_prefix = var.cidr_block # from variable
}
`,
"complex.tf": `# Subnet with map index access
resource "test_subnet" "map_lookup" {
name = var.subnets[var.env].name
address_prefix = var.cidr_map[var.region]
}
// Subnet with try and nested object access
resource "test_subnet" "complex_expr" {
name = try(var.subnet_overrides[var.env], "${var.project}-subnet")
address_prefix = try(var.override_cidr, var.network_config.subnets.primary.cidr)
}
/* Subnet with for expression and list index */
resource "test_subnet" "for_expr" {
name = tostring(var.subnet_names[0])
address_prefix = [for s in var.subnet_configs : s.cidr if s.primary][0]
}
`,
},
mutate: func(t *testing.T, mod *Module) {
for _, r := range mod.FindBlocks("resource", "test_subnet") {
r.Block.RemoveAttribute("address_prefix")
r.Block.SetAttributeValue("address_prefixes", cty.ListVal([]cty.Value{
cty.StringVal("10.0.1.0/24"),
}))
}
},
want: map[string]string{
"main.tf": `# Primary subnet with literal CIDR
resource "test_subnet" "example" {
name = "subnet-a" # the subnet name
address_prefixes = ["10.0.1.0/24"]
}
// Subnet referencing locals
resource "test_subnet" "from_local" {
name = local.subnet_name
address_prefixes = ["10.0.1.0/24"]
}
/* Reference the subnet to prove surrounding code is preserved */
output "subnet_id" {
value = test_subnet.example.id
}
`,
"expressions.tf": `# Subnet with conditional CIDR expression
resource "test_subnet" "conditional" {
name = var.subnet_name
address_prefixes = ["10.0.1.0/24"]
}
/* Subnet mixing string literal with resource reference */
resource "test_subnet" "interpolated" {
name = "${var.project}-subnet"
address_prefixes = ["10.0.1.0/24"]
}
// Subnet with variable reference
resource "test_subnet" "from_var" {
name = var.name
address_prefixes = ["10.0.1.0/24"]
}
`,
"complex.tf": `# Subnet with map index access
resource "test_subnet" "map_lookup" {
name = var.subnets[var.env].name
address_prefixes = ["10.0.1.0/24"]
}
// Subnet with try and nested object access
resource "test_subnet" "complex_expr" {
name = try(var.subnet_overrides[var.env], "${var.project}-subnet")
address_prefixes = ["10.0.1.0/24"]
}
/* Subnet with for expression and list index */
resource "test_subnet" "for_expr" {
name = tostring(var.subnet_names[0])
address_prefixes = ["10.0.1.0/24"]
}
`,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var files []*File
for name, content := range tc.files {
f, err := ParseFile([]byte(content), name, nil)
if err != nil {
t.Fatalf("parsing %s: %s", name, err)
}
files = append(files, f)
}
mod := NewModule(files, "", true, nil)
tc.mutate(t, mod)
got := mod.Bytes()
for name, wantContent := range tc.want {
gotContent, ok := got[name]
if !ok {
t.Errorf("missing output file %s", name)
continue
}
if string(gotContent) != wantContent {
t.Errorf("file %s mismatch\n--- want ---\n%s\n--- got ---\n%s", name, wantContent, string(gotContent))
}
}
})
}
}
Loading…
Cancel
Save