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