github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/refactoring/move_validate_test.go (about)

     1  package refactoring
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  	"testing"
     7  
     8  	"github.com/hashicorp/hcl/v2"
     9  	"github.com/hashicorp/hcl/v2/hclsyntax"
    10  	"github.com/zclconf/go-cty/cty/gocty"
    11  
    12  	"github.com/hashicorp/terraform/internal/addrs"
    13  	"github.com/hashicorp/terraform/internal/configs"
    14  	"github.com/hashicorp/terraform/internal/configs/configload"
    15  	"github.com/hashicorp/terraform/internal/initwd"
    16  	"github.com/hashicorp/terraform/internal/instances"
    17  	"github.com/hashicorp/terraform/internal/registry"
    18  	"github.com/hashicorp/terraform/internal/tfdiags"
    19  )
    20  
    21  func TestValidateMoves(t *testing.T) {
    22  	rootCfg, instances := loadRefactoringFixture(t, "testdata/move-validate-zoo")
    23  
    24  	tests := map[string]struct {
    25  		Statements []MoveStatement
    26  		WantError  string
    27  	}{
    28  		"no move statements": {
    29  			Statements: nil,
    30  			WantError:  ``,
    31  		},
    32  		"some valid statements": {
    33  			Statements: []MoveStatement{
    34  				// This is just a grab bag of various valid cases that don't
    35  				// generate any errors at all.
    36  				makeTestMoveStmt(t,
    37  					``,
    38  					`test.nonexist1`,
    39  					`test.target1`,
    40  				),
    41  				makeTestMoveStmt(t,
    42  					`single`,
    43  					`test.nonexist1`,
    44  					`test.target1`,
    45  				),
    46  				makeTestMoveStmt(t,
    47  					``,
    48  					`test.nonexist2`,
    49  					`module.nonexist.test.nonexist2`,
    50  				),
    51  				makeTestMoveStmt(t,
    52  					``,
    53  					`module.single.test.nonexist3`,
    54  					`module.single.test.single`,
    55  				),
    56  				makeTestMoveStmt(t,
    57  					``,
    58  					`module.single.test.nonexist4`,
    59  					`test.target2`,
    60  				),
    61  				makeTestMoveStmt(t,
    62  					``,
    63  					`test.single[0]`, // valid because test.single doesn't have "count" set
    64  					`test.target3`,
    65  				),
    66  				makeTestMoveStmt(t,
    67  					``,
    68  					`test.zero_count[0]`, // valid because test.zero_count has count = 0
    69  					`test.target4`,
    70  				),
    71  				makeTestMoveStmt(t,
    72  					``,
    73  					`test.zero_count[1]`, // valid because test.zero_count has count = 0
    74  					`test.zero_count[0]`,
    75  				),
    76  				makeTestMoveStmt(t,
    77  					``,
    78  					`module.nonexist1`,
    79  					`module.target3`,
    80  				),
    81  				makeTestMoveStmt(t,
    82  					``,
    83  					`module.nonexist1[0]`,
    84  					`module.target4`,
    85  				),
    86  				makeTestMoveStmt(t,
    87  					``,
    88  					`module.single[0]`, // valid because module.single doesn't have "count" set
    89  					`module.target5`,
    90  				),
    91  				makeTestMoveStmt(t,
    92  					``,
    93  					`module.for_each["nonexist1"]`,
    94  					`module.for_each["a"]`,
    95  				),
    96  				makeTestMoveStmt(t,
    97  					``,
    98  					`module.for_each["nonexist2"]`,
    99  					`module.nonexist.module.nonexist`,
   100  				),
   101  				makeTestMoveStmt(t,
   102  					``,
   103  					`module.for_each["nonexist3"].test.single`, // valid because module.for_each doesn't currently have a "nonexist3"
   104  					`module.for_each["a"].test.single`,
   105  				),
   106  			},
   107  			WantError: ``,
   108  		},
   109  		"two statements with the same endpoints": {
   110  			Statements: []MoveStatement{
   111  				makeTestMoveStmt(t,
   112  					``,
   113  					`module.a`,
   114  					`module.b`,
   115  				),
   116  				makeTestMoveStmt(t,
   117  					``,
   118  					`module.a`,
   119  					`module.b`,
   120  				),
   121  			},
   122  			WantError: ``,
   123  		},
   124  		"moving nowhere": {
   125  			Statements: []MoveStatement{
   126  				makeTestMoveStmt(t,
   127  					``,
   128  					`module.a`,
   129  					`module.a`,
   130  				),
   131  			},
   132  			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.`,
   133  		},
   134  		"cyclic chain": {
   135  			Statements: []MoveStatement{
   136  				makeTestMoveStmt(t,
   137  					``,
   138  					`module.a`,
   139  					`module.b`,
   140  				),
   141  				makeTestMoveStmt(t,
   142  					``,
   143  					`module.b`,
   144  					`module.c`,
   145  				),
   146  				makeTestMoveStmt(t,
   147  					``,
   148  					`module.c`,
   149  					`module.a`,
   150  				),
   151  			},
   152  			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:
   153    - test:1,1: module.a[*] → module.b[*]
   154    - test:1,1: module.b[*] → module.c[*]
   155    - test:1,1: module.c[*] → module.a[*]
   156  
   157  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.`,
   158  		},
   159  		"module.single as a call still exists in configuration": {
   160  			Statements: []MoveStatement{
   161  				makeTestMoveStmt(t,
   162  					``,
   163  					`module.single`,
   164  					`module.other`,
   165  				),
   166  			},
   167  			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.
   168  
   169  Change your configuration so that this call will be declared as module.other instead.`,
   170  		},
   171  		"module.single as an instance still exists in configuration": {
   172  			Statements: []MoveStatement{
   173  				makeTestMoveStmt(t,
   174  					``,
   175  					`module.single`,
   176  					`module.other[0]`,
   177  				),
   178  			},
   179  			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.
   180  
   181  Change your configuration so that this instance will be declared as module.other[0] instead.`,
   182  		},
   183  		"module.count[0] still exists in configuration": {
   184  			Statements: []MoveStatement{
   185  				makeTestMoveStmt(t,
   186  					``,
   187  					`module.count[0]`,
   188  					`module.other`,
   189  				),
   190  			},
   191  			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.
   192  
   193  Change your configuration so that this instance will be declared as module.other instead.`,
   194  		},
   195  		`module.for_each["a"] still exists in configuration`: {
   196  			Statements: []MoveStatement{
   197  				makeTestMoveStmt(t,
   198  					``,
   199  					`module.for_each["a"]`,
   200  					`module.other`,
   201  				),
   202  			},
   203  			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.
   204  
   205  Change your configuration so that this instance will be declared as module.other instead.`,
   206  		},
   207  		"test.single as a resource still exists in configuration": {
   208  			Statements: []MoveStatement{
   209  				makeTestMoveStmt(t,
   210  					``,
   211  					`test.single`,
   212  					`test.other`,
   213  				),
   214  			},
   215  			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.
   216  
   217  Change your configuration so that this resource will be declared as test.other instead.`,
   218  		},
   219  		"test.single as an instance still exists in configuration": {
   220  			Statements: []MoveStatement{
   221  				makeTestMoveStmt(t,
   222  					``,
   223  					`test.single`,
   224  					`test.other[0]`,
   225  				),
   226  			},
   227  			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.
   228  
   229  Change your configuration so that this instance will be declared as test.other[0] instead.`,
   230  		},
   231  		"module.single.test.single as a resource still exists in configuration": {
   232  			Statements: []MoveStatement{
   233  				makeTestMoveStmt(t,
   234  					``,
   235  					`module.single.test.single`,
   236  					`test.other`,
   237  				),
   238  			},
   239  			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.
   240  
   241  Change your configuration so that this resource will be declared as test.other instead.`,
   242  		},
   243  		"module.single.test.single as a resource declared in module.single still exists in configuration": {
   244  			Statements: []MoveStatement{
   245  				makeTestMoveStmt(t,
   246  					`single`,
   247  					`test.single`,
   248  					`test.other`,
   249  				),
   250  			},
   251  			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.
   252  
   253  Change your configuration so that this resource will be declared as module.single.test.other instead.`,
   254  		},
   255  		"module.single.test.single as an instance still exists in configuration": {
   256  			Statements: []MoveStatement{
   257  				makeTestMoveStmt(t,
   258  					``,
   259  					`module.single.test.single`,
   260  					`test.other[0]`,
   261  				),
   262  			},
   263  			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.
   264  
   265  Change your configuration so that this instance will be declared as test.other[0] instead.`,
   266  		},
   267  		"module.count[0].test.single still exists in configuration": {
   268  			Statements: []MoveStatement{
   269  				makeTestMoveStmt(t,
   270  					``,
   271  					`module.count[0].test.single`,
   272  					`test.other`,
   273  				),
   274  			},
   275  			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.
   276  
   277  Change your configuration so that this resource will be declared as test.other instead.`,
   278  		},
   279  		"two different moves from test.nonexist": {
   280  			Statements: []MoveStatement{
   281  				makeTestMoveStmt(t,
   282  					``,
   283  					`test.nonexist`,
   284  					`test.other1`,
   285  				),
   286  				makeTestMoveStmt(t,
   287  					``,
   288  					`test.nonexist`,
   289  					`test.other2`,
   290  				),
   291  			},
   292  			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.
   293  
   294  Each resource can move to only one destination resource.`,
   295  		},
   296  		"two different moves to test.single": {
   297  			Statements: []MoveStatement{
   298  				makeTestMoveStmt(t,
   299  					``,
   300  					`test.other1`,
   301  					`test.single`,
   302  				),
   303  				makeTestMoveStmt(t,
   304  					``,
   305  					`test.other2`,
   306  					`test.single`,
   307  				),
   308  			},
   309  			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.
   310  
   311  Each resource can have moved from only one source resource.`,
   312  		},
   313  		"two different moves to module.count[0].test.single across two modules": {
   314  			Statements: []MoveStatement{
   315  				makeTestMoveStmt(t,
   316  					``,
   317  					`test.other1`,
   318  					`module.count[0].test.single`,
   319  				),
   320  				makeTestMoveStmt(t,
   321  					`count`,
   322  					`test.other2`,
   323  					`test.single`,
   324  				),
   325  			},
   326  			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.
   327  
   328  Each resource can have moved from only one source resource.`,
   329  		},
   330  		"move from resource in another module package": {
   331  			Statements: []MoveStatement{
   332  				makeTestMoveStmt(t,
   333  					``,
   334  					`module.fake_external.test.thing`,
   335  					`test.thing`,
   336  				),
   337  			},
   338  			WantError: ``,
   339  		},
   340  		"move to resource in another module package": {
   341  			Statements: []MoveStatement{
   342  				makeTestMoveStmt(t,
   343  					``,
   344  					`test.thing`,
   345  					`module.fake_external.test.thing`,
   346  				),
   347  			},
   348  			WantError: ``,
   349  		},
   350  		"move from module call in another module package": {
   351  			Statements: []MoveStatement{
   352  				makeTestMoveStmt(t,
   353  					``,
   354  					`module.fake_external.module.a`,
   355  					`module.b`,
   356  				),
   357  			},
   358  			WantError: ``,
   359  		},
   360  		"move to module call in another module package": {
   361  			Statements: []MoveStatement{
   362  				makeTestMoveStmt(t,
   363  					``,
   364  					`module.a`,
   365  					`module.fake_external.module.b`,
   366  				),
   367  			},
   368  			WantError: ``,
   369  		},
   370  		"implied move from resource in another module package": {
   371  			Statements: []MoveStatement{
   372  				makeTestImpliedMoveStmt(t,
   373  					``,
   374  					`module.fake_external.test.thing`,
   375  					`test.thing`,
   376  				),
   377  			},
   378  			// Implied move statements are not subject to the cross-package restriction
   379  			WantError: ``,
   380  		},
   381  		"implied move to resource in another module package": {
   382  			Statements: []MoveStatement{
   383  				makeTestImpliedMoveStmt(t,
   384  					``,
   385  					`test.thing`,
   386  					`module.fake_external.test.thing`,
   387  				),
   388  			},
   389  			// Implied move statements are not subject to the cross-package restriction
   390  			WantError: ``,
   391  		},
   392  		"implied move from module call in another module package": {
   393  			Statements: []MoveStatement{
   394  				makeTestImpliedMoveStmt(t,
   395  					``,
   396  					`module.fake_external.module.a`,
   397  					`module.b`,
   398  				),
   399  			},
   400  			// Implied move statements are not subject to the cross-package restriction
   401  			WantError: ``,
   402  		},
   403  		"implied move to module call in another module package": {
   404  			Statements: []MoveStatement{
   405  				makeTestImpliedMoveStmt(t,
   406  					``,
   407  					`module.a`,
   408  					`module.fake_external.module.b`,
   409  				),
   410  			},
   411  			// Implied move statements are not subject to the cross-package restriction
   412  			WantError: ``,
   413  		},
   414  		"move to a call that refers to another module package": {
   415  			Statements: []MoveStatement{
   416  				makeTestMoveStmt(t,
   417  					``,
   418  					`module.nonexist`,
   419  					`module.fake_external`,
   420  				),
   421  			},
   422  			WantError: ``, // This is okay because the call itself is not considered to be inside the package it refers to
   423  		},
   424  		"move to instance of a call that refers to another module package": {
   425  			Statements: []MoveStatement{
   426  				makeTestMoveStmt(t,
   427  					``,
   428  					`module.nonexist`,
   429  					`module.fake_external[0]`,
   430  				),
   431  			},
   432  			WantError: ``, // This is okay because the call itself is not considered to be inside the package it refers to
   433  		},
   434  		"resource type mismatch": {
   435  			Statements: []MoveStatement{
   436  				makeTestMoveStmt(t, ``,
   437  					`test.nonexist1`,
   438  					`other.single`,
   439  				),
   440  			},
   441  			WantError: `Resource type mismatch: This statement declares a move from test.nonexist1 to other.single, which is a resource of a different type.`,
   442  		},
   443  		"resource instance type mismatch": {
   444  			Statements: []MoveStatement{
   445  				makeTestMoveStmt(t, ``,
   446  					`test.nonexist1[0]`,
   447  					`other.single`,
   448  				),
   449  			},
   450  			WantError: `Resource type mismatch: This statement declares a move from test.nonexist1[0] to other.single, which is a resource instance of a different type.`,
   451  		},
   452  		"crossing nested statements": {
   453  			// overlapping nested moves will result in a cycle.
   454  			Statements: []MoveStatement{
   455  				makeTestMoveStmt(t, ``,
   456  					`module.nonexist.test.single`,
   457  					`module.count[0].test.count[0]`,
   458  				),
   459  				makeTestMoveStmt(t, ``,
   460  					`module.nonexist`,
   461  					`module.count[0]`,
   462  				),
   463  			},
   464  			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:
   465    - test:1,1: module.nonexist → module.count[0]
   466    - test:1,1: module.nonexist.test.single → module.count[0].test.count[0]
   467  
   468  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.`,
   469  		},
   470  		"fully contained nested statements": {
   471  			// we have to avoid a cycle because the nested moves appear in both
   472  			// the from and to address of the parent when only the module index
   473  			// is changing.
   474  			Statements: []MoveStatement{
   475  				makeTestMoveStmt(t, `count`,
   476  					`test.count`,
   477  					`test.count[0]`,
   478  				),
   479  				makeTestMoveStmt(t, ``,
   480  					`module.count`,
   481  					`module.count[0]`,
   482  				),
   483  			},
   484  		},
   485  		"double fully contained nested statements": {
   486  			// we have to avoid a cycle because the nested moves appear in both
   487  			// the from and to address of the parent when only the module index
   488  			// is changing.
   489  			Statements: []MoveStatement{
   490  				makeTestMoveStmt(t, `count`,
   491  					`module.count`,
   492  					`module.count[0]`,
   493  				),
   494  				makeTestMoveStmt(t, `count.count`,
   495  					`test.count`,
   496  					`test.count[0]`,
   497  				),
   498  				makeTestMoveStmt(t, ``,
   499  					`module.count`,
   500  					`module.count[0]`,
   501  				),
   502  			},
   503  		},
   504  	}
   505  
   506  	for name, test := range tests {
   507  		t.Run(name, func(t *testing.T) {
   508  			gotDiags := ValidateMoves(test.Statements, rootCfg, instances)
   509  
   510  			switch {
   511  			case test.WantError != "":
   512  				if !gotDiags.HasErrors() {
   513  					t.Fatalf("unexpected success\nwant error: %s", test.WantError)
   514  				}
   515  				if got, want := gotDiags.Err().Error(), test.WantError; got != want {
   516  					t.Fatalf("wrong error\ngot error:  %s\nwant error: %s", got, want)
   517  				}
   518  			default:
   519  				if gotDiags.HasErrors() {
   520  					t.Fatalf("unexpected error\ngot error: %s", gotDiags.Err().Error())
   521  				}
   522  			}
   523  		})
   524  	}
   525  }
   526  
   527  // loadRefactoringFixture reads a configuration from the given directory and
   528  // does some naive static processing on any count and for_each expressions
   529  // inside, in order to get a realistic-looking instances.Set for what it
   530  // declares without having to run a full Terraform plan.
   531  func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instances.Set) {
   532  	t.Helper()
   533  
   534  	loader, cleanup := configload.NewLoaderForTests(t)
   535  	defer cleanup()
   536  
   537  	inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil))
   538  	_, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{})
   539  	if instDiags.HasErrors() {
   540  		t.Fatal(instDiags.Err())
   541  	}
   542  
   543  	// Since module installer has modified the module manifest on disk, we need
   544  	// to refresh the cache of it in the loader.
   545  	if err := loader.RefreshModules(); err != nil {
   546  		t.Fatalf("failed to refresh modules after installation: %s", err)
   547  	}
   548  
   549  	rootCfg, diags := loader.LoadConfig(dir)
   550  	if diags.HasErrors() {
   551  		t.Fatalf("failed to load root module: %s", diags.Error())
   552  	}
   553  
   554  	expander := instances.NewExpander()
   555  	staticPopulateExpanderModule(t, rootCfg, addrs.RootModuleInstance, expander)
   556  	return rootCfg, expander.AllInstances()
   557  }
   558  
   559  func staticPopulateExpanderModule(t *testing.T, rootCfg *configs.Config, moduleAddr addrs.ModuleInstance, expander *instances.Expander) {
   560  	t.Helper()
   561  
   562  	modCfg := rootCfg.DescendentForInstance(moduleAddr)
   563  	if modCfg == nil {
   564  		t.Fatalf("no configuration for %s", moduleAddr)
   565  	}
   566  
   567  	if len(modCfg.Path) > 0 && modCfg.Path[len(modCfg.Path)-1] == "fake_external" {
   568  		// As a funny special case we modify the source address of this
   569  		// module to be something that counts as a separate package,
   570  		// so we can test rules relating to crossing package boundaries
   571  		// even though we really just loaded the module from a local path.
   572  		modCfg.SourceAddr = fakeExternalModuleSource
   573  	}
   574  
   575  	for _, call := range modCfg.Module.ModuleCalls {
   576  		callAddr := addrs.ModuleCall{Name: call.Name}
   577  
   578  		if call.Name == "fake_external" {
   579  			// As a funny special case we modify the source address of this
   580  			// module to be something that counts as a separate package,
   581  			// so we can test rules relating to crossing package boundaries
   582  			// even though we really just loaded the module from a local path.
   583  			call.SourceAddr = fakeExternalModuleSource
   584  		}
   585  
   586  		// In order to get a valid, useful set of instances here we're going
   587  		// to just statically evaluate the count and for_each expressions.
   588  		// Normally it's valid to use references and functions there, but for
   589  		// our unit tests we'll just limit it to literal values to avoid
   590  		// bringing all of the core evaluator complexity.
   591  		switch {
   592  		case call.ForEach != nil:
   593  			val, diags := call.ForEach.Value(nil)
   594  			if diags.HasErrors() {
   595  				t.Fatalf("invalid for_each: %s", diags.Error())
   596  			}
   597  			expander.SetModuleForEach(moduleAddr, callAddr, val.AsValueMap())
   598  		case call.Count != nil:
   599  			val, diags := call.Count.Value(nil)
   600  			if diags.HasErrors() {
   601  				t.Fatalf("invalid count: %s", diags.Error())
   602  			}
   603  			var count int
   604  			err := gocty.FromCtyValue(val, &count)
   605  			if err != nil {
   606  				t.Fatalf("invalid count at %s: %s", call.Count.Range(), err)
   607  			}
   608  			expander.SetModuleCount(moduleAddr, callAddr, count)
   609  		default:
   610  			expander.SetModuleSingle(moduleAddr, callAddr)
   611  		}
   612  
   613  		// We need to recursively analyze the child modules too.
   614  		calledMod := modCfg.Path.Child(call.Name)
   615  		for _, inst := range expander.ExpandModule(calledMod) {
   616  			staticPopulateExpanderModule(t, rootCfg, inst, expander)
   617  		}
   618  	}
   619  
   620  	for _, rc := range modCfg.Module.ManagedResources {
   621  		staticPopulateExpanderResource(t, moduleAddr, rc, expander)
   622  	}
   623  	for _, rc := range modCfg.Module.DataResources {
   624  		staticPopulateExpanderResource(t, moduleAddr, rc, expander)
   625  	}
   626  
   627  }
   628  
   629  func staticPopulateExpanderResource(t *testing.T, moduleAddr addrs.ModuleInstance, rCfg *configs.Resource, expander *instances.Expander) {
   630  	t.Helper()
   631  
   632  	addr := rCfg.Addr()
   633  	switch {
   634  	case rCfg.ForEach != nil:
   635  		val, diags := rCfg.ForEach.Value(nil)
   636  		if diags.HasErrors() {
   637  			t.Fatalf("invalid for_each: %s", diags.Error())
   638  		}
   639  		expander.SetResourceForEach(moduleAddr, addr, val.AsValueMap())
   640  	case rCfg.Count != nil:
   641  		val, diags := rCfg.Count.Value(nil)
   642  		if diags.HasErrors() {
   643  			t.Fatalf("invalid count: %s", diags.Error())
   644  		}
   645  		var count int
   646  		err := gocty.FromCtyValue(val, &count)
   647  		if err != nil {
   648  			t.Fatalf("invalid count at %s: %s", rCfg.Count.Range(), err)
   649  		}
   650  		expander.SetResourceCount(moduleAddr, addr, count)
   651  	default:
   652  		expander.SetResourceSingle(moduleAddr, addr)
   653  	}
   654  }
   655  
   656  func makeTestMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) MoveStatement {
   657  	t.Helper()
   658  
   659  	module := addrs.RootModule
   660  	if moduleStr != "" {
   661  		module = addrs.Module(strings.Split(moduleStr, "."))
   662  	}
   663  
   664  	traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(fromStr), "", hcl.InitialPos)
   665  	if hclDiags.HasErrors() {
   666  		t.Fatalf("invalid from address: %s", hclDiags.Error())
   667  	}
   668  	fromEP, diags := addrs.ParseMoveEndpoint(traversal)
   669  	if diags.HasErrors() {
   670  		t.Fatalf("invalid from address: %s", diags.Err().Error())
   671  	}
   672  
   673  	traversal, hclDiags = hclsyntax.ParseTraversalAbs([]byte(toStr), "", hcl.InitialPos)
   674  	if hclDiags.HasErrors() {
   675  		t.Fatalf("invalid to address: %s", hclDiags.Error())
   676  	}
   677  	toEP, diags := addrs.ParseMoveEndpoint(traversal)
   678  	if diags.HasErrors() {
   679  		t.Fatalf("invalid to address: %s", diags.Err().Error())
   680  	}
   681  
   682  	fromInModule, toInModule := addrs.UnifyMoveEndpoints(module, fromEP, toEP)
   683  	if fromInModule == nil || toInModule == nil {
   684  		t.Fatalf("incompatible move endpoints")
   685  	}
   686  
   687  	return MoveStatement{
   688  		From: fromInModule,
   689  		To:   toInModule,
   690  		DeclRange: tfdiags.SourceRange{
   691  			Filename: "test",
   692  			Start:    tfdiags.SourcePos{Line: 1, Column: 1},
   693  			End:      tfdiags.SourcePos{Line: 1, Column: 1},
   694  		},
   695  	}
   696  }
   697  
   698  func makeTestImpliedMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) MoveStatement {
   699  	t.Helper()
   700  	ret := makeTestMoveStmt(t, moduleStr, fromStr, toStr)
   701  	ret.Implied = true
   702  	return ret
   703  }
   704  
   705  var fakeExternalModuleSource = addrs.ModuleSourceRemote{
   706  	Package: addrs.ModulePackage("fake-external:///"),
   707  }