55b607ac30
The try(...) and can(...) functions are intended to make it more convenient to work with deep data structures of unknown shape, by allowing a caller to concisely try a complex traversal operation against a value without having to guard against each possible failure mode individually. These rely on the customdecode extension to get access to their argument expressions directly, rather than only the results of evaluating those expressions. The expressions can then be evaluated in a controlled manner so that any resulting errors can be recognized and suppressed as appropriate.
194 lines
4.7 KiB
Go
194 lines
4.7 KiB
Go
package tryfunc
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
)
|
|
|
|
func TestTryFunc(t *testing.T) {
|
|
tests := map[string]struct {
|
|
expr string
|
|
vars map[string]cty.Value
|
|
want cty.Value
|
|
wantErr string
|
|
}{
|
|
"one argument succeeds": {
|
|
`try(1)`,
|
|
nil,
|
|
cty.NumberIntVal(1),
|
|
``,
|
|
},
|
|
"two arguments, first succeeds": {
|
|
`try(1, 2)`,
|
|
nil,
|
|
cty.NumberIntVal(1),
|
|
``,
|
|
},
|
|
"two arguments, first fails": {
|
|
`try(nope, 2)`,
|
|
nil,
|
|
cty.NumberIntVal(2),
|
|
``,
|
|
},
|
|
"two arguments, first depends on unknowns": {
|
|
`try(unknown, 2)`,
|
|
map[string]cty.Value{
|
|
"unknown": cty.UnknownVal(cty.Number),
|
|
},
|
|
cty.DynamicVal, // can't proceed until first argument is known
|
|
``,
|
|
},
|
|
"two arguments, first succeeds and second depends on unknowns": {
|
|
`try(1, unknown)`,
|
|
map[string]cty.Value{
|
|
"unknown": cty.UnknownVal(cty.Number),
|
|
},
|
|
cty.NumberIntVal(1), // we know 1st succeeds, so it doesn't matter that 2nd is unknown
|
|
``,
|
|
},
|
|
"two arguments, first depends on unknowns deeply": {
|
|
`try(has_unknowns, 2)`,
|
|
map[string]cty.Value{
|
|
"has_unknowns": cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
|
|
},
|
|
cty.DynamicVal, // can't proceed until first argument is wholly known
|
|
``,
|
|
},
|
|
"two arguments, first traverses through an unkown": {
|
|
`try(unknown.baz, 2)`,
|
|
map[string]cty.Value{
|
|
"unknown": cty.UnknownVal(cty.Map(cty.String)),
|
|
},
|
|
cty.DynamicVal, // can't proceed until first argument is wholly known
|
|
``,
|
|
},
|
|
"three arguments, all fail": {
|
|
`try(this, that, this_thing_in_particular)`,
|
|
nil,
|
|
cty.NumberIntVal(2),
|
|
// The grammar of this stringification of the message is unfortunate,
|
|
// but caller can type-assert our result to get the original
|
|
// diagnostics directly in order to produce a better result.
|
|
`test.hcl:1,1-5: Error in function call; Call to function "try" failed: no expression succeeded:
|
|
- Variables not allowed (at test.hcl:1,5-9)
|
|
Variables may not be used here.
|
|
- Variables not allowed (at test.hcl:1,11-15)
|
|
Variables may not be used here.
|
|
- Variables not allowed (at test.hcl:1,17-41)
|
|
Variables may not be used here.
|
|
|
|
At least one expression must produce a successful result.`,
|
|
},
|
|
"no arguments": {
|
|
`try()`,
|
|
nil,
|
|
cty.NilVal,
|
|
`test.hcl:1,1-5: Error in function call; Call to function "try" failed: at least one argument is required.`,
|
|
},
|
|
}
|
|
|
|
for k, test := range tests {
|
|
t.Run(k, func(t *testing.T) {
|
|
expr, diags := hclsyntax.ParseExpression([]byte(test.expr), "test.hcl", hcl.Pos{Line: 1, Column: 1})
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected problems: %s", diags.Error())
|
|
}
|
|
|
|
ctx := &hcl.EvalContext{
|
|
Variables: test.vars,
|
|
Functions: map[string]function.Function{
|
|
"try": TryFunc,
|
|
},
|
|
}
|
|
|
|
got, err := expr.Value(ctx)
|
|
|
|
if err != nil {
|
|
if test.wantErr != "" {
|
|
if got, want := err.Error(), test.wantErr; got != want {
|
|
t.Errorf("wrong error\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
} else {
|
|
t.Errorf("unexpected error\ngot: %s\nwant: <nil>", err)
|
|
}
|
|
return
|
|
}
|
|
if test.wantErr != "" {
|
|
t.Errorf("wrong error\ngot: <nil>\nwant: %s", test.wantErr)
|
|
}
|
|
|
|
if !test.want.RawEquals(got) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCanFunc(t *testing.T) {
|
|
tests := map[string]struct {
|
|
expr string
|
|
vars map[string]cty.Value
|
|
want cty.Value
|
|
}{
|
|
"succeeds": {
|
|
`can(1)`,
|
|
nil,
|
|
cty.True,
|
|
},
|
|
"fails": {
|
|
`can(nope)`,
|
|
nil,
|
|
cty.False,
|
|
},
|
|
"simple unknown": {
|
|
`can(unknown)`,
|
|
map[string]cty.Value{
|
|
"unknown": cty.UnknownVal(cty.Number),
|
|
},
|
|
cty.UnknownVal(cty.Bool),
|
|
},
|
|
"traversal through unknown": {
|
|
`can(unknown.foo)`,
|
|
map[string]cty.Value{
|
|
"unknown": cty.UnknownVal(cty.Map(cty.Number)),
|
|
},
|
|
cty.UnknownVal(cty.Bool),
|
|
},
|
|
"deep unknown": {
|
|
`can(has_unknown)`,
|
|
map[string]cty.Value{
|
|
"has_unknown": cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
|
|
},
|
|
cty.UnknownVal(cty.Bool),
|
|
},
|
|
}
|
|
|
|
for k, test := range tests {
|
|
t.Run(k, func(t *testing.T) {
|
|
expr, diags := hclsyntax.ParseExpression([]byte(test.expr), "test.hcl", hcl.Pos{Line: 1, Column: 1})
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected problems: %s", diags.Error())
|
|
}
|
|
|
|
ctx := &hcl.EvalContext{
|
|
Variables: test.vars,
|
|
Functions: map[string]function.Function{
|
|
"can": CanFunc,
|
|
},
|
|
}
|
|
|
|
got, err := expr.Value(ctx)
|
|
if err != nil {
|
|
t.Errorf("unexpected error\ngot: %s\nwant: <nil>", err)
|
|
}
|
|
if !test.want.RawEquals(got) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|