559 lines
9.9 KiB
Go
559 lines
9.9 KiB
Go
package parser
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"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 = "foo"`},
|
|
{token.NUMBER, `foo = 123`},
|
|
{token.NUMBER, `foo = -29`},
|
|
{token.FLOAT, `foo = 123.12`},
|
|
{token.FLOAT, `foo = -123.12`},
|
|
{token.BOOL, `foo = true`},
|
|
{token.HEREDOC, "foo = <<EOF\nHello\nWorld\nEOF"},
|
|
}
|
|
|
|
for _, l := range literals {
|
|
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 = [1,
|
|
"string",
|
|
<<EOF
|
|
heredoc contents
|
|
EOF
|
|
]`,
|
|
[]token.Type{token.NUMBER, token.STRING, token.HEREDOC},
|
|
},
|
|
}
|
|
|
|
for _, l := range literals {
|
|
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 {
|
|
if tp, ok := li.(*ast.LiteralType); ok {
|
|
tokens = append(tokens, tp.Token.Type)
|
|
}
|
|
}
|
|
|
|
equals(t, l.tokens, tokens)
|
|
}
|
|
}
|
|
|
|
func TestListOfMaps(t *testing.T) {
|
|
src := `foo = [
|
|
{key = "bar"},
|
|
{key = "baz", key2 = "qux"},
|
|
]`
|
|
p := newParser([]byte(src))
|
|
|
|
file, err := p.Parse()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// Here we make all sorts of assumptions about the input structure w/ type
|
|
// assertions. The intent is only for this to be a "smoke test" ensuring
|
|
// parsing actually performed its duty - giving this test something a bit
|
|
// more robust than _just_ "no error occurred".
|
|
expected := []string{`"bar"`, `"baz"`, `"qux"`}
|
|
actual := make([]string, 0, 3)
|
|
ol := file.Node.(*ast.ObjectList)
|
|
objItem := ol.Items[0]
|
|
list := objItem.Val.(*ast.ListType)
|
|
for _, node := range list.List {
|
|
obj := node.(*ast.ObjectType)
|
|
for _, item := range obj.List.Items {
|
|
val := item.Val.(*ast.LiteralType)
|
|
actual = append(actual, val.Token.Text)
|
|
}
|
|
|
|
}
|
|
if !reflect.DeepEqual(expected, actual) {
|
|
t.Fatalf("Expected: %#v, got %#v", expected, actual)
|
|
}
|
|
}
|
|
|
|
func TestListOfMaps_requiresComma(t *testing.T) {
|
|
src := `foo = [
|
|
{key = "bar"}
|
|
{key = "baz"}
|
|
]`
|
|
p := newParser([]byte(src))
|
|
|
|
_, err := p.Parse()
|
|
if err == nil {
|
|
t.Fatalf("Expected error, got none!")
|
|
}
|
|
|
|
expected := "error parsing list, expected comma or list end"
|
|
if !strings.Contains(err.Error(), expected) {
|
|
t.Fatalf("Expected err:\n %s\nTo contain:\n %s\n", err, expected)
|
|
}
|
|
}
|
|
|
|
func TestListType_leadComment(t *testing.T) {
|
|
var literals = []struct {
|
|
src string
|
|
comment []string
|
|
}{
|
|
{
|
|
`foo = [
|
|
1,
|
|
# bar
|
|
2,
|
|
3,
|
|
]`,
|
|
[]string{"", "# bar", ""},
|
|
},
|
|
}
|
|
|
|
for _, l := range literals {
|
|
p := newParser([]byte(l.src))
|
|
item, err := p.objectItem()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
list, ok := item.Val.(*ast.ListType)
|
|
if !ok {
|
|
t.Fatalf("node should be of type LiteralType, got: %T", item.Val)
|
|
}
|
|
|
|
if len(list.List) != len(l.comment) {
|
|
t.Fatalf("bad: %d", len(list.List))
|
|
}
|
|
|
|
for i, li := range list.List {
|
|
lt := li.(*ast.LiteralType)
|
|
comment := l.comment[i]
|
|
|
|
if (lt.LeadComment == nil) != (comment == "") {
|
|
t.Fatalf("bad: %#v", lt)
|
|
}
|
|
|
|
if comment == "" {
|
|
continue
|
|
}
|
|
|
|
actual := lt.LeadComment.List[0].Text
|
|
if actual != comment {
|
|
t.Fatalf("bad: %q %q", actual, comment)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListType_lineComment(t *testing.T) {
|
|
var literals = []struct {
|
|
src string
|
|
comment []string
|
|
}{
|
|
{
|
|
`foo = [
|
|
1,
|
|
2, # bar
|
|
3,
|
|
]`,
|
|
[]string{"", "# bar", ""},
|
|
},
|
|
}
|
|
|
|
for _, l := range literals {
|
|
p := newParser([]byte(l.src))
|
|
item, err := p.objectItem()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
list, ok := item.Val.(*ast.ListType)
|
|
if !ok {
|
|
t.Fatalf("node should be of type LiteralType, got: %T", item.Val)
|
|
}
|
|
|
|
if len(list.List) != len(l.comment) {
|
|
t.Fatalf("bad: %d", len(list.List))
|
|
}
|
|
|
|
for i, li := range list.List {
|
|
lt := li.(*ast.LiteralType)
|
|
comment := l.comment[i]
|
|
|
|
if (lt.LineComment == nil) != (comment == "") {
|
|
t.Fatalf("bad: %s", lt)
|
|
}
|
|
|
|
if comment == "" {
|
|
continue
|
|
}
|
|
|
|
actual := lt.LineComment.List[0].Text
|
|
if actual != comment {
|
|
t.Fatalf("bad: %q %q", actual, comment)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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("Source: %s", l.src)
|
|
|
|
p := newParser([]byte(l.src))
|
|
// p.enableTrace = true
|
|
item, err := p.objectItem()
|
|
if err != nil {
|
|
t.Error(err)
|
|
continue
|
|
}
|
|
|
|
// 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)
|
|
continue
|
|
}
|
|
|
|
// 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 TestObjectKey(t *testing.T) {
|
|
keys := []struct {
|
|
exp []token.Type
|
|
src string
|
|
}{
|
|
{[]token.Type{token.IDENT}, `foo {}`},
|
|
{[]token.Type{token.IDENT}, `foo = {}`},
|
|
{[]token.Type{token.IDENT}, `foo = bar`},
|
|
{[]token.Type{token.IDENT}, `foo = 123`},
|
|
{[]token.Type{token.IDENT}, `foo = "${var.bar}`},
|
|
{[]token.Type{token.STRING}, `"foo" {}`},
|
|
{[]token.Type{token.STRING}, `"foo" = {}`},
|
|
{[]token.Type{token.STRING}, `"foo" = "${var.bar}`},
|
|
{[]token.Type{token.IDENT, token.IDENT}, `foo bar {}`},
|
|
{[]token.Type{token.IDENT, token.STRING}, `foo "bar" {}`},
|
|
{[]token.Type{token.STRING, token.IDENT}, `"foo" bar {}`},
|
|
{[]token.Type{token.IDENT, token.IDENT, token.IDENT}, `foo bar baz {}`},
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommentGroup(t *testing.T) {
|
|
var cases = []struct {
|
|
src string
|
|
groups int
|
|
}{
|
|
{"# Hello\n# World", 1},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.src, func(t *testing.T) {
|
|
p := newParser([]byte(tc.src))
|
|
file, err := p.Parse()
|
|
if err != nil {
|
|
t.Fatalf("parse error: %s", err)
|
|
}
|
|
|
|
if len(file.Comments) != tc.groups {
|
|
t.Fatalf("bad: %#v", file.Comments)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Official HCL tests
|
|
func TestParse(t *testing.T) {
|
|
cases := []struct {
|
|
Name string
|
|
Err bool
|
|
}{
|
|
{
|
|
"assign_colon.hcl",
|
|
true,
|
|
},
|
|
{
|
|
"comment.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"comment_lastline.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"comment_single.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"empty.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"list_comma.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"multiple.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"object_list_comma.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"structure.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"structure_basic.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"structure_empty.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"complex.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"types.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"array_comment.hcl",
|
|
false,
|
|
},
|
|
{
|
|
"array_comment_2.hcl",
|
|
true,
|
|
},
|
|
{
|
|
"missing_braces.hcl",
|
|
true,
|
|
},
|
|
{
|
|
"unterminated_object.hcl",
|
|
true,
|
|
},
|
|
{
|
|
"unterminated_object_2.hcl",
|
|
true,
|
|
},
|
|
{
|
|
"key_without_value.hcl",
|
|
true,
|
|
},
|
|
{
|
|
"object_key_without_value.hcl",
|
|
true,
|
|
},
|
|
{
|
|
"object_key_assign_without_value.hcl",
|
|
true,
|
|
},
|
|
{
|
|
"object_key_assign_without_value2.hcl",
|
|
true,
|
|
},
|
|
{
|
|
"object_key_assign_without_value3.hcl",
|
|
true,
|
|
},
|
|
}
|
|
|
|
const fixtureDir = "./test-fixtures"
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
d, err := ioutil.ReadFile(filepath.Join(fixtureDir, tc.Name))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
v, err := Parse(d)
|
|
if (err != nil) != tc.Err {
|
|
t.Fatalf("Input: %s\n\nError: %s\n\nAST: %#v", tc.Name, err, v)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParse_inline(t *testing.T) {
|
|
cases := []struct {
|
|
Value string
|
|
Err bool
|
|
}{
|
|
{"t t e{{}}", true},
|
|
{"o{{}}", true},
|
|
{"t t e d N{{}}", true},
|
|
{"t t e d{{}}", true},
|
|
{"N{}N{{}}", true},
|
|
{"v\nN{{}}", true},
|
|
{"v=/\n[,", true},
|
|
{"v=10kb", true},
|
|
{"v=/foo", true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Logf("Testing: %q", tc.Value)
|
|
ast, err := Parse([]byte(tc.Value))
|
|
if (err != nil) != tc.Err {
|
|
t.Fatalf("Input: %q\n\nError: %s\n\nAST: %#v", tc.Value, err, ast)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act)
|
|
tb.FailNow()
|
|
}
|
|
}
|