diff --git a/internal/refactoring/move_statement_test.go b/internal/refactoring/move_statement_test.go index c80a53288a..975f026930 100644 --- a/internal/refactoring/move_statement_test.go +++ b/internal/refactoring/move_statement_test.go @@ -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 }) diff --git a/internal/refactoring/move_validate_test.go b/internal/refactoring/move_validate_test.go index e0b2ca1e30..598a2eaf41 100644 --- a/internal/refactoring/move_validate_test.go +++ b/internal/refactoring/move_validate_test.go @@ -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:///"), -} diff --git a/internal/refactoring/remove_statement_test.go b/internal/refactoring/remove_statement_test.go index 049e7aad36..e15ceaa15f 100644 --- a/internal/refactoring/remove_statement_test.go +++ b/internal/refactoring/remove_statement_test.go @@ -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()) } diff --git a/internal/refactoring/testing_helpers.go b/internal/refactoring/testing_helpers.go new file mode 100644 index 0000000000..32d7b15750 --- /dev/null +++ b/internal/refactoring/testing_helpers.go @@ -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) + } +} diff --git a/internal/refactoring/testing_test.go b/internal/refactoring/testing_test.go new file mode 100644 index 0000000000..1fab18b0b2 --- /dev/null +++ b/internal/refactoring/testing_test.go @@ -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() +}