github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/terraform/context_test.go (about) 1 package terraform 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "path/filepath" 8 "sort" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/google/go-cmp/cmp" 14 "github.com/google/go-cmp/cmp/cmpopts" 15 "github.com/hashicorp/go-version" 16 "github.com/hashicorp/terraform/internal/configs" 17 "github.com/hashicorp/terraform/internal/configs/configload" 18 "github.com/hashicorp/terraform/internal/configs/configschema" 19 "github.com/hashicorp/terraform/internal/configs/hcl2shim" 20 "github.com/hashicorp/terraform/internal/plans" 21 "github.com/hashicorp/terraform/internal/plans/planfile" 22 "github.com/hashicorp/terraform/internal/providers" 23 "github.com/hashicorp/terraform/internal/provisioners" 24 "github.com/hashicorp/terraform/internal/states" 25 "github.com/hashicorp/terraform/internal/states/statefile" 26 "github.com/hashicorp/terraform/internal/tfdiags" 27 tfversion "github.com/hashicorp/terraform/version" 28 "github.com/zclconf/go-cty/cty" 29 ) 30 31 var ( 32 equateEmpty = cmpopts.EquateEmpty() 33 typeComparer = cmp.Comparer(cty.Type.Equals) 34 valueComparer = cmp.Comparer(cty.Value.RawEquals) 35 valueTrans = cmp.Transformer("hcl2shim", hcl2shim.ConfigValueFromHCL2) 36 ) 37 38 func TestNewContextRequiredVersion(t *testing.T) { 39 cases := []struct { 40 Name string 41 Module string 42 Version string 43 Value string 44 Err bool 45 }{ 46 { 47 "no requirement", 48 "", 49 "0.1.0", 50 "", 51 false, 52 }, 53 54 { 55 "doesn't match", 56 "", 57 "0.1.0", 58 "> 0.6.0", 59 true, 60 }, 61 62 { 63 "matches", 64 "", 65 "0.7.0", 66 "> 0.6.0", 67 false, 68 }, 69 70 { 71 "prerelease doesn't match with inequality", 72 "", 73 "0.8.0", 74 "> 0.7.0-beta", 75 true, 76 }, 77 78 { 79 "prerelease doesn't match with equality", 80 "", 81 "0.7.0", 82 "0.7.0-beta", 83 true, 84 }, 85 86 { 87 "module matches", 88 "context-required-version-module", 89 "0.5.0", 90 "", 91 false, 92 }, 93 94 { 95 "module doesn't match", 96 "context-required-version-module", 97 "0.4.0", 98 "", 99 true, 100 }, 101 } 102 103 for i, tc := range cases { 104 t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { 105 // Reset the version for the tests 106 old := tfversion.SemVer 107 tfversion.SemVer = version.Must(version.NewVersion(tc.Version)) 108 defer func() { tfversion.SemVer = old }() 109 110 name := "context-required-version" 111 if tc.Module != "" { 112 name = tc.Module 113 } 114 mod := testModule(t, name) 115 if tc.Value != "" { 116 constraint, err := version.NewConstraint(tc.Value) 117 if err != nil { 118 t.Fatalf("can't parse %q as version constraint", tc.Value) 119 } 120 mod.Module.CoreVersionConstraints = append(mod.Module.CoreVersionConstraints, configs.VersionConstraint{ 121 Required: constraint, 122 }) 123 } 124 c, diags := NewContext(&ContextOpts{}) 125 if diags.HasErrors() { 126 t.Fatalf("unexpected NewContext errors: %s", diags.Err()) 127 } 128 129 diags = c.Validate(mod) 130 if diags.HasErrors() != tc.Err { 131 t.Fatalf("err: %s", diags.Err()) 132 } 133 }) 134 } 135 } 136 137 func TestContext_missingPlugins(t *testing.T) { 138 ctx, diags := NewContext(&ContextOpts{}) 139 assertNoDiagnostics(t, diags) 140 141 configSrc := ` 142 terraform { 143 required_providers { 144 explicit = { 145 source = "example.com/foo/beep" 146 } 147 builtin = { 148 source = "terraform.io/builtin/nonexist" 149 } 150 } 151 } 152 153 resource "implicit_thing" "a" { 154 provisioner "nonexist" { 155 } 156 } 157 158 resource "implicit_thing" "b" { 159 provider = implicit2 160 } 161 ` 162 163 cfg := testModuleInline(t, map[string]string{ 164 "main.tf": configSrc, 165 }) 166 167 // Validate and Plan are the two entry points where we explicitly verify 168 // the available plugins match what the configuration needs. For other 169 // operations we typically fail more deeply in Terraform Core, with 170 // potentially-less-helpful error messages, because getting there would 171 // require doing some pretty weird things that aren't common enough to 172 // be worth the complexity to check for them. 173 174 validateDiags := ctx.Validate(cfg) 175 _, planDiags := ctx.Plan(cfg, nil, DefaultPlanOpts) 176 177 tests := map[string]tfdiags.Diagnostics{ 178 "validate": validateDiags, 179 "plan": planDiags, 180 } 181 182 for testName, gotDiags := range tests { 183 t.Run(testName, func(t *testing.T) { 184 var wantDiags tfdiags.Diagnostics 185 wantDiags = wantDiags.Append( 186 tfdiags.Sourceless( 187 tfdiags.Error, 188 "Missing required provider", 189 "This configuration requires built-in provider terraform.io/builtin/nonexist, but that provider isn't available in this Terraform version.", 190 ), 191 tfdiags.Sourceless( 192 tfdiags.Error, 193 "Missing required provider", 194 "This configuration requires provider example.com/foo/beep, but that provider isn't available. You may be able to install it automatically by running:\n terraform init", 195 ), 196 tfdiags.Sourceless( 197 tfdiags.Error, 198 "Missing required provider", 199 "This configuration requires provider registry.terraform.io/hashicorp/implicit, but that provider isn't available. You may be able to install it automatically by running:\n terraform init", 200 ), 201 tfdiags.Sourceless( 202 tfdiags.Error, 203 "Missing required provider", 204 "This configuration requires provider registry.terraform.io/hashicorp/implicit2, but that provider isn't available. You may be able to install it automatically by running:\n terraform init", 205 ), 206 tfdiags.Sourceless( 207 tfdiags.Error, 208 "Missing required provisioner plugin", 209 `This configuration requires provisioner plugin "nonexist", which isn't available. If you're intending to use an external provisioner plugin, you must install it manually into one of the plugin search directories before running Terraform.`, 210 ), 211 ) 212 assertDiagnosticsMatch(t, gotDiags, wantDiags) 213 }) 214 } 215 } 216 217 func testContext2(t *testing.T, opts *ContextOpts) *Context { 218 t.Helper() 219 220 ctx, diags := NewContext(opts) 221 if diags.HasErrors() { 222 t.Fatalf("failed to create test context\n\n%s\n", diags.Err()) 223 } 224 225 return ctx 226 } 227 228 func testApplyFn(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 229 resp.NewState = req.PlannedState 230 if req.PlannedState.IsNull() { 231 resp.NewState = cty.NullVal(req.PriorState.Type()) 232 return 233 } 234 235 planned := req.PlannedState.AsValueMap() 236 if planned == nil { 237 planned = map[string]cty.Value{} 238 } 239 240 id, ok := planned["id"] 241 if !ok || id.IsNull() || !id.IsKnown() { 242 planned["id"] = cty.StringVal("foo") 243 } 244 245 // our default schema has a computed "type" attr 246 if ty, ok := planned["type"]; ok && !ty.IsNull() { 247 planned["type"] = cty.StringVal(req.TypeName) 248 } 249 250 if cmp, ok := planned["compute"]; ok && !cmp.IsNull() { 251 computed := cmp.AsString() 252 if val, ok := planned[computed]; ok && !val.IsKnown() { 253 planned[computed] = cty.StringVal("computed_value") 254 } 255 } 256 257 for k, v := range planned { 258 if k == "unknown" { 259 // "unknown" should cause an error 260 continue 261 } 262 263 if !v.IsKnown() { 264 switch k { 265 case "type": 266 planned[k] = cty.StringVal(req.TypeName) 267 default: 268 planned[k] = cty.NullVal(v.Type()) 269 } 270 } 271 } 272 273 resp.NewState = cty.ObjectVal(planned) 274 return 275 } 276 277 func testDiffFn(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 278 var planned map[string]cty.Value 279 280 // this is a destroy plan 281 if req.ProposedNewState.IsNull() { 282 resp.PlannedState = req.ProposedNewState 283 resp.PlannedPrivate = req.PriorPrivate 284 return resp 285 } 286 287 if !req.ProposedNewState.IsNull() { 288 planned = req.ProposedNewState.AsValueMap() 289 } 290 if planned == nil { 291 planned = map[string]cty.Value{} 292 } 293 294 // id is always computed for the tests 295 if id, ok := planned["id"]; ok && id.IsNull() { 296 planned["id"] = cty.UnknownVal(cty.String) 297 } 298 299 // the old tests have require_new replace on every plan 300 if _, ok := planned["require_new"]; ok { 301 resp.RequiresReplace = append(resp.RequiresReplace, cty.Path{cty.GetAttrStep{Name: "require_new"}}) 302 } 303 304 for k := range planned { 305 requiresNewKey := "__" + k + "_requires_new" 306 _, ok := planned[requiresNewKey] 307 if ok { 308 resp.RequiresReplace = append(resp.RequiresReplace, cty.Path{cty.GetAttrStep{Name: requiresNewKey}}) 309 } 310 } 311 312 if v, ok := planned["compute"]; ok && !v.IsNull() { 313 k := v.AsString() 314 unknown := cty.UnknownVal(cty.String) 315 if strings.HasSuffix(k, ".#") { 316 k = k[:len(k)-2] 317 unknown = cty.UnknownVal(cty.List(cty.String)) 318 } 319 planned[k] = unknown 320 } 321 322 if t, ok := planned["type"]; ok && t.IsNull() { 323 planned["type"] = cty.UnknownVal(cty.String) 324 } 325 326 resp.PlannedState = cty.ObjectVal(planned) 327 return 328 } 329 330 func testProvider(prefix string) *MockProvider { 331 p := new(MockProvider) 332 p.GetProviderSchemaResponse = testProviderSchema(prefix) 333 334 return p 335 } 336 337 func testProvisioner() *MockProvisioner { 338 p := new(MockProvisioner) 339 p.GetSchemaResponse = provisioners.GetSchemaResponse{ 340 Provisioner: &configschema.Block{ 341 Attributes: map[string]*configschema.Attribute{ 342 "command": { 343 Type: cty.String, 344 Optional: true, 345 }, 346 "order": { 347 Type: cty.String, 348 Optional: true, 349 }, 350 "when": { 351 Type: cty.String, 352 Optional: true, 353 }, 354 }, 355 }, 356 } 357 return p 358 } 359 360 func checkStateString(t *testing.T, state *states.State, expected string) { 361 t.Helper() 362 actual := strings.TrimSpace(state.String()) 363 expected = strings.TrimSpace(expected) 364 365 if actual != expected { 366 t.Fatalf("incorrect state\ngot:\n%s\n\nwant:\n%s", actual, expected) 367 } 368 } 369 370 // Test helper that gives a function 3 seconds to finish, assumes deadlock and 371 // fails test if it does not. 372 func testCheckDeadlock(t *testing.T, f func()) { 373 t.Helper() 374 timeout := make(chan bool, 1) 375 done := make(chan bool, 1) 376 go func() { 377 time.Sleep(3 * time.Second) 378 timeout <- true 379 }() 380 go func(f func(), done chan bool) { 381 defer func() { done <- true }() 382 f() 383 }(f, done) 384 select { 385 case <-timeout: 386 t.Fatalf("timed out! probably deadlock") 387 case <-done: 388 // ok 389 } 390 } 391 392 func testProviderSchema(name string) *providers.GetProviderSchemaResponse { 393 return getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 394 Provider: &configschema.Block{ 395 Attributes: map[string]*configschema.Attribute{ 396 "region": { 397 Type: cty.String, 398 Optional: true, 399 }, 400 "foo": { 401 Type: cty.String, 402 Optional: true, 403 }, 404 "value": { 405 Type: cty.String, 406 Optional: true, 407 }, 408 "root": { 409 Type: cty.Number, 410 Optional: true, 411 }, 412 }, 413 }, 414 ResourceTypes: map[string]*configschema.Block{ 415 name + "_instance": { 416 Attributes: map[string]*configschema.Attribute{ 417 "id": { 418 Type: cty.String, 419 Computed: true, 420 }, 421 "ami": { 422 Type: cty.String, 423 Optional: true, 424 }, 425 "dep": { 426 Type: cty.String, 427 Optional: true, 428 }, 429 "num": { 430 Type: cty.Number, 431 Optional: true, 432 }, 433 "require_new": { 434 Type: cty.String, 435 Optional: true, 436 }, 437 "var": { 438 Type: cty.String, 439 Optional: true, 440 }, 441 "foo": { 442 Type: cty.String, 443 Optional: true, 444 Computed: true, 445 }, 446 "bar": { 447 Type: cty.String, 448 Optional: true, 449 }, 450 "compute": { 451 Type: cty.String, 452 Optional: true, 453 Computed: false, 454 }, 455 "compute_value": { 456 Type: cty.String, 457 Optional: true, 458 Computed: true, 459 }, 460 "value": { 461 Type: cty.String, 462 Optional: true, 463 Computed: true, 464 }, 465 "output": { 466 Type: cty.String, 467 Optional: true, 468 }, 469 "write": { 470 Type: cty.String, 471 Optional: true, 472 }, 473 "instance": { 474 Type: cty.String, 475 Optional: true, 476 }, 477 "vpc_id": { 478 Type: cty.String, 479 Optional: true, 480 }, 481 "type": { 482 Type: cty.String, 483 Computed: true, 484 }, 485 486 // Generated by testDiffFn if compute = "unknown" is set in the test config 487 "unknown": { 488 Type: cty.String, 489 Computed: true, 490 }, 491 }, 492 }, 493 name + "_eip": { 494 Attributes: map[string]*configschema.Attribute{ 495 "id": { 496 Type: cty.String, 497 Computed: true, 498 }, 499 "instance": { 500 Type: cty.String, 501 Optional: true, 502 }, 503 }, 504 }, 505 name + "_resource": { 506 Attributes: map[string]*configschema.Attribute{ 507 "id": { 508 Type: cty.String, 509 Computed: true, 510 }, 511 "value": { 512 Type: cty.String, 513 Optional: true, 514 }, 515 "sensitive_value": { 516 Type: cty.String, 517 Sensitive: true, 518 Optional: true, 519 }, 520 "random": { 521 Type: cty.String, 522 Optional: true, 523 }, 524 }, 525 BlockTypes: map[string]*configschema.NestedBlock{ 526 "nesting_single": { 527 Block: configschema.Block{ 528 Attributes: map[string]*configschema.Attribute{ 529 "value": {Type: cty.String, Optional: true}, 530 "sensitive_value": {Type: cty.String, Optional: true, Sensitive: true}, 531 }, 532 }, 533 Nesting: configschema.NestingSingle, 534 }, 535 }, 536 }, 537 name + "_ami_list": { 538 Attributes: map[string]*configschema.Attribute{ 539 "id": { 540 Type: cty.String, 541 Optional: true, 542 Computed: true, 543 }, 544 "ids": { 545 Type: cty.List(cty.String), 546 Optional: true, 547 Computed: true, 548 }, 549 }, 550 }, 551 name + "_remote_state": { 552 Attributes: map[string]*configschema.Attribute{ 553 "id": { 554 Type: cty.String, 555 Optional: true, 556 }, 557 "foo": { 558 Type: cty.String, 559 Optional: true, 560 }, 561 "output": { 562 Type: cty.Map(cty.String), 563 Computed: true, 564 }, 565 }, 566 }, 567 name + "_file": { 568 Attributes: map[string]*configschema.Attribute{ 569 "id": { 570 Type: cty.String, 571 Optional: true, 572 }, 573 "template": { 574 Type: cty.String, 575 Optional: true, 576 }, 577 "rendered": { 578 Type: cty.String, 579 Computed: true, 580 }, 581 "__template_requires_new": { 582 Type: cty.String, 583 Optional: true, 584 }, 585 }, 586 }, 587 }, 588 DataSources: map[string]*configschema.Block{ 589 name + "_data_source": { 590 Attributes: map[string]*configschema.Attribute{ 591 "id": { 592 Type: cty.String, 593 Computed: true, 594 }, 595 "foo": { 596 Type: cty.String, 597 Optional: true, 598 Computed: true, 599 }, 600 }, 601 }, 602 name + "_remote_state": { 603 Attributes: map[string]*configschema.Attribute{ 604 "id": { 605 Type: cty.String, 606 Optional: true, 607 }, 608 "foo": { 609 Type: cty.String, 610 Optional: true, 611 }, 612 "output": { 613 Type: cty.Map(cty.String), 614 Optional: true, 615 }, 616 }, 617 }, 618 name + "_file": { 619 Attributes: map[string]*configschema.Attribute{ 620 "id": { 621 Type: cty.String, 622 Optional: true, 623 }, 624 "template": { 625 Type: cty.String, 626 Optional: true, 627 }, 628 "rendered": { 629 Type: cty.String, 630 Computed: true, 631 }, 632 }, 633 }, 634 }, 635 }) 636 } 637 638 // contextOptsForPlanViaFile is a helper that creates a temporary plan file, 639 // then reads it back in again and produces a ContextOpts object containing the 640 // planned changes, prior state and config from the plan file. 641 // 642 // This is intended for testing the separated plan/apply workflow in a more 643 // convenient way than spelling out all of these steps every time. Normally 644 // only the command and backend packages need to deal with such things, but 645 // our context tests try to exercise lots of stuff at once and so having them 646 // round-trip things through on-disk files is often an important part of 647 // fully representing an old bug in a regression test. 648 func contextOptsForPlanViaFile(t *testing.T, configSnap *configload.Snapshot, plan *plans.Plan) (*ContextOpts, *configs.Config, *plans.Plan, error) { 649 dir := t.TempDir() 650 651 // We'll just create a dummy statefile.File here because we're not going 652 // to run through any of the codepaths that care about Lineage/Serial/etc 653 // here anyway. 654 stateFile := &statefile.File{ 655 State: plan.PriorState, 656 } 657 prevStateFile := &statefile.File{ 658 State: plan.PrevRunState, 659 } 660 661 // To make life a little easier for test authors, we'll populate a simple 662 // backend configuration if they didn't set one, since the backend is 663 // usually dealt with in a calling package and so tests in this package 664 // don't really care about it. 665 if plan.Backend.Config == nil { 666 cfg, err := plans.NewDynamicValue(cty.EmptyObjectVal, cty.EmptyObject) 667 if err != nil { 668 panic(fmt.Sprintf("NewDynamicValue failed: %s", err)) // shouldn't happen because we control the inputs 669 } 670 plan.Backend.Type = "local" 671 plan.Backend.Config = cfg 672 plan.Backend.Workspace = "default" 673 } 674 675 filename := filepath.Join(dir, "tfplan") 676 err := planfile.Create(filename, planfile.CreateArgs{ 677 ConfigSnapshot: configSnap, 678 PreviousRunStateFile: prevStateFile, 679 StateFile: stateFile, 680 Plan: plan, 681 }) 682 if err != nil { 683 return nil, nil, nil, err 684 } 685 686 pr, err := planfile.Open(filename) 687 if err != nil { 688 return nil, nil, nil, err 689 } 690 691 config, diags := pr.ReadConfig() 692 if diags.HasErrors() { 693 return nil, nil, nil, diags.Err() 694 } 695 696 plan, err = pr.ReadPlan() 697 if err != nil { 698 return nil, nil, nil, err 699 } 700 701 // Note: This has grown rather silly over the course of ongoing refactoring, 702 // because ContextOpts is no longer actually responsible for carrying 703 // any information from a plan file and instead all of the information 704 // lives inside the config and plan objects. We continue to return a 705 // silly empty ContextOpts here just to keep all of the calling tests 706 // working. 707 return &ContextOpts{}, config, plan, nil 708 } 709 710 // legacyPlanComparisonString produces a string representation of the changes 711 // from a plan and a given state togther, as was formerly produced by the 712 // String method of terraform.Plan. 713 // 714 // This is here only for compatibility with existing tests that predate our 715 // new plan and state types, and should not be used in new tests. Instead, use 716 // a library like "cmp" to do a deep equality check and diff on the two 717 // data structures. 718 func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string { 719 return fmt.Sprintf( 720 "DIFF:\n\n%s\n\nSTATE:\n\n%s", 721 legacyDiffComparisonString(changes), 722 state.String(), 723 ) 724 } 725 726 // legacyDiffComparisonString produces a string representation of the changes 727 // from a planned changes object, as was formerly produced by the String method 728 // of terraform.Diff. 729 // 730 // This is here only for compatibility with existing tests that predate our 731 // new plan types, and should not be used in new tests. Instead, use a library 732 // like "cmp" to do a deep equality check and diff on the two data structures. 733 func legacyDiffComparisonString(changes *plans.Changes) string { 734 // The old string representation of a plan was grouped by module, but 735 // our new plan structure is not grouped in that way and so we'll need 736 // to preprocess it in order to produce that grouping. 737 type ResourceChanges struct { 738 Current *plans.ResourceInstanceChangeSrc 739 Deposed map[states.DeposedKey]*plans.ResourceInstanceChangeSrc 740 } 741 byModule := map[string]map[string]*ResourceChanges{} 742 resourceKeys := map[string][]string{} 743 var moduleKeys []string 744 for _, rc := range changes.Resources { 745 if rc.Action == plans.NoOp { 746 // We won't mention no-op changes here at all, since the old plan 747 // model we are emulating here didn't have such a concept. 748 continue 749 } 750 moduleKey := rc.Addr.Module.String() 751 if _, exists := byModule[moduleKey]; !exists { 752 moduleKeys = append(moduleKeys, moduleKey) 753 byModule[moduleKey] = make(map[string]*ResourceChanges) 754 } 755 resourceKey := rc.Addr.Resource.String() 756 if _, exists := byModule[moduleKey][resourceKey]; !exists { 757 resourceKeys[moduleKey] = append(resourceKeys[moduleKey], resourceKey) 758 byModule[moduleKey][resourceKey] = &ResourceChanges{ 759 Deposed: make(map[states.DeposedKey]*plans.ResourceInstanceChangeSrc), 760 } 761 } 762 763 if rc.DeposedKey == states.NotDeposed { 764 byModule[moduleKey][resourceKey].Current = rc 765 } else { 766 byModule[moduleKey][resourceKey].Deposed[rc.DeposedKey] = rc 767 } 768 } 769 sort.Strings(moduleKeys) 770 for _, ks := range resourceKeys { 771 sort.Strings(ks) 772 } 773 774 var buf bytes.Buffer 775 776 for _, moduleKey := range moduleKeys { 777 rcs := byModule[moduleKey] 778 var mBuf bytes.Buffer 779 780 for _, resourceKey := range resourceKeys[moduleKey] { 781 rc := rcs[resourceKey] 782 783 crud := "UPDATE" 784 if rc.Current != nil { 785 switch rc.Current.Action { 786 case plans.DeleteThenCreate: 787 crud = "DESTROY/CREATE" 788 case plans.CreateThenDelete: 789 crud = "CREATE/DESTROY" 790 case plans.Delete: 791 crud = "DESTROY" 792 case plans.Create: 793 crud = "CREATE" 794 } 795 } else { 796 // We must be working on a deposed object then, in which 797 // case destroying is the only possible action. 798 crud = "DESTROY" 799 } 800 801 extra := "" 802 if rc.Current == nil && len(rc.Deposed) > 0 { 803 extra = " (deposed only)" 804 } 805 806 fmt.Fprintf( 807 &mBuf, "%s: %s%s\n", 808 crud, resourceKey, extra, 809 ) 810 811 attrNames := map[string]bool{} 812 var oldAttrs map[string]string 813 var newAttrs map[string]string 814 if rc.Current != nil { 815 if before := rc.Current.Before; before != nil { 816 ty, err := before.ImpliedType() 817 if err == nil { 818 val, err := before.Decode(ty) 819 if err == nil { 820 oldAttrs = hcl2shim.FlatmapValueFromHCL2(val) 821 for k := range oldAttrs { 822 attrNames[k] = true 823 } 824 } 825 } 826 } 827 if after := rc.Current.After; after != nil { 828 ty, err := after.ImpliedType() 829 if err == nil { 830 val, err := after.Decode(ty) 831 if err == nil { 832 newAttrs = hcl2shim.FlatmapValueFromHCL2(val) 833 for k := range newAttrs { 834 attrNames[k] = true 835 } 836 } 837 } 838 } 839 } 840 if oldAttrs == nil { 841 oldAttrs = make(map[string]string) 842 } 843 if newAttrs == nil { 844 newAttrs = make(map[string]string) 845 } 846 847 attrNamesOrder := make([]string, 0, len(attrNames)) 848 keyLen := 0 849 for n := range attrNames { 850 attrNamesOrder = append(attrNamesOrder, n) 851 if len(n) > keyLen { 852 keyLen = len(n) 853 } 854 } 855 sort.Strings(attrNamesOrder) 856 857 for _, attrK := range attrNamesOrder { 858 v := newAttrs[attrK] 859 u := oldAttrs[attrK] 860 861 if v == hcl2shim.UnknownVariableValue { 862 v = "<computed>" 863 } 864 // NOTE: we don't support <sensitive> here because we would 865 // need schema to do that. Excluding sensitive values 866 // is now done at the UI layer, and so should not be tested 867 // at the core layer. 868 869 updateMsg := "" 870 // TODO: Mark " (forces new resource)" in updateMsg when appropriate. 871 872 fmt.Fprintf( 873 &mBuf, " %s:%s %#v => %#v%s\n", 874 attrK, 875 strings.Repeat(" ", keyLen-len(attrK)), 876 u, v, 877 updateMsg, 878 ) 879 } 880 } 881 882 if moduleKey == "" { // root module 883 buf.Write(mBuf.Bytes()) 884 buf.WriteByte('\n') 885 continue 886 } 887 888 fmt.Fprintf(&buf, "%s:\n", moduleKey) 889 s := bufio.NewScanner(&mBuf) 890 for s.Scan() { 891 buf.WriteString(fmt.Sprintf(" %s\n", s.Text())) 892 } 893 } 894 895 return buf.String() 896 } 897 898 // assertNoDiagnostics fails the test in progress (using t.Fatal) if the given 899 // diagnostics is non-empty. 900 func assertNoDiagnostics(t *testing.T, diags tfdiags.Diagnostics) { 901 t.Helper() 902 if len(diags) == 0 { 903 return 904 } 905 logDiagnostics(t, diags) 906 t.FailNow() 907 } 908 909 // assertNoDiagnostics fails the test in progress (using t.Fatal) if the given 910 // diagnostics has any errors. 911 func assertNoErrors(t *testing.T, diags tfdiags.Diagnostics) { 912 t.Helper() 913 if !diags.HasErrors() { 914 return 915 } 916 logDiagnostics(t, diags) 917 t.FailNow() 918 } 919 920 // assertDiagnosticsMatch fails the test in progress (using t.Fatal) if the 921 // two sets of diagnostics don't match after being normalized using the 922 // "ForRPC" processing step, which eliminates the specific type information 923 // and HCL expression information of each diagnostic. 924 // 925 // assertDiagnosticsMatch sorts the two sets of diagnostics in the usual way 926 // before comparing them, though diagnostics only have a partial order so that 927 // will not totally normalize the ordering of all diagnostics sets. 928 func assertDiagnosticsMatch(t *testing.T, got, want tfdiags.Diagnostics) { 929 got = got.ForRPC() 930 want = want.ForRPC() 931 got.Sort() 932 want.Sort() 933 if diff := cmp.Diff(want, got); diff != "" { 934 t.Fatalf("wrong diagnostics\n%s", diff) 935 } 936 } 937 938 // logDiagnostics is a test helper that logs the given diagnostics to to the 939 // given testing.T using t.Log, in a way that is hopefully useful in debugging 940 // a test. It does not generate any errors or fail the test. See 941 // assertNoDiagnostics and assertNoErrors for more specific helpers that can 942 // also fail the test. 943 func logDiagnostics(t *testing.T, diags tfdiags.Diagnostics) { 944 t.Helper() 945 for _, diag := range diags { 946 desc := diag.Description() 947 rng := diag.Source() 948 949 var severity string 950 switch diag.Severity() { 951 case tfdiags.Error: 952 severity = "ERROR" 953 case tfdiags.Warning: 954 severity = "WARN" 955 default: 956 severity = "???" // should never happen 957 } 958 959 if subj := rng.Subject; subj != nil { 960 if desc.Detail == "" { 961 t.Logf("[%s@%s] %s", severity, subj.StartString(), desc.Summary) 962 } else { 963 t.Logf("[%s@%s] %s: %s", severity, subj.StartString(), desc.Summary, desc.Detail) 964 } 965 } else { 966 if desc.Detail == "" { 967 t.Logf("[%s] %s", severity, desc.Summary) 968 } else { 969 t.Logf("[%s] %s: %s", severity, desc.Summary, desc.Detail) 970 } 971 } 972 } 973 } 974 975 const testContextRefreshModuleStr = ` 976 aws_instance.web: (tainted) 977 ID = bar 978 provider = provider["registry.terraform.io/hashicorp/aws"] 979 980 module.child: 981 aws_instance.web: 982 ID = new 983 provider = provider["registry.terraform.io/hashicorp/aws"] 984 ` 985 986 const testContextRefreshOutputStr = ` 987 aws_instance.web: 988 ID = foo 989 provider = provider["registry.terraform.io/hashicorp/aws"] 990 foo = bar 991 992 Outputs: 993 994 foo = bar 995 ` 996 997 const testContextRefreshOutputPartialStr = ` 998 <no state> 999 ` 1000 1001 const testContextRefreshTaintedStr = ` 1002 aws_instance.web: (tainted) 1003 ID = foo 1004 provider = provider["registry.terraform.io/hashicorp/aws"] 1005 `