353 lines
7.1 KiB
Go
353 lines
7.1 KiB
Go
package typeexpr
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/hashicorp/hcl/v2/json"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestGetType(t *testing.T) {
|
|
tests := []struct {
|
|
Source string
|
|
Constraint bool
|
|
Want cty.Type
|
|
WantError string
|
|
}{
|
|
// keywords
|
|
{
|
|
`bool`,
|
|
false,
|
|
cty.Bool,
|
|
"",
|
|
},
|
|
{
|
|
`number`,
|
|
false,
|
|
cty.Number,
|
|
"",
|
|
},
|
|
{
|
|
`string`,
|
|
false,
|
|
cty.String,
|
|
"",
|
|
},
|
|
{
|
|
`any`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`The keyword "any" cannot be used in this type specification: an exact type is required.`,
|
|
},
|
|
{
|
|
`any`,
|
|
true,
|
|
cty.DynamicPseudoType,
|
|
"",
|
|
},
|
|
{
|
|
`list`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
"The list type constructor requires one argument specifying the element type.",
|
|
},
|
|
{
|
|
`map`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
"The map type constructor requires one argument specifying the element type.",
|
|
},
|
|
{
|
|
`set`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
"The set type constructor requires one argument specifying the element type.",
|
|
},
|
|
{
|
|
`object`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
"The object type constructor requires one argument specifying the attribute types and values as a map.",
|
|
},
|
|
{
|
|
`tuple`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
"The tuple type constructor requires one argument specifying the element types as a list.",
|
|
},
|
|
|
|
// constructors
|
|
{
|
|
`bool()`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`Primitive type keyword "bool" does not expect arguments.`,
|
|
},
|
|
{
|
|
`number()`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`Primitive type keyword "number" does not expect arguments.`,
|
|
},
|
|
{
|
|
`string()`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`Primitive type keyword "string" does not expect arguments.`,
|
|
},
|
|
{
|
|
`any()`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`Primitive type keyword "any" does not expect arguments.`,
|
|
},
|
|
{
|
|
`any()`,
|
|
true,
|
|
cty.DynamicPseudoType,
|
|
`Primitive type keyword "any" does not expect arguments.`,
|
|
},
|
|
{
|
|
`list(string)`,
|
|
false,
|
|
cty.List(cty.String),
|
|
``,
|
|
},
|
|
{
|
|
`set(string)`,
|
|
false,
|
|
cty.Set(cty.String),
|
|
``,
|
|
},
|
|
{
|
|
`map(string)`,
|
|
false,
|
|
cty.Map(cty.String),
|
|
``,
|
|
},
|
|
{
|
|
`list()`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`The list type constructor requires one argument specifying the element type.`,
|
|
},
|
|
{
|
|
`list(string, string)`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`The list type constructor requires one argument specifying the element type.`,
|
|
},
|
|
{
|
|
`list(any)`,
|
|
false,
|
|
cty.List(cty.DynamicPseudoType),
|
|
`The keyword "any" cannot be used in this type specification: an exact type is required.`,
|
|
},
|
|
{
|
|
`list(any)`,
|
|
true,
|
|
cty.List(cty.DynamicPseudoType),
|
|
``,
|
|
},
|
|
{
|
|
`object({})`,
|
|
false,
|
|
cty.EmptyObject,
|
|
``,
|
|
},
|
|
{
|
|
`object({name=string})`,
|
|
false,
|
|
cty.Object(map[string]cty.Type{"name": cty.String}),
|
|
``,
|
|
},
|
|
{
|
|
`object({"name"=string})`,
|
|
false,
|
|
cty.EmptyObject,
|
|
`Object constructor map keys must be attribute names.`,
|
|
},
|
|
{
|
|
`object({name=nope})`,
|
|
false,
|
|
cty.Object(map[string]cty.Type{"name": cty.DynamicPseudoType}),
|
|
`The keyword "nope" is not a valid type specification.`,
|
|
},
|
|
{
|
|
`object()`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`The object type constructor requires one argument specifying the attribute types and values as a map.`,
|
|
},
|
|
{
|
|
`object(string)`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.`,
|
|
},
|
|
{
|
|
`tuple([])`,
|
|
false,
|
|
cty.EmptyTuple,
|
|
``,
|
|
},
|
|
{
|
|
`tuple([string, bool])`,
|
|
false,
|
|
cty.Tuple([]cty.Type{cty.String, cty.Bool}),
|
|
``,
|
|
},
|
|
{
|
|
`tuple([nope])`,
|
|
false,
|
|
cty.Tuple([]cty.Type{cty.DynamicPseudoType}),
|
|
`The keyword "nope" is not a valid type specification.`,
|
|
},
|
|
{
|
|
`tuple()`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`The tuple type constructor requires one argument specifying the element types as a list.`,
|
|
},
|
|
{
|
|
`tuple(string)`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`Tuple type constructor requires a list of element types.`,
|
|
},
|
|
{
|
|
`shwoop(string)`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
`Keyword "shwoop" is not a valid type constructor.`,
|
|
},
|
|
{
|
|
`list("string")`,
|
|
false,
|
|
cty.List(cty.DynamicPseudoType),
|
|
`A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).`,
|
|
},
|
|
|
|
// More interesting combinations
|
|
{
|
|
`list(object({}))`,
|
|
false,
|
|
cty.List(cty.EmptyObject),
|
|
``,
|
|
},
|
|
{
|
|
`list(map(tuple([])))`,
|
|
false,
|
|
cty.List(cty.Map(cty.EmptyTuple)),
|
|
``,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.Source, func(t *testing.T) {
|
|
expr, diags := hclsyntax.ParseExpression([]byte(test.Source), "", hcl.Pos{Line: 1, Column: 1})
|
|
if diags.HasErrors() {
|
|
t.Fatalf("failed to parse: %s", diags)
|
|
}
|
|
|
|
got, diags := getType(expr, test.Constraint)
|
|
if test.WantError == "" {
|
|
for _, diag := range diags {
|
|
t.Error(diag)
|
|
}
|
|
} else {
|
|
found := false
|
|
for _, diag := range diags {
|
|
t.Log(diag)
|
|
if diag.Severity == hcl.DiagError && diag.Detail == test.WantError {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("missing expected error detail message: %s", test.WantError)
|
|
}
|
|
}
|
|
|
|
if !got.Equals(test.Want) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetTypeJSON(t *testing.T) {
|
|
// We have fewer test cases here because we're mainly exercising the
|
|
// extra indirection in the JSON syntax package, which ultimately calls
|
|
// into the native syntax parser (which we tested extensively in
|
|
// TestGetType).
|
|
tests := []struct {
|
|
Source string
|
|
Constraint bool
|
|
Want cty.Type
|
|
WantError string
|
|
}{
|
|
{
|
|
`{"expr":"bool"}`,
|
|
false,
|
|
cty.Bool,
|
|
"",
|
|
},
|
|
{
|
|
`{"expr":"list(bool)"}`,
|
|
false,
|
|
cty.List(cty.Bool),
|
|
"",
|
|
},
|
|
{
|
|
`{"expr":"list"}`,
|
|
false,
|
|
cty.DynamicPseudoType,
|
|
"The list type constructor requires one argument specifying the element type.",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.Source, func(t *testing.T) {
|
|
file, diags := json.Parse([]byte(test.Source), "")
|
|
if diags.HasErrors() {
|
|
t.Fatalf("failed to parse: %s", diags)
|
|
}
|
|
|
|
type TestContent struct {
|
|
Expr hcl.Expression `hcl:"expr"`
|
|
}
|
|
var content TestContent
|
|
diags = gohcl.DecodeBody(file.Body, nil, &content)
|
|
if diags.HasErrors() {
|
|
t.Fatalf("failed to decode: %s", diags)
|
|
}
|
|
|
|
got, diags := getType(content.Expr, test.Constraint)
|
|
if test.WantError == "" {
|
|
for _, diag := range diags {
|
|
t.Error(diag)
|
|
}
|
|
} else {
|
|
found := false
|
|
for _, diag := range diags {
|
|
t.Log(diag)
|
|
if diag.Severity == hcl.DiagError && diag.Detail == test.WantError {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("missing expected error detail message: %s", test.WantError)
|
|
}
|
|
}
|
|
|
|
if !got.Equals(test.Want) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
|
}
|
|
})
|
|
}
|
|
}
|