From bf9f733ca23f18c148f7cc28b1faded724e89da9 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Wed, 27 Apr 2022 14:16:33 -0400 Subject: [PATCH] typeexpr: Import tests from hashicorp/hcl This package is a fork of the HCL ext/typeexpr package, but we dropped the tests when bringing it across. This commit imports those tests verbatim. --- internal/typeexpr/get_type_test.go | 352 ++++++++++++++++++++++++++ internal/typeexpr/type_string_test.go | 100 ++++++++ internal/typeexpr/type_type_test.go | 118 +++++++++ 3 files changed, 570 insertions(+) create mode 100644 internal/typeexpr/get_type_test.go create mode 100644 internal/typeexpr/type_string_test.go create mode 100644 internal/typeexpr/type_type_test.go diff --git a/internal/typeexpr/get_type_test.go b/internal/typeexpr/get_type_test.go new file mode 100644 index 0000000000..391bf4f938 --- /dev/null +++ b/internal/typeexpr/get_type_test.go @@ -0,0 +1,352 @@ +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) + } + }) + } +} diff --git a/internal/typeexpr/type_string_test.go b/internal/typeexpr/type_string_test.go new file mode 100644 index 0000000000..fbdf3f481d --- /dev/null +++ b/internal/typeexpr/type_string_test.go @@ -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) + } + }) + } +} diff --git a/internal/typeexpr/type_type_test.go b/internal/typeexpr/type_type_test.go new file mode 100644 index 0000000000..2286a2e1a5 --- /dev/null +++ b/internal/typeexpr/type_type_test.go @@ -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: ", err) + } + return + } + if test.wantErr != "" { + t.Errorf("wrong error\ngot: \nwant: %s", test.wantErr) + } + + if !test.want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) + } + }) + } +}