| 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() |
| } |
| } |