github.com/skyscape-cloud-services/terraform@v0.9.2-0.20170609144644-7ece028a1747/terraform/diff_test.go (about)

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