github.com/opentofu/opentofu@v1.7.1/internal/legacy/tofu/diff_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package tofu
     7  
     8  import (
     9  	"fmt"
    10  	"reflect"
    11  	"strconv"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/opentofu/opentofu/internal/addrs"
    16  )
    17  
    18  func TestDiffEmpty(t *testing.T) {
    19  	var diff *Diff
    20  	if !diff.Empty() {
    21  		t.Fatal("should be empty")
    22  	}
    23  
    24  	diff = new(Diff)
    25  	if !diff.Empty() {
    26  		t.Fatal("should be empty")
    27  	}
    28  
    29  	mod := diff.AddModule(addrs.RootModuleInstance)
    30  	mod.Resources["nodeA"] = &InstanceDiff{
    31  		Attributes: map[string]*ResourceAttrDiff{
    32  			"foo": &ResourceAttrDiff{
    33  				Old: "foo",
    34  				New: "bar",
    35  			},
    36  		},
    37  	}
    38  
    39  	if diff.Empty() {
    40  		t.Fatal("should not be empty")
    41  	}
    42  }
    43  
    44  func TestDiffEmpty_taintedIsNotEmpty(t *testing.T) {
    45  	diff := new(Diff)
    46  
    47  	mod := diff.AddModule(addrs.RootModuleInstance)
    48  	mod.Resources["nodeA"] = &InstanceDiff{
    49  		DestroyTainted: true,
    50  	}
    51  
    52  	if diff.Empty() {
    53  		t.Fatal("should not be empty, since DestroyTainted was set")
    54  	}
    55  }
    56  
    57  func TestDiffEqual(t *testing.T) {
    58  	cases := map[string]struct {
    59  		D1, D2 *Diff
    60  		Equal  bool
    61  	}{
    62  		"nil": {
    63  			nil,
    64  			new(Diff),
    65  			false,
    66  		},
    67  
    68  		"empty": {
    69  			new(Diff),
    70  			new(Diff),
    71  			true,
    72  		},
    73  
    74  		"different module order": {
    75  			&Diff{
    76  				Modules: []*ModuleDiff{
    77  					&ModuleDiff{Path: []string{"root", "foo"}},
    78  					&ModuleDiff{Path: []string{"root", "bar"}},
    79  				},
    80  			},
    81  			&Diff{
    82  				Modules: []*ModuleDiff{
    83  					&ModuleDiff{Path: []string{"root", "bar"}},
    84  					&ModuleDiff{Path: []string{"root", "foo"}},
    85  				},
    86  			},
    87  			true,
    88  		},
    89  
    90  		"different module diff destroys": {
    91  			&Diff{
    92  				Modules: []*ModuleDiff{
    93  					&ModuleDiff{Path: []string{"root", "foo"}, Destroy: true},
    94  				},
    95  			},
    96  			&Diff{
    97  				Modules: []*ModuleDiff{
    98  					&ModuleDiff{Path: []string{"root", "foo"}, Destroy: false},
    99  				},
   100  			},
   101  			true,
   102  		},
   103  	}
   104  
   105  	for name, tc := range cases {
   106  		t.Run(name, func(t *testing.T) {
   107  			actual := tc.D1.Equal(tc.D2)
   108  			if actual != tc.Equal {
   109  				t.Fatalf("expected: %v\n\n%#v\n\n%#v", tc.Equal, tc.D1, tc.D2)
   110  			}
   111  		})
   112  	}
   113  }
   114  
   115  func TestDiffPrune(t *testing.T) {
   116  	cases := map[string]struct {
   117  		D1, D2 *Diff
   118  	}{
   119  		"nil": {
   120  			nil,
   121  			nil,
   122  		},
   123  
   124  		"empty": {
   125  			new(Diff),
   126  			new(Diff),
   127  		},
   128  
   129  		"empty module": {
   130  			&Diff{
   131  				Modules: []*ModuleDiff{
   132  					&ModuleDiff{Path: []string{"root", "foo"}},
   133  				},
   134  			},
   135  			&Diff{},
   136  		},
   137  
   138  		"destroy module": {
   139  			&Diff{
   140  				Modules: []*ModuleDiff{
   141  					&ModuleDiff{Path: []string{"root", "foo"}, Destroy: true},
   142  				},
   143  			},
   144  			&Diff{
   145  				Modules: []*ModuleDiff{
   146  					&ModuleDiff{Path: []string{"root", "foo"}, Destroy: true},
   147  				},
   148  			},
   149  		},
   150  	}
   151  
   152  	for name, tc := range cases {
   153  		t.Run(name, func(t *testing.T) {
   154  			tc.D1.Prune()
   155  			if !tc.D1.Equal(tc.D2) {
   156  				t.Fatalf("bad:\n\n%#v\n\n%#v", tc.D1, tc.D2)
   157  			}
   158  		})
   159  	}
   160  }
   161  
   162  func TestModuleDiff_ChangeType(t *testing.T) {
   163  	cases := []struct {
   164  		Diff   *ModuleDiff
   165  		Result DiffChangeType
   166  	}{
   167  		{
   168  			&ModuleDiff{},
   169  			DiffNone,
   170  		},
   171  		{
   172  			&ModuleDiff{
   173  				Resources: map[string]*InstanceDiff{
   174  					"foo": &InstanceDiff{Destroy: true},
   175  				},
   176  			},
   177  			DiffDestroy,
   178  		},
   179  		{
   180  			&ModuleDiff{
   181  				Resources: map[string]*InstanceDiff{
   182  					"foo": &InstanceDiff{
   183  						Attributes: map[string]*ResourceAttrDiff{
   184  							"foo": &ResourceAttrDiff{
   185  								Old: "",
   186  								New: "bar",
   187  							},
   188  						},
   189  					},
   190  				},
   191  			},
   192  			DiffUpdate,
   193  		},
   194  		{
   195  			&ModuleDiff{
   196  				Resources: map[string]*InstanceDiff{
   197  					"foo": &InstanceDiff{
   198  						Attributes: map[string]*ResourceAttrDiff{
   199  							"foo": &ResourceAttrDiff{
   200  								Old:         "",
   201  								New:         "bar",
   202  								RequiresNew: true,
   203  							},
   204  						},
   205  					},
   206  				},
   207  			},
   208  			DiffCreate,
   209  		},
   210  		{
   211  			&ModuleDiff{
   212  				Resources: map[string]*InstanceDiff{
   213  					"foo": &InstanceDiff{
   214  						Destroy: true,
   215  						Attributes: map[string]*ResourceAttrDiff{
   216  							"foo": &ResourceAttrDiff{
   217  								Old:         "",
   218  								New:         "bar",
   219  								RequiresNew: true,
   220  							},
   221  						},
   222  					},
   223  				},
   224  			},
   225  			DiffUpdate,
   226  		},
   227  	}
   228  
   229  	for i, tc := range cases {
   230  		actual := tc.Diff.ChangeType()
   231  		if actual != tc.Result {
   232  			t.Fatalf("%d: %#v", i, actual)
   233  		}
   234  	}
   235  }
   236  
   237  func TestDiff_DeepCopy(t *testing.T) {
   238  	cases := map[string]*Diff{
   239  		"empty": &Diff{},
   240  
   241  		"basic diff": &Diff{
   242  			Modules: []*ModuleDiff{
   243  				&ModuleDiff{
   244  					Path: []string{"root"},
   245  					Resources: map[string]*InstanceDiff{
   246  						"aws_instance.foo": &InstanceDiff{
   247  							Attributes: map[string]*ResourceAttrDiff{
   248  								"num": &ResourceAttrDiff{
   249  									Old: "0",
   250  									New: "2",
   251  								},
   252  							},
   253  						},
   254  					},
   255  				},
   256  			},
   257  		},
   258  	}
   259  
   260  	for name, tc := range cases {
   261  		t.Run(name, func(t *testing.T) {
   262  			dup := tc.DeepCopy()
   263  			if !reflect.DeepEqual(dup, tc) {
   264  				t.Fatalf("\n%#v\n\n%#v", dup, tc)
   265  			}
   266  		})
   267  	}
   268  }
   269  
   270  func TestModuleDiff_Empty(t *testing.T) {
   271  	diff := new(ModuleDiff)
   272  	if !diff.Empty() {
   273  		t.Fatal("should be empty")
   274  	}
   275  
   276  	diff.Resources = map[string]*InstanceDiff{
   277  		"nodeA": &InstanceDiff{},
   278  	}
   279  
   280  	if !diff.Empty() {
   281  		t.Fatal("should be empty")
   282  	}
   283  
   284  	diff.Resources["nodeA"].Attributes = map[string]*ResourceAttrDiff{
   285  		"foo": &ResourceAttrDiff{
   286  			Old: "foo",
   287  			New: "bar",
   288  		},
   289  	}
   290  
   291  	if diff.Empty() {
   292  		t.Fatal("should not be empty")
   293  	}
   294  
   295  	diff.Resources["nodeA"].Attributes = nil
   296  	diff.Resources["nodeA"].Destroy = true
   297  
   298  	if diff.Empty() {
   299  		t.Fatal("should not be empty")
   300  	}
   301  }
   302  
   303  func TestModuleDiff_String(t *testing.T) {
   304  	diff := &ModuleDiff{
   305  		Resources: map[string]*InstanceDiff{
   306  			"nodeA": &InstanceDiff{
   307  				Attributes: map[string]*ResourceAttrDiff{
   308  					"foo": &ResourceAttrDiff{
   309  						Old: "foo",
   310  						New: "bar",
   311  					},
   312  					"bar": &ResourceAttrDiff{
   313  						Old:         "foo",
   314  						NewComputed: true,
   315  					},
   316  					"longfoo": &ResourceAttrDiff{
   317  						Old:         "foo",
   318  						New:         "bar",
   319  						RequiresNew: true,
   320  					},
   321  					"secretfoo": &ResourceAttrDiff{
   322  						Old:       "foo",
   323  						New:       "bar",
   324  						Sensitive: true,
   325  					},
   326  				},
   327  			},
   328  		},
   329  	}
   330  
   331  	actual := strings.TrimSpace(diff.String())
   332  	expected := strings.TrimSpace(moduleDiffStrBasic)
   333  	if actual != expected {
   334  		t.Fatalf("bad:\n%s", actual)
   335  	}
   336  }
   337  
   338  func TestInstanceDiff_ChangeType(t *testing.T) {
   339  	cases := []struct {
   340  		Diff   *InstanceDiff
   341  		Result DiffChangeType
   342  	}{
   343  		{
   344  			&InstanceDiff{},
   345  			DiffNone,
   346  		},
   347  		{
   348  			&InstanceDiff{Destroy: true},
   349  			DiffDestroy,
   350  		},
   351  		{
   352  			&InstanceDiff{
   353  				Attributes: map[string]*ResourceAttrDiff{
   354  					"foo": &ResourceAttrDiff{
   355  						Old: "",
   356  						New: "bar",
   357  					},
   358  				},
   359  			},
   360  			DiffUpdate,
   361  		},
   362  		{
   363  			&InstanceDiff{
   364  				Attributes: map[string]*ResourceAttrDiff{
   365  					"foo": &ResourceAttrDiff{
   366  						Old:         "",
   367  						New:         "bar",
   368  						RequiresNew: true,
   369  					},
   370  				},
   371  			},
   372  			DiffCreate,
   373  		},
   374  		{
   375  			&InstanceDiff{
   376  				Destroy: true,
   377  				Attributes: map[string]*ResourceAttrDiff{
   378  					"foo": &ResourceAttrDiff{
   379  						Old:         "",
   380  						New:         "bar",
   381  						RequiresNew: true,
   382  					},
   383  				},
   384  			},
   385  			DiffDestroyCreate,
   386  		},
   387  		{
   388  			&InstanceDiff{
   389  				DestroyTainted: true,
   390  				Attributes: map[string]*ResourceAttrDiff{
   391  					"foo": &ResourceAttrDiff{
   392  						Old:         "",
   393  						New:         "bar",
   394  						RequiresNew: true,
   395  					},
   396  				},
   397  			},
   398  			DiffDestroyCreate,
   399  		},
   400  	}
   401  
   402  	for i, tc := range cases {
   403  		actual := tc.Diff.ChangeType()
   404  		if actual != tc.Result {
   405  			t.Fatalf("%d: %#v", i, actual)
   406  		}
   407  	}
   408  }
   409  
   410  func TestInstanceDiff_Empty(t *testing.T) {
   411  	var rd *InstanceDiff
   412  
   413  	if !rd.Empty() {
   414  		t.Fatal("should be empty")
   415  	}
   416  
   417  	rd = new(InstanceDiff)
   418  
   419  	if !rd.Empty() {
   420  		t.Fatal("should be empty")
   421  	}
   422  
   423  	rd = &InstanceDiff{Destroy: true}
   424  
   425  	if rd.Empty() {
   426  		t.Fatal("should not be empty")
   427  	}
   428  
   429  	rd = &InstanceDiff{
   430  		Attributes: map[string]*ResourceAttrDiff{
   431  			"foo": &ResourceAttrDiff{
   432  				New: "bar",
   433  			},
   434  		},
   435  	}
   436  
   437  	if rd.Empty() {
   438  		t.Fatal("should not be empty")
   439  	}
   440  }
   441  
   442  func TestModuleDiff_Instances(t *testing.T) {
   443  	yesDiff := &InstanceDiff{Destroy: true}
   444  	noDiff := &InstanceDiff{Destroy: true, DestroyTainted: true}
   445  
   446  	cases := []struct {
   447  		Diff   *ModuleDiff
   448  		Id     string
   449  		Result []*InstanceDiff
   450  	}{
   451  		{
   452  			&ModuleDiff{
   453  				Resources: map[string]*InstanceDiff{
   454  					"foo": yesDiff,
   455  					"bar": noDiff,
   456  				},
   457  			},
   458  			"foo",
   459  			[]*InstanceDiff{
   460  				yesDiff,
   461  			},
   462  		},
   463  
   464  		{
   465  			&ModuleDiff{
   466  				Resources: map[string]*InstanceDiff{
   467  					"foo":   yesDiff,
   468  					"foo.0": yesDiff,
   469  					"bar":   noDiff,
   470  				},
   471  			},
   472  			"foo",
   473  			[]*InstanceDiff{
   474  				yesDiff,
   475  				yesDiff,
   476  			},
   477  		},
   478  
   479  		{
   480  			&ModuleDiff{
   481  				Resources: map[string]*InstanceDiff{
   482  					"foo":     yesDiff,
   483  					"foo.0":   yesDiff,
   484  					"foo_bar": noDiff,
   485  					"bar":     noDiff,
   486  				},
   487  			},
   488  			"foo",
   489  			[]*InstanceDiff{
   490  				yesDiff,
   491  				yesDiff,
   492  			},
   493  		},
   494  	}
   495  
   496  	for i, tc := range cases {
   497  		actual := tc.Diff.Instances(tc.Id)
   498  		if !reflect.DeepEqual(actual, tc.Result) {
   499  			t.Fatalf("%d: %#v", i, actual)
   500  		}
   501  	}
   502  }
   503  
   504  func TestInstanceDiff_RequiresNew(t *testing.T) {
   505  	rd := &InstanceDiff{
   506  		Attributes: map[string]*ResourceAttrDiff{
   507  			"foo": &ResourceAttrDiff{},
   508  		},
   509  	}
   510  
   511  	if rd.RequiresNew() {
   512  		t.Fatal("should not require new")
   513  	}
   514  
   515  	rd.Attributes["foo"].RequiresNew = true
   516  
   517  	if !rd.RequiresNew() {
   518  		t.Fatal("should require new")
   519  	}
   520  }
   521  
   522  func TestInstanceDiff_RequiresNew_nil(t *testing.T) {
   523  	var rd *InstanceDiff
   524  
   525  	if rd.RequiresNew() {
   526  		t.Fatal("should not require new")
   527  	}
   528  }
   529  
   530  func TestInstanceDiffSame(t *testing.T) {
   531  	cases := []struct {
   532  		One, Two *InstanceDiff
   533  		Same     bool
   534  		Reason   string
   535  	}{
   536  		{
   537  			&InstanceDiff{},
   538  			&InstanceDiff{},
   539  			true,
   540  			"",
   541  		},
   542  
   543  		{
   544  			nil,
   545  			nil,
   546  			true,
   547  			"",
   548  		},
   549  
   550  		{
   551  			&InstanceDiff{Destroy: false},
   552  			&InstanceDiff{Destroy: true},
   553  			false,
   554  			"diff: Destroy; old: false, new: true",
   555  		},
   556  
   557  		{
   558  			&InstanceDiff{Destroy: true},
   559  			&InstanceDiff{Destroy: true},
   560  			true,
   561  			"",
   562  		},
   563  
   564  		{
   565  			&InstanceDiff{
   566  				Attributes: map[string]*ResourceAttrDiff{
   567  					"foo": &ResourceAttrDiff{},
   568  				},
   569  			},
   570  			&InstanceDiff{
   571  				Attributes: map[string]*ResourceAttrDiff{
   572  					"foo": &ResourceAttrDiff{},
   573  				},
   574  			},
   575  			true,
   576  			"",
   577  		},
   578  
   579  		{
   580  			&InstanceDiff{
   581  				Attributes: map[string]*ResourceAttrDiff{
   582  					"bar": &ResourceAttrDiff{},
   583  				},
   584  			},
   585  			&InstanceDiff{
   586  				Attributes: map[string]*ResourceAttrDiff{
   587  					"foo": &ResourceAttrDiff{},
   588  				},
   589  			},
   590  			false,
   591  			"attribute mismatch: bar",
   592  		},
   593  
   594  		// Extra attributes
   595  		{
   596  			&InstanceDiff{
   597  				Attributes: map[string]*ResourceAttrDiff{
   598  					"foo": &ResourceAttrDiff{},
   599  				},
   600  			},
   601  			&InstanceDiff{
   602  				Attributes: map[string]*ResourceAttrDiff{
   603  					"foo": &ResourceAttrDiff{},
   604  					"bar": &ResourceAttrDiff{},
   605  				},
   606  			},
   607  			false,
   608  			"extra attributes: bar",
   609  		},
   610  
   611  		{
   612  			&InstanceDiff{
   613  				Attributes: map[string]*ResourceAttrDiff{
   614  					"foo": &ResourceAttrDiff{RequiresNew: true},
   615  				},
   616  			},
   617  			&InstanceDiff{
   618  				Attributes: map[string]*ResourceAttrDiff{
   619  					"foo": &ResourceAttrDiff{RequiresNew: false},
   620  				},
   621  			},
   622  			false,
   623  			"diff RequiresNew; old: true, new: false",
   624  		},
   625  
   626  		// NewComputed on primitive
   627  		{
   628  			&InstanceDiff{
   629  				Attributes: map[string]*ResourceAttrDiff{
   630  					"foo": &ResourceAttrDiff{
   631  						Old:         "",
   632  						New:         "${var.foo}",
   633  						NewComputed: true,
   634  					},
   635  				},
   636  			},
   637  			&InstanceDiff{
   638  				Attributes: map[string]*ResourceAttrDiff{
   639  					"foo": &ResourceAttrDiff{
   640  						Old: "0",
   641  						New: "1",
   642  					},
   643  				},
   644  			},
   645  			true,
   646  			"",
   647  		},
   648  
   649  		// NewComputed on primitive, removed
   650  		{
   651  			&InstanceDiff{
   652  				Attributes: map[string]*ResourceAttrDiff{
   653  					"foo": &ResourceAttrDiff{
   654  						Old:         "",
   655  						New:         "${var.foo}",
   656  						NewComputed: true,
   657  					},
   658  				},
   659  			},
   660  			&InstanceDiff{
   661  				Attributes: map[string]*ResourceAttrDiff{},
   662  			},
   663  			true,
   664  			"",
   665  		},
   666  
   667  		// NewComputed on set, removed
   668  		{
   669  			&InstanceDiff{
   670  				Attributes: map[string]*ResourceAttrDiff{
   671  					"foo.#": &ResourceAttrDiff{
   672  						Old:         "",
   673  						New:         "",
   674  						NewComputed: true,
   675  					},
   676  				},
   677  			},
   678  			&InstanceDiff{
   679  				Attributes: map[string]*ResourceAttrDiff{
   680  					"foo.1": &ResourceAttrDiff{
   681  						Old:        "foo",
   682  						New:        "",
   683  						NewRemoved: true,
   684  					},
   685  					"foo.2": &ResourceAttrDiff{
   686  						Old: "",
   687  						New: "bar",
   688  					},
   689  				},
   690  			},
   691  			true,
   692  			"",
   693  		},
   694  
   695  		{
   696  			&InstanceDiff{
   697  				Attributes: map[string]*ResourceAttrDiff{
   698  					"foo.#": &ResourceAttrDiff{NewComputed: true},
   699  				},
   700  			},
   701  			&InstanceDiff{
   702  				Attributes: map[string]*ResourceAttrDiff{
   703  					"foo.#": &ResourceAttrDiff{
   704  						Old: "0",
   705  						New: "1",
   706  					},
   707  					"foo.0": &ResourceAttrDiff{
   708  						Old: "",
   709  						New: "12",
   710  					},
   711  				},
   712  			},
   713  			true,
   714  			"",
   715  		},
   716  
   717  		{
   718  			&InstanceDiff{
   719  				Attributes: map[string]*ResourceAttrDiff{
   720  					"foo.#": &ResourceAttrDiff{
   721  						Old: "0",
   722  						New: "1",
   723  					},
   724  					"foo.~35964334.bar": &ResourceAttrDiff{
   725  						Old: "",
   726  						New: "${var.foo}",
   727  					},
   728  				},
   729  			},
   730  			&InstanceDiff{
   731  				Attributes: map[string]*ResourceAttrDiff{
   732  					"foo.#": &ResourceAttrDiff{
   733  						Old: "0",
   734  						New: "1",
   735  					},
   736  					"foo.87654323.bar": &ResourceAttrDiff{
   737  						Old: "",
   738  						New: "12",
   739  					},
   740  				},
   741  			},
   742  			true,
   743  			"",
   744  		},
   745  
   746  		{
   747  			&InstanceDiff{
   748  				Attributes: map[string]*ResourceAttrDiff{
   749  					"foo.#": &ResourceAttrDiff{
   750  						Old:         "0",
   751  						NewComputed: true,
   752  					},
   753  				},
   754  			},
   755  			&InstanceDiff{
   756  				Attributes: map[string]*ResourceAttrDiff{},
   757  			},
   758  			true,
   759  			"",
   760  		},
   761  
   762  		// Computed can change RequiresNew by removal, and that's okay
   763  		{
   764  			&InstanceDiff{
   765  				Attributes: map[string]*ResourceAttrDiff{
   766  					"foo.#": &ResourceAttrDiff{
   767  						Old:         "0",
   768  						NewComputed: true,
   769  						RequiresNew: true,
   770  					},
   771  				},
   772  			},
   773  			&InstanceDiff{
   774  				Attributes: map[string]*ResourceAttrDiff{},
   775  			},
   776  			true,
   777  			"",
   778  		},
   779  
   780  		// Computed can change Destroy by removal, and that's okay
   781  		{
   782  			&InstanceDiff{
   783  				Attributes: map[string]*ResourceAttrDiff{
   784  					"foo.#": &ResourceAttrDiff{
   785  						Old:         "0",
   786  						NewComputed: true,
   787  						RequiresNew: true,
   788  					},
   789  				},
   790  
   791  				Destroy: true,
   792  			},
   793  			&InstanceDiff{
   794  				Attributes: map[string]*ResourceAttrDiff{},
   795  			},
   796  			true,
   797  			"",
   798  		},
   799  
   800  		// Computed can change Destroy by elements
   801  		{
   802  			&InstanceDiff{
   803  				Attributes: map[string]*ResourceAttrDiff{
   804  					"foo.#": &ResourceAttrDiff{
   805  						Old:         "0",
   806  						NewComputed: true,
   807  						RequiresNew: true,
   808  					},
   809  				},
   810  
   811  				Destroy: true,
   812  			},
   813  			&InstanceDiff{
   814  				Attributes: map[string]*ResourceAttrDiff{
   815  					"foo.#": &ResourceAttrDiff{
   816  						Old: "1",
   817  						New: "1",
   818  					},
   819  					"foo.12": &ResourceAttrDiff{
   820  						Old:         "4",
   821  						New:         "12",
   822  						RequiresNew: true,
   823  					},
   824  				},
   825  
   826  				Destroy: true,
   827  			},
   828  			true,
   829  			"",
   830  		},
   831  
   832  		// Computed sets may not contain all fields in the original diff, and
   833  		// because multiple entries for the same set can compute to the same
   834  		// hash before the values are computed or interpolated, the overall
   835  		// count can change as well.
   836  		{
   837  			&InstanceDiff{
   838  				Attributes: map[string]*ResourceAttrDiff{
   839  					"foo.#": &ResourceAttrDiff{
   840  						Old: "0",
   841  						New: "1",
   842  					},
   843  					"foo.~35964334.bar": &ResourceAttrDiff{
   844  						Old: "",
   845  						New: "${var.foo}",
   846  					},
   847  				},
   848  			},
   849  			&InstanceDiff{
   850  				Attributes: map[string]*ResourceAttrDiff{
   851  					"foo.#": &ResourceAttrDiff{
   852  						Old: "0",
   853  						New: "2",
   854  					},
   855  					"foo.87654323.bar": &ResourceAttrDiff{
   856  						Old: "",
   857  						New: "12",
   858  					},
   859  					"foo.87654325.bar": &ResourceAttrDiff{
   860  						Old: "",
   861  						New: "12",
   862  					},
   863  					"foo.87654325.baz": &ResourceAttrDiff{
   864  						Old: "",
   865  						New: "12",
   866  					},
   867  				},
   868  			},
   869  			true,
   870  			"",
   871  		},
   872  
   873  		// Computed values in maps will fail the "Same" check as well
   874  		{
   875  			&InstanceDiff{
   876  				Attributes: map[string]*ResourceAttrDiff{
   877  					"foo.%": &ResourceAttrDiff{
   878  						Old:         "",
   879  						New:         "",
   880  						NewComputed: true,
   881  					},
   882  				},
   883  			},
   884  			&InstanceDiff{
   885  				Attributes: map[string]*ResourceAttrDiff{
   886  					"foo.%": &ResourceAttrDiff{
   887  						Old:         "0",
   888  						New:         "1",
   889  						NewComputed: false,
   890  					},
   891  					"foo.val": &ResourceAttrDiff{
   892  						Old: "",
   893  						New: "something",
   894  					},
   895  				},
   896  			},
   897  			true,
   898  			"",
   899  		},
   900  
   901  		// In a DESTROY/CREATE scenario, the plan diff will be run against the
   902  		// state of the old instance, while the apply diff will be run against an
   903  		// empty state (because the state is cleared when the destroy runs.)
   904  		// For complex attributes, this can result in keys that seem to disappear
   905  		// between the two diffs, when in reality everything is working just fine.
   906  		//
   907  		// Same() needs to take into account this scenario by analyzing NewRemoved
   908  		// and treating as "Same" a diff that does indeed have that key removed.
   909  		{
   910  			&InstanceDiff{
   911  				Attributes: map[string]*ResourceAttrDiff{
   912  					"somemap.oldkey": &ResourceAttrDiff{
   913  						Old:        "long ago",
   914  						New:        "",
   915  						NewRemoved: true,
   916  					},
   917  					"somemap.newkey": &ResourceAttrDiff{
   918  						Old: "",
   919  						New: "brave new world",
   920  					},
   921  				},
   922  			},
   923  			&InstanceDiff{
   924  				Attributes: map[string]*ResourceAttrDiff{
   925  					"somemap.newkey": &ResourceAttrDiff{
   926  						Old: "",
   927  						New: "brave new world",
   928  					},
   929  				},
   930  			},
   931  			true,
   932  			"",
   933  		},
   934  
   935  		// Another thing that can occur in DESTROY/CREATE scenarios is that list
   936  		// values that are going to zero have diffs that show up at plan time but
   937  		// are gone at apply time. The NewRemoved handling catches the fields and
   938  		// treats them as OK, but it also needs to treat the .# field itself as
   939  		// okay to be present in the old diff but not in the new one.
   940  		{
   941  			&InstanceDiff{
   942  				Attributes: map[string]*ResourceAttrDiff{
   943  					"reqnew": &ResourceAttrDiff{
   944  						Old:         "old",
   945  						New:         "new",
   946  						RequiresNew: true,
   947  					},
   948  					"somemap.#": &ResourceAttrDiff{
   949  						Old: "1",
   950  						New: "0",
   951  					},
   952  					"somemap.oldkey": &ResourceAttrDiff{
   953  						Old:        "long ago",
   954  						New:        "",
   955  						NewRemoved: true,
   956  					},
   957  				},
   958  			},
   959  			&InstanceDiff{
   960  				Attributes: map[string]*ResourceAttrDiff{
   961  					"reqnew": &ResourceAttrDiff{
   962  						Old:         "",
   963  						New:         "new",
   964  						RequiresNew: true,
   965  					},
   966  				},
   967  			},
   968  			true,
   969  			"",
   970  		},
   971  
   972  		{
   973  			&InstanceDiff{
   974  				Attributes: map[string]*ResourceAttrDiff{
   975  					"reqnew": &ResourceAttrDiff{
   976  						Old:         "old",
   977  						New:         "new",
   978  						RequiresNew: true,
   979  					},
   980  					"somemap.%": &ResourceAttrDiff{
   981  						Old: "1",
   982  						New: "0",
   983  					},
   984  					"somemap.oldkey": &ResourceAttrDiff{
   985  						Old:        "long ago",
   986  						New:        "",
   987  						NewRemoved: true,
   988  					},
   989  				},
   990  			},
   991  			&InstanceDiff{
   992  				Attributes: map[string]*ResourceAttrDiff{
   993  					"reqnew": &ResourceAttrDiff{
   994  						Old:         "",
   995  						New:         "new",
   996  						RequiresNew: true,
   997  					},
   998  				},
   999  			},
  1000  			true,
  1001  			"",
  1002  		},
  1003  
  1004  		// Innner computed set should allow outer change in key
  1005  		{
  1006  			&InstanceDiff{
  1007  				Attributes: map[string]*ResourceAttrDiff{
  1008  					"foo.#": &ResourceAttrDiff{
  1009  						Old: "0",
  1010  						New: "1",
  1011  					},
  1012  					"foo.~1.outer_val": &ResourceAttrDiff{
  1013  						Old: "",
  1014  						New: "foo",
  1015  					},
  1016  					"foo.~1.inner.#": &ResourceAttrDiff{
  1017  						Old: "0",
  1018  						New: "1",
  1019  					},
  1020  					"foo.~1.inner.~2.value": &ResourceAttrDiff{
  1021  						Old:         "",
  1022  						New:         "${var.bar}",
  1023  						NewComputed: true,
  1024  					},
  1025  				},
  1026  			},
  1027  			&InstanceDiff{
  1028  				Attributes: map[string]*ResourceAttrDiff{
  1029  					"foo.#": &ResourceAttrDiff{
  1030  						Old: "0",
  1031  						New: "1",
  1032  					},
  1033  					"foo.12.outer_val": &ResourceAttrDiff{
  1034  						Old: "",
  1035  						New: "foo",
  1036  					},
  1037  					"foo.12.inner.#": &ResourceAttrDiff{
  1038  						Old: "0",
  1039  						New: "1",
  1040  					},
  1041  					"foo.12.inner.42.value": &ResourceAttrDiff{
  1042  						Old: "",
  1043  						New: "baz",
  1044  					},
  1045  				},
  1046  			},
  1047  			true,
  1048  			"",
  1049  		},
  1050  
  1051  		// Innner computed list should allow outer change in key
  1052  		{
  1053  			&InstanceDiff{
  1054  				Attributes: map[string]*ResourceAttrDiff{
  1055  					"foo.#": &ResourceAttrDiff{
  1056  						Old: "0",
  1057  						New: "1",
  1058  					},
  1059  					"foo.~1.outer_val": &ResourceAttrDiff{
  1060  						Old: "",
  1061  						New: "foo",
  1062  					},
  1063  					"foo.~1.inner.#": &ResourceAttrDiff{
  1064  						Old: "0",
  1065  						New: "1",
  1066  					},
  1067  					"foo.~1.inner.0.value": &ResourceAttrDiff{
  1068  						Old:         "",
  1069  						New:         "${var.bar}",
  1070  						NewComputed: true,
  1071  					},
  1072  				},
  1073  			},
  1074  			&InstanceDiff{
  1075  				Attributes: map[string]*ResourceAttrDiff{
  1076  					"foo.#": &ResourceAttrDiff{
  1077  						Old: "0",
  1078  						New: "1",
  1079  					},
  1080  					"foo.12.outer_val": &ResourceAttrDiff{
  1081  						Old: "",
  1082  						New: "foo",
  1083  					},
  1084  					"foo.12.inner.#": &ResourceAttrDiff{
  1085  						Old: "0",
  1086  						New: "1",
  1087  					},
  1088  					"foo.12.inner.0.value": &ResourceAttrDiff{
  1089  						Old: "",
  1090  						New: "baz",
  1091  					},
  1092  				},
  1093  			},
  1094  			true,
  1095  			"",
  1096  		},
  1097  
  1098  		// When removing all collection items, the diff is allowed to contain
  1099  		// nothing when re-creating the resource. This should be the "Same"
  1100  		// since we said we were going from 1 to 0.
  1101  		{
  1102  			&InstanceDiff{
  1103  				Attributes: map[string]*ResourceAttrDiff{
  1104  					"foo.%": &ResourceAttrDiff{
  1105  						Old:         "1",
  1106  						New:         "0",
  1107  						RequiresNew: true,
  1108  					},
  1109  					"foo.bar": &ResourceAttrDiff{
  1110  						Old:         "baz",
  1111  						New:         "",
  1112  						NewRemoved:  true,
  1113  						RequiresNew: true,
  1114  					},
  1115  				},
  1116  			},
  1117  			&InstanceDiff{},
  1118  			true,
  1119  			"",
  1120  		},
  1121  
  1122  		{
  1123  			&InstanceDiff{
  1124  				Attributes: map[string]*ResourceAttrDiff{
  1125  					"foo.#": &ResourceAttrDiff{
  1126  						Old:         "1",
  1127  						New:         "0",
  1128  						RequiresNew: true,
  1129  					},
  1130  					"foo.0": &ResourceAttrDiff{
  1131  						Old:         "baz",
  1132  						New:         "",
  1133  						NewRemoved:  true,
  1134  						RequiresNew: true,
  1135  					},
  1136  				},
  1137  			},
  1138  			&InstanceDiff{},
  1139  			true,
  1140  			"",
  1141  		},
  1142  
  1143  		// Make sure that DestroyTainted diffs pass as well, especially when diff
  1144  		// two works off of no state.
  1145  		{
  1146  			&InstanceDiff{
  1147  				DestroyTainted: true,
  1148  				Attributes: map[string]*ResourceAttrDiff{
  1149  					"foo": &ResourceAttrDiff{
  1150  						Old: "foo",
  1151  						New: "foo",
  1152  					},
  1153  				},
  1154  			},
  1155  			&InstanceDiff{
  1156  				DestroyTainted: true,
  1157  				Attributes: map[string]*ResourceAttrDiff{
  1158  					"foo": &ResourceAttrDiff{
  1159  						Old: "",
  1160  						New: "foo",
  1161  					},
  1162  				},
  1163  			},
  1164  			true,
  1165  			"",
  1166  		},
  1167  		// RequiresNew in different attribute
  1168  		{
  1169  			&InstanceDiff{
  1170  				Attributes: map[string]*ResourceAttrDiff{
  1171  					"foo": &ResourceAttrDiff{
  1172  						Old: "foo",
  1173  						New: "foo",
  1174  					},
  1175  					"bar": &ResourceAttrDiff{
  1176  						Old:         "bar",
  1177  						New:         "baz",
  1178  						RequiresNew: true,
  1179  					},
  1180  				},
  1181  			},
  1182  			&InstanceDiff{
  1183  				Attributes: map[string]*ResourceAttrDiff{
  1184  					"foo": &ResourceAttrDiff{
  1185  						Old: "",
  1186  						New: "foo",
  1187  					},
  1188  					"bar": &ResourceAttrDiff{
  1189  						Old:         "",
  1190  						New:         "baz",
  1191  						RequiresNew: true,
  1192  					},
  1193  				},
  1194  			},
  1195  			true,
  1196  			"",
  1197  		},
  1198  	}
  1199  
  1200  	for i, tc := range cases {
  1201  		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
  1202  			same, reason := tc.One.Same(tc.Two)
  1203  			if same != tc.Same {
  1204  				t.Fatalf("%d: expected same: %t, got %t (%s)\n\n one: %#v\n\ntwo: %#v",
  1205  					i, tc.Same, same, reason, tc.One, tc.Two)
  1206  			}
  1207  			if reason != tc.Reason {
  1208  				t.Fatalf(
  1209  					"%d: bad reason\n\nexpected: %#v\n\ngot: %#v", i, tc.Reason, reason)
  1210  			}
  1211  		})
  1212  	}
  1213  }
  1214  
  1215  const moduleDiffStrBasic = `
  1216  CREATE: nodeA
  1217    bar:       "foo" => "<computed>"
  1218    foo:       "foo" => "bar"
  1219    longfoo:   "foo" => "bar" (forces new resource)
  1220    secretfoo: "<sensitive>" => "<sensitive>" (attribute changed)
  1221  `
  1222  
  1223  func TestCountFlatmapContainerValues(t *testing.T) {
  1224  	for i, tc := range []struct {
  1225  		attrs map[string]string
  1226  		key   string
  1227  		count string
  1228  	}{
  1229  		{
  1230  			attrs: map[string]string{"set.2.list.#": "9999", "set.2.list.0": "x", "set.2.list.0.z": "y", "set.2.attr": "bar", "set.#": "9999"},
  1231  			key:   "set.2.list.#",
  1232  			count: "1",
  1233  		},
  1234  		{
  1235  			attrs: map[string]string{"set.2.list.#": "9999", "set.2.list.0": "x", "set.2.list.0.z": "y", "set.2.attr": "bar", "set.#": "9999"},
  1236  			key:   "set.#",
  1237  			count: "1",
  1238  		},
  1239  		{
  1240  			attrs: map[string]string{"set.2.list.0": "x", "set.2.list.0.z": "y", "set.2.attr": "bar", "set.#": "9999"},
  1241  			key:   "set.#",
  1242  			count: "1",
  1243  		},
  1244  		{
  1245  			attrs: map[string]string{"map.#": "3", "map.a": "b", "map.a.#": "0", "map.b": "4"},
  1246  			key:   "map.#",
  1247  			count: "2",
  1248  		},
  1249  	} {
  1250  		t.Run(strconv.Itoa(i), func(t *testing.T) {
  1251  			count := countFlatmapContainerValues(tc.key, tc.attrs)
  1252  			if count != tc.count {
  1253  				t.Fatalf("expected %q, got %q", tc.count, count)
  1254  			}
  1255  		})
  1256  	}
  1257  }