b2e6e2d0d0
If we can't find a function in the given EvalContext, we must traverse the context chain until either we run out of contexts or we find a matching function. At present there is no reason for a non-root context to have any functions, so this will always traverse to the root. This may change in future if we introduce constructs that define local functions.
839 lines
14 KiB
Go
839 lines
14 KiB
Go
package zclsyntax
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
"github.com/zclconf/go-cty/cty/function/stdlib"
|
|
"github.com/zclconf/go-zcl/zcl"
|
|
)
|
|
|
|
func TestExpressionParseAndValue(t *testing.T) {
|
|
// This is a combo test that exercises both the parser and the Value
|
|
// method, with the focus on the latter but indirectly testing the former.
|
|
tests := []struct {
|
|
input string
|
|
ctx *zcl.EvalContext
|
|
want cty.Value
|
|
diagCount int
|
|
}{
|
|
{
|
|
`1`,
|
|
nil,
|
|
cty.NumberIntVal(1),
|
|
0,
|
|
},
|
|
{
|
|
`(1)`,
|
|
nil,
|
|
cty.NumberIntVal(1),
|
|
0,
|
|
},
|
|
{
|
|
`(2+3)`,
|
|
nil,
|
|
cty.NumberIntVal(5),
|
|
0,
|
|
},
|
|
{
|
|
`(2+unk)`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"unk": cty.UnknownVal(cty.Number),
|
|
},
|
|
},
|
|
cty.UnknownVal(cty.Number),
|
|
0,
|
|
},
|
|
{
|
|
`(2+unk)`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"unk": cty.DynamicVal,
|
|
},
|
|
},
|
|
cty.UnknownVal(cty.Number),
|
|
0,
|
|
},
|
|
{
|
|
`(unk+unk)`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"unk": cty.DynamicVal,
|
|
},
|
|
},
|
|
cty.UnknownVal(cty.Number),
|
|
0,
|
|
},
|
|
{
|
|
`(2+true)`,
|
|
nil,
|
|
cty.UnknownVal(cty.Number),
|
|
1, // unsuitable type for right operand
|
|
},
|
|
{
|
|
`(false+true)`,
|
|
nil,
|
|
cty.UnknownVal(cty.Number),
|
|
2, // unsuitable type for each operand
|
|
},
|
|
{
|
|
`(5 == 5)`,
|
|
nil,
|
|
cty.True,
|
|
0,
|
|
},
|
|
{
|
|
`(5 == 4)`,
|
|
nil,
|
|
cty.False,
|
|
0,
|
|
},
|
|
{
|
|
`(1 == true)`,
|
|
nil,
|
|
cty.False,
|
|
0,
|
|
},
|
|
{
|
|
`("true" == true)`,
|
|
nil,
|
|
cty.False,
|
|
0,
|
|
},
|
|
{
|
|
`(true == "true")`,
|
|
nil,
|
|
cty.False,
|
|
0,
|
|
},
|
|
{
|
|
`(true != "true")`,
|
|
nil,
|
|
cty.True,
|
|
0,
|
|
},
|
|
{
|
|
`(- 2)`,
|
|
nil,
|
|
cty.NumberIntVal(-2),
|
|
0,
|
|
},
|
|
{
|
|
`(! true)`,
|
|
nil,
|
|
cty.False,
|
|
0,
|
|
},
|
|
{
|
|
`(
|
|
1
|
|
)`,
|
|
nil,
|
|
cty.NumberIntVal(1),
|
|
0,
|
|
},
|
|
{
|
|
`(1`,
|
|
nil,
|
|
cty.NumberIntVal(1),
|
|
1, // Unbalanced parentheses
|
|
},
|
|
{
|
|
`true`,
|
|
nil,
|
|
cty.True,
|
|
0,
|
|
},
|
|
{
|
|
`false`,
|
|
nil,
|
|
cty.False,
|
|
0,
|
|
},
|
|
{
|
|
`null`,
|
|
nil,
|
|
cty.NullVal(cty.DynamicPseudoType),
|
|
0,
|
|
},
|
|
{
|
|
`true true`,
|
|
nil,
|
|
cty.True,
|
|
1, // extra characters after expression
|
|
},
|
|
{
|
|
`"hello"`,
|
|
nil,
|
|
cty.StringVal("hello"),
|
|
0,
|
|
},
|
|
{
|
|
`"hello\nworld"`,
|
|
nil,
|
|
cty.StringVal("hello\nworld"),
|
|
0,
|
|
},
|
|
{
|
|
`"unclosed`,
|
|
nil,
|
|
cty.StringVal("unclosed"),
|
|
1, // Unterminated template string
|
|
},
|
|
{
|
|
`"hello ${"world"}"`,
|
|
nil,
|
|
cty.StringVal("hello world"),
|
|
0,
|
|
},
|
|
{
|
|
`"hello ${12.5}"`,
|
|
nil,
|
|
cty.StringVal("hello 12.5"),
|
|
0,
|
|
},
|
|
{
|
|
`"silly ${"${"nesting"}"}"`,
|
|
nil,
|
|
cty.StringVal("silly nesting"),
|
|
0,
|
|
},
|
|
{
|
|
`"silly ${"${true}"}"`,
|
|
nil,
|
|
cty.StringVal("silly true"),
|
|
0,
|
|
},
|
|
{
|
|
`"hello $${escaped}"`,
|
|
nil,
|
|
cty.StringVal("hello ${escaped}"),
|
|
0,
|
|
},
|
|
{
|
|
`"hello $$nonescape"`,
|
|
nil,
|
|
cty.StringVal("hello $$nonescape"),
|
|
0,
|
|
},
|
|
{
|
|
`upper("foo")`,
|
|
&zcl.EvalContext{
|
|
Functions: map[string]function.Function{
|
|
"upper": stdlib.UpperFunc,
|
|
},
|
|
},
|
|
cty.StringVal("FOO"),
|
|
0,
|
|
},
|
|
{
|
|
`
|
|
upper(
|
|
"foo"
|
|
)
|
|
`,
|
|
&zcl.EvalContext{
|
|
Functions: map[string]function.Function{
|
|
"upper": stdlib.UpperFunc,
|
|
},
|
|
},
|
|
cty.StringVal("FOO"),
|
|
0,
|
|
},
|
|
{
|
|
`upper(["foo"]...)`,
|
|
&zcl.EvalContext{
|
|
Functions: map[string]function.Function{
|
|
"upper": stdlib.UpperFunc,
|
|
},
|
|
},
|
|
cty.StringVal("FOO"),
|
|
0,
|
|
},
|
|
{
|
|
`upper("foo", []...)`,
|
|
&zcl.EvalContext{
|
|
Functions: map[string]function.Function{
|
|
"upper": stdlib.UpperFunc,
|
|
},
|
|
},
|
|
cty.StringVal("FOO"),
|
|
0,
|
|
},
|
|
{
|
|
`upper("foo", "bar")`,
|
|
&zcl.EvalContext{
|
|
Functions: map[string]function.Function{
|
|
"upper": stdlib.UpperFunc,
|
|
},
|
|
},
|
|
cty.DynamicVal,
|
|
1, // too many function arguments
|
|
},
|
|
{
|
|
`upper(["foo", "bar"]...)`,
|
|
&zcl.EvalContext{
|
|
Functions: map[string]function.Function{
|
|
"upper": stdlib.UpperFunc,
|
|
},
|
|
},
|
|
cty.DynamicVal,
|
|
1, // too many function arguments
|
|
},
|
|
{
|
|
`[]`,
|
|
nil,
|
|
cty.EmptyTupleVal,
|
|
0,
|
|
},
|
|
{
|
|
`[1]`,
|
|
nil,
|
|
cty.TupleVal([]cty.Value{cty.NumberIntVal(1)}),
|
|
0,
|
|
},
|
|
{
|
|
`[1,]`,
|
|
nil,
|
|
cty.TupleVal([]cty.Value{cty.NumberIntVal(1)}),
|
|
0,
|
|
},
|
|
{
|
|
`[1,true]`,
|
|
nil,
|
|
cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.True}),
|
|
0,
|
|
},
|
|
{
|
|
`[
|
|
1,
|
|
true
|
|
]`,
|
|
nil,
|
|
cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.True}),
|
|
0,
|
|
},
|
|
{
|
|
`{}`,
|
|
nil,
|
|
cty.EmptyObjectVal,
|
|
0,
|
|
},
|
|
{
|
|
`{"hello": "world"}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{"hello" = "world"}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{hello = "world"}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{hello: "world"}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{"hello" = "world", "goodbye" = "cruel world"}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("world"),
|
|
"goodbye": cty.StringVal("cruel world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{
|
|
"hello" = "world"
|
|
}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{
|
|
"hello" = "world"
|
|
"goodbye" = "cruel world"
|
|
}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("world"),
|
|
"goodbye": cty.StringVal("cruel world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{
|
|
"hello" = "world",
|
|
"goodbye" = "cruel world"
|
|
}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("world"),
|
|
"goodbye": cty.StringVal("cruel world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{
|
|
"hello" = "world",
|
|
"goodbye" = "cruel world",
|
|
}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("world"),
|
|
"goodbye": cty.StringVal("cruel world"),
|
|
}),
|
|
0,
|
|
},
|
|
|
|
{
|
|
`{for k, v in {hello: "world"}: k => v if k == "hello"}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{for k, v in {hello: "world"}: upper(k) => upper(v) if k == "hello"}`,
|
|
&zcl.EvalContext{
|
|
Functions: map[string]function.Function{
|
|
"upper": stdlib.UpperFunc,
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"HELLO": cty.StringVal("WORLD"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{for k, v in ["world"]: k => v if k == 0}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"0": cty.StringVal("world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{for v in ["world"]: v => v}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"world": cty.StringVal("world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{for k, v in {hello: "world"}: k => v if k == "foo"}`,
|
|
nil,
|
|
cty.EmptyObjectVal,
|
|
0,
|
|
},
|
|
{
|
|
`{for k, v in {hello: "world"}: 5 => v}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"5": cty.StringVal("world"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{for k, v in {hello: "world"}: [] => v}`,
|
|
nil,
|
|
cty.DynamicVal,
|
|
1, // key expression has the wrong type
|
|
},
|
|
{
|
|
`{for k, v in {hello: "world"}: k => k if k == "hello"}`,
|
|
nil,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("hello"),
|
|
}),
|
|
0,
|
|
},
|
|
{
|
|
`{for k, v in {hello: "world"}: k => foo}`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"foo": cty.StringVal("foo"),
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.StringVal("foo"),
|
|
}),
|
|
0,
|
|
},
|
|
|
|
{
|
|
`["hello"][0]`,
|
|
nil,
|
|
cty.StringVal("hello"),
|
|
0,
|
|
},
|
|
{
|
|
`[][0]`,
|
|
nil,
|
|
cty.DynamicVal,
|
|
1, // invalid index
|
|
},
|
|
{
|
|
`["hello"][negate(0)]`,
|
|
&zcl.EvalContext{
|
|
Functions: map[string]function.Function{
|
|
"negate": stdlib.NegateFunc,
|
|
},
|
|
},
|
|
cty.StringVal("hello"),
|
|
0,
|
|
},
|
|
{
|
|
`[][negate(0)]`,
|
|
&zcl.EvalContext{
|
|
Functions: map[string]function.Function{
|
|
"negate": stdlib.NegateFunc,
|
|
},
|
|
},
|
|
cty.DynamicVal,
|
|
1, // invalid index
|
|
},
|
|
{
|
|
`["hello"]["0"]`, // key gets converted to number
|
|
nil,
|
|
cty.StringVal("hello"),
|
|
0,
|
|
},
|
|
|
|
{
|
|
`foo`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"foo": cty.StringVal("hello"),
|
|
},
|
|
},
|
|
cty.StringVal("hello"),
|
|
0,
|
|
},
|
|
{
|
|
`bar`,
|
|
&zcl.EvalContext{},
|
|
cty.DynamicVal,
|
|
1, // variables not allowed here
|
|
},
|
|
{
|
|
`foo.bar`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"foo": cty.StringVal("hello"),
|
|
},
|
|
},
|
|
cty.DynamicVal,
|
|
1, // foo does not have attributes
|
|
},
|
|
{
|
|
`foo.baz`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"foo": cty.ObjectVal(map[string]cty.Value{
|
|
"baz": cty.StringVal("hello"),
|
|
}),
|
|
},
|
|
},
|
|
cty.StringVal("hello"),
|
|
0,
|
|
},
|
|
{
|
|
`foo["baz"]`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"foo": cty.ObjectVal(map[string]cty.Value{
|
|
"baz": cty.StringVal("hello"),
|
|
}),
|
|
},
|
|
},
|
|
cty.StringVal("hello"),
|
|
0,
|
|
},
|
|
{
|
|
`foo[true]`, // key is converted to string
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"foo": cty.ObjectVal(map[string]cty.Value{
|
|
"true": cty.StringVal("hello"),
|
|
}),
|
|
},
|
|
},
|
|
cty.StringVal("hello"),
|
|
0,
|
|
},
|
|
{
|
|
`foo[0].baz`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"foo": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"baz": cty.StringVal("hello"),
|
|
}),
|
|
}),
|
|
},
|
|
},
|
|
cty.StringVal("hello"),
|
|
0,
|
|
},
|
|
{
|
|
`unk["baz"]`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"unk": cty.UnknownVal(cty.String),
|
|
},
|
|
},
|
|
cty.DynamicVal,
|
|
1, // value does not have indices (because we know it's a string)
|
|
},
|
|
{
|
|
`unk["boop"]`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"unk": cty.UnknownVal(cty.Map(cty.String)),
|
|
},
|
|
},
|
|
cty.UnknownVal(cty.String), // we know it's a map of string
|
|
0,
|
|
},
|
|
{
|
|
`dyn["boop"]`,
|
|
&zcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"dyn": cty.DynamicVal,
|
|
},
|
|
},
|
|
cty.DynamicVal, // don't know what it is yet
|
|
0,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.input, func(t *testing.T) {
|
|
expr, parseDiags := ParseExpression([]byte(test.input), "", zcl.Pos{Line: 1, Column: 1, Byte: 0})
|
|
|
|
got, valDiags := expr.Value(test.ctx)
|
|
|
|
diagCount := len(parseDiags) + len(valDiags)
|
|
|
|
if diagCount != test.diagCount {
|
|
t.Errorf("wrong number of diagnostics %d; want %d", diagCount, test.diagCount)
|
|
for _, diag := range parseDiags {
|
|
t.Logf(" - %s", diag.Error())
|
|
}
|
|
for _, diag := range valDiags {
|
|
t.Logf(" - %s", diag.Error())
|
|
}
|
|
}
|
|
|
|
if !got.RawEquals(test.want) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestFunctionCallExprValue(t *testing.T) {
|
|
funcs := map[string]function.Function{
|
|
"length": stdlib.StrlenFunc,
|
|
"jsondecode": stdlib.JSONDecodeFunc,
|
|
}
|
|
|
|
tests := map[string]struct {
|
|
expr *FunctionCallExpr
|
|
ctx *zcl.EvalContext
|
|
want cty.Value
|
|
diagCount int
|
|
}{
|
|
"valid call with no conversions": {
|
|
&FunctionCallExpr{
|
|
Name: "length",
|
|
Args: []Expression{
|
|
&LiteralValueExpr{
|
|
Val: cty.StringVal("hello"),
|
|
},
|
|
},
|
|
},
|
|
&zcl.EvalContext{
|
|
Functions: funcs,
|
|
},
|
|
cty.NumberIntVal(5),
|
|
0,
|
|
},
|
|
"valid call with arg conversion": {
|
|
&FunctionCallExpr{
|
|
Name: "length",
|
|
Args: []Expression{
|
|
&LiteralValueExpr{
|
|
Val: cty.BoolVal(true),
|
|
},
|
|
},
|
|
},
|
|
&zcl.EvalContext{
|
|
Functions: funcs,
|
|
},
|
|
cty.NumberIntVal(4), // length of string "true"
|
|
0,
|
|
},
|
|
"valid call with unknown arg": {
|
|
&FunctionCallExpr{
|
|
Name: "length",
|
|
Args: []Expression{
|
|
&LiteralValueExpr{
|
|
Val: cty.UnknownVal(cty.String),
|
|
},
|
|
},
|
|
},
|
|
&zcl.EvalContext{
|
|
Functions: funcs,
|
|
},
|
|
cty.UnknownVal(cty.Number),
|
|
0,
|
|
},
|
|
"valid call with unknown arg needing conversion": {
|
|
&FunctionCallExpr{
|
|
Name: "length",
|
|
Args: []Expression{
|
|
&LiteralValueExpr{
|
|
Val: cty.UnknownVal(cty.Bool),
|
|
},
|
|
},
|
|
},
|
|
&zcl.EvalContext{
|
|
Functions: funcs,
|
|
},
|
|
cty.UnknownVal(cty.Number),
|
|
0,
|
|
},
|
|
"valid call with dynamic arg": {
|
|
&FunctionCallExpr{
|
|
Name: "length",
|
|
Args: []Expression{
|
|
&LiteralValueExpr{
|
|
Val: cty.DynamicVal,
|
|
},
|
|
},
|
|
},
|
|
&zcl.EvalContext{
|
|
Functions: funcs,
|
|
},
|
|
cty.UnknownVal(cty.Number),
|
|
0,
|
|
},
|
|
"invalid arg type": {
|
|
&FunctionCallExpr{
|
|
Name: "length",
|
|
Args: []Expression{
|
|
&LiteralValueExpr{
|
|
Val: cty.ListVal([]cty.Value{cty.StringVal("hello")}),
|
|
},
|
|
},
|
|
},
|
|
&zcl.EvalContext{
|
|
Functions: funcs,
|
|
},
|
|
cty.DynamicVal,
|
|
1,
|
|
},
|
|
"function with dynamic return type": {
|
|
&FunctionCallExpr{
|
|
Name: "jsondecode",
|
|
Args: []Expression{
|
|
&LiteralValueExpr{
|
|
Val: cty.StringVal(`"hello"`),
|
|
},
|
|
},
|
|
},
|
|
&zcl.EvalContext{
|
|
Functions: funcs,
|
|
},
|
|
cty.StringVal("hello"),
|
|
0,
|
|
},
|
|
"function with dynamic return type unknown arg": {
|
|
&FunctionCallExpr{
|
|
Name: "jsondecode",
|
|
Args: []Expression{
|
|
&LiteralValueExpr{
|
|
Val: cty.UnknownVal(cty.String),
|
|
},
|
|
},
|
|
},
|
|
&zcl.EvalContext{
|
|
Functions: funcs,
|
|
},
|
|
cty.DynamicVal, // type depends on arg value
|
|
0,
|
|
},
|
|
"error in function": {
|
|
&FunctionCallExpr{
|
|
Name: "jsondecode",
|
|
Args: []Expression{
|
|
&LiteralValueExpr{
|
|
Val: cty.StringVal("invalid-json"),
|
|
},
|
|
},
|
|
},
|
|
&zcl.EvalContext{
|
|
Functions: funcs,
|
|
},
|
|
cty.DynamicVal,
|
|
1, // JSON parse error
|
|
},
|
|
"unknown function": {
|
|
&FunctionCallExpr{
|
|
Name: "lenth",
|
|
Args: []Expression{},
|
|
},
|
|
&zcl.EvalContext{
|
|
Functions: funcs,
|
|
},
|
|
cty.DynamicVal,
|
|
1,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
got, diags := test.expr.Value(test.ctx)
|
|
|
|
if len(diags) != test.diagCount {
|
|
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.diagCount)
|
|
for _, diag := range diags {
|
|
t.Logf(" - %s", diag.Error())
|
|
}
|
|
}
|
|
|
|
if !got.RawEquals(test.want) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|