mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge pull request #30949 from hashicorp/alisdair/typeexpr-tests
typeexpr: Add test coverage
This commit is contained in:
commit
5e023ecfee
@ -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 {
|
||||
|
403
internal/typeexpr/get_type_test.go
Normal file
403
internal/typeexpr/get_type_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
100
internal/typeexpr/type_string_test.go
Normal file
100
internal/typeexpr/type_string_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
118
internal/typeexpr/type_type_test.go
Normal file
118
internal/typeexpr/type_type_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user