opentofu/plans/objchange/compatible_test.go
Martin Atkins 7216049fdb plans/objchange: Improve precision of AssertObjectCompatible with sets
Previously we were just asserting that the number of elements didn't grow
between planned and actual. We still can't precisely correlate elements in
sets with unknown values, but here we adapt some logic we added earlier
to config/hcl2shim to ensure that we can find a plausible correlation for
each element in each set to at least one element in the other set, and
thus catch more cases where set elements might vanish or appear between
plan and apply, for improved safety.

This will still generate false negatives in some cases where unknown
values are present due to having to assume correlation is intended
wherever it is possible, but we'll catch situations where the actual value
is obviously contrary to what was planned.
2019-02-04 18:19:46 -08:00

1079 lines
25 KiB
Go

package objchange
import (
"fmt"
"testing"
"github.com/apparentlymart/go-dump/dump"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/tfdiags"
)
func TestAssertObjectCompatible(t *testing.T) {
tests := []struct {
Schema *configschema.Block
Planned cty.Value
Actual cty.Value
WantErrs []string
}{
{
&configschema.Block{},
cty.EmptyObjectVal,
cty.EmptyObjectVal,
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"name": {
Type: cty.String,
Required: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"name": cty.StringVal("thingy"),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"name": cty.StringVal("thingy"),
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"name": {
Type: cty.String,
Required: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"name": cty.UnknownVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"name": cty.StringVal("thingy"),
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"name": {
Type: cty.String,
Required: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"name": cty.StringVal("wotsit"),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"name": cty.StringVal("thingy"),
}),
[]string{
`.name: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
},
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"stuff": {
Type: cty.DynamicPseudoType,
Required: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"stuff": cty.DynamicVal,
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"stuff": cty.StringVal("thingy"),
}),
[]string{},
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"stuff": {
Type: cty.DynamicPseudoType,
Required: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"stuff": cty.StringVal("wotsit"),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"stuff": cty.StringVal("thingy"),
}),
[]string{
`.stuff: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
},
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"stuff": {
Type: cty.DynamicPseudoType,
Required: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"stuff": cty.StringVal("true"),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"stuff": cty.True,
}),
[]string{
`.stuff: wrong final value type: string required`,
},
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"stuff": {
Type: cty.DynamicPseudoType,
Required: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"stuff": cty.DynamicVal,
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"stuff": cty.EmptyObjectVal,
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"stuff": {
Type: cty.DynamicPseudoType,
Required: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"stuff": cty.ObjectVal(map[string]cty.Value{
"nonsense": cty.StringVal("yup"),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"stuff": cty.EmptyObjectVal,
}),
[]string{
`.stuff: wrong final value type: attribute "nonsense" is required`,
},
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"tags": {
Type: cty.Map(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.StringVal("thingy"),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.StringVal("thingy"),
}),
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"tags": {
Type: cty.Map(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.UnknownVal(cty.String),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.StringVal("thingy"),
}),
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"tags": {
Type: cty.Map(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.StringVal("wotsit"),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.StringVal("thingy"),
}),
}),
[]string{
`.tags["Name"]: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
},
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"tags": {
Type: cty.Map(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.StringVal("thingy"),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.StringVal("thingy"),
"Env": cty.StringVal("production"),
}),
}),
[]string{
`.tags: new element "Env" has appeared`,
},
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"tags": {
Type: cty.Map(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.StringVal("thingy"),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapValEmpty(cty.String),
}),
[]string{
`.tags: element "Name" has vanished`,
},
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"tags": {
Type: cty.Map(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.UnknownVal(cty.String),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"tags": cty.MapVal(map[string]cty.Value{
"Name": cty.NullVal(cty.String),
}),
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"zones": {
Type: cty.Set(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"zones": cty.SetVal([]cty.Value{
cty.StringVal("thingy"),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"zones": cty.SetVal([]cty.Value{
cty.StringVal("thingy"),
}),
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"zones": {
Type: cty.Set(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"zones": cty.SetVal([]cty.Value{
cty.StringVal("thingy"),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"zones": cty.SetVal([]cty.Value{
cty.StringVal("thingy"),
cty.StringVal("wotsit"),
}),
}),
[]string{
`.zones: actual set element cty.StringVal("wotsit") does not correlate with any element in plan`,
`.zones: length changed from 1 to 2`,
},
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"zones": {
Type: cty.Set(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"zones": cty.SetVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"zones": cty.SetVal([]cty.Value{
// Imagine that both of our unknown values ultimately resolved to "thingy",
// causing them to collapse into a single element. That's valid,
// even though it's also a little confusing and counter-intuitive.
cty.StringVal("thingy"),
}),
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"names": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.ListVal([]cty.Value{
cty.StringVal("thingy"),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.ListVal([]cty.Value{
cty.StringVal("thingy"),
}),
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"names": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.UnknownVal(cty.List(cty.String)),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.ListVal([]cty.Value{
cty.StringVal("thingy"),
}),
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"names": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.ListVal([]cty.Value{
cty.StringVal("thingy"),
}),
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"names": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.ListVal([]cty.Value{
cty.StringVal("thingy"),
cty.UnknownVal(cty.String),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.ListVal([]cty.Value{
cty.StringVal("thingy"),
cty.StringVal("wotsit"),
}),
}),
nil,
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"names": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.StringVal("thingy"),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.ListVal([]cty.Value{
cty.StringVal("thingy"),
cty.StringVal("wotsit"),
}),
}),
[]string{
`.names[1]: was cty.StringVal("thingy"), but now cty.StringVal("wotsit")`,
},
},
{
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"names": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"names": cty.ListVal([]cty.Value{
cty.StringVal("thingy"),
cty.StringVal("wotsit"),
}),
}),
[]string{
`.names: new element 1 has appeared`,
},
},
// NestingSingle blocks
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"key": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"key": cty.EmptyObjectVal,
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"key": cty.EmptyObjectVal,
}),
nil,
},
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"key": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"key": cty.UnknownVal(cty.EmptyObject),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"key": cty.EmptyObjectVal,
}),
nil,
},
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"key": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"key": cty.NullVal(cty.Object(map[string]cty.Type{
"foo": cty.String,
})),
}),
cty.ObjectVal(map[string]cty.Value{
"key": cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
}),
}),
[]string{
`.key: was absent, but now present`,
},
},
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"key": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"key": cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"key": cty.NullVal(cty.Object(map[string]cty.Type{
"foo": cty.String,
})),
}),
[]string{
`.key: was present, but now absent`,
},
},
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"key": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"key": cty.ObjectVal(map[string]cty.Value{
// One wholly unknown block is what "dynamic" blocks
// generate when the for_each expression is unknown.
"foo": cty.UnknownVal(cty.String),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"key": cty.NullVal(cty.Object(map[string]cty.Type{
"foo": cty.String,
})),
}),
nil,
},
// NestingList blocks
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"key": {
Nesting: configschema.NestingList,
Block: configschema.Block{},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"key": cty.ListVal([]cty.Value{
cty.EmptyObjectVal,
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"key": cty.ListVal([]cty.Value{
cty.EmptyObjectVal,
}),
}),
nil,
},
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"key": {
Nesting: configschema.NestingList,
Block: configschema.Block{},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"key": cty.TupleVal([]cty.Value{
cty.EmptyObjectVal,
cty.EmptyObjectVal,
}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"key": cty.TupleVal([]cty.Value{
cty.EmptyObjectVal,
}),
}),
[]string{
`.key: block count changed from 2 to 1`,
},
},
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"key": {
Nesting: configschema.NestingList,
Block: configschema.Block{},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"key": cty.TupleVal([]cty.Value{}),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"key": cty.TupleVal([]cty.Value{
cty.EmptyObjectVal,
cty.EmptyObjectVal,
}),
}),
[]string{
`.key: block count changed from 0 to 2`,
},
},
// NestingSet blocks
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"block": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"block": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("world"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"block": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("world"),
}),
}),
}),
nil,
},
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"block": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"block": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"block": cty.SetVal([]cty.Value{
// This is testing the scenario where the two unknown values
// turned out to be equal after we learned their values,
// and so they coalesced together into a single element.
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
}),
}),
}),
nil,
},
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"block": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"block": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"block": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("world"),
}),
}),
}),
nil,
},
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"block": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"block": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.String),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"block": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("world"),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("nope"),
}),
}),
}),
[]string{
`.block: block set length changed from 2 to 3`,
},
},
{
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"block": {
Nesting: configschema.NestingSet,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
},
},
},
cty.ObjectVal(map[string]cty.Value{
"block": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("world"),
}),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"block": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("howdy"),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("world"),
}),
}),
}),
[]string{
`.block: planned set element cty.Value{ty: cty.Object(map[string]cty.Type{"foo":cty.String}), v: map[string]interface {}{"foo":"hello"}} does not correlate with any element in actual`,
},
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v and %#v", test.Planned, test.Actual), func(t *testing.T) {
errs := AssertObjectCompatible(test.Schema, test.Planned, test.Actual)
wantErrs := make(map[string]struct{})
gotErrs := make(map[string]struct{})
for _, err := range errs {
gotErrs[tfdiags.FormatError(err)] = struct{}{}
}
for _, msg := range test.WantErrs {
wantErrs[msg] = struct{}{}
}
t.Logf("\nplanned: %sactual: %s", dump.Value(test.Planned), dump.Value(test.Actual))
for msg := range wantErrs {
if _, ok := gotErrs[msg]; !ok {
t.Errorf("missing expected error: %s", msg)
}
}
for msg := range gotErrs {
if _, ok := wantErrs[msg]; !ok {
t.Errorf("unexpected extra error: %s", msg)
}
}
})
}
}