github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/compatible_test.go (about)

     1  package objchange
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/apparentlymart/go-dump/dump"
     8  	"github.com/zclconf/go-cty/cty"
     9  
    10  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
    11  	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
    12  )
    13  
    14  func TestAssertObjectCompatible(t *testing.T) {
    15  	schemaWithFoo := configschema.Block{
    16  		Attributes: map[string]*configschema.Attribute{
    17  			"foo": {Type: cty.String, Optional: true},
    18  		},
    19  	}
    20  	fooBlockValue := cty.ObjectVal(map[string]cty.Value{
    21  		"foo": cty.StringVal("bar"),
    22  	})
    23  	schemaWithFooBar := configschema.Block{
    24  		Attributes: map[string]*configschema.Attribute{
    25  			"foo": {Type: cty.String, Optional: true},
    26  			"bar": {Type: cty.String, Optional: true},
    27  		},
    28  	}
    29  	fooBarBlockValue := cty.ObjectVal(map[string]cty.Value{
    30  		"foo": cty.StringVal("bar"),
    31  		"bar": cty.NullVal(cty.String), // simulating the situation where bar isn't set in the config at all
    32  	})
    33  	fooBarBlockDynamicPlaceholder := cty.ObjectVal(map[string]cty.Value{
    34  		"foo": cty.UnknownVal(cty.String),
    35  		"bar": cty.NullVal(cty.String), // simulating the situation where bar isn't set in the config at all
    36  	})
    37  
    38  	tests := []struct {
    39  		Schema   *configschema.Block
    40  		Planned  cty.Value
    41  		Actual   cty.Value
    42  		WantErrs []string
    43  	}{
    44  		{
    45  			&configschema.Block{},
    46  			cty.EmptyObjectVal,
    47  			cty.EmptyObjectVal,
    48  			nil,
    49  		},
    50  		{
    51  			&configschema.Block{
    52  				Attributes: map[string]*configschema.Attribute{
    53  					"id": {
    54  						Type:     cty.String,
    55  						Computed: true,
    56  					},
    57  					"name": {
    58  						Type:     cty.String,
    59  						Required: true,
    60  					},
    61  				},
    62  			},
    63  			cty.ObjectVal(map[string]cty.Value{
    64  				"id":   cty.UnknownVal(cty.String),
    65  				"name": cty.StringVal("thingy"),
    66  			}),
    67  			cty.ObjectVal(map[string]cty.Value{
    68  				"id":   cty.UnknownVal(cty.String),
    69  				"name": cty.StringVal("thingy"),
    70  			}),
    71  			nil,
    72  		},
    73  		{
    74  			&configschema.Block{
    75  				Attributes: map[string]*configschema.Attribute{
    76  					"id": {
    77  						Type:     cty.String,
    78  						Computed: true,
    79  					},
    80  					"name": {
    81  						Type:     cty.String,
    82  						Required: true,
    83  					},
    84  				},
    85  			},
    86  			cty.ObjectVal(map[string]cty.Value{
    87  				"id":   cty.UnknownVal(cty.String),
    88  				"name": cty.UnknownVal(cty.String),
    89  			}),
    90  			cty.ObjectVal(map[string]cty.Value{
    91  				"id":   cty.UnknownVal(cty.String),
    92  				"name": cty.StringVal("thingy"),
    93  			}),
    94  			nil,
    95  		},
    96  		{
    97  			&configschema.Block{
    98  				Attributes: map[string]*configschema.Attribute{
    99  					"id": {
   100  						Type:     cty.String,
   101  						Computed: true,
   102  					},
   103  					"name": {
   104  						Type:     cty.String,
   105  						Required: true,
   106  					},
   107  				},
   108  			},
   109  			cty.ObjectVal(map[string]cty.Value{
   110  				"id":   cty.UnknownVal(cty.String),
   111  				"name": cty.StringVal("wotsit"),
   112  			}),
   113  			cty.ObjectVal(map[string]cty.Value{
   114  				"id":   cty.UnknownVal(cty.String),
   115  				"name": cty.StringVal("thingy"),
   116  			}),
   117  			[]string{
   118  				`.name: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
   119  			},
   120  		},
   121  		{
   122  			&configschema.Block{
   123  				Attributes: map[string]*configschema.Attribute{
   124  					"id": {
   125  						Type:     cty.String,
   126  						Computed: true,
   127  					},
   128  					"name": {
   129  						Type:      cty.String,
   130  						Required:  true,
   131  						Sensitive: true,
   132  					},
   133  				},
   134  			},
   135  			cty.ObjectVal(map[string]cty.Value{
   136  				"id":   cty.UnknownVal(cty.String),
   137  				"name": cty.StringVal("wotsit"),
   138  			}),
   139  			cty.ObjectVal(map[string]cty.Value{
   140  				"id":   cty.UnknownVal(cty.String),
   141  				"name": cty.StringVal("thingy"),
   142  			}),
   143  			[]string{
   144  				`.name: inconsistent values for sensitive attribute`,
   145  			},
   146  		},
   147  		{
   148  			&configschema.Block{
   149  				Attributes: map[string]*configschema.Attribute{
   150  					"id": {
   151  						Type:     cty.String,
   152  						Computed: true,
   153  					},
   154  					"stuff": {
   155  						Type:     cty.DynamicPseudoType,
   156  						Required: true,
   157  					},
   158  				},
   159  			},
   160  			cty.ObjectVal(map[string]cty.Value{
   161  				"id":    cty.UnknownVal(cty.String),
   162  				"stuff": cty.DynamicVal,
   163  			}),
   164  			cty.ObjectVal(map[string]cty.Value{
   165  				"id":    cty.UnknownVal(cty.String),
   166  				"stuff": cty.StringVal("thingy"),
   167  			}),
   168  			[]string{},
   169  		},
   170  		{
   171  			&configschema.Block{
   172  				Attributes: map[string]*configschema.Attribute{
   173  					"id": {
   174  						Type:     cty.String,
   175  						Computed: true,
   176  					},
   177  					"stuff": {
   178  						Type:     cty.DynamicPseudoType,
   179  						Required: true,
   180  					},
   181  				},
   182  			},
   183  			cty.ObjectVal(map[string]cty.Value{
   184  				"id":    cty.UnknownVal(cty.String),
   185  				"stuff": cty.StringVal("wotsit"),
   186  			}),
   187  			cty.ObjectVal(map[string]cty.Value{
   188  				"id":    cty.UnknownVal(cty.String),
   189  				"stuff": cty.StringVal("thingy"),
   190  			}),
   191  			[]string{
   192  				`.stuff: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
   193  			},
   194  		},
   195  		{
   196  			&configschema.Block{
   197  				Attributes: map[string]*configschema.Attribute{
   198  					"id": {
   199  						Type:     cty.String,
   200  						Computed: true,
   201  					},
   202  					"stuff": {
   203  						Type:     cty.DynamicPseudoType,
   204  						Required: true,
   205  					},
   206  				},
   207  			},
   208  			cty.ObjectVal(map[string]cty.Value{
   209  				"id":    cty.UnknownVal(cty.String),
   210  				"stuff": cty.StringVal("true"),
   211  			}),
   212  			cty.ObjectVal(map[string]cty.Value{
   213  				"id":    cty.UnknownVal(cty.String),
   214  				"stuff": cty.True,
   215  			}),
   216  			[]string{
   217  				`.stuff: wrong final value type: string required`,
   218  			},
   219  		},
   220  		{
   221  			&configschema.Block{
   222  				Attributes: map[string]*configschema.Attribute{
   223  					"id": {
   224  						Type:     cty.String,
   225  						Computed: true,
   226  					},
   227  					"stuff": {
   228  						Type:     cty.DynamicPseudoType,
   229  						Required: true,
   230  					},
   231  				},
   232  			},
   233  			cty.ObjectVal(map[string]cty.Value{
   234  				"id":    cty.UnknownVal(cty.String),
   235  				"stuff": cty.DynamicVal,
   236  			}),
   237  			cty.ObjectVal(map[string]cty.Value{
   238  				"id":    cty.UnknownVal(cty.String),
   239  				"stuff": cty.EmptyObjectVal,
   240  			}),
   241  			nil,
   242  		},
   243  		{
   244  			&configschema.Block{
   245  				Attributes: map[string]*configschema.Attribute{
   246  					"id": {
   247  						Type:     cty.String,
   248  						Computed: true,
   249  					},
   250  					"stuff": {
   251  						Type:     cty.DynamicPseudoType,
   252  						Required: true,
   253  					},
   254  				},
   255  			},
   256  			cty.ObjectVal(map[string]cty.Value{
   257  				"id": cty.UnknownVal(cty.String),
   258  				"stuff": cty.ObjectVal(map[string]cty.Value{
   259  					"nonsense": cty.StringVal("yup"),
   260  				}),
   261  			}),
   262  			cty.ObjectVal(map[string]cty.Value{
   263  				"id":    cty.UnknownVal(cty.String),
   264  				"stuff": cty.EmptyObjectVal,
   265  			}),
   266  			[]string{
   267  				`.stuff: wrong final value type: attribute "nonsense" is required`,
   268  			},
   269  		},
   270  		{
   271  			&configschema.Block{
   272  				Attributes: map[string]*configschema.Attribute{
   273  					"id": {
   274  						Type:     cty.String,
   275  						Computed: true,
   276  					},
   277  					"tags": {
   278  						Type:     cty.Map(cty.String),
   279  						Optional: true,
   280  					},
   281  				},
   282  			},
   283  			cty.ObjectVal(map[string]cty.Value{
   284  				"id": cty.UnknownVal(cty.String),
   285  				"tags": cty.MapVal(map[string]cty.Value{
   286  					"Name": cty.StringVal("thingy"),
   287  				}),
   288  			}),
   289  			cty.ObjectVal(map[string]cty.Value{
   290  				"id": cty.UnknownVal(cty.String),
   291  				"tags": cty.MapVal(map[string]cty.Value{
   292  					"Name": cty.StringVal("thingy"),
   293  				}),
   294  			}),
   295  			nil,
   296  		},
   297  		{
   298  			&configschema.Block{
   299  				Attributes: map[string]*configschema.Attribute{
   300  					"id": {
   301  						Type:     cty.String,
   302  						Computed: true,
   303  					},
   304  					"tags": {
   305  						Type:     cty.Map(cty.String),
   306  						Optional: true,
   307  					},
   308  				},
   309  			},
   310  			cty.ObjectVal(map[string]cty.Value{
   311  				"id": cty.UnknownVal(cty.String),
   312  				"tags": cty.MapVal(map[string]cty.Value{
   313  					"Name": cty.UnknownVal(cty.String),
   314  				}),
   315  			}),
   316  			cty.ObjectVal(map[string]cty.Value{
   317  				"id": cty.UnknownVal(cty.String),
   318  				"tags": cty.MapVal(map[string]cty.Value{
   319  					"Name": cty.StringVal("thingy"),
   320  				}),
   321  			}),
   322  			nil,
   323  		},
   324  		{
   325  			&configschema.Block{
   326  				Attributes: map[string]*configschema.Attribute{
   327  					"id": {
   328  						Type:     cty.String,
   329  						Computed: true,
   330  					},
   331  					"tags": {
   332  						Type:     cty.Map(cty.String),
   333  						Optional: true,
   334  					},
   335  				},
   336  			},
   337  			cty.ObjectVal(map[string]cty.Value{
   338  				"id": cty.UnknownVal(cty.String),
   339  				"tags": cty.MapVal(map[string]cty.Value{
   340  					"Name": cty.StringVal("wotsit"),
   341  				}),
   342  			}),
   343  			cty.ObjectVal(map[string]cty.Value{
   344  				"id": cty.UnknownVal(cty.String),
   345  				"tags": cty.MapVal(map[string]cty.Value{
   346  					"Name": cty.StringVal("thingy"),
   347  				}),
   348  			}),
   349  			[]string{
   350  				`.tags["Name"]: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
   351  			},
   352  		},
   353  		{
   354  			&configschema.Block{
   355  				Attributes: map[string]*configschema.Attribute{
   356  					"id": {
   357  						Type:     cty.String,
   358  						Computed: true,
   359  					},
   360  					"tags": {
   361  						Type:     cty.Map(cty.String),
   362  						Optional: true,
   363  					},
   364  				},
   365  			},
   366  			cty.ObjectVal(map[string]cty.Value{
   367  				"id": cty.UnknownVal(cty.String),
   368  				"tags": cty.MapVal(map[string]cty.Value{
   369  					"Name": cty.StringVal("thingy"),
   370  				}),
   371  			}),
   372  			cty.ObjectVal(map[string]cty.Value{
   373  				"id": cty.UnknownVal(cty.String),
   374  				"tags": cty.MapVal(map[string]cty.Value{
   375  					"Name": cty.StringVal("thingy"),
   376  					"Env":  cty.StringVal("production"),
   377  				}),
   378  			}),
   379  			[]string{
   380  				`.tags: new element "Env" has appeared`,
   381  			},
   382  		},
   383  		{
   384  			&configschema.Block{
   385  				Attributes: map[string]*configschema.Attribute{
   386  					"id": {
   387  						Type:     cty.String,
   388  						Computed: true,
   389  					},
   390  					"tags": {
   391  						Type:     cty.Map(cty.String),
   392  						Optional: true,
   393  					},
   394  				},
   395  			},
   396  			cty.ObjectVal(map[string]cty.Value{
   397  				"id": cty.UnknownVal(cty.String),
   398  				"tags": cty.MapVal(map[string]cty.Value{
   399  					"Name": cty.StringVal("thingy"),
   400  				}),
   401  			}),
   402  			cty.ObjectVal(map[string]cty.Value{
   403  				"id":   cty.UnknownVal(cty.String),
   404  				"tags": cty.MapValEmpty(cty.String),
   405  			}),
   406  			[]string{
   407  				`.tags: element "Name" has vanished`,
   408  			},
   409  		},
   410  		{
   411  			&configschema.Block{
   412  				Attributes: map[string]*configschema.Attribute{
   413  					"id": {
   414  						Type:     cty.String,
   415  						Computed: true,
   416  					},
   417  					"tags": {
   418  						Type:     cty.Map(cty.String),
   419  						Optional: true,
   420  					},
   421  				},
   422  			},
   423  			cty.ObjectVal(map[string]cty.Value{
   424  				"id": cty.UnknownVal(cty.String),
   425  				"tags": cty.MapVal(map[string]cty.Value{
   426  					"Name": cty.UnknownVal(cty.String),
   427  				}),
   428  			}),
   429  			cty.ObjectVal(map[string]cty.Value{
   430  				"id": cty.UnknownVal(cty.String),
   431  				"tags": cty.MapVal(map[string]cty.Value{
   432  					"Name": cty.NullVal(cty.String),
   433  				}),
   434  			}),
   435  			nil,
   436  		},
   437  		{
   438  			&configschema.Block{
   439  				Attributes: map[string]*configschema.Attribute{
   440  					"id": {
   441  						Type:     cty.String,
   442  						Computed: true,
   443  					},
   444  					"zones": {
   445  						Type:     cty.Set(cty.String),
   446  						Optional: true,
   447  					},
   448  				},
   449  			},
   450  			cty.ObjectVal(map[string]cty.Value{
   451  				"id": cty.UnknownVal(cty.String),
   452  				"zones": cty.SetVal([]cty.Value{
   453  					cty.StringVal("thingy"),
   454  				}),
   455  			}),
   456  			cty.ObjectVal(map[string]cty.Value{
   457  				"id": cty.UnknownVal(cty.String),
   458  				"zones": cty.SetVal([]cty.Value{
   459  					cty.StringVal("thingy"),
   460  				}),
   461  			}),
   462  			nil,
   463  		},
   464  		{
   465  			&configschema.Block{
   466  				Attributes: map[string]*configschema.Attribute{
   467  					"id": {
   468  						Type:     cty.String,
   469  						Computed: true,
   470  					},
   471  					"zones": {
   472  						Type:     cty.Set(cty.String),
   473  						Optional: true,
   474  					},
   475  				},
   476  			},
   477  			cty.ObjectVal(map[string]cty.Value{
   478  				"id": cty.UnknownVal(cty.String),
   479  				"zones": cty.SetVal([]cty.Value{
   480  					cty.StringVal("thingy"),
   481  				}),
   482  			}),
   483  			cty.ObjectVal(map[string]cty.Value{
   484  				"id": cty.UnknownVal(cty.String),
   485  				"zones": cty.SetVal([]cty.Value{
   486  					cty.StringVal("thingy"),
   487  					cty.StringVal("wotsit"),
   488  				}),
   489  			}),
   490  			[]string{
   491  				`.zones: actual set element cty.StringVal("wotsit") does not correlate with any element in plan`,
   492  				`.zones: length changed from 1 to 2`,
   493  			},
   494  		},
   495  		{
   496  			&configschema.Block{
   497  				Attributes: map[string]*configschema.Attribute{
   498  					"id": {
   499  						Type:     cty.String,
   500  						Computed: true,
   501  					},
   502  					"zones": {
   503  						Type:     cty.Set(cty.String),
   504  						Optional: true,
   505  					},
   506  				},
   507  			},
   508  			cty.ObjectVal(map[string]cty.Value{
   509  				"id": cty.UnknownVal(cty.String),
   510  				"zones": cty.SetVal([]cty.Value{
   511  					cty.UnknownVal(cty.String),
   512  					cty.UnknownVal(cty.String),
   513  				}),
   514  			}),
   515  			cty.ObjectVal(map[string]cty.Value{
   516  				"id": cty.UnknownVal(cty.String),
   517  				"zones": cty.SetVal([]cty.Value{
   518  					// Imagine that both of our unknown values ultimately resolved to "thingy",
   519  					// causing them to collapse into a single element. That's valid,
   520  					// even though it's also a little confusing and counter-intuitive.
   521  					cty.StringVal("thingy"),
   522  				}),
   523  			}),
   524  			nil,
   525  		},
   526  		{
   527  			&configschema.Block{
   528  				Attributes: map[string]*configschema.Attribute{
   529  					"id": {
   530  						Type:     cty.String,
   531  						Computed: true,
   532  					},
   533  					"names": {
   534  						Type:     cty.List(cty.String),
   535  						Optional: true,
   536  					},
   537  				},
   538  			},
   539  			cty.ObjectVal(map[string]cty.Value{
   540  				"id": cty.UnknownVal(cty.String),
   541  				"names": cty.ListVal([]cty.Value{
   542  					cty.StringVal("thingy"),
   543  				}),
   544  			}),
   545  			cty.ObjectVal(map[string]cty.Value{
   546  				"id": cty.UnknownVal(cty.String),
   547  				"names": cty.ListVal([]cty.Value{
   548  					cty.StringVal("thingy"),
   549  				}),
   550  			}),
   551  			nil,
   552  		},
   553  		{
   554  			&configschema.Block{
   555  				Attributes: map[string]*configschema.Attribute{
   556  					"id": {
   557  						Type:     cty.String,
   558  						Computed: true,
   559  					},
   560  					"names": {
   561  						Type:     cty.List(cty.String),
   562  						Optional: true,
   563  					},
   564  				},
   565  			},
   566  			cty.ObjectVal(map[string]cty.Value{
   567  				"id":    cty.UnknownVal(cty.String),
   568  				"names": cty.UnknownVal(cty.List(cty.String)),
   569  			}),
   570  			cty.ObjectVal(map[string]cty.Value{
   571  				"id": cty.UnknownVal(cty.String),
   572  				"names": cty.ListVal([]cty.Value{
   573  					cty.StringVal("thingy"),
   574  				}),
   575  			}),
   576  			nil,
   577  		},
   578  		{
   579  			&configschema.Block{
   580  				Attributes: map[string]*configschema.Attribute{
   581  					"id": {
   582  						Type:     cty.String,
   583  						Computed: true,
   584  					},
   585  					"names": {
   586  						Type:     cty.List(cty.String),
   587  						Optional: true,
   588  					},
   589  				},
   590  			},
   591  			cty.ObjectVal(map[string]cty.Value{
   592  				"id": cty.UnknownVal(cty.String),
   593  				"names": cty.ListVal([]cty.Value{
   594  					cty.UnknownVal(cty.String),
   595  				}),
   596  			}),
   597  			cty.ObjectVal(map[string]cty.Value{
   598  				"id": cty.UnknownVal(cty.String),
   599  				"names": cty.ListVal([]cty.Value{
   600  					cty.StringVal("thingy"),
   601  				}),
   602  			}),
   603  			nil,
   604  		},
   605  		{
   606  			&configschema.Block{
   607  				Attributes: map[string]*configschema.Attribute{
   608  					"id": {
   609  						Type:     cty.String,
   610  						Computed: true,
   611  					},
   612  					"names": {
   613  						Type:     cty.List(cty.String),
   614  						Optional: true,
   615  					},
   616  				},
   617  			},
   618  			cty.ObjectVal(map[string]cty.Value{
   619  				"id": cty.UnknownVal(cty.String),
   620  				"names": cty.ListVal([]cty.Value{
   621  					cty.StringVal("thingy"),
   622  					cty.UnknownVal(cty.String),
   623  				}),
   624  			}),
   625  			cty.ObjectVal(map[string]cty.Value{
   626  				"id": cty.UnknownVal(cty.String),
   627  				"names": cty.ListVal([]cty.Value{
   628  					cty.StringVal("thingy"),
   629  					cty.StringVal("wotsit"),
   630  				}),
   631  			}),
   632  			nil,
   633  		},
   634  		{
   635  			&configschema.Block{
   636  				Attributes: map[string]*configschema.Attribute{
   637  					"id": {
   638  						Type:     cty.String,
   639  						Computed: true,
   640  					},
   641  					"names": {
   642  						Type:     cty.List(cty.String),
   643  						Optional: true,
   644  					},
   645  				},
   646  			},
   647  			cty.ObjectVal(map[string]cty.Value{
   648  				"id": cty.UnknownVal(cty.String),
   649  				"names": cty.ListVal([]cty.Value{
   650  					cty.UnknownVal(cty.String),
   651  					cty.StringVal("thingy"),
   652  				}),
   653  			}),
   654  			cty.ObjectVal(map[string]cty.Value{
   655  				"id": cty.UnknownVal(cty.String),
   656  				"names": cty.ListVal([]cty.Value{
   657  					cty.StringVal("thingy"),
   658  					cty.StringVal("wotsit"),
   659  				}),
   660  			}),
   661  			[]string{
   662  				`.names[1]: was cty.StringVal("thingy"), but now cty.StringVal("wotsit")`,
   663  			},
   664  		},
   665  		{
   666  			&configschema.Block{
   667  				Attributes: map[string]*configschema.Attribute{
   668  					"id": {
   669  						Type:     cty.String,
   670  						Computed: true,
   671  					},
   672  					"names": {
   673  						Type:     cty.List(cty.String),
   674  						Optional: true,
   675  					},
   676  				},
   677  			},
   678  			cty.ObjectVal(map[string]cty.Value{
   679  				"id": cty.UnknownVal(cty.String),
   680  				"names": cty.ListVal([]cty.Value{
   681  					cty.UnknownVal(cty.String),
   682  				}),
   683  			}),
   684  			cty.ObjectVal(map[string]cty.Value{
   685  				"id": cty.UnknownVal(cty.String),
   686  				"names": cty.ListVal([]cty.Value{
   687  					cty.StringVal("thingy"),
   688  					cty.StringVal("wotsit"),
   689  				}),
   690  			}),
   691  			[]string{
   692  				`.names: new element 1 has appeared`,
   693  			},
   694  		},
   695  
   696  		// NestingSingle blocks
   697  		{
   698  			&configschema.Block{
   699  				BlockTypes: map[string]*configschema.NestedBlock{
   700  					"key": {
   701  						Nesting: configschema.NestingSingle,
   702  						Block:   configschema.Block{},
   703  					},
   704  				},
   705  			},
   706  			cty.ObjectVal(map[string]cty.Value{
   707  				"key": cty.EmptyObjectVal,
   708  			}),
   709  			cty.ObjectVal(map[string]cty.Value{
   710  				"key": cty.EmptyObjectVal,
   711  			}),
   712  			nil,
   713  		},
   714  		{
   715  			&configschema.Block{
   716  				BlockTypes: map[string]*configschema.NestedBlock{
   717  					"key": {
   718  						Nesting: configschema.NestingSingle,
   719  						Block:   configschema.Block{},
   720  					},
   721  				},
   722  			},
   723  			cty.ObjectVal(map[string]cty.Value{
   724  				"key": cty.UnknownVal(cty.EmptyObject),
   725  			}),
   726  			cty.ObjectVal(map[string]cty.Value{
   727  				"key": cty.EmptyObjectVal,
   728  			}),
   729  			nil,
   730  		},
   731  		{
   732  			&configschema.Block{
   733  				BlockTypes: map[string]*configschema.NestedBlock{
   734  					"key": {
   735  						Nesting: configschema.NestingSingle,
   736  						Block: configschema.Block{
   737  							Attributes: map[string]*configschema.Attribute{
   738  								"foo": {
   739  									Type:     cty.String,
   740  									Optional: true,
   741  								},
   742  							},
   743  						},
   744  					},
   745  				},
   746  			},
   747  			cty.ObjectVal(map[string]cty.Value{
   748  				"key": cty.NullVal(cty.Object(map[string]cty.Type{
   749  					"foo": cty.String,
   750  				})),
   751  			}),
   752  			cty.ObjectVal(map[string]cty.Value{
   753  				"key": cty.ObjectVal(map[string]cty.Value{
   754  					"foo": cty.StringVal("hello"),
   755  				}),
   756  			}),
   757  			[]string{
   758  				`.key: was absent, but now present`,
   759  			},
   760  		},
   761  		{
   762  			&configschema.Block{
   763  				BlockTypes: map[string]*configschema.NestedBlock{
   764  					"key": {
   765  						Nesting: configschema.NestingSingle,
   766  						Block: configschema.Block{
   767  							Attributes: map[string]*configschema.Attribute{
   768  								"foo": {
   769  									Type:     cty.String,
   770  									Optional: true,
   771  								},
   772  							},
   773  						},
   774  					},
   775  				},
   776  			},
   777  			cty.ObjectVal(map[string]cty.Value{
   778  				"key": cty.ObjectVal(map[string]cty.Value{
   779  					"foo": cty.StringVal("hello"),
   780  				}),
   781  			}),
   782  			cty.ObjectVal(map[string]cty.Value{
   783  				"key": cty.NullVal(cty.Object(map[string]cty.Type{
   784  					"foo": cty.String,
   785  				})),
   786  			}),
   787  			[]string{
   788  				`.key: was present, but now absent`,
   789  			},
   790  		},
   791  		{
   792  			&configschema.Block{
   793  				BlockTypes: map[string]*configschema.NestedBlock{
   794  					"key": {
   795  						Nesting: configschema.NestingSingle,
   796  						Block: configschema.Block{
   797  							Attributes: map[string]*configschema.Attribute{
   798  								"foo": {
   799  									Type:     cty.String,
   800  									Optional: true,
   801  								},
   802  							},
   803  						},
   804  					},
   805  				},
   806  			},
   807  			cty.ObjectVal(map[string]cty.Value{
   808  				"key": cty.ObjectVal(map[string]cty.Value{
   809  					// One wholly unknown block is what "dynamic" blocks
   810  					// generate when the for_each expression is unknown.
   811  					"foo": cty.UnknownVal(cty.String),
   812  				}),
   813  			}),
   814  			cty.ObjectVal(map[string]cty.Value{
   815  				"key": cty.NullVal(cty.Object(map[string]cty.Type{
   816  					"foo": cty.String,
   817  				})),
   818  			}),
   819  			nil,
   820  		},
   821  
   822  		// NestingList blocks
   823  		{
   824  			&configschema.Block{
   825  				BlockTypes: map[string]*configschema.NestedBlock{
   826  					"key": {
   827  						Nesting: configschema.NestingList,
   828  						Block:   schemaWithFoo,
   829  					},
   830  				},
   831  			},
   832  			cty.ObjectVal(map[string]cty.Value{
   833  				"key": cty.ListVal([]cty.Value{
   834  					fooBlockValue,
   835  				}),
   836  			}),
   837  			cty.ObjectVal(map[string]cty.Value{
   838  				"key": cty.ListVal([]cty.Value{
   839  					fooBlockValue,
   840  				}),
   841  			}),
   842  			nil,
   843  		},
   844  		{
   845  			&configschema.Block{
   846  				BlockTypes: map[string]*configschema.NestedBlock{
   847  					"key": {
   848  						Nesting: configschema.NestingList,
   849  						Block:   schemaWithFoo,
   850  					},
   851  				},
   852  			},
   853  			cty.ObjectVal(map[string]cty.Value{
   854  				"key": cty.TupleVal([]cty.Value{
   855  					fooBlockValue,
   856  					fooBlockValue,
   857  				}),
   858  			}),
   859  			cty.ObjectVal(map[string]cty.Value{
   860  				"key": cty.TupleVal([]cty.Value{
   861  					fooBlockValue,
   862  				}),
   863  			}),
   864  			[]string{
   865  				`.key: block count changed from 2 to 1`,
   866  			},
   867  		},
   868  		{
   869  			&configschema.Block{
   870  				BlockTypes: map[string]*configschema.NestedBlock{
   871  					"key": {
   872  						Nesting: configschema.NestingList,
   873  						Block:   schemaWithFoo,
   874  					},
   875  				},
   876  			},
   877  			cty.ObjectVal(map[string]cty.Value{
   878  				"key": cty.TupleVal([]cty.Value{}),
   879  			}),
   880  			cty.ObjectVal(map[string]cty.Value{
   881  				"key": cty.TupleVal([]cty.Value{
   882  					fooBlockValue,
   883  					fooBlockValue,
   884  				}),
   885  			}),
   886  			[]string{
   887  				`.key: block count changed from 0 to 2`,
   888  			},
   889  		},
   890  		{
   891  			&configschema.Block{
   892  				BlockTypes: map[string]*configschema.NestedBlock{
   893  					"key": {
   894  						Nesting: configschema.NestingList,
   895  						Block:   schemaWithFooBar,
   896  					},
   897  				},
   898  			},
   899  			cty.ObjectVal(map[string]cty.Value{
   900  				"key": cty.ListVal([]cty.Value{
   901  					fooBarBlockDynamicPlaceholder, // the presence of this disables some of our checks
   902  				}),
   903  			}),
   904  			cty.ObjectVal(map[string]cty.Value{
   905  				"key": cty.ListVal([]cty.Value{
   906  					cty.ObjectVal(map[string]cty.Value{
   907  						"foo": cty.StringVal("hello"),
   908  					}),
   909  					cty.ObjectVal(map[string]cty.Value{
   910  						"foo": cty.StringVal("world"),
   911  					}),
   912  				}),
   913  			}),
   914  			nil, // a single block whose attrs are all unknown is allowed to expand into multiple, because that's how dynamic blocks behave when for_each is unknown
   915  		},
   916  		{
   917  			&configschema.Block{
   918  				BlockTypes: map[string]*configschema.NestedBlock{
   919  					"key": {
   920  						Nesting: configschema.NestingList,
   921  						Block:   schemaWithFooBar,
   922  					},
   923  				},
   924  			},
   925  			cty.ObjectVal(map[string]cty.Value{
   926  				"key": cty.ListVal([]cty.Value{
   927  					fooBarBlockValue,              // the presence of one static block does not negate that the following element looks like a dynamic placeholder
   928  					fooBarBlockDynamicPlaceholder, // the presence of this disables some of our checks
   929  				}),
   930  			}),
   931  			cty.ObjectVal(map[string]cty.Value{
   932  				"key": cty.ListVal([]cty.Value{
   933  					fooBlockValue,
   934  					cty.ObjectVal(map[string]cty.Value{
   935  						"foo": cty.StringVal("hello"),
   936  					}),
   937  					cty.ObjectVal(map[string]cty.Value{
   938  						"foo": cty.StringVal("world"),
   939  					}),
   940  				}),
   941  			}),
   942  			nil, // as above, the presence of a block whose attrs are all unknown indicates dynamic block expansion, so our usual count checks don't apply
   943  		},
   944  
   945  		// NestingSet blocks
   946  		{
   947  			&configschema.Block{
   948  				BlockTypes: map[string]*configschema.NestedBlock{
   949  					"block": {
   950  						Nesting: configschema.NestingSet,
   951  						Block:   schemaWithFoo,
   952  					},
   953  				},
   954  			},
   955  			cty.ObjectVal(map[string]cty.Value{
   956  				"block": cty.SetVal([]cty.Value{
   957  					cty.ObjectVal(map[string]cty.Value{
   958  						"foo": cty.StringVal("hello"),
   959  					}),
   960  					cty.ObjectVal(map[string]cty.Value{
   961  						"foo": cty.StringVal("world"),
   962  					}),
   963  				}),
   964  			}),
   965  			cty.ObjectVal(map[string]cty.Value{
   966  				"block": cty.SetVal([]cty.Value{
   967  					cty.ObjectVal(map[string]cty.Value{
   968  						"foo": cty.StringVal("hello"),
   969  					}),
   970  					cty.ObjectVal(map[string]cty.Value{
   971  						"foo": cty.StringVal("world"),
   972  					}),
   973  				}),
   974  			}),
   975  			nil,
   976  		},
   977  		{
   978  			&configschema.Block{
   979  				BlockTypes: map[string]*configschema.NestedBlock{
   980  					"block": {
   981  						Nesting: configschema.NestingSet,
   982  						Block:   schemaWithFoo,
   983  					},
   984  				},
   985  			},
   986  			cty.ObjectVal(map[string]cty.Value{
   987  				"block": cty.SetVal([]cty.Value{
   988  					cty.ObjectVal(map[string]cty.Value{
   989  						"foo": cty.UnknownVal(cty.String),
   990  					}),
   991  					cty.ObjectVal(map[string]cty.Value{
   992  						"foo": cty.UnknownVal(cty.String),
   993  					}),
   994  				}),
   995  			}),
   996  			cty.ObjectVal(map[string]cty.Value{
   997  				"block": cty.SetVal([]cty.Value{
   998  					// This is testing the scenario where the two unknown values
   999  					// turned out to be equal after we learned their values,
  1000  					// and so they coalesced together into a single element.
  1001  					cty.ObjectVal(map[string]cty.Value{
  1002  						"foo": cty.StringVal("hello"),
  1003  					}),
  1004  				}),
  1005  			}),
  1006  			nil,
  1007  		},
  1008  		{
  1009  			&configschema.Block{
  1010  				BlockTypes: map[string]*configschema.NestedBlock{
  1011  					"block": {
  1012  						Nesting: configschema.NestingSet,
  1013  						Block:   schemaWithFoo,
  1014  					},
  1015  				},
  1016  			},
  1017  			cty.ObjectVal(map[string]cty.Value{
  1018  				"block": cty.SetVal([]cty.Value{
  1019  					cty.ObjectVal(map[string]cty.Value{
  1020  						"foo": cty.UnknownVal(cty.String),
  1021  					}),
  1022  					cty.ObjectVal(map[string]cty.Value{
  1023  						"foo": cty.UnknownVal(cty.String),
  1024  					}),
  1025  				}),
  1026  			}),
  1027  			cty.ObjectVal(map[string]cty.Value{
  1028  				"block": cty.SetVal([]cty.Value{
  1029  					cty.ObjectVal(map[string]cty.Value{
  1030  						"foo": cty.StringVal("hello"),
  1031  					}),
  1032  					cty.ObjectVal(map[string]cty.Value{
  1033  						"foo": cty.StringVal("world"),
  1034  					}),
  1035  				}),
  1036  			}),
  1037  			nil,
  1038  		},
  1039  		{
  1040  			&configschema.Block{
  1041  				BlockTypes: map[string]*configschema.NestedBlock{
  1042  					"block": {
  1043  						Nesting: configschema.NestingSet,
  1044  						Block:   schemaWithFoo,
  1045  					},
  1046  				},
  1047  			},
  1048  			cty.ObjectVal(map[string]cty.Value{
  1049  				"block": cty.SetVal([]cty.Value{
  1050  					cty.ObjectVal(map[string]cty.Value{
  1051  						"foo": cty.UnknownVal(cty.String),
  1052  					}),
  1053  					cty.ObjectVal(map[string]cty.Value{
  1054  						"foo": cty.UnknownVal(cty.String),
  1055  					}),
  1056  				}),
  1057  			}),
  1058  			cty.ObjectVal(map[string]cty.Value{
  1059  				"block": cty.SetVal([]cty.Value{
  1060  					cty.ObjectVal(map[string]cty.Value{
  1061  						"foo": cty.StringVal("hello"),
  1062  					}),
  1063  					cty.ObjectVal(map[string]cty.Value{
  1064  						"foo": cty.StringVal("world"),
  1065  					}),
  1066  					cty.ObjectVal(map[string]cty.Value{
  1067  						"foo": cty.StringVal("nope"),
  1068  					}),
  1069  				}),
  1070  			}),
  1071  			// there is no error here, because the presence of unknowns
  1072  			// indicates this may be a dynamic block, and the length is unknown
  1073  			nil,
  1074  		},
  1075  		{
  1076  			&configschema.Block{
  1077  				BlockTypes: map[string]*configschema.NestedBlock{
  1078  					"block": {
  1079  						Nesting: configschema.NestingSet,
  1080  						Block:   schemaWithFoo,
  1081  					},
  1082  				},
  1083  			},
  1084  			cty.ObjectVal(map[string]cty.Value{
  1085  				"block": cty.SetVal([]cty.Value{
  1086  					cty.ObjectVal(map[string]cty.Value{
  1087  						"foo": cty.StringVal("hello"),
  1088  					}),
  1089  					cty.ObjectVal(map[string]cty.Value{
  1090  						"foo": cty.StringVal("world"),
  1091  					}),
  1092  				}),
  1093  			}),
  1094  			cty.ObjectVal(map[string]cty.Value{
  1095  				"block": cty.SetVal([]cty.Value{
  1096  					cty.ObjectVal(map[string]cty.Value{
  1097  						"foo": cty.StringVal("howdy"),
  1098  					}),
  1099  					cty.ObjectVal(map[string]cty.Value{
  1100  						"foo": cty.StringVal("world"),
  1101  					}),
  1102  				}),
  1103  			}),
  1104  			[]string{
  1105  				`.block: planned set element cty.ObjectVal(map[string]cty.Value{"foo":cty.StringVal("hello")}) does not correlate with any element in actual`,
  1106  			},
  1107  		},
  1108  		{
  1109  			// This one is an odd situation where the value representing the
  1110  			// block itself is unknown. This is never supposed to be true,
  1111  			// but in legacy SDK mode we allow such things to pass through as
  1112  			// a warning, and so we must tolerate them for matching purposes.
  1113  			&configschema.Block{
  1114  				BlockTypes: map[string]*configschema.NestedBlock{
  1115  					"block": {
  1116  						Nesting: configschema.NestingSet,
  1117  						Block:   schemaWithFoo,
  1118  					},
  1119  				},
  1120  			},
  1121  			cty.ObjectVal(map[string]cty.Value{
  1122  				"block": cty.SetVal([]cty.Value{
  1123  					cty.ObjectVal(map[string]cty.Value{
  1124  						"foo": cty.UnknownVal(cty.String),
  1125  					}),
  1126  					cty.ObjectVal(map[string]cty.Value{
  1127  						"foo": cty.UnknownVal(cty.String),
  1128  					}),
  1129  				}),
  1130  			}),
  1131  			cty.ObjectVal(map[string]cty.Value{
  1132  				"block": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
  1133  					"foo": cty.String,
  1134  				}))),
  1135  			}),
  1136  			nil,
  1137  		},
  1138  		{
  1139  			&configschema.Block{
  1140  				BlockTypes: map[string]*configschema.NestedBlock{
  1141  					"block": {
  1142  						Nesting: configschema.NestingSet,
  1143  						Block:   schemaWithFoo,
  1144  					},
  1145  				},
  1146  			},
  1147  			cty.ObjectVal(map[string]cty.Value{
  1148  				"block": cty.SetVal([]cty.Value{
  1149  					cty.ObjectVal(map[string]cty.Value{
  1150  						"foo": cty.UnknownVal(cty.String),
  1151  					}),
  1152  				}),
  1153  			}),
  1154  			cty.ObjectVal(map[string]cty.Value{
  1155  				"block": cty.SetVal([]cty.Value{
  1156  					cty.ObjectVal(map[string]cty.Value{
  1157  						"foo": cty.StringVal("a"),
  1158  					}),
  1159  					cty.ObjectVal(map[string]cty.Value{
  1160  						"foo": cty.StringVal("b"),
  1161  					}),
  1162  				}),
  1163  			}),
  1164  			nil,
  1165  		},
  1166  		{
  1167  			&configschema.Block{
  1168  				BlockTypes: map[string]*configschema.NestedBlock{
  1169  					"block": {
  1170  						Nesting: configschema.NestingList,
  1171  						Block: configschema.Block{
  1172  							Attributes: map[string]*configschema.Attribute{
  1173  								"foo": {
  1174  									Type:     cty.String,
  1175  									Required: true,
  1176  								},
  1177  							},
  1178  						},
  1179  					},
  1180  				},
  1181  			},
  1182  			cty.ObjectVal(map[string]cty.Value{
  1183  				"block": cty.EmptyObjectVal,
  1184  			}),
  1185  			cty.ObjectVal(map[string]cty.Value{
  1186  				"block": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
  1187  					"foo": cty.String,
  1188  				}))),
  1189  			}),
  1190  			nil,
  1191  		},
  1192  	}
  1193  
  1194  	for i, test := range tests {
  1195  		t.Run(fmt.Sprintf("%02d: %#v and %#v", i, test.Planned, test.Actual), func(t *testing.T) {
  1196  			errs := AssertObjectCompatible(test.Schema, test.Planned, test.Actual)
  1197  
  1198  			wantErrs := make(map[string]struct{})
  1199  			gotErrs := make(map[string]struct{})
  1200  			for _, err := range errs {
  1201  				gotErrs[tfdiags.FormatError(err)] = struct{}{}
  1202  			}
  1203  			for _, msg := range test.WantErrs {
  1204  				wantErrs[msg] = struct{}{}
  1205  			}
  1206  
  1207  			t.Logf("\nplanned: %sactual:  %s", dump.Value(test.Planned), dump.Value(test.Actual))
  1208  			for msg := range wantErrs {
  1209  				if _, ok := gotErrs[msg]; !ok {
  1210  					t.Errorf("missing expected error: %s", msg)
  1211  				}
  1212  			}
  1213  			for msg := range gotErrs {
  1214  				if _, ok := wantErrs[msg]; !ok {
  1215  					t.Errorf("unexpected extra error: %s", msg)
  1216  				}
  1217  			}
  1218  		})
  1219  	}
  1220  }