github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/terraform/diff_test.go (about)

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