Files
opentf/internal/legacy/hcl2shim/values_test.go
Martin Atkins 868dc2f01b hcl2shim: Split out legacy subset
Due to some past confusion about the purpose of this package, it has grown
to include a confusing mix of currently-viable code and legacy support
code from the move to HCL 2. This has in turn caused confusion about which
parts of this package _should_ be used for new code.

To help clarify that distinction we'll move the legacy support code into
a package under the "legacy" directory, which is also where most of its
callers live.

There are unfortunately still some callers to these outside of the legacy
tree, but the vast majority are either old tests written before HCL 2
adoption or helper code used only by those tests. The one dubious exception
is the use in ResourceInstanceObjectSrc.Decode, which makes a best effort
to shim flatmap as a concession to the fact that not all state-loading
codepaths are able to run the provider state upgrade function that would
normally be responsible for the flatmap-to-JSON conversion, which is
explained in a new comment inline.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-07-10 08:13:25 -07:00

421 lines
9.7 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package hcl2shim
import (
"fmt"
"reflect"
"testing"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/zclconf/go-cty/cty"
)
func TestConfigValueFromHCL2Block(t *testing.T) {
tests := []struct {
Input cty.Value
Schema *configschema.Block
Want map[string]interface{}
}{
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Ermintrude"),
"age": cty.NumberIntVal(19),
"address": cty.ObjectVal(map[string]cty.Value{
"street": cty.ListVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}),
"city": cty.StringVal("Fridgewater"),
"state": cty.StringVal("MA"),
"zip": cty.StringVal("91037"),
}),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {Type: cty.String, Optional: true},
"age": {Type: cty.Number, Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"street": {Type: cty.List(cty.String), Optional: true},
"city": {Type: cty.String, Optional: true},
"state": {Type: cty.String, Optional: true},
"zip": {Type: cty.String, Optional: true},
},
},
},
},
},
map[string]interface{}{
"name": "Ermintrude",
"age": int(19),
"address": map[string]interface{}{
"street": []interface{}{"421 Shoreham Loop"},
"city": "Fridgewater",
"state": "MA",
"zip": "91037",
},
},
},
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Ermintrude"),
"age": cty.NumberIntVal(19),
"address": cty.NullVal(cty.Object(map[string]cty.Type{
"street": cty.List(cty.String),
"city": cty.String,
"state": cty.String,
"zip": cty.String,
})),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {Type: cty.String, Optional: true},
"age": {Type: cty.Number, Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"street": {Type: cty.List(cty.String), Optional: true},
"city": {Type: cty.String, Optional: true},
"state": {Type: cty.String, Optional: true},
"zip": {Type: cty.String, Optional: true},
},
},
},
},
},
map[string]interface{}{
"name": "Ermintrude",
"age": int(19),
},
},
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Ermintrude"),
"age": cty.NumberIntVal(19),
"address": cty.ObjectVal(map[string]cty.Value{
"street": cty.ListVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}),
"city": cty.StringVal("Fridgewater"),
"state": cty.StringVal("MA"),
"zip": cty.NullVal(cty.String), // should be omitted altogether in result
}),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {Type: cty.String, Optional: true},
"age": {Type: cty.Number, Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"street": {Type: cty.List(cty.String), Optional: true},
"city": {Type: cty.String, Optional: true},
"state": {Type: cty.String, Optional: true},
"zip": {Type: cty.String, Optional: true},
},
},
},
},
},
map[string]interface{}{
"name": "Ermintrude",
"age": int(19),
"address": map[string]interface{}{
"street": []interface{}{"421 Shoreham Loop"},
"city": "Fridgewater",
"state": "MA",
},
},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingList,
Block: configschema.Block{},
},
},
},
map[string]interface{}{
"address": []interface{}{
map[string]interface{}{},
},
},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.ListValEmpty(cty.EmptyObject), // should be omitted altogether in result
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingList,
Block: configschema.Block{},
},
},
},
map[string]interface{}{},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.SetVal([]cty.Value{cty.EmptyObjectVal}),
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingSet,
Block: configschema.Block{},
},
},
},
map[string]interface{}{
"address": []interface{}{
map[string]interface{}{},
},
},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.SetValEmpty(cty.EmptyObject),
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingSet,
Block: configschema.Block{},
},
},
},
map[string]interface{}{},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingMap,
Block: configschema.Block{},
},
},
},
map[string]interface{}{
"address": map[string]interface{}{
"foo": map[string]interface{}{},
},
},
},
{
cty.ObjectVal(map[string]cty.Value{
"address": cty.MapValEmpty(cty.EmptyObject),
}),
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"address": {
Nesting: configschema.NestingMap,
Block: configschema.Block{},
},
},
},
map[string]interface{}{},
},
{
cty.NullVal(cty.EmptyObject),
&configschema.Block{},
nil,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) {
got := ConfigValueFromHCL2Block(test.Input, test.Schema)
if !reflect.DeepEqual(got, test.Want) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want)
}
})
}
}
func TestConfigValueFromHCL2(t *testing.T) {
tests := []struct {
Input cty.Value
Want interface{}
}{
{
cty.True,
true,
},
{
cty.False,
false,
},
{
cty.NumberIntVal(12),
int(12),
},
{
cty.NumberFloatVal(12.5),
float64(12.5),
},
{
cty.StringVal("hello world"),
"hello world",
},
{
cty.ObjectVal(map[string]cty.Value{
"name": cty.StringVal("Ermintrude"),
"age": cty.NumberIntVal(19),
"address": cty.ObjectVal(map[string]cty.Value{
"street": cty.ListVal([]cty.Value{cty.StringVal("421 Shoreham Loop")}),
"city": cty.StringVal("Fridgewater"),
"state": cty.StringVal("MA"),
"zip": cty.StringVal("91037"),
}),
}),
map[string]interface{}{
"name": "Ermintrude",
"age": int(19),
"address": map[string]interface{}{
"street": []interface{}{"421 Shoreham Loop"},
"city": "Fridgewater",
"state": "MA",
"zip": "91037",
},
},
},
{
cty.MapVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
"bar": cty.StringVal("baz"),
}),
map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
},
{
cty.TupleVal([]cty.Value{
cty.StringVal("foo"),
cty.True,
}),
[]interface{}{
"foo",
true,
},
},
{
cty.NullVal(cty.String),
nil,
},
{
cty.UnknownVal(cty.String),
UnknownVariableValue,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) {
got := ConfigValueFromHCL2(test.Input)
if !reflect.DeepEqual(got, test.Want) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want)
}
})
}
}
func TestHCL2ValueFromConfigValue(t *testing.T) {
tests := []struct {
Input interface{}
Want cty.Value
}{
{
nil,
cty.NullVal(cty.DynamicPseudoType),
},
{
UnknownVariableValue,
cty.DynamicVal,
},
{
true,
cty.True,
},
{
false,
cty.False,
},
{
int(12),
cty.NumberIntVal(12),
},
{
int(0),
cty.Zero,
},
{
float64(12.5),
cty.NumberFloatVal(12.5),
},
{
"hello world",
cty.StringVal("hello world"),
},
{
"O\u0308", // decomposed letter + diacritic
cty.StringVal("\u00D6"), // NFC-normalized on entry into cty
},
{
[]interface{}{},
cty.EmptyTupleVal,
},
{
[]interface{}(nil),
cty.EmptyTupleVal,
},
{
[]interface{}{"hello", "world"},
cty.TupleVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}),
},
{
map[string]interface{}{},
cty.EmptyObjectVal,
},
{
map[string]interface{}(nil),
cty.EmptyObjectVal,
},
{
map[string]interface{}{
"foo": "bar",
"bar": "baz",
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
"bar": cty.StringVal("baz"),
}),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.Input), func(t *testing.T) {
got := HCL2ValueFromConfigValue(test.Input)
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ninput: %#v\ngot: %#v\nwant: %#v", test.Input, got, test.Want)
}
})
}
}