hcl/decoder_test.go
Mitchell Hashimoto 40a9504600
fix panic when decoding invalid value structure into struct
When decoding an object into a struct where the object structure doesn't
match the Go struct structure, the case tested here would panic. This
introduces additional checks to guard against the edge case being hit to
avoid the panic.

The specific checks being added are: if an item being decoded into a
struct is a literal type, the item to be decoded must be non-nil in
order to use it. This isn't super clear and to be honest I also don't
fully understand it but this fixes the problem without introducing any
more test failures and without significant code complexity.
2016-08-21 23:44:35 -07:00

840 lines
15 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",
"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,
},
},
},
},
},
},
},
},
{
"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",
},
},
},
},
}
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)
}
}