hcl/decoder_test.go
Mitchell Hashimoto c03d57b578
json/parser: empty list value should not flatten
Fixes https://github.com/hashicorp/terraform/issues/8886

While parsing JSON, an empty list value would be assumed to be a
non-existent list of objects, and would be removed from the result. This
may have never been the correct behavior but always worked okay because
we previously didn't support lists as first class types.

With the support of lists, we need to actually return the empty list as
the type. If we return nothing, then projects like Terraform will think
that the value was never set, which is false.
2016-10-25 10:44:39 -07:00

1098 lines
20 KiB
Go

package hcl
import (
"io/ioutil"
"path/filepath"
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"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",
"qux": "back\\slash",
"bar": "new\nline",
"qax": `slash\:colon`,
"nested": `${HH\:mm\:ss}`,
"nestedquotes": `${"\"stringwrappedinquotes\""}`,
},
},
{
"float.hcl",
false,
map[string]interface{}{
"a": 1.02,
},
},
{
"multiline_bad.hcl",
true,
nil,
},
{
"multiline_literal.hcl",
false,
map[string]interface{}{"multiline_literal": testhelper.Unix2dos(`hello
world`)},
},
{
"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": testhelper.Unix2dos(" bar\n baz\n")},
},
{
"multiline_no_hanging_indent.hcl",
false,
map[string]interface{}{"foo": testhelper.Unix2dos(" 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"},
},
{
"null_strings.json",
false,
map[string]interface{}{
"module": []map[string]interface{}{
map[string]interface{}{
"app": []map[string]interface{}{
map[string]interface{}{"foo": ""},
},
},
},
},
},
{
"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},
},
},
},
},
},
{
"list_of_maps.hcl",
false,
map[string]interface{}{
"foo": []interface{}{
map[string]interface{}{"somekey1": "someval1"},
map[string]interface{}{"somekey2": "someval2", "someextrakey": "someextraval"},
},
},
},
{
"assign_deep.hcl",
false,
map[string]interface{}{
"resource": []interface{}{
map[string]interface{}{
"foo": []interface{}{
map[string]interface{}{
"bar": []map[string]interface{}{
map[string]interface{}{}}}}}}},
},
{
"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,
},
},
},
},
},
},
},
},
{
"structure_list_empty.json",
false,
map[string]interface{}{
"foo": []interface{}{},
},
},
{
"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,
nil,
},
{
"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",
},
},
},
},
},
},
},
},
},
},
},
},
// Terraform GH-8295 sanity test that basic decoding into
// interface{} works.
{
"terraform_variable_invalid.json",
false,
map[string]interface{}{
"variable": []map[string]interface{}{
map[string]interface{}{
"whatever": "abc123",
},
},
},
},
{
"interpolate.json",
false,
map[string]interface{}{
"default": `${replace("europe-west", "-", " ")}`,
},
},
{
"block_assign.hcl",
true,
nil,
},
}
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_interfaceInline(t *testing.T) {
cases := []struct {
Value string
Err bool
Out interface{}
}{
{"t t e{{}}", true, nil},
{"t=0t d {}", true, map[string]interface{}{"t": 0}},
{"v=0E0v d{}", true, map[string]interface{}{"v": float64(0)}},
}
for _, tc := range cases {
t.Logf("Testing: %q", tc.Value)
var out interface{}
err := Decode(&out, tc.Value)
if (err != nil) != tc.Err {
t.Fatalf("Input: %q\n\nError: %s", tc.Value, err)
}
if !reflect.DeepEqual(out, tc.Out) {
t.Fatalf("Input: %q. Actual, Expected.\n\n%#v\n\n%#v", tc.Value, out, tc.Out)
}
var v interface{}
err = Unmarshal([]byte(tc.Value), &v)
if (err != nil) != tc.Err {
t.Fatalf("Input: %q\n\nError: %s", tc.Value, err)
}
if !reflect.DeepEqual(v, tc.Out) {
t.Fatalf("Input: %q. Actual, Expected.\n\n%#v\n\n%#v", tc.Value, 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 Embedded interface{}
type V struct {
Embedded `hcl:"-"`
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_structureMapInvalid(t *testing.T) {
// Terraform GH-8295
type hclVariable struct {
Default interface{}
Description string
Fields []string `hcl:",decodedFields"`
}
type rawConfig struct {
Variable map[string]*hclVariable
}
var actual rawConfig
err := Decode(&actual, testReadFile(t, "terraform_variable_invalid.json"))
if err == nil {
t.Fatal("expected error")
}
}
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)
}
}
func TestDecode_flattenedJSON(t *testing.T) {
// make sure we can also correctly extract a Name key too
type V struct {
Name string `hcl:",key"`
Description string
Default map[string]string
}
type Vars struct {
Variable []*V
}
cases := []struct {
JSON string
Out interface{}
Expected interface{}
}{
{ // Nested object, no sibling keys
JSON: `
{
"var_name": {
"default": {
"key1": "a",
"key2": "b"
}
}
}
`,
Out: &[]*V{},
Expected: &[]*V{
&V{
Name: "var_name",
Default: map[string]string{"key1": "a", "key2": "b"},
},
},
},
{ // Nested object with a sibling key (this worked previously)
JSON: `
{
"var_name": {
"description": "Described",
"default": {
"key1": "a",
"key2": "b"
}
}
}
`,
Out: &[]*V{},
Expected: &[]*V{
&V{
Name: "var_name",
Description: "Described",
Default: map[string]string{"key1": "a", "key2": "b"},
},
},
},
{ // Multiple nested objects, one with a sibling key
JSON: `
{
"variable": {
"var_1": {
"default": {
"key1": "a",
"key2": "b"
}
},
"var_2": {
"description": "Described",
"default": {
"key1": "a",
"key2": "b"
}
}
}
}
`,
Out: &Vars{},
Expected: &Vars{
Variable: []*V{
&V{
Name: "var_1",
Default: map[string]string{"key1": "a", "key2": "b"},
},
&V{
Name: "var_2",
Description: "Described",
Default: map[string]string{"key1": "a", "key2": "b"},
},
},
},
},
{ // Nested object to maps
JSON: `
{
"variable": {
"var_name": {
"description": "Described",
"default": {
"key1": "a",
"key2": "b"
}
}
}
}
`,
Out: &[]map[string]interface{}{},
Expected: &[]map[string]interface{}{
{
"variable": []map[string]interface{}{
{
"var_name": []map[string]interface{}{
{
"description": "Described",
"default": []map[string]interface{}{
{
"key1": "a",
"key2": "b",
},
},
},
},
},
},
},
},
},
{ // Nested object to maps without a sibling key should decode the same as above
JSON: `
{
"variable": {
"var_name": {
"default": {
"key1": "a",
"key2": "b"
}
}
}
}
`,
Out: &[]map[string]interface{}{},
Expected: &[]map[string]interface{}{
{
"variable": []map[string]interface{}{
{
"var_name": []map[string]interface{}{
{
"default": []map[string]interface{}{
{
"key1": "a",
"key2": "b",
},
},
},
},
},
},
},
},
},
{ // Nested objects, one with a sibling key, and one without
JSON: `
{
"variable": {
"var_1": {
"default": {
"key1": "a",
"key2": "b"
}
},
"var_2": {
"description": "Described",
"default": {
"key1": "a",
"key2": "b"
}
}
}
}
`,
Out: &[]map[string]interface{}{},
Expected: &[]map[string]interface{}{
{
"variable": []map[string]interface{}{
{
"var_1": []map[string]interface{}{
{
"default": []map[string]interface{}{
{
"key1": "a",
"key2": "b",
},
},
},
},
},
},
},
{
"variable": []map[string]interface{}{
{
"var_2": []map[string]interface{}{
{
"description": "Described",
"default": []map[string]interface{}{
{
"key1": "a",
"key2": "b",
},
},
},
},
},
},
},
},
},
}
for i, tc := range cases {
err := Decode(tc.Out, tc.JSON)
if err != nil {
t.Fatalf("[%d] err: %s", i, err)
}
if !reflect.DeepEqual(tc.Out, tc.Expected) {
t.Fatalf("[%d]\ngot: %s\nexpected: %s\n", i, spew.Sdump(tc.Out), spew.Sdump(tc.Expected))
}
}
}