opentofu/terraform/eval_for_each_test.go
Martin Atkins e2c64bc255 core: Annotate for_each errors with expression info
Our diagnostics model allows for optionally annotating an error or warning
with information about the expression and eval context it was generated
from, which the diagnostic renderer for the UI will then use to give the
user some additional hints about what values may have contributed to the
error.

We previously didn't have those annotations on the results of evaluating
for_each expressions though, because in that case we were using the helper
function to evaluate an expression in one shot and thus we didn't ever
have a reference to the EvalContext in order to include it in the
diagnostic values.

Now, at the expense of having to handle the evaluation at a slightly lower
level of abstraction, we'll annotate all of the for_each error messages
with source expression information. This is valuable because we see users
often confused as to how their complex for_each expressions ended up being
invalid, and hopefully giving some information about what the inputs were
will allow more users to self-solve.
2020-10-29 09:07:48 -07:00

189 lines
5.7 KiB
Go

package terraform
import (
"reflect"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
func TestEvaluateForEachExpression_valid(t *testing.T) {
tests := map[string]struct {
Expr hcl.Expression
ForEachMap map[string]cty.Value
}{
"empty set": {
hcltest.MockExprLiteral(cty.SetValEmpty(cty.String)),
map[string]cty.Value{},
},
"multi-value string set": {
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
map[string]cty.Value{
"a": cty.StringVal("a"),
"b": cty.StringVal("b"),
},
},
"empty map": {
hcltest.MockExprLiteral(cty.MapValEmpty(cty.Bool)),
map[string]cty.Value{},
},
"map": {
hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
"a": cty.BoolVal(true),
"b": cty.BoolVal(false),
})),
map[string]cty.Value{
"a": cty.BoolVal(true),
"b": cty.BoolVal(false),
},
},
"map containing unknown values": {
hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.Bool),
"b": cty.UnknownVal(cty.Bool),
})),
map[string]cty.Value{
"a": cty.UnknownVal(cty.Bool),
"b": cty.UnknownVal(cty.Bool),
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
ctx := &MockEvalContext{}
ctx.installSimpleEval()
forEachMap, diags := evaluateForEachExpression(test.Expr, ctx)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !reflect.DeepEqual(forEachMap, test.ForEachMap) {
t.Errorf(
"wrong map value\ngot: %swant: %s",
spew.Sdump(forEachMap), spew.Sdump(test.ForEachMap),
)
}
})
}
}
func TestEvaluateForEachExpression_errors(t *testing.T) {
tests := map[string]struct {
Expr hcl.Expression
Summary, DetailSubstring string
}{
"null set": {
hcltest.MockExprLiteral(cty.NullVal(cty.Set(cty.String))),
"Invalid for_each argument",
`the given "for_each" argument value is null`,
},
"string": {
hcltest.MockExprLiteral(cty.StringVal("i am definitely a set")),
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type string",
},
"list": {
hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")})),
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type list",
},
"tuple": {
hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type tuple",
},
"unknown string set": {
hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))),
"Invalid for_each argument",
"depends on resource attributes that cannot be determined until apply",
},
"unknown map": {
hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))),
"Invalid for_each argument",
"depends on resource attributes that cannot be determined until apply",
},
"set containing booleans": {
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.BoolVal(true)})),
"Invalid for_each set argument",
"supports maps and sets of strings, but you have provided a set containing type bool",
},
"set containing null": {
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.NullVal(cty.String)})),
"Invalid for_each set argument",
"must not contain null values",
},
"set containing unknown value": {
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)})),
"Invalid for_each argument",
"depends on resource attributes that cannot be determined until apply",
},
"set containing dynamic unknown value": {
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.DynamicPseudoType)})),
"Invalid for_each argument",
"depends on resource attributes that cannot be determined until apply",
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
ctx := &MockEvalContext{}
ctx.installSimpleEval()
_, diags := evaluateForEachExpression(test.Expr, ctx)
if len(diags) != 1 {
t.Fatalf("got %d diagnostics; want 1", diags)
}
if got, want := diags[0].Severity(), tfdiags.Error; got != want {
t.Errorf("wrong diagnostic severity %#v; want %#v", got, want)
}
if got, want := diags[0].Description().Summary, test.Summary; got != want {
t.Errorf("wrong diagnostic summary %#v; want %#v", got, want)
}
if got, want := diags[0].Description().Detail, test.DetailSubstring; !strings.Contains(got, want) {
t.Errorf("wrong diagnostic detail %#v; want %#v", got, want)
}
if fromExpr := diags[0].FromExpr(); fromExpr != nil {
if fromExpr.Expression == nil {
t.Errorf("diagnostic does not refer to an expression")
}
if fromExpr.EvalContext == nil {
t.Errorf("diagnostic does not refer to an EvalContext")
}
} else {
t.Errorf("diagnostic does not support FromExpr\ngot: %s", spew.Sdump(diags[0]))
}
})
}
}
func TestEvaluateForEachExpressionKnown(t *testing.T) {
tests := map[string]hcl.Expression{
"unknown string set": hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))),
"unknown map": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))),
}
for name, expr := range tests {
t.Run(name, func(t *testing.T) {
ctx := &MockEvalContext{}
ctx.installSimpleEval()
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, true)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if forEachVal.IsKnown() {
t.Error("got known, want unknown")
}
})
}
}