f5480db646
This commit adds support for removing a hanging indent from HEREDOC token contents if the marker is prefixed with <<-. For example, given the HCL definition: my_long_var_name = <<-EOF { "key": "value" } EOF The value of the HEREDOC will be: { "key": "value" } This is useful for use cases where indentation or leading whitespace is important. The rule applied is that the prefix on the terminating marker will be removed from each each line in the HEREDOC, providing all the lines have that prefix (i.e. every line is at least as indented as the terminating marker, and using the same mechanism of tabs vs spaces).
731 lines
13 KiB
Go
731 lines
13 KiB
Go
package hcl
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl/hcl/ast"
|
|
"github.com/hashicorp/hcl/testhelper"
|
|
)
|
|
|
|
func TestDecode_interface(t *testing.T) {
|
|
cases := []struct {
|
|
File string
|
|
Err bool
|
|
Out interface{}
|
|
}{
|
|
{
|
|
"basic.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"foo": "bar",
|
|
"bar": "${file(\"bing/bong.txt\")}",
|
|
},
|
|
},
|
|
{
|
|
"basic_squish.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"foo": "bar",
|
|
"bar": "${file(\"bing/bong.txt\")}",
|
|
"foo-bar": "baz",
|
|
},
|
|
},
|
|
{
|
|
"empty.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"resource": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"foo": []map[string]interface{}{
|
|
map[string]interface{}{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"tfvars.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"regularvar": "Should work",
|
|
"map.key1": "Value",
|
|
"map.key2": "Other value",
|
|
},
|
|
},
|
|
{
|
|
"escape.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"foo": "bar\"baz\\n",
|
|
},
|
|
},
|
|
{
|
|
"interpolate_escape.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"foo": "${file(\"bing/bong.txt\")}",
|
|
},
|
|
},
|
|
{
|
|
"float.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"a": 1.02,
|
|
},
|
|
},
|
|
{
|
|
"multiline_bad.hcl",
|
|
true,
|
|
nil,
|
|
},
|
|
{
|
|
"multiline_no_marker.hcl",
|
|
true,
|
|
nil,
|
|
},
|
|
{
|
|
"multiline.hcl",
|
|
false,
|
|
map[string]interface{}{"foo": testhelper.Unix2dos("bar\nbaz\n")},
|
|
},
|
|
{
|
|
"multiline_indented.hcl",
|
|
false,
|
|
map[string]interface{}{"foo": " bar\n baz\n"},
|
|
},
|
|
{
|
|
"multiline_no_hanging_indent.hcl",
|
|
false,
|
|
map[string]interface{}{"foo": " baz\n bar\n foo\n"},
|
|
},
|
|
{
|
|
"multiline_no_eof.hcl",
|
|
false,
|
|
map[string]interface{}{"foo": testhelper.Unix2dos("bar\nbaz\n"), "key": "value"},
|
|
},
|
|
{
|
|
"multiline.json",
|
|
false,
|
|
map[string]interface{}{"foo": "bar\nbaz"},
|
|
},
|
|
{
|
|
"scientific.json",
|
|
false,
|
|
map[string]interface{}{
|
|
"a": 1e-10,
|
|
"b": 1e+10,
|
|
"c": 1e10,
|
|
"d": 1.2e-10,
|
|
"e": 1.2e+10,
|
|
"f": 1.2e10,
|
|
},
|
|
},
|
|
{
|
|
"scientific.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"a": 1e-10,
|
|
"b": 1e+10,
|
|
"c": 1e10,
|
|
"d": 1.2e-10,
|
|
"e": 1.2e+10,
|
|
"f": 1.2e10,
|
|
},
|
|
},
|
|
{
|
|
"terraform_heroku.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"name": "terraform-test-app",
|
|
"config_vars": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"FOO": "bar",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"structure_multi.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"foo": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"baz": []map[string]interface{}{
|
|
map[string]interface{}{"key": 7},
|
|
},
|
|
},
|
|
map[string]interface{}{
|
|
"bar": []map[string]interface{}{
|
|
map[string]interface{}{"key": 12},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"structure_multi.json",
|
|
false,
|
|
map[string]interface{}{
|
|
"foo": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"baz": []map[string]interface{}{
|
|
map[string]interface{}{"key": 7},
|
|
},
|
|
},
|
|
map[string]interface{}{
|
|
"bar": []map[string]interface{}{
|
|
map[string]interface{}{"key": 12},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"structure_list.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"foo": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"key": 7,
|
|
},
|
|
map[string]interface{}{
|
|
"key": 12,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"structure_list.json",
|
|
false,
|
|
map[string]interface{}{
|
|
"foo": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"key": 7,
|
|
},
|
|
map[string]interface{}{
|
|
"key": 12,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"structure_list_deep.json",
|
|
false,
|
|
map[string]interface{}{
|
|
"bar": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"foo": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"name": "terraform_example",
|
|
"ingress": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"from_port": 22,
|
|
},
|
|
map[string]interface{}{
|
|
"from_port": 80,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
{
|
|
"nested_block_comment.hcl",
|
|
false,
|
|
map[string]interface{}{
|
|
"bar": "value",
|
|
},
|
|
},
|
|
|
|
{
|
|
"unterminated_block_comment.hcl",
|
|
true,
|
|
nil,
|
|
},
|
|
|
|
{
|
|
"unterminated_brace.hcl",
|
|
true,
|
|
nil,
|
|
},
|
|
|
|
{
|
|
"nested_provider_bad.hcl",
|
|
true,
|
|
// This is not ideal but without significant rework of the decoder
|
|
// we get a partial result back as well as an error.
|
|
map[string]interface{}{},
|
|
},
|
|
|
|
{
|
|
"object_list.json",
|
|
false,
|
|
map[string]interface{}{
|
|
"resource": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"aws_instance": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"db": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"vpc": "foo",
|
|
"provisioner": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"file": []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"source": "foo",
|
|
"destination": "bar",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Logf("Testing: %s", tc.File)
|
|
d, err := ioutil.ReadFile(filepath.Join(fixtureDir, tc.File))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
var out interface{}
|
|
err = Decode(&out, string(d))
|
|
if (err != nil) != tc.Err {
|
|
t.Fatalf("Input: %s\n\nError: %s", tc.File, err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(out, tc.Out) {
|
|
t.Fatalf("Input: %s. Actual, Expected.\n\n%#v\n\n%#v", tc.File, out, tc.Out)
|
|
}
|
|
|
|
var v interface{}
|
|
err = Unmarshal(d, &v)
|
|
if (err != nil) != tc.Err {
|
|
t.Fatalf("Input: %s\n\nError: %s", tc.File, err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(v, tc.Out) {
|
|
t.Fatalf("Input: %s. Actual, Expected.\n\n%#v\n\n%#v", tc.File, out, tc.Out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecode_equal(t *testing.T) {
|
|
cases := []struct {
|
|
One, Two string
|
|
}{
|
|
{
|
|
"basic.hcl",
|
|
"basic.json",
|
|
},
|
|
{
|
|
"float.hcl",
|
|
"float.json",
|
|
},
|
|
/*
|
|
{
|
|
"structure.hcl",
|
|
"structure.json",
|
|
},
|
|
*/
|
|
{
|
|
"structure.hcl",
|
|
"structure_flat.json",
|
|
},
|
|
{
|
|
"terraform_heroku.hcl",
|
|
"terraform_heroku.json",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
p1 := filepath.Join(fixtureDir, tc.One)
|
|
p2 := filepath.Join(fixtureDir, tc.Two)
|
|
|
|
d1, err := ioutil.ReadFile(p1)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
d2, err := ioutil.ReadFile(p2)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
var i1, i2 interface{}
|
|
err = Decode(&i1, string(d1))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
err = Decode(&i2, string(d2))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(i1, i2) {
|
|
t.Fatalf(
|
|
"%s != %s\n\n%#v\n\n%#v",
|
|
tc.One, tc.Two,
|
|
i1, i2)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecode_flatMap(t *testing.T) {
|
|
var val map[string]map[string]string
|
|
|
|
err := Decode(&val, testReadFile(t, "structure_flatmap.hcl"))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
expected := map[string]map[string]string{
|
|
"foo": map[string]string{
|
|
"foo": "bar",
|
|
"key": "7",
|
|
},
|
|
}
|
|
|
|
if !reflect.DeepEqual(val, expected) {
|
|
t.Fatalf("Actual: %#v\n\nExpected: %#v", val, expected)
|
|
}
|
|
}
|
|
|
|
func TestDecode_structure(t *testing.T) {
|
|
type V struct {
|
|
Key int
|
|
Foo string
|
|
}
|
|
|
|
var actual V
|
|
|
|
err := Decode(&actual, testReadFile(t, "flat.hcl"))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
expected := V{
|
|
Key: 7,
|
|
Foo: "bar",
|
|
}
|
|
|
|
if !reflect.DeepEqual(actual, expected) {
|
|
t.Fatalf("Actual: %#v\n\nExpected: %#v", actual, expected)
|
|
}
|
|
}
|
|
|
|
func TestDecode_structurePtr(t *testing.T) {
|
|
type V struct {
|
|
Key int
|
|
Foo string
|
|
}
|
|
|
|
var actual *V
|
|
|
|
err := Decode(&actual, testReadFile(t, "flat.hcl"))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
expected := &V{
|
|
Key: 7,
|
|
Foo: "bar",
|
|
}
|
|
|
|
if !reflect.DeepEqual(actual, expected) {
|
|
t.Fatalf("Actual: %#v\n\nExpected: %#v", actual, expected)
|
|
}
|
|
}
|
|
|
|
func TestDecode_structureArray(t *testing.T) {
|
|
// This test is extracted from a failure in Consul (consul.io),
|
|
// hence the interesting structure naming.
|
|
|
|
type KeyPolicyType string
|
|
|
|
type KeyPolicy struct {
|
|
Prefix string `hcl:",key"`
|
|
Policy KeyPolicyType
|
|
}
|
|
|
|
type Policy struct {
|
|
Keys []KeyPolicy `hcl:"key,expand"`
|
|
}
|
|
|
|
expected := Policy{
|
|
Keys: []KeyPolicy{
|
|
KeyPolicy{
|
|
Prefix: "",
|
|
Policy: "read",
|
|
},
|
|
KeyPolicy{
|
|
Prefix: "foo/",
|
|
Policy: "write",
|
|
},
|
|
KeyPolicy{
|
|
Prefix: "foo/bar/",
|
|
Policy: "read",
|
|
},
|
|
KeyPolicy{
|
|
Prefix: "foo/bar/baz",
|
|
Policy: "deny",
|
|
},
|
|
},
|
|
}
|
|
|
|
files := []string{
|
|
"decode_policy.hcl",
|
|
"decode_policy.json",
|
|
}
|
|
|
|
for _, f := range files {
|
|
var actual Policy
|
|
|
|
err := Decode(&actual, testReadFile(t, f))
|
|
if err != nil {
|
|
t.Fatalf("Input: %s\n\nerr: %s", f, err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(actual, expected) {
|
|
t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecode_sliceExpand(t *testing.T) {
|
|
type testInner struct {
|
|
Name string `hcl:",key"`
|
|
Key string
|
|
}
|
|
|
|
type testStruct struct {
|
|
Services []testInner `hcl:"service,expand"`
|
|
}
|
|
|
|
expected := testStruct{
|
|
Services: []testInner{
|
|
testInner{
|
|
Name: "my-service-0",
|
|
Key: "value",
|
|
},
|
|
testInner{
|
|
Name: "my-service-1",
|
|
Key: "value",
|
|
},
|
|
},
|
|
}
|
|
|
|
files := []string{
|
|
"slice_expand.hcl",
|
|
}
|
|
|
|
for _, f := range files {
|
|
t.Logf("Testing: %s", f)
|
|
|
|
var actual testStruct
|
|
err := Decode(&actual, testReadFile(t, f))
|
|
if err != nil {
|
|
t.Fatalf("Input: %s\n\nerr: %s", f, err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(actual, expected) {
|
|
t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecode_structureMap(t *testing.T) {
|
|
// This test is extracted from a failure in Terraform (terraform.io),
|
|
// hence the interesting structure naming.
|
|
|
|
type hclVariable struct {
|
|
Default interface{}
|
|
Description string
|
|
Fields []string `hcl:",decodedFields"`
|
|
}
|
|
|
|
type rawConfig struct {
|
|
Variable map[string]hclVariable
|
|
}
|
|
|
|
expected := rawConfig{
|
|
Variable: map[string]hclVariable{
|
|
"foo": hclVariable{
|
|
Default: "bar",
|
|
Description: "bar",
|
|
Fields: []string{"Default", "Description"},
|
|
},
|
|
|
|
"amis": hclVariable{
|
|
Default: []map[string]interface{}{
|
|
map[string]interface{}{
|
|
"east": "foo",
|
|
},
|
|
},
|
|
Fields: []string{"Default"},
|
|
},
|
|
},
|
|
}
|
|
|
|
files := []string{
|
|
"decode_tf_variable.hcl",
|
|
"decode_tf_variable.json",
|
|
}
|
|
|
|
for _, f := range files {
|
|
t.Logf("Testing: %s", f)
|
|
|
|
var actual rawConfig
|
|
err := Decode(&actual, testReadFile(t, f))
|
|
if err != nil {
|
|
t.Fatalf("Input: %s\n\nerr: %s", f, err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(actual, expected) {
|
|
t.Fatalf("Input: %s\n\nActual: %#v\n\nExpected: %#v", f, actual, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecode_interfaceNonPointer(t *testing.T) {
|
|
var value interface{}
|
|
err := Decode(value, testReadFile(t, "basic_int_string.hcl"))
|
|
if err == nil {
|
|
t.Fatal("should error")
|
|
}
|
|
}
|
|
|
|
func TestDecode_intString(t *testing.T) {
|
|
var value struct {
|
|
Count int
|
|
}
|
|
|
|
err := Decode(&value, testReadFile(t, "basic_int_string.hcl"))
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if value.Count != 3 {
|
|
t.Fatalf("bad: %#v", value.Count)
|
|
}
|
|
}
|
|
|
|
func TestDecode_Node(t *testing.T) {
|
|
// given
|
|
var value struct {
|
|
Content ast.Node
|
|
Nested struct {
|
|
Content ast.Node
|
|
}
|
|
}
|
|
|
|
content := `
|
|
content {
|
|
hello = "world"
|
|
}
|
|
`
|
|
|
|
// when
|
|
err := Decode(&value, content)
|
|
|
|
// then
|
|
if err != nil {
|
|
t.Errorf("unable to decode content, %v", err)
|
|
return
|
|
}
|
|
|
|
// verify ast.Node can be decoded later
|
|
var v map[string]interface{}
|
|
err = DecodeObject(&v, value.Content)
|
|
if err != nil {
|
|
t.Errorf("unable to decode content, %v", err)
|
|
return
|
|
}
|
|
|
|
if v["hello"] != "world" {
|
|
t.Errorf("expected mapping to be returned")
|
|
}
|
|
}
|
|
|
|
func TestDecode_NestedNode(t *testing.T) {
|
|
// given
|
|
var value struct {
|
|
Nested struct {
|
|
Content ast.Node
|
|
}
|
|
}
|
|
|
|
content := `
|
|
nested "content" {
|
|
hello = "world"
|
|
}
|
|
`
|
|
|
|
// when
|
|
err := Decode(&value, content)
|
|
|
|
// then
|
|
if err != nil {
|
|
t.Errorf("unable to decode content, %v", err)
|
|
return
|
|
}
|
|
|
|
// verify ast.Node can be decoded later
|
|
var v map[string]interface{}
|
|
err = DecodeObject(&v, value.Nested.Content)
|
|
if err != nil {
|
|
t.Errorf("unable to decode content, %v", err)
|
|
return
|
|
}
|
|
|
|
if v["hello"] != "world" {
|
|
t.Errorf("expected mapping to be returned")
|
|
}
|
|
}
|
|
|
|
// https://github.com/hashicorp/hcl/issues/60
|
|
func TestDecode_topLevelKeys(t *testing.T) {
|
|
type Template struct {
|
|
Source string
|
|
}
|
|
|
|
templates := struct {
|
|
Templates []*Template `hcl:"template"`
|
|
}{}
|
|
|
|
err := Decode(&templates, `
|
|
template {
|
|
source = "blah"
|
|
}
|
|
|
|
template {
|
|
source = "blahblah"
|
|
}`)
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if templates.Templates[0].Source != "blah" {
|
|
t.Errorf("bad source: %s", templates.Templates[0].Source)
|
|
}
|
|
|
|
if templates.Templates[1].Source != "blahblah" {
|
|
t.Errorf("bad source: %s", templates.Templates[1].Source)
|
|
}
|
|
}
|