c03d57b578
Fixes https://github.com/hashicorp/terraform/issues/8886 While parsing JSON, an empty list value would be assumed to be a non-existent list of objects, and would be removed from the result. This may have never been the correct behavior but always worked okay because we previously didn't support lists as first class types. With the support of lists, we need to actually return the empty list as the type. If we return nothing, then projects like Terraform will think that the value was never set, which is false.
385 lines
6.4 KiB
Go
385 lines
6.4 KiB
Go
package parser
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl/hcl/ast"
|
|
"github.com/hashicorp/hcl/hcl/token"
|
|
)
|
|
|
|
func TestType(t *testing.T) {
|
|
var literals = []struct {
|
|
typ token.Type
|
|
src string
|
|
}{
|
|
{token.STRING, `"foo": "bar"`},
|
|
{token.NUMBER, `"foo": 123`},
|
|
{token.FLOAT, `"foo": 123.12`},
|
|
{token.FLOAT, `"foo": -123.12`},
|
|
{token.BOOL, `"foo": true`},
|
|
{token.STRING, `"foo": null`},
|
|
}
|
|
|
|
for _, l := range literals {
|
|
t.Logf("Testing: %s", l.src)
|
|
|
|
p := newParser([]byte(l.src))
|
|
item, err := p.objectItem()
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
lit, ok := item.Val.(*ast.LiteralType)
|
|
if !ok {
|
|
t.Errorf("node should be of type LiteralType, got: %T", item.Val)
|
|
}
|
|
|
|
if lit.Token.Type != l.typ {
|
|
t.Errorf("want: %s, got: %s", l.typ, lit.Token.Type)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListType(t *testing.T) {
|
|
var literals = []struct {
|
|
src string
|
|
tokens []token.Type
|
|
}{
|
|
{
|
|
`"foo": ["123", 123]`,
|
|
[]token.Type{token.STRING, token.NUMBER},
|
|
},
|
|
{
|
|
`"foo": [123, "123",]`,
|
|
[]token.Type{token.NUMBER, token.STRING},
|
|
},
|
|
{
|
|
`"foo": []`,
|
|
[]token.Type{},
|
|
},
|
|
{
|
|
`"foo": ["123", 123]`,
|
|
[]token.Type{token.STRING, token.NUMBER},
|
|
},
|
|
{
|
|
`"foo": ["123", {}]`,
|
|
[]token.Type{token.STRING, token.LBRACE},
|
|
},
|
|
}
|
|
|
|
for _, l := range literals {
|
|
t.Logf("Testing: %s", l.src)
|
|
|
|
p := newParser([]byte(l.src))
|
|
item, err := p.objectItem()
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
list, ok := item.Val.(*ast.ListType)
|
|
if !ok {
|
|
t.Errorf("node should be of type LiteralType, got: %T", item.Val)
|
|
}
|
|
|
|
tokens := []token.Type{}
|
|
for _, li := range list.List {
|
|
switch v := li.(type) {
|
|
case *ast.LiteralType:
|
|
tokens = append(tokens, v.Token.Type)
|
|
case *ast.ObjectType:
|
|
tokens = append(tokens, token.LBRACE)
|
|
}
|
|
}
|
|
|
|
equals(t, l.tokens, tokens)
|
|
}
|
|
}
|
|
|
|
func TestObjectType(t *testing.T) {
|
|
var literals = []struct {
|
|
src string
|
|
nodeType []ast.Node
|
|
itemLen int
|
|
}{
|
|
{
|
|
`"foo": {}`,
|
|
nil,
|
|
0,
|
|
},
|
|
{
|
|
`"foo": {
|
|
"bar": "fatih"
|
|
}`,
|
|
[]ast.Node{&ast.LiteralType{}},
|
|
1,
|
|
},
|
|
{
|
|
`"foo": {
|
|
"bar": "fatih",
|
|
"baz": ["arslan"]
|
|
}`,
|
|
[]ast.Node{
|
|
&ast.LiteralType{},
|
|
&ast.ListType{},
|
|
},
|
|
2,
|
|
},
|
|
{
|
|
`"foo": {
|
|
"bar": {}
|
|
}`,
|
|
[]ast.Node{
|
|
&ast.ObjectType{},
|
|
},
|
|
1,
|
|
},
|
|
{
|
|
`"foo": {
|
|
"bar": {},
|
|
"foo": true
|
|
}`,
|
|
[]ast.Node{
|
|
&ast.ObjectType{},
|
|
&ast.LiteralType{},
|
|
},
|
|
2,
|
|
},
|
|
}
|
|
|
|
for _, l := range literals {
|
|
t.Logf("Testing:\n%s\n", l.src)
|
|
|
|
p := newParser([]byte(l.src))
|
|
// p.enableTrace = true
|
|
item, err := p.objectItem()
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// we know that the ObjectKey name is foo for all cases, what matters
|
|
// is the object
|
|
obj, ok := item.Val.(*ast.ObjectType)
|
|
if !ok {
|
|
t.Errorf("node should be of type LiteralType, got: %T", item.Val)
|
|
}
|
|
|
|
// check if the total length of items are correct
|
|
equals(t, l.itemLen, len(obj.List.Items))
|
|
|
|
// check if the types are correct
|
|
for i, item := range obj.List.Items {
|
|
equals(t, reflect.TypeOf(l.nodeType[i]), reflect.TypeOf(item.Val))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFlattenObjects(t *testing.T) {
|
|
var literals = []struct {
|
|
src string
|
|
nodeType []ast.Node
|
|
itemLen int
|
|
}{
|
|
{
|
|
`{
|
|
"foo": [
|
|
{
|
|
"foo": "svh",
|
|
"bar": "fatih"
|
|
}
|
|
]
|
|
}`,
|
|
[]ast.Node{
|
|
&ast.ObjectType{},
|
|
&ast.LiteralType{},
|
|
&ast.LiteralType{},
|
|
},
|
|
3,
|
|
},
|
|
{
|
|
`{
|
|
"variable": {
|
|
"foo": {}
|
|
}
|
|
}`,
|
|
[]ast.Node{
|
|
&ast.ObjectType{},
|
|
},
|
|
1,
|
|
},
|
|
{
|
|
`{
|
|
"empty": []
|
|
}`,
|
|
[]ast.Node{
|
|
&ast.ListType{},
|
|
},
|
|
1,
|
|
},
|
|
{
|
|
`{
|
|
"basic": [1, 2, 3]
|
|
}`,
|
|
[]ast.Node{
|
|
&ast.ListType{},
|
|
},
|
|
1,
|
|
},
|
|
}
|
|
|
|
for _, l := range literals {
|
|
t.Logf("Testing:\n%s\n", l.src)
|
|
|
|
f, err := Parse([]byte(l.src))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
// the first object is always an ObjectList so just assert that one
|
|
// so we can use it as such
|
|
obj, ok := f.Node.(*ast.ObjectList)
|
|
if !ok {
|
|
t.Errorf("node should be *ast.ObjectList, got: %T", f.Node)
|
|
}
|
|
|
|
// check if the types are correct
|
|
var i int
|
|
for _, item := range obj.Items {
|
|
equals(t, reflect.TypeOf(l.nodeType[i]), reflect.TypeOf(item.Val))
|
|
i++
|
|
|
|
if obj, ok := item.Val.(*ast.ObjectType); ok {
|
|
for _, item := range obj.List.Items {
|
|
equals(t, reflect.TypeOf(l.nodeType[i]), reflect.TypeOf(item.Val))
|
|
i++
|
|
}
|
|
}
|
|
}
|
|
|
|
// check if the number of items is correct
|
|
equals(t, l.itemLen, i)
|
|
|
|
}
|
|
}
|
|
|
|
func TestObjectKey(t *testing.T) {
|
|
keys := []struct {
|
|
exp []token.Type
|
|
src string
|
|
}{
|
|
{[]token.Type{token.STRING}, `"foo": {}`},
|
|
}
|
|
|
|
for _, k := range keys {
|
|
p := newParser([]byte(k.src))
|
|
keys, err := p.objectKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tokens := []token.Type{}
|
|
for _, o := range keys {
|
|
tokens = append(tokens, o.Token.Type)
|
|
}
|
|
|
|
equals(t, k.exp, tokens)
|
|
}
|
|
|
|
errKeys := []struct {
|
|
src string
|
|
}{
|
|
{`foo 12 {}`},
|
|
{`foo bar = {}`},
|
|
{`foo []`},
|
|
{`12 {}`},
|
|
}
|
|
|
|
for _, k := range errKeys {
|
|
p := newParser([]byte(k.src))
|
|
_, err := p.objectKey()
|
|
if err == nil {
|
|
t.Errorf("case '%s' should give an error", k.src)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Official HCL tests
|
|
func TestParse(t *testing.T) {
|
|
cases := []struct {
|
|
Name string
|
|
Err bool
|
|
}{
|
|
{
|
|
"array.json",
|
|
false,
|
|
},
|
|
{
|
|
"basic.json",
|
|
false,
|
|
},
|
|
{
|
|
"object.json",
|
|
false,
|
|
},
|
|
{
|
|
"types.json",
|
|
false,
|
|
},
|
|
{
|
|
"bad_input_128.json",
|
|
true,
|
|
},
|
|
{
|
|
"bad_input_tf_8110.json",
|
|
true,
|
|
},
|
|
{
|
|
"good_input_tf_8110.json",
|
|
false,
|
|
},
|
|
}
|
|
|
|
const fixtureDir = "./test-fixtures"
|
|
|
|
for _, tc := range cases {
|
|
d, err := ioutil.ReadFile(filepath.Join(fixtureDir, tc.Name))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
_, err = Parse(d)
|
|
if (err != nil) != tc.Err {
|
|
t.Fatalf("Input: %s\n\nError: %s", tc.Name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParse_inline(t *testing.T) {
|
|
cases := []struct {
|
|
Value string
|
|
Err bool
|
|
}{
|
|
{"{:{", true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
_, err := Parse([]byte(tc.Value))
|
|
if (err != nil) != tc.Err {
|
|
t.Fatalf("Input: %q\n\nError: %s", tc.Value, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// equals fails the test if exp is not equal to act.
|
|
func equals(tb testing.TB, exp, act interface{}) {
|
|
if !reflect.DeepEqual(exp, act) {
|
|
_, file, line, _ := runtime.Caller(1)
|
|
fmt.Printf("\033[31m%s:%d:\n\n\texp: %s\n\n\tgot: %s\033[39m\n\n", filepath.Base(file), line, exp, act)
|
|
tb.FailNow()
|
|
}
|
|
}
|