hcl/zcl/zclsyntax/expression_test.go
Martin Atkins 6052cb9938 zclsyntax: parse expressions in newline-ignoring mode
This applies both two the whole of bare expressions and to any nested
expressions within parentheses. The latter means that an attribute value
can span over multiple lines if it's wrapped in parens:

    foo = (
        1 + 2 + 3 + 4 + 5
    )
2017-06-02 07:26:45 -07:00

330 lines
5.7 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,
},
{
`(
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,
},
}
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)
}
})
}
}