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