hcl/hcldec/public_test.go

1123 lines
17 KiB
Go

package hcldec
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
func TestDecode(t *testing.T) {
tests := []struct {
config string
spec Spec
ctx *hcl.EvalContext
want cty.Value
diagCount int
}{
{
``,
&ObjectSpec{},
nil,
cty.EmptyObjectVal,
0,
},
{
"a = 1\n",
&ObjectSpec{},
nil,
cty.EmptyObjectVal,
1, // attribute named "a" is not expected here
},
{
"a = 1\n",
&ObjectSpec{
"a": &AttrSpec{
Name: "a",
Type: cty.Number,
},
},
nil,
cty.ObjectVal(map[string]cty.Value{
"a": cty.NumberIntVal(1),
}),
0,
},
{
"a = 1\n",
&AttrSpec{
Name: "a",
Type: cty.Number,
},
nil,
cty.NumberIntVal(1),
0,
},
{
"a = 1\n",
&DefaultSpec{
Primary: &AttrSpec{
Name: "a",
Type: cty.Number,
},
Default: &LiteralSpec{
Value: cty.NumberIntVal(10),
},
},
nil,
cty.NumberIntVal(1),
0,
},
{
"",
&DefaultSpec{
Primary: &AttrSpec{
Name: "a",
Type: cty.Number,
},
Default: &LiteralSpec{
Value: cty.NumberIntVal(10),
},
},
nil,
cty.NumberIntVal(10),
0,
},
{
"a = 1\n",
ObjectSpec{
"foo": &DefaultSpec{
Primary: &AttrSpec{
Name: "a",
Type: cty.Number,
},
Default: &LiteralSpec{
Value: cty.NumberIntVal(10),
},
},
},
nil,
cty.ObjectVal(map[string]cty.Value{"foo": cty.NumberIntVal(1)}),
0,
},
{
"a = \"1\"\n",
&AttrSpec{
Name: "a",
Type: cty.Number,
},
nil,
cty.NumberIntVal(1),
0,
},
{
"a = true\n",
&AttrSpec{
Name: "a",
Type: cty.Number,
},
nil,
cty.UnknownVal(cty.Number),
1, // incorrect type - number required.
},
{
``,
&AttrSpec{
Name: "a",
Type: cty.Number,
Required: true,
},
nil,
cty.NullVal(cty.Number),
1, // attribute "a" is required
},
{
`
b {
}
`,
&BlockSpec{
TypeName: "b",
Nested: ObjectSpec{},
},
nil,
cty.EmptyObjectVal,
0,
},
{
`
b "baz" {
}
`,
&BlockSpec{
TypeName: "b",
Nested: &BlockLabelSpec{
Index: 0,
Name: "name",
},
},
nil,
cty.StringVal("baz"),
0,
},
{
`
b "baz" {}
b "foo" {}
`,
&BlockSpec{
TypeName: "b",
Nested: &BlockLabelSpec{
Index: 0,
Name: "name",
},
},
nil,
cty.StringVal("baz"),
1, // duplicate "b" block
},
{
`
b {
}
`,
&BlockSpec{
TypeName: "b",
Nested: &BlockLabelSpec{
Index: 0,
Name: "name",
},
},
nil,
cty.NullVal(cty.String),
1, // missing name label
},
{
``,
&BlockSpec{
TypeName: "b",
Nested: ObjectSpec{},
},
nil,
cty.NullVal(cty.EmptyObject),
0,
},
{
"a {}\n",
&BlockSpec{
TypeName: "b",
Nested: ObjectSpec{},
},
nil,
cty.NullVal(cty.EmptyObject),
1, // blocks of type "a" are not supported
},
{
``,
&BlockSpec{
TypeName: "b",
Nested: ObjectSpec{},
Required: true,
},
nil,
cty.NullVal(cty.EmptyObject),
1, // a block of type "b" is required
},
{
`
b {}
b {}
`,
&BlockSpec{
TypeName: "b",
Nested: ObjectSpec{},
Required: true,
},
nil,
cty.EmptyObjectVal,
1, // only one "b" block is allowed
},
{
`
b {
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapValEmpty(cty.String),
0,
},
{
`
b {
hello = "world"
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
0,
},
{
`
b {
hello = true
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("true"),
}),
0,
},
{
`
b {
hello = true
goodbye = 5
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("true"),
"goodbye": cty.StringVal("5"),
}),
0,
},
{
``,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.NullVal(cty.Map(cty.String)),
0,
},
{
``,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
Required: true,
},
nil,
cty.NullVal(cty.Map(cty.String)),
1, // missing b block
},
{
`
b {
}
b {
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
},
nil,
cty.MapValEmpty(cty.String),
1, // duplicate b block
},
{
`
b {
}
b {
}
`,
&BlockAttrsSpec{
TypeName: "b",
ElementType: cty.String,
Required: true,
},
nil,
cty.MapValEmpty(cty.String),
1, // duplicate b block
},
{
`
b {}
b {}
`,
&BlockListSpec{
TypeName: "b",
Nested: ObjectSpec{},
},
nil,
cty.ListVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
0,
},
{
``,
&BlockListSpec{
TypeName: "b",
Nested: ObjectSpec{},
},
nil,
cty.ListValEmpty(cty.EmptyObject),
0,
},
{
`
b "foo" {}
b "bar" {}
`,
&BlockListSpec{
TypeName: "b",
Nested: &BlockLabelSpec{
Name: "name",
Index: 0,
},
},
nil,
cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}),
0,
},
{
`
b {}
b {}
b {}
`,
&BlockListSpec{
TypeName: "b",
Nested: ObjectSpec{},
MaxItems: 2,
},
nil,
cty.ListVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal, cty.EmptyObjectVal}),
1, // too many b blocks
},
{
`
b {}
b {}
`,
&BlockListSpec{
TypeName: "b",
Nested: ObjectSpec{},
MinItems: 10,
},
nil,
cty.ListVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
1, // insufficient b blocks
},
{
`
b {
a = true
}
b {
a = 1
}
`,
&BlockListSpec{
TypeName: "b",
Nested: &AttrSpec{
Name: "a",
Type: cty.DynamicPseudoType,
},
},
nil,
cty.DynamicVal,
1, // Unconsistent argument types in b blocks
},
{
`
b {
a = true
}
b {
a = "not a bool"
}
`,
&BlockListSpec{
TypeName: "b",
Nested: &AttrSpec{
Name: "a",
Type: cty.DynamicPseudoType,
},
},
nil,
cty.ListVal([]cty.Value{
cty.StringVal("true"), // type unification generalizes all the values to strings
cty.StringVal("not a bool"),
}),
0,
},
{
`
b {}
b {}
`,
&BlockSetSpec{
TypeName: "b",
Nested: ObjectSpec{},
MaxItems: 2,
},
nil,
cty.SetVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
0,
},
{
`
b "foo" "bar" {}
b "bar" "baz" {}
`,
&BlockSetSpec{
TypeName: "b",
Nested: TupleSpec{
&BlockLabelSpec{
Name: "name",
Index: 1,
},
&BlockLabelSpec{
Name: "type",
Index: 0,
},
},
},
nil,
cty.SetVal([]cty.Value{
cty.TupleVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("foo")}),
cty.TupleVal([]cty.Value{cty.StringVal("baz"), cty.StringVal("bar")}),
}),
0,
},
{
`
b {
a = true
}
b {
a = 1
}
`,
&BlockSetSpec{
TypeName: "b",
Nested: &AttrSpec{
Name: "a",
Type: cty.DynamicPseudoType,
},
},
nil,
cty.DynamicVal,
1, // Unconsistent argument types in b blocks
},
{
`
b {
a = true
}
b {
a = "not a bool"
}
`,
&BlockSetSpec{
TypeName: "b",
Nested: &AttrSpec{
Name: "a",
Type: cty.DynamicPseudoType,
},
},
nil,
cty.SetVal([]cty.Value{
cty.StringVal("true"), // type unification generalizes all the values to strings
cty.StringVal("not a bool"),
}),
0,
},
{
`
b "foo" {}
b "bar" {}
`,
&BlockMapSpec{
TypeName: "b",
LabelNames: []string{"key"},
Nested: ObjectSpec{},
},
nil,
cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal, "bar": cty.EmptyObjectVal}),
0,
},
{
`
b "foo" "bar" {}
b "bar" "baz" {}
`,
&BlockMapSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.MapVal(map[string]cty.Value{
"foo": cty.MapVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
}),
"bar": cty.MapVal(map[string]cty.Value{
"baz": cty.EmptyObjectVal,
}),
}),
0,
},
{
`
b "foo" "bar" {}
b "bar" "bar" {}
`,
&BlockMapSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.MapVal(map[string]cty.Value{
"foo": cty.MapVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
}),
"bar": cty.MapVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
}),
}),
0,
},
{
`
b "foo" "bar" {}
b "foo" "baz" {}
`,
&BlockMapSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.MapVal(map[string]cty.Value{
"foo": cty.MapVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
"baz": cty.EmptyObjectVal,
}),
}),
0,
},
{
`
b "foo" "bar" {}
`,
&BlockMapSpec{
TypeName: "b",
LabelNames: []string{"key"},
Nested: ObjectSpec{},
},
nil,
cty.MapValEmpty(cty.EmptyObject),
1, // too many labels
},
{
`
b "bar" {}
`,
&BlockMapSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.MapValEmpty(cty.EmptyObject),
1, // not enough labels
},
{
`
b "foo" {}
b "foo" {}
`,
&BlockMapSpec{
TypeName: "b",
LabelNames: []string{"key"},
Nested: ObjectSpec{},
},
nil,
cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
1, // duplicate b block
},
{
`
b "foo" "bar" {}
b "foo" "bar" {}
`,
&BlockMapSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.MapVal(map[string]cty.Value{"foo": cty.MapVal(map[string]cty.Value{"bar": cty.EmptyObjectVal})}),
1, // duplicate b block
},
{
`
b "foo" "bar" {}
b "bar" "baz" {}
`,
&BlockMapSpec{
TypeName: "b",
LabelNames: []string{"type"},
Nested: &BlockLabelSpec{
Name: "name",
Index: 0,
},
},
nil,
cty.MapVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
"bar": cty.StringVal("baz"),
}),
0,
},
{
`
b "foo" {}
`,
&BlockMapSpec{
TypeName: "b",
LabelNames: []string{"type"},
Nested: &BlockLabelSpec{
Name: "name",
Index: 0,
},
},
nil,
cty.MapValEmpty(cty.String),
1, // missing name
},
{
`
b {}
b {}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: ObjectSpec{},
},
nil,
cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
0,
},
{
``,
&BlockTupleSpec{
TypeName: "b",
Nested: ObjectSpec{},
},
nil,
cty.EmptyTupleVal,
0,
},
{
`
b "foo" {}
b "bar" {}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: &BlockLabelSpec{
Name: "name",
Index: 0,
},
},
nil,
cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}),
0,
},
{
`
b {}
b {}
b {}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: ObjectSpec{},
MaxItems: 2,
},
nil,
cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal, cty.EmptyObjectVal}),
1, // too many b blocks
},
{
`
b {}
b {}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: ObjectSpec{},
MinItems: 10,
},
nil,
cty.TupleVal([]cty.Value{cty.EmptyObjectVal, cty.EmptyObjectVal}),
1, // insufficient b blocks
},
{
`
b {
a = true
}
b {
a = 1
}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: &AttrSpec{
Name: "a",
Type: cty.DynamicPseudoType,
},
},
nil,
cty.TupleVal([]cty.Value{
cty.True,
cty.NumberIntVal(1),
}),
0,
},
{
`
b {
a = true
}
b {
a = "not a bool"
}
`,
&BlockTupleSpec{
TypeName: "b",
Nested: &AttrSpec{
Name: "a",
Type: cty.DynamicPseudoType,
},
},
nil,
cty.TupleVal([]cty.Value{
cty.True,
cty.StringVal("not a bool"),
}),
0,
},
{
`
b "foo" {}
b "bar" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{"foo": cty.EmptyObjectVal, "bar": cty.EmptyObjectVal}),
0,
},
{
`
b "foo" "bar" {}
b "bar" "baz" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
}),
"bar": cty.ObjectVal(map[string]cty.Value{
"baz": cty.EmptyObjectVal,
}),
}),
0,
},
{
`
b "foo" "bar" {}
b "bar" "bar" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
}),
"bar": cty.ObjectVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
}),
}),
0,
},
{
`
b "foo" "bar" {}
b "foo" "baz" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"bar": cty.EmptyObjectVal,
"baz": cty.EmptyObjectVal,
}),
}),
0,
},
{
`
b "foo" "bar" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key"},
Nested: ObjectSpec{},
},
nil,
cty.EmptyObjectVal,
1, // too many labels
},
{
`
b "bar" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.EmptyObjectVal,
1, // not enough labels
},
{
`
b "foo" {}
b "foo" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
1, // duplicate b block
},
{
`
b "foo" "bar" {}
b "foo" "bar" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"key1", "key2"},
Nested: ObjectSpec{},
},
nil,
cty.ObjectVal(map[string]cty.Value{"foo": cty.ObjectVal(map[string]cty.Value{"bar": cty.EmptyObjectVal})}),
1, // duplicate b block
},
{
`
b "foo" "bar" {}
b "bar" "baz" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"type"},
Nested: &BlockLabelSpec{
Name: "name",
Index: 0,
},
},
nil,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
"bar": cty.StringVal("baz"),
}),
0,
},
{
`
b "foo" {}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"type"},
Nested: &BlockLabelSpec{
Name: "name",
Index: 0,
},
},
nil,
cty.EmptyObjectVal,
1, // missing name
},
{
`
b "foo" {
arg = true
}
b "bar" {
arg = 1
}
`,
&BlockObjectSpec{
TypeName: "b",
LabelNames: []string{"type"},
Nested: &AttrSpec{
Name: "arg",
Type: cty.DynamicPseudoType,
},
},
nil,
cty.ObjectVal(map[string]cty.Value{
"foo": cty.True,
"bar": cty.NumberIntVal(1),
}),
0,
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d-%s", i, test.config), func(t *testing.T) {
file, parseDiags := hclsyntax.ParseConfig([]byte(test.config), "", hcl.Pos{Line: 1, Column: 1, Byte: 0})
body := file.Body
got, valDiags := Decode(body, test.spec, test.ctx)
var diags hcl.Diagnostics
diags = append(diags, parseDiags...)
diags = append(diags, valDiags...)
if len(diags) != test.diagCount {
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.diagCount)
for _, diag := range diags {
t.Logf(" - %s", diag.Error())
}
}
if !got.RawEquals(test.want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
}
})
}
}
func TestSourceRange(t *testing.T) {
tests := []struct {
config string
spec Spec
want hcl.Range
}{
{
"a = 1\n",
&AttrSpec{
Name: "a",
},
hcl.Range{
Start: hcl.Pos{Line: 1, Column: 5, Byte: 4},
End: hcl.Pos{Line: 1, Column: 6, Byte: 5},
},
},
{
`
b {
a = 1
}
`,
&BlockSpec{
TypeName: "b",
Nested: &AttrSpec{
Name: "a",
},
},
hcl.Range{
Start: hcl.Pos{Line: 3, Column: 7, Byte: 11},
End: hcl.Pos{Line: 3, Column: 8, Byte: 12},
},
},
{
`
b {
c {
a = 1
}
}
`,
&BlockSpec{
TypeName: "b",
Nested: &BlockSpec{
TypeName: "c",
Nested: &AttrSpec{
Name: "a",
},
},
},
hcl.Range{
Start: hcl.Pos{Line: 4, Column: 9, Byte: 19},
End: hcl.Pos{Line: 4, Column: 10, Byte: 20},
},
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d-%s", i, test.config), func(t *testing.T) {
file, diags := hclsyntax.ParseConfig([]byte(test.config), "", hcl.Pos{Line: 1, Column: 1, Byte: 0})
if len(diags) != 0 {
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), 0)
for _, diag := range diags {
t.Logf(" - %s", diag.Error())
}
}
body := file.Body
got := SourceRange(body, test.spec)
if !reflect.DeepEqual(got, test.want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
}
})
}
}