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  }