pulumi/pkg/codegen/hcl2/model/binder_expression_test.go

780 lines
24 KiB
Go

// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"fmt"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
"github.com/stretchr/testify/assert"
"github.com/zclconf/go-cty/cty"
)
func assertConvertibleFrom(t *testing.T, to, from Type) {
assert.NotEqual(t, NoConversion, to.ConversionFrom(from))
}
func TestBindLiteral(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText("false", nil, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, BoolType, expr.Type())
lit, ok := expr.(*LiteralValueExpression)
assert.True(t, ok)
assert.Equal(t, cty.False, lit.Value)
assert.Equal(t, "false", fmt.Sprintf("%v", expr))
expr, diags = BindExpressionText("true", nil, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, BoolType, expr.Type())
lit, ok = expr.(*LiteralValueExpression)
assert.True(t, ok)
assert.Equal(t, cty.True, lit.Value)
assert.Equal(t, "true", fmt.Sprintf("%v", expr))
expr, diags = BindExpressionText("0", nil, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, NumberType, expr.Type())
lit, ok = expr.(*LiteralValueExpression)
assert.True(t, ok)
assert.True(t, cty.NumberIntVal(0).RawEquals(lit.Value))
assert.Equal(t, "0", fmt.Sprintf("%v", expr))
expr, diags = BindExpressionText("3.14", nil, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, NumberType, expr.Type())
lit, ok = expr.(*LiteralValueExpression)
assert.True(t, ok)
assert.True(t, cty.MustParseNumberVal("3.14").RawEquals(lit.Value))
assert.Equal(t, "3.14", fmt.Sprintf("%v", expr))
expr, diags = BindExpressionText(`"foo"`, nil, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, StringType, expr.Type())
template, ok := expr.(*TemplateExpression)
assert.True(t, ok)
assert.Len(t, template.Parts, 1)
lit, ok = template.Parts[0].(*LiteralValueExpression)
assert.True(t, ok)
assert.Equal(t, cty.StringVal("foo"), lit.Value)
assert.Equal(t, "\"foo\"", fmt.Sprintf("%v", expr))
}
type environment map[string]interface{}
func (e environment) scope() *Scope {
s := NewRootScope(syntax.None)
for name, typeOrFunction := range e {
switch typeOrFunction := typeOrFunction.(type) {
case *Function:
s.DefineFunction(name, typeOrFunction)
case Type:
s.Define(name, &Variable{Name: name, VariableType: typeOrFunction})
}
}
return s
}
type exprTestCase struct {
x string
t Type
xt Expression
}
func TestBindBinaryOp(t *testing.T) {
t.Parallel()
env := environment(map[string]interface{}{
"a": NewOutputType(BoolType),
"b": NewPromiseType(BoolType),
"c": NewOutputType(NumberType),
"d": NewPromiseType(NumberType),
})
scope := env.scope()
cases := []exprTestCase{
// Comparisons
{x: "0 == 0", t: BoolType},
{x: "0 != 0", t: BoolType},
{x: "0 < 0", t: BoolType},
{x: "0 > 0", t: BoolType},
{x: "0 <= 0", t: BoolType},
{x: "0 >= 0", t: BoolType},
// Arithmetic
{x: "0 + 0", t: NumberType},
{x: "0 - 0", t: NumberType},
{x: "0 * 0", t: NumberType},
{x: "0 / 0", t: NumberType},
{x: "0 % 0", t: NumberType},
// Logical
{x: "false && false", t: BoolType},
{x: "false || false", t: BoolType},
// Lifted operations
{x: "a == true", t: NewOutputType(BoolType)},
{x: "b == true", t: NewPromiseType(BoolType)},
{x: "c + 0", t: NewOutputType(NumberType)},
{x: "d + 0", t: NewPromiseType(NumberType)},
{x: "a && true", t: NewOutputType(BoolType)},
{x: "b && true", t: NewPromiseType(BoolType)},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
_, ok := expr.(*BinaryOpExpression)
assert.True(t, ok)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
})
}
}
func TestBindConditional(t *testing.T) {
t.Parallel()
env := environment(map[string]interface{}{
"a": NewOutputType(BoolType),
"b": NewPromiseType(BoolType),
})
scope := env.scope()
cases := []exprTestCase{
{x: "true ? 0 : 1", t: NumberType},
{x: "true ? 0 : false", t: NewUnionType(NumberType, BoolType)},
{x: "true ? a : b", t: NewOutputType(BoolType)},
// Lifted operations
{x: "a ? 0 : 1", t: NewOutputType(NumberType)},
{x: "b ? 0 : 1", t: NewPromiseType(NumberType)},
{x: "a ? 0 : false", t: NewOutputType(NewUnionType(NumberType, BoolType))},
{x: "b ? 0 : false", t: NewPromiseType(NewUnionType(NumberType, BoolType))},
{x: "a ? a : b", t: NewOutputType(BoolType)},
{x: "b ? b : b", t: NewPromiseType(BoolType)},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
_, ok := expr.(*ConditionalExpression)
assert.True(t, ok)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
})
}
}
func TestBindFor(t *testing.T) {
t.Parallel()
// TODO: union collection types
env := environment(map[string]interface{}{
"a": NewMapType(StringType),
"aa": NewMapType(NewOutputType(StringType)),
"b": NewOutputType(NewMapType(StringType)),
"c": NewPromiseType(NewMapType(StringType)),
"d": NewListType(StringType),
"dd": NewListType(NewOutputType(StringType)),
"e": NewOutputType(NewListType(StringType)),
"f": NewPromiseType(NewListType(StringType)),
"g": BoolType,
"h": NewOutputType(BoolType),
"i": NewPromiseType(BoolType),
"j": StringType,
"k": NewOutputType(StringType),
"l": NewPromiseType(StringType),
})
scope := env.scope()
cases := []exprTestCase{
// Object for
{x: `{for k, v in {}: k => v}`, t: NewMapType(NoneType)},
{x: `{for k, v in {foo = "bar"}: k => v}`, t: NewMapType(StringType)},
{x: `{for k, v in {foo = "bar"}: k => 0}`, t: NewMapType(NumberType)},
{x: `{for k, v in {foo = 0}: k => v}`, t: NewMapType(NumberType)},
{x: `{for k, v in a: k => v}`, t: NewMapType(StringType)},
{x: `{for k, v in aa: k => v}`, t: NewMapType(NewOutputType(StringType))},
{x: `{for k, v in a: k => 0}`, t: NewMapType(NumberType)},
{x: `{for k, v in d: v => k}`, t: NewMapType(NumberType)},
{x: `{for k, v in d: v => k...}`, t: NewMapType(NewListType(NumberType))},
{x: `{for k, v in d: v => k if k > 10}`, t: NewMapType(NumberType)},
// List for
{x: `[for k, v in {}: [k, v]]`, t: NewListType(NewTupleType(StringType, NoneType))},
{x: `[for k, _ in {}: k]`, t: NewListType(StringType)},
{x: `[for v in []: v]`, t: NewListType(NoneType)},
// Lifted operations
{x: `{for k, v in b: k => v}`, t: NewOutputType(NewMapType(StringType))},
{x: `{for k, v in c: k => v}`, t: NewPromiseType(NewMapType(StringType))},
{x: `{for k, v in {}: k => v if h}`, t: NewOutputType(NewMapType(NoneType))},
{x: `{for k, v in {}: k => v if i}`, t: NewPromiseType(NewMapType(NoneType))},
{x: `[for v in e: v]`, t: NewOutputType(NewListType(StringType))},
{x: `[for v in f: v]`, t: NewPromiseType(NewListType(StringType))},
{x: `[for v in []: v if h]`, t: NewOutputType(NewListType(NoneType))},
{x: `[for v in []: v if i]`, t: NewPromiseType(NewListType(NoneType))},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
_, ok := expr.(*ForExpression)
assert.True(t, ok)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
})
}
}
func TestBindFunctionCall(t *testing.T) {
t.Parallel()
env := environment(map[string]interface{}{
"f0": NewFunction(StaticFunctionSignature{
Parameters: []Parameter{
{Name: "foo", Type: StringType},
{Name: "bar", Type: IntType},
},
ReturnType: BoolType,
}),
"f1": NewFunction(StaticFunctionSignature{
Parameters: []Parameter{
{Name: "foo", Type: StringType},
},
VarargsParameter: &Parameter{
Name: "bar", Type: IntType,
},
ReturnType: BoolType,
}),
"a": NewOutputType(StringType),
"b": NewPromiseType(StringType),
"c": NewOutputType(IntType),
"d": NewPromiseType(IntType),
})
scope := env.scope()
cases := []exprTestCase{
// Standard calls
{x: `f0("foo", 0)`, t: BoolType},
{x: `f1("foo")`, t: BoolType},
{x: `f1("foo", 1, 2, 3)`, t: BoolType},
// Lifted calls
{x: `f0(a, 0)`, t: NewOutputType(BoolType)},
{x: `f0(b, 0)`, t: NewPromiseType(BoolType)},
{x: `f0("foo", c)`, t: NewOutputType(BoolType)},
{x: `f0("foo", d)`, t: NewPromiseType(BoolType)},
{x: `f0(a, d)`, t: NewOutputType(BoolType)},
{x: `f0(b, c)`, t: NewOutputType(BoolType)},
{x: `f1(a)`, t: NewOutputType(BoolType)},
{x: `f1(b)`, t: NewPromiseType(BoolType)},
{x: `f1("foo", c)`, t: NewOutputType(BoolType)},
{x: `f1("foo", d)`, t: NewPromiseType(BoolType)},
{x: `f1("foo", c, d)`, t: NewOutputType(BoolType)},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
_, ok := expr.(*FunctionCallExpression)
assert.True(t, ok)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
})
}
}
func TestBindIndex(t *testing.T) {
t.Parallel()
env := environment(map[string]interface{}{
"a": StringType,
"b": IntType,
"c": NewOutputType(StringType),
"d": NewOutputType(IntType),
"e": NewPromiseType(StringType),
"f": NewPromiseType(IntType),
"g": NewListType(BoolType),
"h": NewMapType(BoolType),
"i": NewObjectType(map[string]Type{"foo": BoolType}),
"j": NewOutputType(NewListType(BoolType)),
"k": NewOutputType(NewMapType(BoolType)),
"l": NewOutputType(NewObjectType(map[string]Type{"foo": BoolType})),
"m": NewPromiseType(NewListType(BoolType)),
"n": NewPromiseType(NewMapType(BoolType)),
"o": NewPromiseType(NewObjectType(map[string]Type{"foo": BoolType})),
})
scope := env.scope()
cases := []exprTestCase{
// Standard operations
{x: "g[a]", t: BoolType},
{x: "g[b]", t: BoolType},
{x: "h[a]", t: BoolType},
{x: "h[b]", t: BoolType},
{x: "i[a]", t: BoolType},
{x: "i[b]", t: BoolType},
// Lifted operations
{x: "g[c]", t: NewOutputType(BoolType)},
{x: "g[d]", t: NewOutputType(BoolType)},
{x: "h[c]", t: NewOutputType(BoolType)},
{x: "h[d]", t: NewOutputType(BoolType)},
{x: "i[c]", t: NewOutputType(BoolType)},
{x: "i[d]", t: NewOutputType(BoolType)},
{x: "g[e]", t: NewPromiseType(BoolType)},
{x: "g[f]", t: NewPromiseType(BoolType)},
{x: "h[e]", t: NewPromiseType(BoolType)},
{x: "h[f]", t: NewPromiseType(BoolType)},
{x: "i[e]", t: NewPromiseType(BoolType)},
{x: "i[f]", t: NewPromiseType(BoolType)},
{x: "j[a]", t: NewOutputType(BoolType)},
{x: "j[b]", t: NewOutputType(BoolType)},
{x: "k[a]", t: NewOutputType(BoolType)},
{x: "k[b]", t: NewOutputType(BoolType)},
{x: "l[a]", t: NewOutputType(BoolType)},
{x: "l[b]", t: NewOutputType(BoolType)},
{x: "m[a]", t: NewPromiseType(BoolType)},
{x: "m[b]", t: NewPromiseType(BoolType)},
{x: "n[a]", t: NewPromiseType(BoolType)},
{x: "n[b]", t: NewPromiseType(BoolType)},
{x: "o[a]", t: NewPromiseType(BoolType)},
{x: "o[b]", t: NewPromiseType(BoolType)},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
_, ok := expr.(*IndexExpression)
assert.True(t, ok)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
})
}
}
func TestBindObjectCons(t *testing.T) {
t.Parallel()
env := environment(map[string]interface{}{
"a": StringType,
"b": NumberType,
"c": BoolType,
"d": NewOutputType(StringType),
"e": NewOutputType(NumberType),
"f": NewOutputType(BoolType),
"g": NewPromiseType(StringType),
"h": NewPromiseType(NumberType),
"i": NewPromiseType(BoolType),
})
scope := env.scope()
ot := NewObjectType(map[string]Type{"foo": StringType, "0": NumberType, "false": BoolType})
mt := NewMapType(StringType)
cases := []exprTestCase{
// Standard operations
{x: `{"foo": "oof", 0: 42, false: true}`, t: ot},
{x: `{(a): a, (b): b, (c): c}`, t: mt},
// Lifted operations
{x: `{(d): a, (b): b, (c): c}`, t: NewOutputType(mt)},
{x: `{(a): a, (e): b, (c): c}`, t: NewOutputType(mt)},
{x: `{(a): a, (b): b, (f): c}`, t: NewOutputType(mt)},
{x: `{(g): a, (b): b, (c): c}`, t: NewPromiseType(mt)},
{x: `{(a): a, (h): b, (c): c}`, t: NewPromiseType(mt)},
{x: `{(a): a, (b): b, (i): c}`, t: NewPromiseType(mt)},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
_, ok := expr.(*ObjectConsExpression)
assert.True(t, ok)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
})
}
}
func TestBindRelativeTraversal(t *testing.T) {
t.Parallel()
env := environment(map[string]interface{}{
"a": NewMapType(StringType),
"aa": NewMapType(NewOutputType(StringType)),
"b": NewOutputType(NewMapType(StringType)),
"c": NewPromiseType(NewMapType(StringType)),
"d": NewListType(StringType),
"dd": NewListType(NewOutputType(StringType)),
"e": NewOutputType(NewListType(StringType)),
"f": NewPromiseType(NewListType(StringType)),
"g": BoolType,
"h": NewOutputType(BoolType),
"i": NewPromiseType(BoolType),
"j": StringType,
"k": NewOutputType(StringType),
"l": NewPromiseType(StringType),
})
scope := env.scope()
cases := []exprTestCase{
// Object for
{x: `{for k, v in {foo: "bar"}: k => v}.foo`, t: StringType},
{x: `{for k, v in {foo: "bar"}: k => 0}.foo`, t: NumberType},
{x: `{for k, v in {foo: 0}: k => v}.foo`, t: NumberType},
{x: `{for k, v in a: k => v}.foo`, t: StringType},
{x: `{for k, v in aa: k => v}.foo`, t: NewOutputType(StringType)},
{x: `{for k, v in a: k => 0}.foo`, t: NumberType},
{x: `{for k, v in d: v => k}.foo`, t: NumberType},
{x: `{for k, v in d: v => k...}.foo[0]`, t: NumberType},
{x: `{for k, v in d: v => k if k > 10}.foo`, t: NumberType},
// List for
{x: `[for k, v in {}: [k, v]].0`, t: NewTupleType(StringType, NoneType)},
{x: `[for k, _ in {}: k].0`, t: StringType},
// Lifted operations
{x: `{for k, v in b: k => v}.foo`, t: NewOutputType(StringType)},
{x: `{for k, v in c: k => v}.foo`, t: NewPromiseType(StringType)},
{x: `[for v in e: v].foo`, t: NewOutputType(StringType)},
{x: `[for v in f: v].foo`, t: NewPromiseType(StringType)},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
_, ok := expr.(*RelativeTraversalExpression)
assert.True(t, ok)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
})
}
}
func TestBindScopeTraversal(t *testing.T) {
t.Parallel()
ot := NewObjectType(map[string]Type{
"foo": NewListType(StringType),
"bar": NewObjectType(map[string]Type{
"baz": StringType,
}),
})
env := environment(map[string]interface{}{
"a": StringType,
"b": IntType,
"c": NewListType(BoolType),
"d": NewMapType(BoolType),
"e": ot,
"f": NewOutputType(StringType),
"g": NewOutputType(IntType),
"h": NewOutputType(NewListType(BoolType)),
"i": NewOutputType(NewMapType(BoolType)),
"j": NewOutputType(ot),
"k": NewPromiseType(StringType),
"l": NewPromiseType(IntType),
"m": NewPromiseType(NewListType(BoolType)),
"n": NewPromiseType(NewMapType(BoolType)),
"o": NewPromiseType(ot),
})
scope := env.scope()
cases := []exprTestCase{
// Standard traversals
{x: `a`, t: StringType},
{x: `b`, t: IntType},
{x: `c`, t: NewListType(BoolType)},
{x: `d`, t: NewMapType(BoolType)},
{x: `e`, t: ot},
{x: `f`, t: NewOutputType(StringType)},
{x: `g`, t: NewOutputType(IntType)},
{x: `k`, t: NewPromiseType(StringType)},
{x: `l`, t: NewPromiseType(IntType)},
{x: `c.0`, t: BoolType},
{x: `d.foo`, t: BoolType},
{x: `e.foo`, t: NewListType(StringType)},
{x: `e.foo.0`, t: StringType},
{x: `e.bar`, t: ot.Properties["bar"]},
{x: `e.bar.baz`, t: StringType},
// Lifted traversals
{x: `h.0`, t: NewOutputType(BoolType)},
{x: `i.foo`, t: NewOutputType(BoolType)},
{x: `j.foo`, t: NewOutputType(NewListType(StringType))},
{x: `j.foo.0`, t: NewOutputType(StringType)},
{x: `j.bar`, t: NewOutputType(ot.Properties["bar"])},
{x: `j.bar.baz`, t: NewOutputType(StringType)},
{x: `m.0`, t: NewPromiseType(BoolType)},
{x: `n.foo`, t: NewPromiseType(BoolType)},
{x: `o.foo`, t: NewPromiseType(NewListType(StringType))},
{x: `o.foo.0`, t: NewPromiseType(StringType)},
{x: `o.bar`, t: NewPromiseType(ot.Properties["bar"])},
{x: `o.bar.baz`, t: NewPromiseType(StringType)},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
_, ok := expr.(*ScopeTraversalExpression)
assert.True(t, ok)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
})
}
}
func TestBindSplat(t *testing.T) {
t.Parallel()
ot := NewObjectType(map[string]Type{
"foo": NewListType(StringType),
"bar": NewObjectType(map[string]Type{
"baz": StringType,
}),
})
env := environment(map[string]interface{}{
"a": NewListType(NewListType(StringType)),
"b": NewListType(ot),
"c": NewSetType(NewListType(StringType)),
"d": NewSetType(ot),
// "e": NewTupleType(NewListType(StringType)),
// "f": NewTupleType(ot),
"g": NewListType(NewListType(NewOutputType(StringType))),
"h": NewListType(NewListType(NewPromiseType(StringType))),
// "i": NewSetType(NewListType(NewOutputType(StringType))),
// "j": NewSetType(NewListType(NewPromiseType(StringType))),
// "k": NewTupleType(NewListType(NewOutputType(StringType))),
// "l": NewTupleType(NewListType(NewPromiseType(StringType))),
"m": NewOutputType(NewListType(ot)),
"n": NewPromiseType(NewListType(ot)),
// "o": NewOutputType(NewSetType(ot)),
// "p": NewPromiseType(NewSetType(ot)),
// "q": NewOutputType(NewTupleType(ot)),
// "r": NewPromiseType(NewTupleType(ot)),
})
scope := env.scope()
cases := []exprTestCase{
// Standard operations
{x: `a[*][0]`, t: NewListType(StringType)},
{x: `b[*].bar.baz`, t: NewListType(StringType)},
{x: `b.*.bar.baz`, t: NewListType(StringType)},
// {x: `c[*][0]`, t: NewSetType(StringType)},
// {x: `d[*].bar.baz`, t: NewSetType(StringType)},
// {x: `e[*][0]`, t: NewTupleType(StringType)},
// {x: `f[*].bar.baz`, t: NewTupleType(StringType)},
{x: `g[*][0]`, t: NewListType(NewOutputType(StringType))},
{x: `h[*][0]`, t: NewListType(NewPromiseType(StringType))},
// {x: `i[*][0]`, t: NewListType(NewOutputType(StringType))},
// {x: `j[*][0]`, t: NewListType(NewPromiseType(StringType))},
// {x: `k[*][0]`, t: NewTupleType(NewOutputType(StringType))},
// {x: `l[*][0]`, t: NewTupleType(NewPromiseType(StringType))},
// Lifted operations
{x: `m[*].bar.baz`, t: NewOutputType(NewListType(StringType))},
{x: `n[*].bar.baz`, t: NewPromiseType(NewListType(StringType))},
{x: `m.*.bar.baz`, t: NewOutputType(NewListType(StringType))},
{x: `n.*.bar.baz`, t: NewPromiseType(NewListType(StringType))},
// {x: `o[*].bar.baz`, t: NewOutputType(NewListType(StringType))},
// {x: `p[*].bar.baz`, t: NewPromiseType(NewListType(StringType))},
// {x: `q[*].bar.baz`, t: NewOutputType(NewTupleType(StringType))},
// {x: `r[*].bar.baz`, t: NewPromiseType(NewTupleType(StringType))},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
_, ok := expr.(*SplatExpression)
assert.True(t, ok)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
})
}
}
func TestBindTemplate(t *testing.T) {
t.Parallel()
env := environment(map[string]interface{}{
"a": StringType,
"b": NumberType,
"c": BoolType,
"d": NewListType(StringType),
"e": NewOutputType(StringType),
"f": NewOutputType(NumberType),
"g": NewOutputType(BoolType),
"h": NewOutputType(NewListType(StringType)),
"i": NewPromiseType(StringType),
"j": NewPromiseType(NumberType),
"k": NewPromiseType(BoolType),
"l": NewPromiseType(NewListType(StringType)),
})
scope := env.scope()
cases := []exprTestCase{
// Unwrapped interpolations
{x: `"${0}"`, t: NumberType, xt: &LiteralValueExpression{}},
{x: `"${true}"`, t: BoolType, xt: &LiteralValueExpression{}},
{x: `"${d}"`, t: NewListType(StringType), xt: &ScopeTraversalExpression{}},
{x: `"${e}"`, t: NewOutputType(StringType), xt: &ScopeTraversalExpression{}},
{x: `"${i}"`, t: NewPromiseType(StringType), xt: &ScopeTraversalExpression{}},
// Simple interpolations
{x: `"v: ${a}"`, t: StringType},
{x: `"v: ${b}"`, t: StringType},
{x: `"v: ${c}"`, t: StringType},
{x: `"v: ${d}"`, t: StringType},
// Template control expressions
{x: `"%{if c} v: ${a} %{endif}"`, t: StringType},
{x: `"%{for v in d} v: ${v} %{endfor}"`, t: StringType},
// Lifted operations
{x: `"v: ${e}"`, t: NewOutputType(StringType)},
{x: `"v: ${f}"`, t: NewOutputType(StringType)},
{x: `"v: ${g}"`, t: NewOutputType(StringType)},
{x: `"v: ${h}"`, t: NewOutputType(StringType)},
{x: `"%{if g} v: ${a} %{endif}"`, t: NewOutputType(StringType)},
{x: `"%{for v in h} v: ${v} %{endfor}"`, t: NewOutputType(StringType)},
{x: `"v: ${i}"`, t: NewPromiseType(StringType)},
{x: `"v: ${j}"`, t: NewPromiseType(StringType)},
{x: `"v: ${k}"`, t: NewPromiseType(StringType)},
{x: `"v: ${l}"`, t: NewPromiseType(StringType)},
{x: `"%{if k} v: ${a} %{endif}"`, t: NewPromiseType(StringType)},
{x: `"%{for v in l} v: ${v} %{endfor}"`, t: NewPromiseType(StringType)},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
var ok bool
switch c.xt.(type) {
case *LiteralValueExpression:
_, ok = expr.(*LiteralValueExpression)
case *ScopeTraversalExpression:
_, ok = expr.(*ScopeTraversalExpression)
default:
_, ok = expr.(*TemplateExpression)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
}
assert.True(t, ok)
})
}
}
func TestBindTupleCons(t *testing.T) {
t.Parallel()
env := environment(map[string]interface{}{
"a": NewOutputType(StringType),
"b": NewPromiseType(StringType),
"c": NewUnionType(StringType, BoolType),
})
scope := env.scope()
cases := []exprTestCase{
{x: `["foo", "bar", "baz"]`, t: NewTupleType(StringType, StringType, StringType)},
{x: `[0, "foo", true]`, t: NewTupleType(NumberType, StringType, BoolType)},
{x: `[a, b, c]`, t: NewTupleType(env["a"].(Type), env["b"].(Type), env["c"].(Type))},
{x: `[{"foo": "bar"}]`, t: NewTupleType(NewObjectType(map[string]Type{"foo": StringType}))},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
_, ok := expr.(*TupleConsExpression)
assert.True(t, ok)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
})
}
}
func TestBindUnaryOp(t *testing.T) {
t.Parallel()
env := environment(map[string]interface{}{
"a": NumberType,
"b": BoolType,
"c": NewOutputType(NumberType),
"d": NewOutputType(BoolType),
"e": NewPromiseType(NumberType),
"f": NewPromiseType(BoolType),
})
scope := env.scope()
cases := []exprTestCase{
// Standard operations
{x: `-a`, t: NumberType},
{x: `!b`, t: BoolType},
// Lifted operations
{x: `-c`, t: NewOutputType(NumberType)},
{x: `-e`, t: NewPromiseType(NumberType)},
{x: `!d`, t: NewOutputType(BoolType)},
{x: `!f`, t: NewPromiseType(BoolType)},
}
for _, c := range cases {
c := c
t.Run(c.x, func(t *testing.T) {
t.Parallel()
expr, diags := BindExpressionText(c.x, scope, hcl.Pos{})
assert.Len(t, diags, 0)
assertConvertibleFrom(t, c.t, expr.Type())
_, ok := expr.(*UnaryOpExpression)
assert.True(t, ok)
assert.Equal(t, c.x, fmt.Sprintf("%v", expr))
})
}
}