Merge pull request #30949 from hashicorp/alisdair/typeexpr-tests

typeexpr: Add test coverage
This commit is contained in:
Alisdair McDiarmid 2022-04-27 15:44:00 -04:00 committed by GitHub
commit 5e023ecfee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 629 additions and 1 deletions

View File

@ -77,13 +77,20 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
}
switch call.Name {
case "bool", "string", "number", "any":
case "bool", "string", "number":
return cty.DynamicPseudoType, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name),
Subject: &call.ArgsRange,
}}
case "any":
return cty.DynamicPseudoType, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: invalidTypeSummary,
Detail: fmt.Sprintf("Type constraint keyword %q does not expect arguments.", call.Name),
Subject: &call.ArgsRange,
}}
}
if len(call.Arguments) != 1 {

View File

@ -0,0 +1,403 @@
package typeexpr
import (
"fmt"
"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,
`Type constraint keyword "any" does not expect arguments.`,
},
{
`any()`,
true,
cty.DynamicPseudoType,
`Type constraint 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)),
``,
},
// Optional modifier
{
`object({name=string,age=optional(number)})`,
true,
cty.ObjectWithOptionalAttrs(map[string]cty.Type{
"name": cty.String,
"age": cty.Number,
}, []string{"age"}),
``,
},
{
`object({name=string,meta=optional(any)})`,
true,
cty.ObjectWithOptionalAttrs(map[string]cty.Type{
"name": cty.String,
"meta": cty.DynamicPseudoType,
}, []string{"meta"}),
``,
},
{
`object({name=string,age=optional(number)})`,
false,
cty.Object(map[string]cty.Type{
"name": cty.String,
"age": cty.Number,
}),
`Optional attribute modifier is only for type constraints, not for exact types.`,
},
{
`object({name=string,meta=optional(any)})`,
false,
cty.Object(map[string]cty.Type{
"name": cty.String,
"meta": cty.DynamicPseudoType,
}),
`Optional attribute modifier is only for type constraints, not for exact types.`,
},
{
`optional(string)`,
false,
cty.DynamicPseudoType,
`Keyword "optional" is valid only as a modifier for object type attributes.`,
},
{
`optional`,
false,
cty.DynamicPseudoType,
`The keyword "optional" is not a valid type specification.`,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s (constraint=%v)", test.Source, test.Constraint), 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)
}
})
}
}

View File

@ -0,0 +1,100 @@
package typeexpr
import (
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestTypeString(t *testing.T) {
tests := []struct {
Type cty.Type
Want string
}{
{
cty.DynamicPseudoType,
"any",
},
{
cty.String,
"string",
},
{
cty.Number,
"number",
},
{
cty.Bool,
"bool",
},
{
cty.List(cty.Number),
"list(number)",
},
{
cty.Set(cty.Bool),
"set(bool)",
},
{
cty.Map(cty.String),
"map(string)",
},
{
cty.EmptyObject,
"object({})",
},
{
cty.Object(map[string]cty.Type{"foo": cty.Bool}),
"object({foo=bool})",
},
{
cty.Object(map[string]cty.Type{"foo": cty.Bool, "bar": cty.String}),
"object({bar=string,foo=bool})",
},
{
cty.EmptyTuple,
"tuple([])",
},
{
cty.Tuple([]cty.Type{cty.Bool}),
"tuple([bool])",
},
{
cty.Tuple([]cty.Type{cty.Bool, cty.String}),
"tuple([bool,string])",
},
{
cty.List(cty.DynamicPseudoType),
"list(any)",
},
{
cty.Tuple([]cty.Type{cty.DynamicPseudoType}),
"tuple([any])",
},
{
cty.Object(map[string]cty.Type{"foo": cty.DynamicPseudoType}),
"object({foo=any})",
},
{
// We don't expect to find attributes that aren't valid identifiers
// because we only promise to support types that this package
// would've created, but we allow this situation during rendering
// just because it's convenient for applications trying to produce
// error messages about mismatched types. Note that the quoted
// attribute name is not actually accepted by our Type and
// TypeConstraint functions, so this is one situation where the
// TypeString result cannot be re-parsed by those functions.
cty.Object(map[string]cty.Type{"foo bar baz": cty.String}),
`object({"foo bar baz"=string})`,
},
}
for _, test := range tests {
t.Run(test.Type.GoString(), func(t *testing.T) {
got := TypeString(test.Type)
if got != test.Want {
t.Errorf("wrong result\ntype: %#v\ngot: %s\nwant: %s", test.Type, got, test.Want)
}
})
}
}

View File

@ -0,0 +1,118 @@
package typeexpr
import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestTypeConstraintType(t *testing.T) {
tyVal1 := TypeConstraintVal(cty.String)
tyVal2 := TypeConstraintVal(cty.String)
tyVal3 := TypeConstraintVal(cty.Number)
if !tyVal1.RawEquals(tyVal2) {
t.Errorf("tyVal1 not equal to tyVal2\ntyVal1: %#v\ntyVal2: %#v", tyVal1, tyVal2)
}
if tyVal1.RawEquals(tyVal3) {
t.Errorf("tyVal1 equal to tyVal2, but should not be\ntyVal1: %#v\ntyVal3: %#v", tyVal1, tyVal3)
}
if got, want := TypeConstraintFromVal(tyVal1), cty.String; !got.Equals(want) {
t.Errorf("wrong type extracted from tyVal1\ngot: %#v\nwant: %#v", got, want)
}
if got, want := TypeConstraintFromVal(tyVal3), cty.Number; !got.Equals(want) {
t.Errorf("wrong type extracted from tyVal3\ngot: %#v\nwant: %#v", got, want)
}
}
func TestConvertFunc(t *testing.T) {
// This is testing the convert function directly, skipping over the HCL
// parsing and evaluation steps that would normally lead there. There is
// another test in the "integrationtest" package called TestTypeConvertFunc
// that exercises the full path to this function via the hclsyntax parser.
tests := []struct {
val, ty cty.Value
want cty.Value
wantErr string
}{
// The goal here is not an exhaustive set of conversions, since that's
// already covered in cty/convert, but rather exercising different
// permutations of success and failure to make sure the function
// handles all of the results in a reasonable way.
{
cty.StringVal("hello"),
TypeConstraintVal(cty.String),
cty.StringVal("hello"),
``,
},
{
cty.True,
TypeConstraintVal(cty.String),
cty.StringVal("true"),
``,
},
{
cty.StringVal("hello"),
TypeConstraintVal(cty.Bool),
cty.NilVal,
`a bool is required`,
},
{
cty.UnknownVal(cty.Bool),
TypeConstraintVal(cty.Bool),
cty.UnknownVal(cty.Bool),
``,
},
{
cty.DynamicVal,
TypeConstraintVal(cty.Bool),
cty.UnknownVal(cty.Bool),
``,
},
{
cty.NullVal(cty.Bool),
TypeConstraintVal(cty.Bool),
cty.NullVal(cty.Bool),
``,
},
{
cty.NullVal(cty.DynamicPseudoType),
TypeConstraintVal(cty.Bool),
cty.NullVal(cty.Bool),
``,
},
{
cty.StringVal("hello").Mark(1),
TypeConstraintVal(cty.String),
cty.StringVal("hello").Mark(1),
``,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v to %#v", test.val, test.ty), func(t *testing.T) {
got, err := ConvertFunc.Call([]cty.Value{test.val, test.ty})
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)
}
})
}
}