github.com/opentofu/opentofu@v1.7.1/internal/tofu/context_apply_checks_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package tofu 7 8 import ( 9 "testing" 10 11 "github.com/zclconf/go-cty/cty" 12 13 "github.com/opentofu/opentofu/internal/addrs" 14 "github.com/opentofu/opentofu/internal/checks" 15 "github.com/opentofu/opentofu/internal/configs/configschema" 16 "github.com/opentofu/opentofu/internal/plans" 17 "github.com/opentofu/opentofu/internal/providers" 18 "github.com/opentofu/opentofu/internal/states" 19 "github.com/opentofu/opentofu/internal/tfdiags" 20 ) 21 22 // This file contains 'integration' tests for the OpenTofu check blocks. 23 // 24 // These tests could live in context_apply_test or context_apply2_test but given 25 // the size of those files, it makes sense to keep these check related tests 26 // grouped together. 27 28 type checksTestingStatus struct { 29 status checks.Status 30 messages []string 31 } 32 33 func TestContextChecks(t *testing.T) { 34 tests := map[string]struct { 35 configs map[string]string 36 plan map[string]checksTestingStatus 37 planError string 38 planWarning string 39 apply map[string]checksTestingStatus 40 applyError string 41 applyWarning string 42 state *states.State 43 provider *MockProvider 44 providerHook func(*MockProvider) 45 }{ 46 "passing": { 47 configs: map[string]string{ 48 "main.tf": ` 49 provider "checks" {} 50 51 check "passing" { 52 data "checks_object" "positive" {} 53 54 assert { 55 condition = data.checks_object.positive.number >= 0 56 error_message = "negative number" 57 } 58 } 59 `, 60 }, 61 plan: map[string]checksTestingStatus{ 62 "passing": { 63 status: checks.StatusPass, 64 }, 65 }, 66 apply: map[string]checksTestingStatus{ 67 "passing": { 68 status: checks.StatusPass, 69 }, 70 }, 71 provider: &MockProvider{ 72 Meta: "checks", 73 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 74 DataSources: map[string]providers.Schema{ 75 "checks_object": { 76 Block: &configschema.Block{ 77 Attributes: map[string]*configschema.Attribute{ 78 "number": { 79 Type: cty.Number, 80 Computed: true, 81 }, 82 }, 83 }, 84 }, 85 }, 86 }, 87 ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 88 return providers.ReadDataSourceResponse{ 89 State: cty.ObjectVal(map[string]cty.Value{ 90 "number": cty.NumberIntVal(0), 91 }), 92 } 93 }, 94 }, 95 }, 96 "failing": { 97 configs: map[string]string{ 98 "main.tf": ` 99 provider "checks" {} 100 101 check "failing" { 102 data "checks_object" "positive" {} 103 104 assert { 105 condition = data.checks_object.positive.number >= 0 106 error_message = "negative number" 107 } 108 } 109 `, 110 }, 111 plan: map[string]checksTestingStatus{ 112 "failing": { 113 status: checks.StatusFail, 114 messages: []string{"negative number"}, 115 }, 116 }, 117 planWarning: "Check block assertion failed: negative number", 118 apply: map[string]checksTestingStatus{ 119 "failing": { 120 status: checks.StatusFail, 121 messages: []string{"negative number"}, 122 }, 123 }, 124 applyWarning: "Check block assertion failed: negative number", 125 provider: &MockProvider{ 126 Meta: "checks", 127 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 128 DataSources: map[string]providers.Schema{ 129 "checks_object": { 130 Block: &configschema.Block{ 131 Attributes: map[string]*configschema.Attribute{ 132 "number": { 133 Type: cty.Number, 134 Computed: true, 135 }, 136 }, 137 }, 138 }, 139 }, 140 }, 141 ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 142 return providers.ReadDataSourceResponse{ 143 State: cty.ObjectVal(map[string]cty.Value{ 144 "number": cty.NumberIntVal(-1), 145 }), 146 } 147 }, 148 }, 149 }, 150 "mixed": { 151 configs: map[string]string{ 152 "main.tf": ` 153 provider "checks" {} 154 155 check "failing" { 156 data "checks_object" "neutral" {} 157 158 assert { 159 condition = data.checks_object.neutral.number >= 0 160 error_message = "negative number" 161 } 162 163 assert { 164 condition = data.checks_object.neutral.number < 0 165 error_message = "positive number" 166 } 167 } 168 `, 169 }, 170 plan: map[string]checksTestingStatus{ 171 "failing": { 172 status: checks.StatusFail, 173 messages: []string{"positive number"}, 174 }, 175 }, 176 planWarning: "Check block assertion failed: positive number", 177 apply: map[string]checksTestingStatus{ 178 "failing": { 179 status: checks.StatusFail, 180 messages: []string{"positive number"}, 181 }, 182 }, 183 applyWarning: "Check block assertion failed: positive number", 184 provider: &MockProvider{ 185 Meta: "checks", 186 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 187 DataSources: map[string]providers.Schema{ 188 "checks_object": { 189 Block: &configschema.Block{ 190 Attributes: map[string]*configschema.Attribute{ 191 "number": { 192 Type: cty.Number, 193 Computed: true, 194 }, 195 }, 196 }, 197 }, 198 }, 199 }, 200 ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 201 return providers.ReadDataSourceResponse{ 202 State: cty.ObjectVal(map[string]cty.Value{ 203 "number": cty.NumberIntVal(0), 204 }), 205 } 206 }, 207 }, 208 }, 209 "nested data blocks reload during apply": { 210 configs: map[string]string{ 211 "main.tf": ` 212 provider "checks" {} 213 214 data "checks_object" "data_block" {} 215 216 check "data_block" { 217 assert { 218 condition = data.checks_object.data_block.number >= 0 219 error_message = "negative number" 220 } 221 } 222 223 check "nested_data_block" { 224 data "checks_object" "nested_data_block" {} 225 226 assert { 227 condition = data.checks_object.nested_data_block.number >= 0 228 error_message = "negative number" 229 } 230 } 231 `, 232 }, 233 plan: map[string]checksTestingStatus{ 234 "nested_data_block": { 235 status: checks.StatusFail, 236 messages: []string{"negative number"}, 237 }, 238 "data_block": { 239 status: checks.StatusFail, 240 messages: []string{"negative number"}, 241 }, 242 }, 243 planWarning: "2 warnings:\n\n- Check block assertion failed: negative number\n- Check block assertion failed: negative number", 244 apply: map[string]checksTestingStatus{ 245 "nested_data_block": { 246 status: checks.StatusPass, 247 }, 248 "data_block": { 249 status: checks.StatusFail, 250 messages: []string{"negative number"}, 251 }, 252 }, 253 applyWarning: "Check block assertion failed: negative number", 254 provider: &MockProvider{ 255 Meta: "checks", 256 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 257 DataSources: map[string]providers.Schema{ 258 "checks_object": { 259 Block: &configschema.Block{ 260 Attributes: map[string]*configschema.Attribute{ 261 "number": { 262 Type: cty.Number, 263 Computed: true, 264 }, 265 }, 266 }, 267 }, 268 }, 269 }, 270 ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 271 return providers.ReadDataSourceResponse{ 272 State: cty.ObjectVal(map[string]cty.Value{ 273 "number": cty.NumberIntVal(-1), 274 }), 275 } 276 }, 277 }, 278 providerHook: func(provider *MockProvider) { 279 provider.ReadDataSourceFn = func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 280 // The data returned by the data sources are changing 281 // between the plan and apply stage. The nested data block 282 // will update to reflect this while the normal data block 283 // will not detect the change. 284 return providers.ReadDataSourceResponse{ 285 State: cty.ObjectVal(map[string]cty.Value{ 286 "number": cty.NumberIntVal(0), 287 }), 288 } 289 } 290 }, 291 }, 292 "returns unknown for unknown config": { 293 configs: map[string]string{ 294 "main.tf": ` 295 provider "checks" {} 296 297 resource "checks_object" "resource_block" {} 298 299 check "resource_block" { 300 data "checks_object" "data_block" { 301 id = checks_object.resource_block.id 302 } 303 304 assert { 305 condition = data.checks_object.data_block.number >= 0 306 error_message = "negative number" 307 } 308 } 309 `, 310 }, 311 plan: map[string]checksTestingStatus{ 312 "resource_block": { 313 status: checks.StatusUnknown, 314 }, 315 }, 316 planWarning: "Check block assertion known after apply: The condition could not be evaluated at this time, a result will be known when this plan is applied.", 317 apply: map[string]checksTestingStatus{ 318 "resource_block": { 319 status: checks.StatusPass, 320 }, 321 }, 322 provider: &MockProvider{ 323 Meta: "checks", 324 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 325 ResourceTypes: map[string]providers.Schema{ 326 "checks_object": { 327 Block: &configschema.Block{ 328 Attributes: map[string]*configschema.Attribute{ 329 "id": { 330 Type: cty.String, 331 Computed: true, 332 }, 333 }, 334 }, 335 }, 336 }, 337 DataSources: map[string]providers.Schema{ 338 "checks_object": { 339 Block: &configschema.Block{ 340 Attributes: map[string]*configschema.Attribute{ 341 "id": { 342 Type: cty.String, 343 Required: true, 344 }, 345 "number": { 346 Type: cty.Number, 347 Computed: true, 348 }, 349 }, 350 }, 351 }, 352 }, 353 }, 354 PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 355 return providers.PlanResourceChangeResponse{ 356 PlannedState: cty.ObjectVal(map[string]cty.Value{ 357 "id": cty.UnknownVal(cty.String), 358 }), 359 } 360 }, 361 ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { 362 return providers.ApplyResourceChangeResponse{ 363 NewState: cty.ObjectVal(map[string]cty.Value{ 364 "id": cty.StringVal("7A9F887D-44C7-4281-80E5-578E41F99DFC"), 365 }), 366 } 367 }, 368 ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 369 values := request.Config.AsValueMap() 370 if id, ok := values["id"]; ok { 371 if id.IsKnown() && id.AsString() == "7A9F887D-44C7-4281-80E5-578E41F99DFC" { 372 return providers.ReadDataSourceResponse{ 373 State: cty.ObjectVal(map[string]cty.Value{ 374 "id": cty.StringVal("7A9F887D-44C7-4281-80E5-578E41F99DFC"), 375 "number": cty.NumberIntVal(0), 376 }), 377 } 378 } 379 } 380 381 return providers.ReadDataSourceResponse{ 382 Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "shouldn't make it here", "really shouldn't make it here")}, 383 } 384 }, 385 }, 386 }, 387 "failing nested data source doesn't block the plan": { 388 configs: map[string]string{ 389 "main.tf": ` 390 provider "checks" {} 391 392 check "error" { 393 data "checks_object" "data_block" {} 394 395 assert { 396 condition = data.checks_object.data_block.number >= 0 397 error_message = "negative number" 398 } 399 } 400 `, 401 }, 402 plan: map[string]checksTestingStatus{ 403 "error": { 404 status: checks.StatusFail, 405 messages: []string{ 406 "data source read failed: something bad happened and the provider couldn't read the data source", 407 }, 408 }, 409 }, 410 planWarning: "data source read failed: something bad happened and the provider couldn't read the data source", 411 apply: map[string]checksTestingStatus{ 412 "error": { 413 status: checks.StatusFail, 414 messages: []string{ 415 "data source read failed: something bad happened and the provider couldn't read the data source", 416 }, 417 }, 418 }, 419 applyWarning: "data source read failed: something bad happened and the provider couldn't read the data source", 420 provider: &MockProvider{ 421 Meta: "checks", 422 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 423 DataSources: map[string]providers.Schema{ 424 "checks_object": { 425 Block: &configschema.Block{ 426 Attributes: map[string]*configschema.Attribute{ 427 "number": { 428 Type: cty.Number, 429 Computed: true, 430 }, 431 }, 432 }, 433 }, 434 }, 435 }, 436 ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 437 return providers.ReadDataSourceResponse{ 438 Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "data source read failed", "something bad happened and the provider couldn't read the data source")}, 439 } 440 }, 441 }, 442 }, "failing nested data source should prevent checks from executing": { 443 configs: map[string]string{ 444 "main.tf": ` 445 provider "checks" {} 446 447 resource "checks_object" "resource_block" { 448 number = -1 449 } 450 451 check "error" { 452 data "checks_object" "data_block" {} 453 454 assert { 455 condition = checks_object.resource_block.number >= 0 456 error_message = "negative number" 457 } 458 } 459 `, 460 }, 461 state: states.BuildState(func(state *states.SyncState) { 462 state.SetResourceInstanceCurrent( 463 addrs.Resource{ 464 Mode: addrs.ManagedResourceMode, 465 Type: "checks_object", 466 Name: "resource_block", 467 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 468 &states.ResourceInstanceObjectSrc{ 469 Status: states.ObjectReady, 470 AttrsJSON: []byte(`{"number": -1}`), 471 }, 472 addrs.AbsProviderConfig{ 473 Provider: addrs.NewDefaultProvider("test"), 474 Module: addrs.RootModule, 475 }) 476 }), 477 plan: map[string]checksTestingStatus{ 478 "error": { 479 status: checks.StatusFail, 480 messages: []string{ 481 "data source read failed: something bad happened and the provider couldn't read the data source", 482 }, 483 }, 484 }, 485 planWarning: "data source read failed: something bad happened and the provider couldn't read the data source", 486 apply: map[string]checksTestingStatus{ 487 "error": { 488 status: checks.StatusFail, 489 messages: []string{ 490 "data source read failed: something bad happened and the provider couldn't read the data source", 491 }, 492 }, 493 }, 494 applyWarning: "data source read failed: something bad happened and the provider couldn't read the data source", 495 provider: &MockProvider{ 496 Meta: "checks", 497 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 498 ResourceTypes: map[string]providers.Schema{ 499 "checks_object": { 500 Block: &configschema.Block{ 501 Attributes: map[string]*configschema.Attribute{ 502 "number": { 503 Type: cty.Number, 504 Required: true, 505 }, 506 }, 507 }, 508 }, 509 }, 510 DataSources: map[string]providers.Schema{ 511 "checks_object": { 512 Block: &configschema.Block{ 513 Attributes: map[string]*configschema.Attribute{ 514 "number": { 515 Type: cty.Number, 516 Computed: true, 517 }, 518 }, 519 }, 520 }, 521 }, 522 }, 523 PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 524 return providers.PlanResourceChangeResponse{ 525 PlannedState: cty.ObjectVal(map[string]cty.Value{ 526 "number": cty.NumberIntVal(-1), 527 }), 528 } 529 }, 530 ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { 531 return providers.ApplyResourceChangeResponse{ 532 NewState: cty.ObjectVal(map[string]cty.Value{ 533 "number": cty.NumberIntVal(-1), 534 }), 535 } 536 }, 537 ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 538 return providers.ReadDataSourceResponse{ 539 Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "data source read failed", "something bad happened and the provider couldn't read the data source")}, 540 } 541 }, 542 }, 543 }, 544 "check failing in state and passing after plan and apply": { 545 configs: map[string]string{ 546 "main.tf": ` 547 provider "checks" {} 548 549 resource "checks_object" "resource" { 550 number = 0 551 } 552 553 check "passing" { 554 assert { 555 condition = checks_object.resource.number >= 0 556 error_message = "negative number" 557 } 558 } 559 `, 560 }, 561 state: states.BuildState(func(state *states.SyncState) { 562 state.SetResourceInstanceCurrent( 563 addrs.Resource{ 564 Mode: addrs.ManagedResourceMode, 565 Type: "checks_object", 566 Name: "resource", 567 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 568 &states.ResourceInstanceObjectSrc{ 569 Status: states.ObjectReady, 570 AttrsJSON: []byte(`{"number": -1}`), 571 }, 572 addrs.AbsProviderConfig{ 573 Provider: addrs.NewDefaultProvider("test"), 574 Module: addrs.RootModule, 575 }) 576 }), 577 plan: map[string]checksTestingStatus{ 578 "passing": { 579 status: checks.StatusPass, 580 }, 581 }, 582 apply: map[string]checksTestingStatus{ 583 "passing": { 584 status: checks.StatusPass, 585 }, 586 }, 587 provider: &MockProvider{ 588 Meta: "checks", 589 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 590 ResourceTypes: map[string]providers.Schema{ 591 "checks_object": { 592 Block: &configschema.Block{ 593 Attributes: map[string]*configschema.Attribute{ 594 "number": { 595 Type: cty.Number, 596 Required: true, 597 }, 598 }, 599 }, 600 }, 601 }, 602 }, 603 PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 604 return providers.PlanResourceChangeResponse{ 605 PlannedState: cty.ObjectVal(map[string]cty.Value{ 606 "number": cty.NumberIntVal(0), 607 }), 608 } 609 }, 610 ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { 611 return providers.ApplyResourceChangeResponse{ 612 NewState: cty.ObjectVal(map[string]cty.Value{ 613 "number": cty.NumberIntVal(0), 614 }), 615 } 616 }, 617 }, 618 }, 619 "failing data source does block the plan": { 620 configs: map[string]string{ 621 "main.tf": ` 622 provider "checks" {} 623 624 data "checks_object" "data_block" {} 625 626 check "error" { 627 assert { 628 condition = data.checks_object.data_block.number >= 0 629 error_message = "negative number" 630 } 631 } 632 `, 633 }, 634 planError: "data source read failed: something bad happened and the provider couldn't read the data source", 635 provider: &MockProvider{ 636 Meta: "checks", 637 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 638 DataSources: map[string]providers.Schema{ 639 "checks_object": { 640 Block: &configschema.Block{ 641 Attributes: map[string]*configschema.Attribute{ 642 "number": { 643 Type: cty.Number, 644 Computed: true, 645 }, 646 }, 647 }, 648 }, 649 }, 650 }, 651 ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 652 return providers.ReadDataSourceResponse{ 653 Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "data source read failed", "something bad happened and the provider couldn't read the data source")}, 654 } 655 }, 656 }, 657 }, 658 "invalid reference into check block": { 659 configs: map[string]string{ 660 "main.tf": ` 661 provider "checks" {} 662 663 data "checks_object" "data_block" { 664 id = data.checks_object.nested_data_block.id 665 } 666 667 check "error" { 668 data "checks_object" "nested_data_block" {} 669 670 assert { 671 condition = data.checks_object.data_block.number >= 0 672 error_message = "negative number" 673 } 674 } 675 `, 676 }, 677 planError: "Reference to scoped resource: The referenced data resource \"checks_object\" \"nested_data_block\" is not available from this context.", 678 provider: &MockProvider{ 679 Meta: "checks", 680 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 681 DataSources: map[string]providers.Schema{ 682 "checks_object": { 683 Block: &configschema.Block{ 684 Attributes: map[string]*configschema.Attribute{ 685 "id": { 686 Type: cty.String, 687 Computed: true, 688 Optional: true, 689 }, 690 }, 691 }, 692 }, 693 }, 694 }, 695 ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 696 input := request.Config.AsValueMap() 697 if _, ok := input["id"]; ok { 698 return providers.ReadDataSourceResponse{ 699 State: request.Config, 700 } 701 } 702 703 return providers.ReadDataSourceResponse{ 704 State: cty.ObjectVal(map[string]cty.Value{ 705 "id": cty.UnknownVal(cty.String), 706 }), 707 } 708 }, 709 }, 710 }, 711 } 712 for name, test := range tests { 713 t.Run(name, func(t *testing.T) { 714 configs := testModuleInline(t, test.configs) 715 ctx := testContext2(t, &ContextOpts{ 716 Providers: map[addrs.Provider]providers.Factory{ 717 addrs.NewDefaultProvider(test.provider.Meta.(string)): testProviderFuncFixed(test.provider), 718 }, 719 }) 720 721 initialState := states.NewState() 722 if test.state != nil { 723 initialState = test.state 724 } 725 726 plan, diags := ctx.Plan(configs, initialState, &PlanOpts{ 727 Mode: plans.NormalMode, 728 }) 729 if validateCheckDiagnostics(t, "planning", test.planWarning, test.planError, diags) { 730 return 731 } 732 validateCheckResults(t, "planning", test.plan, plan.Checks) 733 734 if test.providerHook != nil { 735 // This gives an opportunity to change the behaviour of the 736 // provider between the plan and apply stages. 737 test.providerHook(test.provider) 738 } 739 740 state, diags := ctx.Apply(plan, configs) 741 if validateCheckDiagnostics(t, "apply", test.applyWarning, test.applyError, diags) { 742 return 743 } 744 validateCheckResults(t, "apply", test.apply, state.CheckResults) 745 }) 746 } 747 } 748 749 func validateCheckDiagnostics(t *testing.T, stage string, expectedWarning, expectedError string, actual tfdiags.Diagnostics) bool { 750 if expectedError != "" { 751 if !actual.HasErrors() { 752 t.Errorf("expected %s to error with \"%s\", but no errors were returned", stage, expectedError) 753 } else if expectedError != actual.Err().Error() { 754 t.Errorf("expected %s to error with \"%s\" but found \"%s\"", stage, expectedError, actual.Err()) 755 } 756 757 // If we expected an error then we won't finish the rest of the test. 758 return true 759 } 760 761 if expectedWarning != "" { 762 warnings := actual.ErrWithWarnings() 763 if actual.ErrWithWarnings() == nil { 764 t.Errorf("expected %s to warn with \"%s\", but no errors were returned", stage, expectedWarning) 765 } else if expectedWarning != warnings.Error() { 766 t.Errorf("expected %s to warn with \"%s\" but found \"%s\"", stage, expectedWarning, warnings) 767 } 768 } else { 769 if actual.ErrWithWarnings() != nil { 770 t.Errorf("expected %s to produce no diagnostics but found \"%s\"", stage, actual.ErrWithWarnings()) 771 } 772 } 773 774 assertNoErrors(t, actual) 775 return false 776 } 777 778 func validateCheckResults(t *testing.T, stage string, expected map[string]checksTestingStatus, actual *states.CheckResults) { 779 780 // Just a quick sanity check that the plan or apply process didn't create 781 // some non-existent checks. 782 if len(expected) != len(actual.ConfigResults.Keys()) { 783 t.Errorf("expected %d check results but found %d after %s", len(expected), len(actual.ConfigResults.Keys()), stage) 784 } 785 786 // Now, lets make sure the checks all match what we expect. 787 for check, want := range expected { 788 results := actual.GetObjectResult(addrs.Check{ 789 Name: check, 790 }.Absolute(addrs.RootModuleInstance)) 791 792 if results.Status != want.status { 793 t.Errorf("%s: wanted %s but got %s after %s", check, want.status, results.Status, stage) 794 } 795 796 if len(want.messages) != len(results.FailureMessages) { 797 t.Errorf("%s: expected %d failure messages but had %d after %s", check, len(want.messages), len(results.FailureMessages), stage) 798 } 799 800 max := len(want.messages) 801 if len(results.FailureMessages) > max { 802 max = len(results.FailureMessages) 803 } 804 805 for ix := 0; ix < max; ix++ { 806 var expected, actual string 807 if ix < len(want.messages) { 808 expected = want.messages[ix] 809 } 810 if ix < len(results.FailureMessages) { 811 actual = results.FailureMessages[ix] 812 } 813 814 // Order matters! 815 if actual != expected { 816 t.Errorf("%s: expected failure message at %d to be \"%s\" but was \"%s\" after %s", check, ix, expected, actual, stage) 817 } 818 } 819 820 } 821 }