blob: 06fa0944d365ee146ef17443dfbbd761e31900b9 [file] [log] [blame]
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)
}
})
}
})
}