blob: 2f5218e4bfcef7c8dc86adea571622ef673d68c0 [file] [log] [blame]
package parser
import (
"fmt"
"io/ioutil"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"google3/third_party/golang/hashicorp/hcl/hcl/ast/ast"
"google3/third_party/golang/hashicorp/hcl/hcl/token/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 = [false]`,
[]token.Type{token.BOOL},
},
{
`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},
{"# Hello\r\n# Windows", 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_crlf.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,
},
{
"complex_crlf.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,
},
{
"git_crypt.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()
}
}