terraform/internal/refactoring/move_validate_test.go
2026-03-06 11:59:59 +01:00

553 lines
17 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package refactoring_test
import (
"strings"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/refactoring"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func TestValidateMoves(t *testing.T) {
rootCfg, instances := loadRefactoringFixture(t, "testdata/move-validate-zoo")
tests := map[string]struct {
Statements []refactoring.MoveStatement
WantError string
}{
"no move statements": {
Statements: nil,
WantError: ``,
},
"some valid statements": {
Statements: []refactoring.MoveStatement{
// This is just a grab bag of various valid cases that don't
// generate any errors at all.
makeTestMoveStmt(t,
``,
`test.nonexist1`,
`test.target1`,
),
makeTestMoveStmt(t,
`single`,
`test.nonexist1`,
`test.target1`,
),
makeTestMoveStmt(t,
``,
`test.nonexist2`,
`module.nonexist.test.nonexist2`,
),
makeTestMoveStmt(t,
``,
`module.single.test.nonexist3`,
`module.single.test.single`,
),
makeTestMoveStmt(t,
``,
`module.single.test.nonexist4`,
`test.target2`,
),
makeTestMoveStmt(t,
``,
`test.single[0]`, // valid because test.single doesn't have "count" set
`test.target3`,
),
makeTestMoveStmt(t,
``,
`test.zero_count[0]`, // valid because test.zero_count has count = 0
`test.target4`,
),
makeTestMoveStmt(t,
``,
`test.zero_count[1]`, // valid because test.zero_count has count = 0
`test.zero_count[0]`,
),
makeTestMoveStmt(t,
``,
`module.nonexist1`,
`module.target3`,
),
makeTestMoveStmt(t,
``,
`module.nonexist1[0]`,
`module.target4`,
),
makeTestMoveStmt(t,
``,
`module.single[0]`, // valid because module.single doesn't have "count" set
`module.target5`,
),
makeTestMoveStmt(t,
``,
`module.for_each["nonexist1"]`,
`module.for_each["a"]`,
),
makeTestMoveStmt(t,
``,
`module.for_each["nonexist2"]`,
`module.nonexist.module.nonexist`,
),
makeTestMoveStmt(t,
``,
`module.for_each["nonexist3"].test.single`, // valid because module.for_each doesn't currently have a "nonexist3"
`module.for_each["a"].test.single`,
),
},
WantError: ``,
},
"two statements with the same endpoints": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.a`,
`module.b`,
),
makeTestMoveStmt(t,
``,
`module.a`,
`module.b`,
),
},
WantError: ``,
},
"moving nowhere": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.a`,
`module.a`,
),
},
WantError: `Redundant move statement: This statement declares a move from module.a to the same address, which is the same as not declaring this move at all.`,
},
"cyclic chain": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.a`,
`module.b`,
),
makeTestMoveStmt(t,
``,
`module.b`,
`module.c`,
),
makeTestMoveStmt(t,
``,
`module.c`,
`module.a`,
),
},
WantError: `Cyclic dependency in move statements: The following chained move statements form a cycle, and so there is no final location to move objects to:
- test:1,1: module.a[*] → module.b[*]
- test:1,1: module.b[*] → module.c[*]
- test:1,1: module.c[*] → module.a[*]
A chain of move statements must end with an address that doesn't appear in any other statements, and which typically also refers to an object still declared in the configuration.`,
},
"module.single as a call still exists in configuration": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.single`,
`module.other`,
),
},
WantError: `Moved object still exists: This statement declares a move from module.single, but that module call is still declared at testdata/move-validate-zoo/move-validate-root.tf:6,1.
Change your configuration so that this call will be declared as module.other instead.`,
},
"module.single as an instance still exists in configuration": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.single`,
`module.other[0]`,
),
},
WantError: `Moved object still exists: This statement declares a move from module.single, but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:6,1.
Change your configuration so that this instance will be declared as module.other[0] instead.`,
},
"module.count[0] still exists in configuration": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.count[0]`,
`module.other`,
),
},
WantError: `Moved object still exists: This statement declares a move from module.count[0], but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:12,12.
Change your configuration so that this instance will be declared as module.other instead.`,
},
`module.for_each["a"] still exists in configuration`: {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.for_each["a"]`,
`module.other`,
),
},
WantError: `Moved object still exists: This statement declares a move from module.for_each["a"], but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:22,14.
Change your configuration so that this instance will be declared as module.other instead.`,
},
"test.single as a resource still exists in configuration": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.single`,
`test.other`,
),
},
WantError: `Moved object still exists: This statement declares a move from test.single, but that resource is still declared at testdata/move-validate-zoo/move-validate-root.tf:27,1.
Change your configuration so that this resource will be declared as test.other instead.`,
},
"test.single as an instance still exists in configuration": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.single`,
`test.other[0]`,
),
},
WantError: `Moved object still exists: This statement declares a move from test.single, but that resource instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:27,1.
Change your configuration so that this instance will be declared as test.other[0] instead.`,
},
"module.single.test.single as a resource still exists in configuration": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.single.test.single`,
`test.other`,
),
},
WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1.
Change your configuration so that this resource will be declared as test.other instead.`,
},
"module.single.test.single as a resource declared in module.single still exists in configuration": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
`single`,
`test.single`,
`test.other`,
),
},
WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1.
Change your configuration so that this resource will be declared as module.single.test.other instead.`,
},
"module.single.test.single as an instance still exists in configuration": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.single.test.single`,
`test.other[0]`,
),
},
WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource instance is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1.
Change your configuration so that this instance will be declared as test.other[0] instead.`,
},
"module.count[0].test.single still exists in configuration": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.count[0].test.single`,
`test.other`,
),
},
WantError: `Moved object still exists: This statement declares a move from module.count[0].test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1.
Change your configuration so that this resource will be declared as test.other instead.`,
},
"two different moves from test.nonexist": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.nonexist`,
`test.other1`,
),
makeTestMoveStmt(t,
``,
`test.nonexist`,
`test.other2`,
),
},
WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.nonexist moved to test.other1, but this statement instead declares that it moved to test.other2.
Each resource can move to only one destination resource.`,
},
"two different moves to test.single": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.other1`,
`test.single`,
),
makeTestMoveStmt(t,
``,
`test.other2`,
`test.single`,
),
},
WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.other1 moved to test.single, but this statement instead declares that test.other2 moved there.
Each resource can have moved from only one source resource.`,
},
"two different moves to module.count[0].test.single across two modules": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.other1`,
`module.count[0].test.single`,
),
makeTestMoveStmt(t,
`count`,
`test.other2`,
`test.single`,
),
},
WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.other1 moved to module.count[0].test.single, but this statement instead declares that module.count[0].test.other2 moved there.
Each resource can have moved from only one source resource.`,
},
"move from resource in another module package": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.fake_external.test.thing`,
`test.thing`,
),
},
WantError: ``,
},
"move to resource in another module package": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.thing`,
`module.fake_external.test.thing`,
),
},
WantError: ``,
},
"move from module call in another module package": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.fake_external.module.a`,
`module.b`,
),
},
WantError: ``,
},
"move to module call in another module package": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.a`,
`module.fake_external.module.b`,
),
},
WantError: ``,
},
"implied move from resource in another module package": {
Statements: []refactoring.MoveStatement{
makeTestImpliedMoveStmt(t,
``,
`module.fake_external.test.thing`,
`test.thing`,
),
},
// Implied move statements are not subject to the cross-package restriction
WantError: ``,
},
"implied move to resource in another module package": {
Statements: []refactoring.MoveStatement{
makeTestImpliedMoveStmt(t,
``,
`test.thing`,
`module.fake_external.test.thing`,
),
},
// Implied move statements are not subject to the cross-package restriction
WantError: ``,
},
"implied move from module call in another module package": {
Statements: []refactoring.MoveStatement{
makeTestImpliedMoveStmt(t,
``,
`module.fake_external.module.a`,
`module.b`,
),
},
// Implied move statements are not subject to the cross-package restriction
WantError: ``,
},
"implied move to module call in another module package": {
Statements: []refactoring.MoveStatement{
makeTestImpliedMoveStmt(t,
``,
`module.a`,
`module.fake_external.module.b`,
),
},
// Implied move statements are not subject to the cross-package restriction
WantError: ``,
},
"move to a call that refers to another module package": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.nonexist`,
`module.fake_external`,
),
},
WantError: ``, // This is okay because the call itself is not considered to be inside the package it refers to
},
"move to instance of a call that refers to another module package": {
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.nonexist`,
`module.fake_external[0]`,
),
},
WantError: ``, // This is okay because the call itself is not considered to be inside the package it refers to
},
"crossing nested statements": {
// overlapping nested moves will result in a cycle.
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t, ``,
`module.nonexist.test.single`,
`module.count[0].test.count[0]`,
),
makeTestMoveStmt(t, ``,
`module.nonexist`,
`module.count[0]`,
),
},
WantError: `Cyclic dependency in move statements: The following chained move statements form a cycle, and so there is no final location to move objects to:
- test:1,1: module.nonexist → module.count[0]
- test:1,1: module.nonexist.test.single → module.count[0].test.count[0]
A chain of move statements must end with an address that doesn't appear in any other statements, and which typically also refers to an object still declared in the configuration.`,
},
"fully contained nested statements": {
// we have to avoid a cycle because the nested moves appear in both
// the from and to address of the parent when only the module index
// is changing.
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t, `count`,
`test.count`,
`test.count[0]`,
),
makeTestMoveStmt(t, ``,
`module.count`,
`module.count[0]`,
),
},
},
"double fully contained nested statements": {
// we have to avoid a cycle because the nested moves appear in both
// the from and to address of the parent when only the module index
// is changing.
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t, `count`,
`module.count`,
`module.count[0]`,
),
makeTestMoveStmt(t, `count.count`,
`test.count`,
`test.count[0]`,
),
makeTestMoveStmt(t, ``,
`module.count`,
`module.count[0]`,
),
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
gotDiags := refactoring.ValidateMoves(test.Statements, rootCfg, instances)
switch {
case test.WantError != "":
if !gotDiags.HasErrors() {
t.Fatalf("unexpected success\nwant error: %s", test.WantError)
}
if got, want := gotDiags.Err().Error(), test.WantError; got != want {
t.Fatalf("wrong error\ngot error: %s\nwant error: %s", got, want)
}
default:
if gotDiags.HasErrors() {
t.Fatalf("unexpected error\ngot error: %s", gotDiags.Err().Error())
}
}
})
}
}
func makeTestMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) refactoring.MoveStatement {
t.Helper()
module := addrs.RootModule
if moduleStr != "" {
module = addrs.Module(strings.Split(moduleStr, "."))
}
traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(fromStr), "", hcl.InitialPos)
if hclDiags.HasErrors() {
t.Fatalf("invalid from address: %s", hclDiags.Error())
}
fromEP, diags := addrs.ParseMoveEndpoint(traversal)
if diags.HasErrors() {
t.Fatalf("invalid from address: %s", diags.Err().Error())
}
traversal, hclDiags = hclsyntax.ParseTraversalAbs([]byte(toStr), "", hcl.InitialPos)
if hclDiags.HasErrors() {
t.Fatalf("invalid to address: %s", hclDiags.Error())
}
toEP, diags := addrs.ParseMoveEndpoint(traversal)
if diags.HasErrors() {
t.Fatalf("invalid to address: %s", diags.Err().Error())
}
fromInModule, toInModule := addrs.UnifyMoveEndpoints(module, fromEP, toEP)
if fromInModule == nil || toInModule == nil {
t.Fatalf("incompatible move endpoints")
}
return refactoring.MoveStatement{
From: fromInModule,
To: toInModule,
DeclRange: tfdiags.SourceRange{
Filename: "test",
Start: tfdiags.SourcePos{Line: 1, Column: 1},
End: tfdiags.SourcePos{Line: 1, Column: 1},
},
}
}
func makeTestImpliedMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) refactoring.MoveStatement {
t.Helper()
ret := makeTestMoveStmt(t, moduleStr, fromStr, toStr)
ret.Implied = true
return ret
}