hcl/decoder_test.go
Phillip T. George 65a6292f01 Allow strings "true", "false", "1" and "0" to be used where booleans are expected
There is precedent for allowing strings containing digits where numbers are expected,
and so this extends that to also allow for boolean values to be given as strings.

This applies only to callers going through the decoder API. Direct access via the AST
will reflect exactly what was given in the input configuration.
2018-09-06 11:38:39 -07:00

1249 lines
22 KiB
Go

package hcl
import (
"io/ioutil"
"path/filepath"
"reflect"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl/hcl/ast"
)
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,
"b": 2,
},
},
{
"multiline_bad.hcl",
true,
nil,
},
{
"multiline_literal.hcl",
true,
nil,
},
{
"multiline_literal_with_hil.hcl",
false,
map[string]interface{}{"multiline_literal_with_hil": "${hello\n world}"},
},
{
"multiline_no_marker.hcl",
true,
nil,
},
{
"multiline.hcl",
false,
map[string]interface{}{"foo": "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": "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_lists.hcl",
false,
map[string]interface{}{
"foo": []interface{}{
[]interface{}{"foo"},
[]interface{}{"bar"},
},
},
},
{
"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,
},
{
"escape_backslash.hcl",
false,
map[string]interface{}{
"output": []map[string]interface{}{
map[string]interface{}{
"one": `${replace(var.sub_domain, ".", "\\.")}`,
"two": `${replace(var.sub_domain, ".", "\\\\.")}`,
"many": `${replace(var.sub_domain, ".", "\\\\\\\\.")}`,
},
},
},
},
{
"git_crypt.hcl",
true,
nil,
},
{
"object_with_bool.hcl",
false,
map[string]interface{}{
"path": []map[string]interface{}{
map[string]interface{}{
"policy": "write",
"permissions": []map[string]interface{}{
map[string]interface{}{
"bool": []interface{}{false},
},
},
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.File, func(t *testing.T) {
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_boolString(t *testing.T) {
var value struct {
Boolean bool
}
err := Decode(&value, testReadFile(t, "basic_bool_string.hcl"))
if err != nil {
t.Fatalf("err: %s", err)
}
if value.Boolean != true {
t.Fatalf("bad: %#v", value.Boolean)
}
}
func TestDecode_boolInt(t *testing.T) {
var value struct {
Boolean bool
}
err := Decode(&value, testReadFile(t, "basic_bool_int.hcl"))
if err != nil {
t.Fatalf("err: %s", err)
}
if value.Boolean != true {
t.Fatalf("bad: %#v", value.Boolean)
}
}
func TestDecode_bool(t *testing.T) {
var value struct {
Boolean bool
}
err := Decode(&value, testReadFile(t, "basic_bool.hcl"))
if err != nil {
t.Fatalf("err: %s", err)
}
if value.Boolean != true {
t.Fatalf("bad: %#v", value.Boolean)
}
}
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_float32(t *testing.T) {
var value struct {
A float32 `hcl:"a"`
B float32 `hcl:"b"`
}
err := Decode(&value, testReadFile(t, "float.hcl"))
if err != nil {
t.Fatalf("err: %s", err)
}
if got, want := value.A, float32(1.02); got != want {
t.Fatalf("wrong result %#v; want %#v", got, want)
}
if got, want := value.B, float32(2); got != want {
t.Fatalf("wrong result %#v; want %#v", got, want)
}
}
func TestDecode_float64(t *testing.T) {
var value struct {
A float64 `hcl:"a"`
B float64 `hcl:"b"`
}
err := Decode(&value, testReadFile(t, "float.hcl"))
if err != nil {
t.Fatalf("err: %s", err)
}
if got, want := value.A, float64(1.02); got != want {
t.Fatalf("wrong result %#v; want %#v", got, want)
}
if got, want := value.B, float64(2); got != want {
t.Fatalf("wrong result %#v; want %#v", got, want)
}
}
func TestDecode_intStringAliased(t *testing.T) {
var value struct {
Count time.Duration
}
err := Decode(&value, testReadFile(t, "basic_int_string.hcl"))
if err != nil {
t.Fatalf("err: %s", err)
}
if value.Count != time.Duration(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))
}
}
}