// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package hclwrite import ( "bytes" "math/big" "sort" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" ) func TestTokensForValue(t *testing.T) { tests := []struct { Val cty.Value Want Tokens }{ { cty.NullVal(cty.DynamicPseudoType), Tokens{ { Type: hclsyntax.TokenIdent, Bytes: []byte(`null`), }, }, }, { cty.True, Tokens{ { Type: hclsyntax.TokenIdent, Bytes: []byte(`true`), }, }, }, { cty.False, Tokens{ { Type: hclsyntax.TokenIdent, Bytes: []byte(`false`), }, }, }, { cty.NumberIntVal(0), Tokens{ { Type: hclsyntax.TokenNumberLit, Bytes: []byte(`0`), }, }, }, { cty.NumberFloatVal(0.5), Tokens{ { Type: hclsyntax.TokenNumberLit, Bytes: []byte(`0.5`), }, }, }, { cty.NumberVal(big.NewFloat(0).SetPrec(512).Mul(big.NewFloat(40000000), big.NewFloat(2000000))), Tokens{ { Type: hclsyntax.TokenNumberLit, Bytes: []byte(`80000000000000`), }, }, }, { cty.StringVal(""), Tokens{ { Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), }, { Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`), }, }, }, { cty.StringVal("foo"), Tokens{ { Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), }, { Type: hclsyntax.TokenQuotedLit, Bytes: []byte(`foo`), }, { Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`), }, }, }, { cty.StringVal(`"foo"`), Tokens{ { Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), }, { Type: hclsyntax.TokenQuotedLit, Bytes: []byte(`\"foo\"`), }, { Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`), }, }, }, { cty.StringVal("hello\nworld\n"), Tokens{ { Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), }, { Type: hclsyntax.TokenQuotedLit, Bytes: []byte(`hello\nworld\n`), }, { Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`), }, }, }, { cty.StringVal("hello\r\nworld\r\n"), Tokens{ { Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), }, { Type: hclsyntax.TokenQuotedLit, Bytes: []byte(`hello\r\nworld\r\n`), }, { Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`), }, }, }, { cty.StringVal(`what\what`), Tokens{ { Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), }, { Type: hclsyntax.TokenQuotedLit, Bytes: []byte(`what\\what`), }, { Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`), }, }, }, { cty.StringVal("𝄞"), Tokens{ { Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), }, { Type: hclsyntax.TokenQuotedLit, Bytes: []byte("𝄞"), }, { Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`), }, }, }, { cty.StringVal("👩🏾"), Tokens{ { Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), }, { Type: hclsyntax.TokenQuotedLit, Bytes: []byte(`👩🏾`), }, { Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`), }, }, }, { cty.EmptyTupleVal, Tokens{ { Type: hclsyntax.TokenOBrack, Bytes: []byte(`[`), }, { Type: hclsyntax.TokenCBrack, Bytes: []byte(`]`), }, }, }, { cty.TupleVal([]cty.Value{cty.EmptyTupleVal}), Tokens{ { Type: hclsyntax.TokenOBrack, Bytes: []byte(`[`), }, { Type: hclsyntax.TokenOBrack, Bytes: []byte(`[`), }, { Type: hclsyntax.TokenCBrack, Bytes: []byte(`]`), }, { Type: hclsyntax.TokenCBrack, Bytes: []byte(`]`), }, }, }, { cty.ListValEmpty(cty.String), Tokens{ { Type: hclsyntax.TokenOBrack, Bytes: []byte(`[`), }, { Type: hclsyntax.TokenCBrack, Bytes: []byte(`]`), }, }, }, { cty.SetValEmpty(cty.Bool), Tokens{ { Type: hclsyntax.TokenOBrack, Bytes: []byte(`[`), }, { Type: hclsyntax.TokenCBrack, Bytes: []byte(`]`), }, }, }, { cty.TupleVal([]cty.Value{cty.True}), Tokens{ { Type: hclsyntax.TokenOBrack, Bytes: []byte(`[`), }, { Type: hclsyntax.TokenIdent, Bytes: []byte(`true`), }, { Type: hclsyntax.TokenCBrack, Bytes: []byte(`]`), }, }, }, { cty.TupleVal([]cty.Value{cty.True, cty.NumberIntVal(0)}), Tokens{ { Type: hclsyntax.TokenOBrack, Bytes: []byte(`[`), }, { Type: hclsyntax.TokenIdent, Bytes: []byte(`true`), }, { Type: hclsyntax.TokenComma, Bytes: []byte(`,`), }, { Type: hclsyntax.TokenNumberLit, Bytes: []byte(`0`), SpacesBefore: 1, }, { Type: hclsyntax.TokenCBrack, Bytes: []byte(`]`), }, }, }, { cty.EmptyObjectVal, Tokens{ { Type: hclsyntax.TokenOBrace, Bytes: []byte(`{`), }, { Type: hclsyntax.TokenCBrace, Bytes: []byte(`}`), }, }, }, { cty.MapValEmpty(cty.Bool), Tokens{ { Type: hclsyntax.TokenOBrace, Bytes: []byte(`{`), }, { Type: hclsyntax.TokenCBrace, Bytes: []byte(`}`), }, }, }, { cty.ObjectVal(map[string]cty.Value{ "foo": cty.True, }), Tokens{ { Type: hclsyntax.TokenOBrace, Bytes: []byte(`{`), }, { Type: hclsyntax.TokenNewline, Bytes: []byte("\n"), }, { Type: hclsyntax.TokenIdent, Bytes: []byte(`foo`), SpacesBefore: 2, }, { Type: hclsyntax.TokenEqual, Bytes: []byte(`=`), SpacesBefore: 1, }, { Type: hclsyntax.TokenIdent, Bytes: []byte(`true`), SpacesBefore: 1, }, { Type: hclsyntax.TokenNewline, Bytes: []byte("\n"), }, { Type: hclsyntax.TokenCBrace, Bytes: []byte(`}`), }, }, }, { cty.ObjectVal(map[string]cty.Value{ "foo": cty.True, "bar": cty.NumberIntVal(0), }), Tokens{ { Type: hclsyntax.TokenOBrace, Bytes: []byte(`{`), }, { Type: hclsyntax.TokenNewline, Bytes: []byte("\n"), }, { Type: hclsyntax.TokenIdent, Bytes: []byte(`bar`), SpacesBefore: 2, }, { Type: hclsyntax.TokenEqual, Bytes: []byte(`=`), SpacesBefore: 1, }, { Type: hclsyntax.TokenNumberLit, Bytes: []byte(`0`), SpacesBefore: 1, }, { Type: hclsyntax.TokenNewline, Bytes: []byte("\n"), }, { Type: hclsyntax.TokenIdent, Bytes: []byte(`foo`), SpacesBefore: 2, }, { Type: hclsyntax.TokenEqual, Bytes: []byte(`=`), SpacesBefore: 1, }, { Type: hclsyntax.TokenIdent, Bytes: []byte(`true`), SpacesBefore: 1, }, { Type: hclsyntax.TokenNewline, Bytes: []byte("\n"), }, { Type: hclsyntax.TokenCBrace, Bytes: []byte(`}`), }, }, }, { cty.ObjectVal(map[string]cty.Value{ "foo bar": cty.True, }), Tokens{ { Type: hclsyntax.TokenOBrace, Bytes: []byte(`{`), }, { Type: hclsyntax.TokenNewline, Bytes: []byte("\n"), }, { Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), SpacesBefore: 2, }, { Type: hclsyntax.TokenQuotedLit, Bytes: []byte(`foo bar`), }, { Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`), }, { Type: hclsyntax.TokenEqual, Bytes: []byte(`=`), SpacesBefore: 1, }, { Type: hclsyntax.TokenIdent, Bytes: []byte(`true`), SpacesBefore: 1, }, { Type: hclsyntax.TokenNewline, Bytes: []byte("\n"), }, { Type: hclsyntax.TokenCBrace, Bytes: []byte(`}`), }, }, }, } for _, test := range tests { t.Run(test.Val.GoString(), func(t *testing.T) { got := TokensForValue(test.Val) if !cmp.Equal(got, test.Want) { diff := cmp.Diff(got, test.Want, cmp.Comparer(func(a, b []byte) bool { return bytes.Equal(a, b) })) var gotBuf, wantBuf bytes.Buffer got.WriteTo(&gotBuf) test.Want.WriteTo(&wantBuf) t.Errorf( "wrong result\nvalue: %#v\ngot: %s\nwant: %s\ndiff: %s", test.Val, gotBuf.String(), wantBuf.String(), diff, ) } }) } } func TestTokensForTraversal(t *testing.T) { tests := []struct { Val hcl.Traversal Want Tokens }{ { hcl.Traversal{ hcl.TraverseRoot{Name: "root"}, hcl.TraverseAttr{Name: "attr"}, hcl.TraverseIndex{Key: cty.StringVal("index")}, }, Tokens{ {Type: hclsyntax.TokenIdent, Bytes: []byte("root")}, {Type: hclsyntax.TokenDot, Bytes: []byte(".")}, {Type: hclsyntax.TokenIdent, Bytes: []byte("attr")}, {Type: hclsyntax.TokenOBrack, Bytes: []byte{'['}}, {Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`)}, {Type: hclsyntax.TokenQuotedLit, Bytes: []byte("index")}, {Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)}, {Type: hclsyntax.TokenCBrack, Bytes: []byte{']'}}, }, }, } for _, test := range tests { got := TokensForTraversal(test.Val) if !cmp.Equal(got, test.Want) { diff := cmp.Diff(got, test.Want, cmp.Comparer(func(a, b []byte) bool { return bytes.Equal(a, b) })) var gotBuf, wantBuf bytes.Buffer got.WriteTo(&gotBuf) test.Want.WriteTo(&wantBuf) t.Errorf( "wrong result\nvalue: %#v\ngot: %s\nwant: %s\ndiff: %s", test.Val, gotBuf.String(), wantBuf.String(), diff, ) } } } func TestTokensForTuple(t *testing.T) { tests := map[string]struct { Val []Tokens Want Tokens }{ "no elements": { nil, Tokens{ {Type: hclsyntax.TokenOBrack, Bytes: []byte{'['}}, {Type: hclsyntax.TokenCBrack, Bytes: []byte{']'}}, }, }, "one element": { []Tokens{ TokensForValue(cty.StringVal("foo")), }, Tokens{ {Type: hclsyntax.TokenOBrack, Bytes: []byte{'['}}, {Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`)}, {Type: hclsyntax.TokenQuotedLit, Bytes: []byte("foo")}, {Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)}, {Type: hclsyntax.TokenCBrack, Bytes: []byte{']'}}, }, }, "two elements": { []Tokens{ TokensForTraversal(hcl.Traversal{ hcl.TraverseRoot{Name: "root"}, hcl.TraverseAttr{Name: "attr"}, }), TokensForValue(cty.StringVal("foo")), }, Tokens{ {Type: hclsyntax.TokenOBrack, Bytes: []byte{'['}}, {Type: hclsyntax.TokenIdent, Bytes: []byte("root")}, {Type: hclsyntax.TokenDot, Bytes: []byte(".")}, {Type: hclsyntax.TokenIdent, Bytes: []byte("attr")}, {Type: hclsyntax.TokenComma, Bytes: []byte{','}}, {Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), SpacesBefore: 1}, {Type: hclsyntax.TokenQuotedLit, Bytes: []byte("foo")}, {Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)}, {Type: hclsyntax.TokenCBrack, Bytes: []byte{']'}}, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { got := TokensForTuple(test.Val) if !cmp.Equal(got, test.Want) { diff := cmp.Diff(got, test.Want, cmp.Comparer(func(a, b []byte) bool { return bytes.Equal(a, b) })) var gotBuf, wantBuf bytes.Buffer got.WriteTo(&gotBuf) test.Want.WriteTo(&wantBuf) t.Errorf( "wrong result\nvalue: %#v\ngot: %s\nwant: %s\ndiff: %s", test.Val, gotBuf.String(), wantBuf.String(), diff, ) } }) } } func TestTokensForObject(t *testing.T) { tests := map[string]struct { Val []ObjectAttrTokens Want Tokens }{ "no attributes": { nil, Tokens{ {Type: hclsyntax.TokenOBrace, Bytes: []byte{'{'}}, {Type: hclsyntax.TokenCBrace, Bytes: []byte{'}'}}, }, }, "one attribute": { []ObjectAttrTokens{ { Name: TokensForTraversal(hcl.Traversal{ hcl.TraverseRoot{Name: "bar"}, }), Value: TokensForValue(cty.StringVal("baz")), }, }, Tokens{ {Type: hclsyntax.TokenOBrace, Bytes: []byte{'{'}}, {Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}}, {Type: hclsyntax.TokenIdent, Bytes: []byte("bar"), SpacesBefore: 2}, {Type: hclsyntax.TokenEqual, Bytes: []byte{'='}, SpacesBefore: 1}, {Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), SpacesBefore: 1}, {Type: hclsyntax.TokenQuotedLit, Bytes: []byte("baz")}, {Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)}, {Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}}, {Type: hclsyntax.TokenCBrace, Bytes: []byte{'}'}}, }, }, "two attributes": { []ObjectAttrTokens{ { Name: TokensForTraversal(hcl.Traversal{ hcl.TraverseRoot{Name: "foo"}, }), Value: TokensForTraversal(hcl.Traversal{ hcl.TraverseRoot{Name: "root"}, hcl.TraverseAttr{Name: "attr"}, }), }, { Name: TokensForTraversal(hcl.Traversal{ hcl.TraverseRoot{Name: "bar"}, }), Value: TokensForValue(cty.StringVal("baz")), }, }, Tokens{ {Type: hclsyntax.TokenOBrace, Bytes: []byte{'{'}}, {Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}}, {Type: hclsyntax.TokenIdent, Bytes: []byte("foo"), SpacesBefore: 2}, {Type: hclsyntax.TokenEqual, Bytes: []byte{'='}, SpacesBefore: 1}, {Type: hclsyntax.TokenIdent, Bytes: []byte("root"), SpacesBefore: 1}, {Type: hclsyntax.TokenDot, Bytes: []byte(".")}, {Type: hclsyntax.TokenIdent, Bytes: []byte("attr")}, {Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}}, {Type: hclsyntax.TokenIdent, Bytes: []byte("bar"), SpacesBefore: 2}, {Type: hclsyntax.TokenEqual, Bytes: []byte{'='}, SpacesBefore: 1}, {Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), SpacesBefore: 1}, {Type: hclsyntax.TokenQuotedLit, Bytes: []byte("baz")}, {Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)}, {Type: hclsyntax.TokenNewline, Bytes: []byte{'\n'}}, {Type: hclsyntax.TokenCBrace, Bytes: []byte{'}'}}, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { got := TokensForObject(test.Val) if !cmp.Equal(got, test.Want) { diff := cmp.Diff(got, test.Want, cmp.Comparer(func(a, b []byte) bool { return bytes.Equal(a, b) })) var gotBuf, wantBuf bytes.Buffer got.WriteTo(&gotBuf) test.Want.WriteTo(&wantBuf) t.Errorf( "wrong result\nvalue: %#v\ngot: %s\nwant: %s\ndiff: %s", test.Val, gotBuf.String(), wantBuf.String(), diff, ) } }) } } func TestTokensForFunctionCall(t *testing.T) { tests := map[string]struct { FuncName string Val []Tokens Want Tokens }{ "no arguments": { "uuid", nil, Tokens{ {Type: hclsyntax.TokenIdent, Bytes: []byte("uuid")}, {Type: hclsyntax.TokenOParen, Bytes: []byte{'('}}, {Type: hclsyntax.TokenCParen, Bytes: []byte(")")}, }, }, "one argument": { "strlen", []Tokens{ TokensForValue(cty.StringVal("hello")), }, Tokens{ {Type: hclsyntax.TokenIdent, Bytes: []byte("strlen")}, {Type: hclsyntax.TokenOParen, Bytes: []byte{'('}}, {Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`)}, {Type: hclsyntax.TokenQuotedLit, Bytes: []byte("hello")}, {Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)}, {Type: hclsyntax.TokenCParen, Bytes: []byte(")")}, }, }, "two arguments": { "list", []Tokens{ TokensForIdentifier("string"), TokensForIdentifier("int"), }, Tokens{ {Type: hclsyntax.TokenIdent, Bytes: []byte("list")}, {Type: hclsyntax.TokenOParen, Bytes: []byte{'('}}, {Type: hclsyntax.TokenIdent, Bytes: []byte("string")}, {Type: hclsyntax.TokenComma, Bytes: []byte(",")}, {Type: hclsyntax.TokenIdent, Bytes: []byte("int"), SpacesBefore: 1}, {Type: hclsyntax.TokenCParen, Bytes: []byte(")")}, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { got := TokensForFunctionCall(test.FuncName, test.Val...) if !cmp.Equal(got, test.Want) { diff := cmp.Diff(got, test.Want, cmp.Comparer(func(a, b []byte) bool { return bytes.Equal(a, b) })) var gotBuf, wantBuf bytes.Buffer got.WriteTo(&gotBuf) test.Want.WriteTo(&wantBuf) t.Errorf( "wrong result\nvalue: %#v\ngot: %s\nwant: %s\ndiff: %s", test.Val, gotBuf.String(), wantBuf.String(), diff, ) } }) } } func TestTokenGenerateConsistency(t *testing.T) { bytesComparer := cmp.Comparer(func(a, b []byte) bool { return bytes.Equal(a, b) }) // This test verifies that different ways of generating equivalent token // sequences all generate identical tokens, to help us keep them all in // sync under future maintanence. t.Run("tuple constructor", func(t *testing.T) { tests := map[string]struct { elems []cty.Value }{ "no elements": { nil, }, "one element": { []cty.Value{ cty.StringVal("hello"), }, }, "two elements": { []cty.Value{ cty.StringVal("hello"), cty.StringVal("world"), }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { var listVal cty.Value if len(test.elems) > 0 { listVal = cty.ListVal(test.elems) } else { listVal = cty.ListValEmpty(cty.DynamicPseudoType) } fromListValue := TokensForValue(listVal) fromTupleValue := TokensForValue(cty.TupleVal(test.elems)) elemTokens := make([]Tokens, len(test.elems)) for i, v := range test.elems { elemTokens[i] = TokensForValue(v) } fromTupleTokens := TokensForTuple(elemTokens) if diff := cmp.Diff(fromListValue, fromTupleTokens, bytesComparer); diff != "" { t.Errorf("inconsistency between TokensForValue(list) and TokensForTuple\n%s", diff) } if diff := cmp.Diff(fromTupleValue, fromTupleTokens, bytesComparer); diff != "" { t.Errorf("inconsistency between TokensForValue(tuple) and TokensForTuple\n%s", diff) } }) } }) t.Run("object constructor", func(t *testing.T) { tests := map[string]struct { attrs map[string]cty.Value }{ "no elements": { nil, }, "one element": { map[string]cty.Value{ "greeting": cty.StringVal("hello"), }, }, "two elements": { map[string]cty.Value{ "greeting1": cty.StringVal("hello"), "greeting2": cty.StringVal("world"), }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { var mapVal cty.Value if len(test.attrs) > 0 { mapVal = cty.MapVal(test.attrs) } else { mapVal = cty.MapValEmpty(cty.DynamicPseudoType) } fromMapValue := TokensForValue(mapVal) fromObjectValue := TokensForValue(cty.ObjectVal(test.attrs)) attrTokens := make([]ObjectAttrTokens, 0, len(test.attrs)) // TokensForValue always writes the keys/attributes in cty's // standard iteration order, but TokensForObject gives the // caller direct control of the ordering. The result is // therefore consistent only if the given attributes are // pre-sorted into the same iteration order, which is a lexical // sort by attribute name. keys := make([]string, 0, len(test.attrs)) for k := range test.attrs { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { v := test.attrs[k] attrTokens = append(attrTokens, ObjectAttrTokens{ Name: TokensForIdentifier(k), Value: TokensForValue(v), }) } fromObjectTokens := TokensForObject(attrTokens) if diff := cmp.Diff(fromMapValue, fromObjectTokens, bytesComparer); diff != "" { t.Errorf("inconsistency between TokensForValue(map) and TokensForObject\n%s", diff) } if diff := cmp.Diff(fromObjectValue, fromObjectTokens, bytesComparer); diff != "" { t.Errorf("inconsistency between TokensForValue(object) and TokensForObject\n%s", diff) } }) } }) }