move refactoring package to use graph config loading

pull/38226/head
Daniel Schmidt 3 months ago
parent 0c8d86989e
commit 051310751f

@ -1,7 +1,7 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package refactoring
package refactoring_test
import (
"sort"
@ -10,6 +10,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/refactoring"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
@ -114,9 +115,9 @@ func TestImpliedMoveStatements(t *testing.T) {
)
})
explicitStmts := FindMoveStatements(rootCfg)
got := ImpliedMoveStatements(rootCfg, prevRunState, explicitStmts)
want := []MoveStatement{
explicitStmts := refactoring.FindMoveStatements(rootCfg)
got := refactoring.ImpliedMoveStatements(rootCfg, prevRunState, explicitStmts)
want := []refactoring.MoveStatement{
{
From: addrs.ImpliedMoveStatementEndpoint(resourceAddr("formerly_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}),
To: addrs.ImpliedMoveStatementEndpoint(resourceAddr("formerly_count").Instance(addrs.NoKey), tfdiags.SourceRange{}),
@ -199,7 +200,7 @@ func TestImpliedMoveStatements(t *testing.T) {
sort.Slice(got, func(i, j int) bool {
// This is just an arbitrary sort to make the result consistent
// regardless of what order the ImpliedMoveStatements function
// regardless of what order the refactoring.ImpliedMoveStatements function
// visits the entries in the state/config.
return got[i].DeclRange.Start.Line < got[j].DeclRange.Start.Line
})

@ -1,25 +1,17 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package refactoring
package refactoring_test
import (
"context"
"strings"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/registry"
"github.com/hashicorp/terraform/internal/refactoring"
"github.com/hashicorp/terraform/internal/tfdiags"
)
@ -27,7 +19,7 @@ func TestValidateMoves(t *testing.T) {
rootCfg, instances := loadRefactoringFixture(t, "testdata/move-validate-zoo")
tests := map[string]struct {
Statements []MoveStatement
Statements []refactoring.MoveStatement
WantError string
}{
"no move statements": {
@ -35,7 +27,7 @@ func TestValidateMoves(t *testing.T) {
WantError: ``,
},
"some valid statements": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
// This is just a grab bag of various valid cases that don't
// generate any errors at all.
makeTestMoveStmt(t,
@ -112,7 +104,7 @@ func TestValidateMoves(t *testing.T) {
WantError: ``,
},
"two statements with the same endpoints": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.a`,
@ -127,7 +119,7 @@ func TestValidateMoves(t *testing.T) {
WantError: ``,
},
"moving nowhere": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.a`,
@ -137,7 +129,7 @@ func TestValidateMoves(t *testing.T) {
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: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.a`,
@ -162,7 +154,7 @@ func TestValidateMoves(t *testing.T) {
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: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.single`,
@ -174,7 +166,7 @@ A chain of move statements must end with an address that doesn't appear in any o
Change your configuration so that this call will be declared as module.other instead.`,
},
"module.single as an instance still exists in configuration": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.single`,
@ -186,7 +178,7 @@ Change your configuration so that this call will be declared as module.other ins
Change your configuration so that this instance will be declared as module.other[0] instead.`,
},
"module.count[0] still exists in configuration": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.count[0]`,
@ -198,7 +190,7 @@ Change your configuration so that this instance will be declared as module.other
Change your configuration so that this instance will be declared as module.other instead.`,
},
`module.for_each["a"] still exists in configuration`: {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.for_each["a"]`,
@ -210,7 +202,7 @@ Change your configuration so that this instance will be declared as module.other
Change your configuration so that this instance will be declared as module.other instead.`,
},
"test.single as a resource still exists in configuration": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.single`,
@ -222,7 +214,7 @@ Change your configuration so that this instance will be declared as module.other
Change your configuration so that this resource will be declared as test.other instead.`,
},
"test.single as an instance still exists in configuration": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.single`,
@ -234,7 +226,7 @@ Change your configuration so that this resource will be declared as test.other i
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: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.single.test.single`,
@ -246,7 +238,7 @@ Change your configuration so that this instance will be declared as test.other[0
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: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
`single`,
`test.single`,
@ -258,7 +250,7 @@ Change your configuration so that this resource will be declared as test.other i
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: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.single.test.single`,
@ -270,7 +262,7 @@ Change your configuration so that this resource will be declared as module.singl
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: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.count[0].test.single`,
@ -282,7 +274,7 @@ Change your configuration so that this instance will be declared as test.other[0
Change your configuration so that this resource will be declared as test.other instead.`,
},
"two different moves from test.nonexist": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.nonexist`,
@ -299,7 +291,7 @@ Change your configuration so that this resource will be declared as test.other i
Each resource can move to only one destination resource.`,
},
"two different moves to test.single": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.other1`,
@ -316,7 +308,7 @@ Each resource can move to only one destination resource.`,
Each resource can have moved from only one source resource.`,
},
"two different moves to module.count[0].test.single across two modules": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.other1`,
@ -333,7 +325,7 @@ Each resource can have moved from only one source resource.`,
Each resource can have moved from only one source resource.`,
},
"move from resource in another module package": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.fake_external.test.thing`,
@ -343,7 +335,7 @@ Each resource can have moved from only one source resource.`,
WantError: ``,
},
"move to resource in another module package": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`test.thing`,
@ -353,7 +345,7 @@ Each resource can have moved from only one source resource.`,
WantError: ``,
},
"move from module call in another module package": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.fake_external.module.a`,
@ -363,7 +355,7 @@ Each resource can have moved from only one source resource.`,
WantError: ``,
},
"move to module call in another module package": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.a`,
@ -373,7 +365,7 @@ Each resource can have moved from only one source resource.`,
WantError: ``,
},
"implied move from resource in another module package": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestImpliedMoveStmt(t,
``,
`module.fake_external.test.thing`,
@ -384,7 +376,7 @@ Each resource can have moved from only one source resource.`,
WantError: ``,
},
"implied move to resource in another module package": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestImpliedMoveStmt(t,
``,
`test.thing`,
@ -395,7 +387,7 @@ Each resource can have moved from only one source resource.`,
WantError: ``,
},
"implied move from module call in another module package": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestImpliedMoveStmt(t,
``,
`module.fake_external.module.a`,
@ -406,7 +398,7 @@ Each resource can have moved from only one source resource.`,
WantError: ``,
},
"implied move to module call in another module package": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestImpliedMoveStmt(t,
``,
`module.a`,
@ -417,7 +409,7 @@ Each resource can have moved from only one source resource.`,
WantError: ``,
},
"move to a call that refers to another module package": {
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.nonexist`,
@ -427,7 +419,7 @@ Each resource can have moved from only one source resource.`,
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: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t,
``,
`module.nonexist`,
@ -438,7 +430,7 @@ Each resource can have moved from only one source resource.`,
},
"crossing nested statements": {
// overlapping nested moves will result in a cycle.
Statements: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t, ``,
`module.nonexist.test.single`,
`module.count[0].test.count[0]`,
@ -458,7 +450,7 @@ A chain of move statements must end with an address that doesn't appear in any o
// 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: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t, `count`,
`test.count`,
`test.count[0]`,
@ -473,7 +465,7 @@ A chain of move statements must end with an address that doesn't appear in any o
// 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: []MoveStatement{
Statements: []refactoring.MoveStatement{
makeTestMoveStmt(t, `count`,
`module.count`,
`module.count[0]`,
@ -492,7 +484,7 @@ A chain of move statements must end with an address that doesn't appear in any o
for name, test := range tests {
t.Run(name, func(t *testing.T) {
gotDiags := ValidateMoves(test.Statements, rootCfg, instances)
gotDiags := refactoring.ValidateMoves(test.Statements, rootCfg, instances)
switch {
case test.WantError != "":
@ -511,149 +503,7 @@ A chain of move statements must end with an address that doesn't appear in any o
}
}
// loadRefactoringFixture reads a configuration from the given directory and
// does some naive static processing on any count and for_each expressions
// inside, in order to get a realistic-looking instances.Set for what it
// declares without having to run a full Terraform plan.
func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instances.Set) {
t.Helper()
loader, cleanup := configload.NewLoaderForTests(t)
defer cleanup()
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}
// Since module installer has modified the module manifest on disk, we need
// to refresh the cache of it in the loader.
if err := loader.RefreshModules(); err != nil {
t.Fatalf("failed to refresh modules after installation: %s", err)
}
// Note: This test uses BuildConfig instead of
// terraform.BuildConfigWithGraph to avoid an import cycle (terraform
// imports the refactoring package). Since this test only needs basic config
// structure without expression evaluation, the static loader is appropriate.
rootMod, diags := loader.LoadRootModule(dir)
if diags.HasErrors() {
t.Fatalf("invalid root module: %s", diags.Error())
}
rootCfg, buildDiags := configs.BuildConfig(
rootMod,
loader.ModuleWalker(),
configs.MockDataLoaderFunc(loader.LoadExternalMockData),
)
if buildDiags.HasErrors() {
t.Fatalf("invalid configuration: %s", buildDiags.Error())
}
expander := instances.NewExpander(nil)
staticPopulateExpanderModule(t, rootCfg, addrs.RootModuleInstance, expander)
return rootCfg, expander.AllInstances()
}
func staticPopulateExpanderModule(t *testing.T, rootCfg *configs.Config, moduleAddr addrs.ModuleInstance, expander *instances.Expander) {
t.Helper()
modCfg := rootCfg.DescendantForInstance(moduleAddr)
if modCfg == nil {
t.Fatalf("no configuration for %s", moduleAddr)
}
if len(modCfg.Path) > 0 && modCfg.Path[len(modCfg.Path)-1] == "fake_external" {
// As a funny special case we modify the source address of this
// module to be something that counts as a separate package,
// so we can test rules relating to crossing package boundaries
// even though we really just loaded the module from a local path.
modCfg.SourceAddr = fakeExternalModuleSource
}
for _, call := range modCfg.Module.ModuleCalls {
callAddr := addrs.ModuleCall{Name: call.Name}
if call.Name == "fake_external" {
// As a funny special case we modify the source address of this
// module to be something that counts as a separate package,
// so we can test rules relating to crossing package boundaries
// even though we really just loaded the module from a local path.
call.SourceExpr = hcltest.MockExprLiteral(cty.StringVal(fakeExternalModuleSource.String()))
}
// In order to get a valid, useful set of instances here we're going
// to just statically evaluate the count and for_each expressions.
// Normally it's valid to use references and functions there, but for
// our unit tests we'll just limit it to literal values to avoid
// bringing all of the core evaluator complexity.
switch {
case call.ForEach != nil:
val, diags := call.ForEach.Value(nil)
if diags.HasErrors() {
t.Fatalf("invalid for_each: %s", diags.Error())
}
expander.SetModuleForEach(moduleAddr, callAddr, val.AsValueMap())
case call.Count != nil:
val, diags := call.Count.Value(nil)
if diags.HasErrors() {
t.Fatalf("invalid count: %s", diags.Error())
}
var count int
err := gocty.FromCtyValue(val, &count)
if err != nil {
t.Fatalf("invalid count at %s: %s", call.Count.Range(), err)
}
expander.SetModuleCount(moduleAddr, callAddr, count)
default:
expander.SetModuleSingle(moduleAddr, callAddr)
}
// We need to recursively analyze the child modules too.
calledMod := modCfg.Path.Child(call.Name)
for _, inst := range expander.ExpandModule(calledMod, false) {
staticPopulateExpanderModule(t, rootCfg, inst, expander)
}
}
for _, rc := range modCfg.Module.ManagedResources {
staticPopulateExpanderResource(t, moduleAddr, rc, expander)
}
for _, rc := range modCfg.Module.DataResources {
staticPopulateExpanderResource(t, moduleAddr, rc, expander)
}
}
func staticPopulateExpanderResource(t *testing.T, moduleAddr addrs.ModuleInstance, rCfg *configs.Resource, expander *instances.Expander) {
t.Helper()
addr := rCfg.Addr()
switch {
case rCfg.ForEach != nil:
val, diags := rCfg.ForEach.Value(nil)
if diags.HasErrors() {
t.Fatalf("invalid for_each: %s", diags.Error())
}
expander.SetResourceForEach(moduleAddr, addr, val.AsValueMap())
case rCfg.Count != nil:
val, diags := rCfg.Count.Value(nil)
if diags.HasErrors() {
t.Fatalf("invalid count: %s", diags.Error())
}
var count int
err := gocty.FromCtyValue(val, &count)
if err != nil {
t.Fatalf("invalid count at %s: %s", rCfg.Count.Range(), err)
}
expander.SetResourceCount(moduleAddr, addr, count)
default:
expander.SetResourceSingle(moduleAddr, addr)
}
}
func makeTestMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) MoveStatement {
func makeTestMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) refactoring.MoveStatement {
t.Helper()
module := addrs.RootModule
@ -684,7 +534,7 @@ func makeTestMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) MoveStatem
t.Fatalf("incompatible move endpoints")
}
return MoveStatement{
return refactoring.MoveStatement{
From: fromInModule,
To: toInModule,
DeclRange: tfdiags.SourceRange{
@ -695,13 +545,9 @@ func makeTestMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) MoveStatem
}
}
func makeTestImpliedMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) MoveStatement {
func makeTestImpliedMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) refactoring.MoveStatement {
t.Helper()
ret := makeTestMoveStmt(t, moduleStr, fromStr, toStr)
ret.Implied = true
return ret
}
var fakeExternalModuleSource = addrs.ModuleSourceRemote{
Package: addrs.ModulePackage("fake-external:///"),
}

@ -1,7 +1,7 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package refactoring
package refactoring_test
import (
"testing"
@ -11,6 +11,7 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/refactoring"
"github.com/hashicorp/terraform/internal/tfdiags"
)
@ -59,8 +60,8 @@ func TestFindRemoveStatements(t *testing.T) {
configModuleInModule := addrs.Module{"child", "grandchild"}
want := addrs.MakeMap[addrs.ConfigMoveable, RemoveStatement](
addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configResourceBasic, RemoveStatement{
want := addrs.MakeMap[addrs.ConfigMoveable, refactoring.RemoveStatement](
addrs.MakeMapElem[addrs.ConfigMoveable, refactoring.RemoveStatement](configResourceBasic, refactoring.RemoveStatement{
From: configResourceBasic,
Destroy: false,
DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{
@ -69,7 +70,7 @@ func TestFindRemoveStatements(t *testing.T) {
End: hcl.Pos{Line: 2, Column: 8, Byte: 34},
}),
}),
addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configResourceWithModule, RemoveStatement{
addrs.MakeMapElem[addrs.ConfigMoveable, refactoring.RemoveStatement](configResourceWithModule, refactoring.RemoveStatement{
From: configResourceWithModule,
Destroy: false,
DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{
@ -78,7 +79,7 @@ func TestFindRemoveStatements(t *testing.T) {
End: hcl.Pos{Line: 10, Column: 8, Byte: 145},
}),
}),
addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configModuleBasic, RemoveStatement{
addrs.MakeMapElem[addrs.ConfigMoveable, refactoring.RemoveStatement](configModuleBasic, refactoring.RemoveStatement{
From: configModuleBasic,
Destroy: false,
DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{
@ -87,7 +88,7 @@ func TestFindRemoveStatements(t *testing.T) {
End: hcl.Pos{Line: 18, Column: 8, Byte: 260},
}),
}),
addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configResourceOverridden, RemoveStatement{
addrs.MakeMapElem[addrs.ConfigMoveable, refactoring.RemoveStatement](configResourceOverridden, refactoring.RemoveStatement{
From: configResourceOverridden,
Destroy: true,
DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{
@ -96,7 +97,7 @@ func TestFindRemoveStatements(t *testing.T) {
End: hcl.Pos{Line: 30, Column: 8, Byte: 435},
}),
}),
addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configResourceInModule, RemoveStatement{
addrs.MakeMapElem[addrs.ConfigMoveable, refactoring.RemoveStatement](configResourceInModule, refactoring.RemoveStatement{
From: configResourceInModule,
Destroy: true,
DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{
@ -105,7 +106,7 @@ func TestFindRemoveStatements(t *testing.T) {
End: hcl.Pos{Line: 10, Column: 8, Byte: 148},
}),
}),
addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configModuleInModule, RemoveStatement{
addrs.MakeMapElem[addrs.ConfigMoveable, refactoring.RemoveStatement](configModuleInModule, refactoring.RemoveStatement{
From: configModuleInModule,
Destroy: false,
DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{
@ -116,7 +117,7 @@ func TestFindRemoveStatements(t *testing.T) {
}),
)
got, diags := FindRemoveStatements(rootCfg)
got, diags := refactoring.FindRemoveStatements(rootCfg)
if diags.HasErrors() {
t.Fatal(diags.Err().Error())
}

@ -0,0 +1,109 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package refactoring
import (
"testing"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/instances"
)
// FakeExternalModuleSource is used in tests to simulate an external module source.
var FakeExternalModuleSource = addrs.ModuleSourceRemote{
Package: addrs.ModulePackage("example.com/test/fake"),
}
// StaticPopulateExpanderModule populates an expander for testing by statically
// evaluating count and for_each expressions in a configuration.
//
// This is exported so that test code in package refactoring_test can use it
// without creating an import cycle with the terraform package.
func StaticPopulateExpanderModule(t *testing.T, rootCfg *configs.Config, moduleAddr addrs.ModuleInstance, expander *instances.Expander) {
t.Helper()
modCfg := rootCfg.DescendantForInstance(moduleAddr)
if modCfg == nil {
t.Fatalf("no configuration for %s", moduleAddr)
}
if len(modCfg.Path) > 0 && modCfg.Path[len(modCfg.Path)-1] == "fake_external" {
modCfg.SourceAddr = FakeExternalModuleSource
}
for _, call := range modCfg.Module.ModuleCalls {
callAddr := addrs.ModuleCall{Name: call.Name}
if call.Name == "fake_external" {
call.SourceExpr = hcltest.MockExprLiteral(cty.StringVal(FakeExternalModuleSource.String()))
}
switch {
case call.ForEach != nil:
val, diags := call.ForEach.Value(nil)
if diags.HasErrors() {
t.Fatalf("invalid for_each: %s", diags.Error())
}
expander.SetModuleForEach(moduleAddr, callAddr, val.AsValueMap())
case call.Count != nil:
val, diags := call.Count.Value(nil)
if diags.HasErrors() {
t.Fatalf("invalid count: %s", diags.Error())
}
var count int
err := gocty.FromCtyValue(val, &count)
if err != nil {
t.Fatalf("invalid count at %s: %s", call.Count.Range(), err)
}
expander.SetModuleCount(moduleAddr, callAddr, count)
default:
expander.SetModuleSingle(moduleAddr, callAddr)
}
calledMod := modCfg.Path.Child(call.Name)
for _, inst := range expander.ExpandModule(calledMod, false) {
StaticPopulateExpanderModule(t, rootCfg, inst, expander)
}
}
for _, rc := range modCfg.Module.ManagedResources {
StaticPopulateExpanderResource(t, moduleAddr, rc, expander)
}
for _, rc := range modCfg.Module.DataResources {
StaticPopulateExpanderResource(t, moduleAddr, rc, expander)
}
}
// StaticPopulateExpanderResource populates resource instances in an expander for testing.
func StaticPopulateExpanderResource(t *testing.T, moduleAddr addrs.ModuleInstance, rCfg *configs.Resource, expander *instances.Expander) {
t.Helper()
addr := rCfg.Addr()
switch {
case rCfg.ForEach != nil:
val, diags := rCfg.ForEach.Value(nil)
if diags.HasErrors() {
t.Fatalf("invalid for_each: %s", diags.Error())
}
expander.SetResourceForEach(moduleAddr, addr, val.AsValueMap())
case rCfg.Count != nil:
val, diags := rCfg.Count.Value(nil)
if diags.HasErrors() {
t.Fatalf("invalid count: %s", diags.Error())
}
var count int
err := gocty.FromCtyValue(val, &count)
if err != nil {
t.Fatalf("invalid count at %s: %s", rCfg.Count.Range(), err)
}
expander.SetResourceCount(moduleAddr, addr, count)
default:
expander.SetResourceSingle(moduleAddr, addr)
}
}

@ -0,0 +1,58 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package refactoring_test
import (
"context"
"testing"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/refactoring"
"github.com/hashicorp/terraform/internal/registry"
"github.com/hashicorp/terraform/internal/terraform"
)
// loadRefactoringFixture reads a configuration from the given directory and
// does some naive static processing on any count and for_each expressions
// inside, in order to get a realistic-looking instances.Set for what it
// declares without having to run a full Terraform plan.
func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instances.Set) {
t.Helper()
loader, cleanup := configload.NewLoaderForTests(t)
defer cleanup()
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}
if err := loader.RefreshModules(); err != nil {
t.Fatalf("failed to refresh modules after installation: %s", err)
}
rootMod, diags := loader.LoadRootModule(dir)
if diags.HasErrors() {
t.Fatalf("invalid root module: %s", diags.Error())
}
rootCfg, buildDiags := terraform.BuildConfigWithGraph(
rootMod,
loader.ModuleWalker(),
nil,
configs.MockDataLoaderFunc(loader.LoadExternalMockData),
)
if buildDiags.HasErrors() {
t.Fatalf("invalid configuration: %s", buildDiags.Err())
}
expander := instances.NewExpander(nil)
refactoring.StaticPopulateExpanderModule(t, rootCfg, addrs.RootModuleInstance, expander)
return rootCfg, expander.AllInstances()
}
Loading…
Cancel
Save