github.com/hashicorp/terraform-plugin-sdk@v1.17.2/terraform/diff_test.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 "reflect" 6 "strconv" 7 "strings" 8 "testing" 9 10 "github.com/hashicorp/terraform-plugin-sdk/internal/addrs" 11 ) 12 13 func 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": { 28 Old: "foo", 29 New: "bar", 30 }, 31 }, 32 } 33 34 if diff.Empty() { 35 t.Fatal("should not be empty") 36 } 37 } 38 39 func 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 52 func 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 {Path: []string{"root", "foo"}}, 73 {Path: []string{"root", "bar"}}, 74 }, 75 }, 76 &Diff{ 77 Modules: []*ModuleDiff{ 78 {Path: []string{"root", "bar"}}, 79 {Path: []string{"root", "foo"}}, 80 }, 81 }, 82 true, 83 }, 84 85 "different module diff destroys": { 86 &Diff{ 87 Modules: []*ModuleDiff{ 88 {Path: []string{"root", "foo"}, Destroy: true}, 89 }, 90 }, 91 &Diff{ 92 Modules: []*ModuleDiff{ 93 {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 110 func 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 {Path: []string{"root", "foo"}}, 128 }, 129 }, 130 &Diff{}, 131 }, 132 133 "destroy module": { 134 &Diff{ 135 Modules: []*ModuleDiff{ 136 {Path: []string{"root", "foo"}, Destroy: true}, 137 }, 138 }, 139 &Diff{ 140 Modules: []*ModuleDiff{ 141 {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 157 func 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": {Destroy: true}, 170 }, 171 }, 172 DiffDestroy, 173 }, 174 { 175 &ModuleDiff{ 176 Resources: map[string]*InstanceDiff{ 177 "foo": { 178 Attributes: map[string]*ResourceAttrDiff{ 179 "foo": { 180 Old: "", 181 New: "bar", 182 }, 183 }, 184 }, 185 }, 186 }, 187 DiffUpdate, 188 }, 189 { 190 &ModuleDiff{ 191 Resources: map[string]*InstanceDiff{ 192 "foo": { 193 Attributes: map[string]*ResourceAttrDiff{ 194 "foo": { 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": { 209 Destroy: true, 210 Attributes: map[string]*ResourceAttrDiff{ 211 "foo": { 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 232 func TestDiff_DeepCopy(t *testing.T) { 233 cases := map[string]*Diff{ 234 "empty": {}, 235 236 "basic diff": { 237 Modules: []*ModuleDiff{ 238 { 239 Path: []string{"root"}, 240 Resources: map[string]*InstanceDiff{ 241 "aws_instance.foo": { 242 Attributes: map[string]*ResourceAttrDiff{ 243 "num": { 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 265 func 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": {}, 273 } 274 275 if !diff.Empty() { 276 t.Fatal("should be empty") 277 } 278 279 diff.Resources["nodeA"].Attributes = map[string]*ResourceAttrDiff{ 280 "foo": { 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 298 func TestModuleDiff_String(t *testing.T) { 299 diff := &ModuleDiff{ 300 Resources: map[string]*InstanceDiff{ 301 "nodeA": { 302 Attributes: map[string]*ResourceAttrDiff{ 303 "foo": { 304 Old: "foo", 305 New: "bar", 306 }, 307 "bar": { 308 Old: "foo", 309 NewComputed: true, 310 }, 311 "longfoo": { 312 Old: "foo", 313 New: "bar", 314 RequiresNew: true, 315 }, 316 "secretfoo": { 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 333 func 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": { 350 Old: "", 351 New: "bar", 352 }, 353 }, 354 }, 355 DiffUpdate, 356 }, 357 { 358 &InstanceDiff{ 359 Attributes: map[string]*ResourceAttrDiff{ 360 "foo": { 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": { 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": { 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 405 func 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": { 427 New: "bar", 428 }, 429 }, 430 } 431 432 if rd.Empty() { 433 t.Fatal("should not be empty") 434 } 435 } 436 437 func 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 499 func TestInstanceDiff_RequiresNew(t *testing.T) { 500 rd := &InstanceDiff{ 501 Attributes: map[string]*ResourceAttrDiff{ 502 "foo": {}, 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 517 func 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 525 func 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": {}, 563 }, 564 }, 565 &InstanceDiff{ 566 Attributes: map[string]*ResourceAttrDiff{ 567 "foo": {}, 568 }, 569 }, 570 true, 571 "", 572 }, 573 574 { 575 &InstanceDiff{ 576 Attributes: map[string]*ResourceAttrDiff{ 577 "bar": {}, 578 }, 579 }, 580 &InstanceDiff{ 581 Attributes: map[string]*ResourceAttrDiff{ 582 "foo": {}, 583 }, 584 }, 585 false, 586 "attribute mismatch: bar", 587 }, 588 589 // Extra attributes 590 { 591 &InstanceDiff{ 592 Attributes: map[string]*ResourceAttrDiff{ 593 "foo": {}, 594 }, 595 }, 596 &InstanceDiff{ 597 Attributes: map[string]*ResourceAttrDiff{ 598 "foo": {}, 599 "bar": {}, 600 }, 601 }, 602 false, 603 "extra attributes: bar", 604 }, 605 606 { 607 &InstanceDiff{ 608 Attributes: map[string]*ResourceAttrDiff{ 609 "foo": {RequiresNew: true}, 610 }, 611 }, 612 &InstanceDiff{ 613 Attributes: map[string]*ResourceAttrDiff{ 614 "foo": {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": { 626 Old: "", 627 New: "${var.foo}", 628 NewComputed: true, 629 }, 630 }, 631 }, 632 &InstanceDiff{ 633 Attributes: map[string]*ResourceAttrDiff{ 634 "foo": { 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": { 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.#": { 667 Old: "", 668 New: "", 669 NewComputed: true, 670 }, 671 }, 672 }, 673 &InstanceDiff{ 674 Attributes: map[string]*ResourceAttrDiff{ 675 "foo.1": { 676 Old: "foo", 677 New: "", 678 NewRemoved: true, 679 }, 680 "foo.2": { 681 Old: "", 682 New: "bar", 683 }, 684 }, 685 }, 686 true, 687 "", 688 }, 689 690 { 691 &InstanceDiff{ 692 Attributes: map[string]*ResourceAttrDiff{ 693 "foo.#": {NewComputed: true}, 694 }, 695 }, 696 &InstanceDiff{ 697 Attributes: map[string]*ResourceAttrDiff{ 698 "foo.#": { 699 Old: "0", 700 New: "1", 701 }, 702 "foo.0": { 703 Old: "", 704 New: "12", 705 }, 706 }, 707 }, 708 true, 709 "", 710 }, 711 712 { 713 &InstanceDiff{ 714 Attributes: map[string]*ResourceAttrDiff{ 715 "foo.#": { 716 Old: "0", 717 New: "1", 718 }, 719 "foo.~35964334.bar": { 720 Old: "", 721 New: "${var.foo}", 722 }, 723 }, 724 }, 725 &InstanceDiff{ 726 Attributes: map[string]*ResourceAttrDiff{ 727 "foo.#": { 728 Old: "0", 729 New: "1", 730 }, 731 "foo.87654323.bar": { 732 Old: "", 733 New: "12", 734 }, 735 }, 736 }, 737 true, 738 "", 739 }, 740 741 { 742 &InstanceDiff{ 743 Attributes: map[string]*ResourceAttrDiff{ 744 "foo.#": { 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.#": { 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.#": { 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.#": { 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.#": { 811 Old: "1", 812 New: "1", 813 }, 814 "foo.12": { 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.#": { 835 Old: "0", 836 New: "1", 837 }, 838 "foo.~35964334.bar": { 839 Old: "", 840 New: "${var.foo}", 841 }, 842 }, 843 }, 844 &InstanceDiff{ 845 Attributes: map[string]*ResourceAttrDiff{ 846 "foo.#": { 847 Old: "0", 848 New: "2", 849 }, 850 "foo.87654323.bar": { 851 Old: "", 852 New: "12", 853 }, 854 "foo.87654325.bar": { 855 Old: "", 856 New: "12", 857 }, 858 "foo.87654325.baz": { 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.%": { 873 Old: "", 874 New: "", 875 NewComputed: true, 876 }, 877 }, 878 }, 879 &InstanceDiff{ 880 Attributes: map[string]*ResourceAttrDiff{ 881 "foo.%": { 882 Old: "0", 883 New: "1", 884 NewComputed: false, 885 }, 886 "foo.val": { 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": { 908 Old: "long ago", 909 New: "", 910 NewRemoved: true, 911 }, 912 "somemap.newkey": { 913 Old: "", 914 New: "brave new world", 915 }, 916 }, 917 }, 918 &InstanceDiff{ 919 Attributes: map[string]*ResourceAttrDiff{ 920 "somemap.newkey": { 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": { 939 Old: "old", 940 New: "new", 941 RequiresNew: true, 942 }, 943 "somemap.#": { 944 Old: "1", 945 New: "0", 946 }, 947 "somemap.oldkey": { 948 Old: "long ago", 949 New: "", 950 NewRemoved: true, 951 }, 952 }, 953 }, 954 &InstanceDiff{ 955 Attributes: map[string]*ResourceAttrDiff{ 956 "reqnew": { 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": { 971 Old: "old", 972 New: "new", 973 RequiresNew: true, 974 }, 975 "somemap.%": { 976 Old: "1", 977 New: "0", 978 }, 979 "somemap.oldkey": { 980 Old: "long ago", 981 New: "", 982 NewRemoved: true, 983 }, 984 }, 985 }, 986 &InstanceDiff{ 987 Attributes: map[string]*ResourceAttrDiff{ 988 "reqnew": { 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.#": { 1004 Old: "0", 1005 New: "1", 1006 }, 1007 "foo.~1.outer_val": { 1008 Old: "", 1009 New: "foo", 1010 }, 1011 "foo.~1.inner.#": { 1012 Old: "0", 1013 New: "1", 1014 }, 1015 "foo.~1.inner.~2.value": { 1016 Old: "", 1017 New: "${var.bar}", 1018 NewComputed: true, 1019 }, 1020 }, 1021 }, 1022 &InstanceDiff{ 1023 Attributes: map[string]*ResourceAttrDiff{ 1024 "foo.#": { 1025 Old: "0", 1026 New: "1", 1027 }, 1028 "foo.12.outer_val": { 1029 Old: "", 1030 New: "foo", 1031 }, 1032 "foo.12.inner.#": { 1033 Old: "0", 1034 New: "1", 1035 }, 1036 "foo.12.inner.42.value": { 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.#": { 1051 Old: "0", 1052 New: "1", 1053 }, 1054 "foo.~1.outer_val": { 1055 Old: "", 1056 New: "foo", 1057 }, 1058 "foo.~1.inner.#": { 1059 Old: "0", 1060 New: "1", 1061 }, 1062 "foo.~1.inner.0.value": { 1063 Old: "", 1064 New: "${var.bar}", 1065 NewComputed: true, 1066 }, 1067 }, 1068 }, 1069 &InstanceDiff{ 1070 Attributes: map[string]*ResourceAttrDiff{ 1071 "foo.#": { 1072 Old: "0", 1073 New: "1", 1074 }, 1075 "foo.12.outer_val": { 1076 Old: "", 1077 New: "foo", 1078 }, 1079 "foo.12.inner.#": { 1080 Old: "0", 1081 New: "1", 1082 }, 1083 "foo.12.inner.0.value": { 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.%": { 1100 Old: "1", 1101 New: "0", 1102 RequiresNew: true, 1103 }, 1104 "foo.bar": { 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.#": { 1121 Old: "1", 1122 New: "0", 1123 RequiresNew: true, 1124 }, 1125 "foo.0": { 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": { 1145 Old: "foo", 1146 New: "foo", 1147 }, 1148 }, 1149 }, 1150 &InstanceDiff{ 1151 DestroyTainted: true, 1152 Attributes: map[string]*ResourceAttrDiff{ 1153 "foo": { 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": { 1167 Old: "foo", 1168 New: "foo", 1169 }, 1170 "bar": { 1171 Old: "bar", 1172 New: "baz", 1173 RequiresNew: true, 1174 }, 1175 }, 1176 }, 1177 &InstanceDiff{ 1178 Attributes: map[string]*ResourceAttrDiff{ 1179 "foo": { 1180 Old: "", 1181 New: "foo", 1182 }, 1183 "bar": { 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 1210 const moduleDiffStrBasic = ` 1211 CREATE: nodeA 1212 bar: "foo" => "<computed>" 1213 foo: "foo" => "bar" 1214 longfoo: "foo" => "bar" (forces new resource) 1215 secretfoo: "<sensitive>" => "<sensitive>" (attribute changed) 1216 ` 1217 1218 func 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 }