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, }, { "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() } }