Files
opentf/internal/legacy/hcl2shim/flatmap_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

758 lines
16 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"
"testing"
"github.com/go-test/deep"
"github.com/zclconf/go-cty/cty"
)
func TestFlatmapValueFromHCL2(t *testing.T) {
tests := []struct {
Value cty.Value
Want map[string]string
}{
{
cty.EmptyObjectVal,
map[string]string{},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
}),
map[string]string{
"foo": "hello",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.Bool),
}),
map[string]string{
"foo": UnknownVariableValue,
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NumberIntVal(12),
}),
map[string]string{
"foo": "12",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.True,
"bar": cty.False,
}),
map[string]string{
"foo": "true",
"bar": "false",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("hello"),
"bar": cty.StringVal("world"),
"baz": cty.StringVal("whelp"),
}),
map[string]string{
"foo": "hello",
"bar": "world",
"baz": "whelp",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListValEmpty(cty.String),
}),
map[string]string{
"foo.#": "0",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.List(cty.String)),
}),
map[string]string{
"foo.#": UnknownVariableValue,
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.StringVal("hello"),
}),
}),
map[string]string{
"foo.#": "1",
"foo.0": "hello",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.StringVal("hello"),
cty.StringVal("world"),
}),
}),
map[string]string{
"foo.#": "2",
"foo.0": "hello",
"foo.1": "world",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.MapVal(map[string]cty.Value{
"hello": cty.NumberIntVal(12),
"hello.world": cty.NumberIntVal(10),
}),
}),
map[string]string{
"foo.%": "2",
"foo.hello": "12",
"foo.hello.world": "10",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.Map(cty.String)),
}),
map[string]string{
"foo.%": UnknownVariableValue,
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.MapVal(map[string]cty.Value{
"hello": cty.NumberIntVal(12),
"hello.world": cty.NumberIntVal(10),
}),
}),
map[string]string{
"foo.%": "2",
"foo.hello": "12",
"foo.hello.world": "10",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.SetVal([]cty.Value{
cty.StringVal("hello"),
cty.StringVal("world"),
}),
}),
map[string]string{
"foo.#": "2",
"foo.0": "hello",
"foo.1": "world",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.Set(cty.Number)),
}),
map[string]string{
"foo.#": UnknownVariableValue,
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("hello"),
"baz": cty.StringVal("world"),
}),
cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("bloo"),
"baz": cty.StringVal("blaa"),
}),
}),
}),
map[string]string{
"foo.#": "2",
"foo.0.bar": "hello",
"foo.0.baz": "world",
"foo.1.bar": "bloo",
"foo.1.baz": "blaa",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("hello"),
"baz": cty.ListVal([]cty.Value{
cty.True,
cty.True,
}),
}),
cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("bloo"),
"baz": cty.ListVal([]cty.Value{
cty.False,
cty.True,
}),
}),
}),
}),
map[string]string{
"foo.#": "2",
"foo.0.bar": "hello",
"foo.0.baz.#": "2",
"foo.0.baz.0": "true",
"foo.0.baz.1": "true",
"foo.1.bar": "bloo",
"foo.1.baz.#": "2",
"foo.1.baz.0": "false",
"foo.1.baz.1": "true",
},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.UnknownVal(cty.Object(map[string]cty.Type{
"bar": cty.String,
"baz": cty.List(cty.Bool),
"bap": cty.Map(cty.Number),
})),
}),
}),
map[string]string{
"foo.#": "1",
"foo.0.bar": UnknownVariableValue,
"foo.0.baz.#": UnknownVariableValue,
"foo.0.bap.%": UnknownVariableValue,
},
},
{
cty.NullVal(cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.Object(map[string]cty.Type{
"bar": cty.String,
})),
})),
nil,
},
}
for _, test := range tests {
t.Run(test.Value.GoString(), func(t *testing.T) {
got := FlatmapValueFromHCL2(test.Value)
for _, problem := range deep.Equal(got, test.Want) {
t.Error(problem)
}
})
}
}
func TestFlatmapValueFromHCL2FromFlatmap(t *testing.T) {
tests := []struct {
Name string
Map map[string]string
Type cty.Type
}{
{
"empty flatmap with collections",
map[string]string{},
cty.Object(map[string]cty.Type{
"foo": cty.Map(cty.String),
"bar": cty.Set(cty.String),
}),
},
{
"nil flatmap with collections",
nil,
cty.Object(map[string]cty.Type{
"foo": cty.Map(cty.String),
"bar": cty.Set(cty.String),
}),
},
{
"empty flatmap with nested collections",
map[string]string{},
cty.Object(map[string]cty.Type{
"foo": cty.Object(
map[string]cty.Type{
"baz": cty.Map(cty.String),
},
),
"bar": cty.Set(cty.String),
}),
},
{
"partial flatmap with nested collections",
map[string]string{
"foo.baz.%": "1",
"foo.baz.key": "val",
},
cty.Object(map[string]cty.Type{
"foo": cty.Object(
map[string]cty.Type{
"baz": cty.Map(cty.String),
"biz": cty.Map(cty.String),
},
),
"bar": cty.Set(cty.String),
}),
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
val, err := HCL2ValueFromFlatmap(test.Map, test.Type)
if err != nil {
t.Fatal(err)
}
got := FlatmapValueFromHCL2(val)
for _, problem := range deep.Equal(got, test.Map) {
t.Error(problem)
}
})
}
}
func TestHCL2ValueFromFlatmap(t *testing.T) {
tests := []struct {
Flatmap map[string]string
Type cty.Type
Want cty.Value
WantErr string
}{
{
Flatmap: map[string]string{},
Type: cty.EmptyObject,
Want: cty.EmptyObjectVal,
},
{
Flatmap: map[string]string{
"ignored": "foo",
},
Type: cty.EmptyObject,
Want: cty.EmptyObjectVal,
},
{
Flatmap: map[string]string{
"foo": "blah",
"bar": "true",
"baz": "12.5",
"unk": UnknownVariableValue,
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.String,
"bar": cty.Bool,
"baz": cty.Number,
"unk": cty.Bool,
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("blah"),
"bar": cty.True,
"baz": cty.NumberFloatVal(12.5),
"unk": cty.UnknownVal(cty.Bool),
}),
},
{
Flatmap: map[string]string{
"foo.#": "0",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.List(cty.String),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListValEmpty(cty.String),
}),
},
{
Flatmap: map[string]string{
"foo.#": UnknownVariableValue,
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.List(cty.String),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.List(cty.String)),
}),
},
{
Flatmap: map[string]string{
"foo.#": "1",
"foo.0": "hello",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.List(cty.String),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.StringVal("hello"),
}),
}),
},
{
Flatmap: map[string]string{
"foo.#": "2",
"foo.0": "true",
"foo.1": "false",
"foo.2": "ignored", // (because the count is 2, so this is out of range)
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.List(cty.Bool),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.True,
cty.False,
}),
}),
},
{
Flatmap: map[string]string{
"foo.#": "2",
"foo.0": "hello",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Tuple([]cty.Type{
cty.String,
cty.Bool,
}),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.TupleVal([]cty.Value{
cty.StringVal("hello"),
cty.NullVal(cty.Bool),
}),
}),
},
{
Flatmap: map[string]string{
"foo.#": UnknownVariableValue,
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Tuple([]cty.Type{
cty.String,
cty.Bool,
}),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.Tuple([]cty.Type{
cty.String,
cty.Bool,
})),
}),
},
{
Flatmap: map[string]string{
"foo.#": "0",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.String),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.SetValEmpty(cty.String),
}),
},
{
Flatmap: map[string]string{
"foo.#": UnknownVariableValue,
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.String),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.Set(cty.String)),
}),
},
{
Flatmap: map[string]string{
"foo.#": "1",
"foo.24534534": "hello",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.String),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.SetVal([]cty.Value{
cty.StringVal("hello"),
}),
}),
},
{
Flatmap: map[string]string{
"foo.#": "1",
"foo.24534534": "true",
"foo.95645644": "true",
"foo.34533452": "false",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.Bool),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.SetVal([]cty.Value{
cty.True,
cty.False,
}),
}),
},
{
Flatmap: map[string]string{
"foo.%": "0",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Map(cty.String),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.MapValEmpty(cty.String),
}),
},
{
Flatmap: map[string]string{
"foo.%": "2",
"foo.baz": "true",
"foo.bar.baz": "false",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Map(cty.Bool),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.MapVal(map[string]cty.Value{
"baz": cty.True,
"bar.baz": cty.False,
}),
}),
},
{
Flatmap: map[string]string{
"foo.%": UnknownVariableValue,
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Map(cty.Bool),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.UnknownVal(cty.Map(cty.Bool)),
}),
},
{
Flatmap: map[string]string{
"foo.#": "2",
"foo.0.bar": "hello",
"foo.0.baz": "1",
"foo.1.bar": "world",
"foo.1.baz": "false",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.List(cty.Object(map[string]cty.Type{
"bar": cty.String,
"baz": cty.Bool,
})),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("hello"),
"baz": cty.True,
}),
cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("world"),
"baz": cty.False,
}),
}),
}),
},
{
Flatmap: map[string]string{
"foo.#": "2",
"foo.34534534.bar": "hello",
"foo.34534534.baz": "1",
"foo.93453345.bar": "world",
"foo.93453345.baz": "false",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.Object(map[string]cty.Type{
"bar": cty.String,
"baz": cty.Bool,
})),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("hello"),
"baz": cty.True,
}),
cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("world"),
"baz": cty.False,
}),
}),
}),
},
{
Flatmap: map[string]string{
"foo.#": "not-valid",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.List(cty.String),
}),
WantErr: `invalid count value for "foo." in state: strconv.Atoi: parsing "not-valid": invalid syntax`,
},
{
Flatmap: nil,
Type: cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.Object(map[string]cty.Type{
"bar": cty.String,
})),
}),
Want: cty.NullVal(cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.Object(map[string]cty.Type{
"bar": cty.String,
})),
})),
},
{
Flatmap: map[string]string{
"foo.#": "2",
"foo.0.%": "2",
"foo.0.a": "a",
"foo.0.b": "b",
"foo.1.%": "1",
"foo.1.a": "a",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.List(cty.Map(cty.String)),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("a"),
"b": cty.StringVal("b"),
}),
cty.MapVal(map[string]cty.Value{
"a": cty.StringVal("a"),
}),
}),
}),
},
{
Flatmap: map[string]string{
"single.#": "1",
"single.~1.value": "a",
"single.~1.optional": UnknownVariableValue,
"two.#": "2",
"two.~2381914684.value": "a",
"two.~2381914684.optional": UnknownVariableValue,
"two.~2798940671.value": "b",
"two.~2798940671.optional": UnknownVariableValue,
},
Type: cty.Object(map[string]cty.Type{
"single": cty.Set(
cty.Object(map[string]cty.Type{
"value": cty.String,
"optional": cty.String,
}),
),
"two": cty.Set(
cty.Object(map[string]cty.Type{
"optional": cty.String,
"value": cty.String,
}),
),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"single": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("a"),
"optional": cty.UnknownVal(cty.String),
}),
}),
"two": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("a"),
"optional": cty.UnknownVal(cty.String),
}),
cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("b"),
"optional": cty.UnknownVal(cty.String),
}),
}),
}),
},
{
Flatmap: map[string]string{
"foo.#": "1",
},
Type: cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.Object(map[string]cty.Type{
"bar": cty.String,
})),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"foo": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"bar": cty.NullVal(cty.String),
}),
}),
}),
},
{
Flatmap: map[string]string{
"multi.#": "1",
"multi.2.set.#": "1",
"multi.2.set.3.required": "val",
},
Type: cty.Object(map[string]cty.Type{
"multi": cty.Set(cty.Object(map[string]cty.Type{
"set": cty.Set(cty.Object(map[string]cty.Type{
"required": cty.String,
})),
})),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"multi": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"set": cty.SetVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"required": cty.StringVal("val"),
}),
}),
}),
}),
}),
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%d %#v as %#v", i, test.Flatmap, test.Type), func(t *testing.T) {
got, err := HCL2ValueFromFlatmap(test.Flatmap, test.Type)
if test.WantErr != "" {
if err == nil {
t.Fatalf("succeeded; want error: %s", test.WantErr)
}
if got, want := err.Error(), test.WantErr; got != want {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
if got == cty.NilVal {
t.Fatalf("result is cty.NilVal; want valid placeholder value")
}
return
} else {
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}