Files
opentf/internal/legacy/helper/schema/resource_diff_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

2051 lines
44 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 schema
import (
"fmt"
"reflect"
"sort"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/opentofu/opentofu/internal/legacy/hcl2shim"
"github.com/opentofu/opentofu/internal/legacy/tofu"
)
// testSetFunc is a very simple function we use to test a foo/bar complex set.
// Both "foo" and "bar" are int values.
//
// This is not foolproof as since it performs sums, you can run into
// collisions. Spec tests accordingly. :P
func testSetFunc(v interface{}) int {
m := v.(map[string]interface{})
return m["foo"].(int) + m["bar"].(int)
}
// resourceDiffTestCase provides a test case struct for SetNew and SetDiff.
type resourceDiffTestCase struct {
Name string
Schema map[string]*Schema
State *tofu.InstanceState
Config *tofu.ResourceConfig
Diff *tofu.InstanceDiff
Key string
OldValue interface{}
NewValue interface{}
Expected *tofu.InstanceDiff
ExpectedKeys []string
ExpectedError bool
}
// testDiffCases produces a list of test cases for use with SetNew and SetDiff.
func testDiffCases(t *testing.T, oldPrefix string, oldOffset int, computed bool) []resourceDiffTestCase {
return []resourceDiffTestCase{
resourceDiffTestCase{
Name: "basic primitive diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
NewValue: "qux",
Expected: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: func() string {
if computed {
return ""
}
return "qux"
}(),
NewComputed: computed,
},
},
},
},
resourceDiffTestCase{
Name: "basic set diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeString},
Set: HashString,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo.#": "1",
"foo.1996459178": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": []interface{}{"baz"},
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo.1996459178": &tofu.ResourceAttrDiff{
Old: "bar",
New: "",
NewRemoved: true,
},
"foo.2015626392": &tofu.ResourceAttrDiff{
Old: "",
New: "baz",
},
},
},
Key: "foo",
NewValue: []interface{}{"qux"},
Expected: &tofu.InstanceDiff{
Attributes: func() map[string]*tofu.ResourceAttrDiff {
result := map[string]*tofu.ResourceAttrDiff{}
if computed {
result["foo.#"] = &tofu.ResourceAttrDiff{
Old: "1",
New: "",
NewComputed: true,
}
} else {
result["foo.1494962232"] = &tofu.ResourceAttrDiff{
Old: "",
New: "qux",
}
result["foo.1996459178"] = &tofu.ResourceAttrDiff{
Old: "bar",
New: "",
NewRemoved: true,
}
}
return result
}(),
},
},
resourceDiffTestCase{
Name: "basic list diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeList,
Optional: true,
Computed: true,
Elem: &Schema{Type: TypeString},
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo.#": "1",
"foo.0": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": []interface{}{"baz"},
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo.0": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
NewValue: []interface{}{"qux"},
Expected: &tofu.InstanceDiff{
Attributes: func() map[string]*tofu.ResourceAttrDiff {
result := make(map[string]*tofu.ResourceAttrDiff)
if computed {
result["foo.#"] = &tofu.ResourceAttrDiff{
Old: "1",
New: "",
NewComputed: true,
}
} else {
result["foo.0"] = &tofu.ResourceAttrDiff{
Old: "bar",
New: "qux",
}
}
return result
}(),
},
},
resourceDiffTestCase{
Name: "basic map diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeMap,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo.%": "1",
"foo.bar": "baz",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": map[string]interface{}{"bar": "qux"},
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo.bar": &tofu.ResourceAttrDiff{
Old: "baz",
New: "qux",
},
},
},
Key: "foo",
NewValue: map[string]interface{}{"bar": "quux"},
Expected: &tofu.InstanceDiff{
Attributes: func() map[string]*tofu.ResourceAttrDiff {
result := make(map[string]*tofu.ResourceAttrDiff)
if computed {
result["foo.%"] = &tofu.ResourceAttrDiff{
Old: "",
New: "",
NewComputed: true,
}
result["foo.bar"] = &tofu.ResourceAttrDiff{
Old: "baz",
New: "",
NewRemoved: true,
}
} else {
result["foo.bar"] = &tofu.ResourceAttrDiff{
Old: "baz",
New: "quux",
}
}
return result
}(),
},
},
resourceDiffTestCase{
Name: "additional diff with primitive",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
},
"one": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
"one": "two",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "one",
NewValue: "four",
Expected: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
"one": &tofu.ResourceAttrDiff{
Old: "two",
New: func() string {
if computed {
return ""
}
return "four"
}(),
NewComputed: computed,
},
},
},
},
resourceDiffTestCase{
Name: "additional diff with primitive computed only",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
},
"one": &Schema{
Type: TypeString,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
"one": "two",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "one",
NewValue: "three",
Expected: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
"one": &tofu.ResourceAttrDiff{
Old: "two",
New: func() string {
if computed {
return ""
}
return "three"
}(),
NewComputed: computed,
},
},
},
},
resourceDiffTestCase{
Name: "complex-ish set diff",
Schema: map[string]*Schema{
"top": &Schema{
Type: TypeSet,
Optional: true,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
Computed: true,
},
"bar": &Schema{
Type: TypeInt,
Optional: true,
Computed: true,
},
},
},
Set: testSetFunc,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"top.#": "2",
"top.3.foo": "1",
"top.3.bar": "2",
"top.23.foo": "11",
"top.23.bar": "12",
},
},
Config: testConfig(t, map[string]interface{}{
"top": []interface{}{
map[string]interface{}{
"foo": 1,
"bar": 3,
},
map[string]interface{}{
"foo": 12,
"bar": 12,
},
},
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"top.4.foo": &tofu.ResourceAttrDiff{
Old: "",
New: "1",
},
"top.4.bar": &tofu.ResourceAttrDiff{
Old: "",
New: "3",
},
"top.24.foo": &tofu.ResourceAttrDiff{
Old: "",
New: "12",
},
"top.24.bar": &tofu.ResourceAttrDiff{
Old: "",
New: "12",
},
},
},
Key: "top",
NewValue: NewSet(testSetFunc, []interface{}{
map[string]interface{}{
"foo": 1,
"bar": 4,
},
map[string]interface{}{
"foo": 13,
"bar": 12,
},
map[string]interface{}{
"foo": 21,
"bar": 22,
},
}),
Expected: &tofu.InstanceDiff{
Attributes: func() map[string]*tofu.ResourceAttrDiff {
result := make(map[string]*tofu.ResourceAttrDiff)
if computed {
result["top.#"] = &tofu.ResourceAttrDiff{
Old: "2",
New: "",
NewComputed: true,
}
} else {
result["top.#"] = &tofu.ResourceAttrDiff{
Old: "2",
New: "3",
}
result["top.5.foo"] = &tofu.ResourceAttrDiff{
Old: "",
New: "1",
}
result["top.5.bar"] = &tofu.ResourceAttrDiff{
Old: "",
New: "4",
}
result["top.25.foo"] = &tofu.ResourceAttrDiff{
Old: "",
New: "13",
}
result["top.25.bar"] = &tofu.ResourceAttrDiff{
Old: "",
New: "12",
}
result["top.43.foo"] = &tofu.ResourceAttrDiff{
Old: "",
New: "21",
}
result["top.43.bar"] = &tofu.ResourceAttrDiff{
Old: "",
New: "22",
}
}
return result
}(),
},
},
resourceDiffTestCase{
Name: "primitive, no diff, no refresh",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &tofu.InstanceDiff{Attributes: map[string]*tofu.ResourceAttrDiff{}},
Key: "foo",
NewValue: "baz",
Expected: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: func() string {
if computed {
return ""
}
return "baz"
}(),
NewComputed: computed,
},
},
},
},
resourceDiffTestCase{
Name: "non-computed key, should error",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Required: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
NewValue: "qux",
ExpectedError: true,
},
resourceDiffTestCase{
Name: "bad key, should error",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Required: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "bad",
NewValue: "qux",
ExpectedError: true,
},
resourceDiffTestCase{
// NOTE: This case is technically impossible in the current
// implementation, because optional+computed values never show up in the
// diff, and we actually clear existing diffs when SetNew or
// SetNewComputed is run. This test is here to ensure that if either of
// these behaviors change that we don't introduce regressions.
Name: "NewRemoved in diff for Optional and Computed, should be fully overridden",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "",
NewRemoved: true,
},
},
},
Key: "foo",
NewValue: "qux",
Expected: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: func() string {
if computed {
return ""
}
return "qux"
}(),
NewComputed: computed,
},
},
},
},
resourceDiffTestCase{
Name: "NewComputed should always propagate",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "",
},
ID: "pre-existing",
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &tofu.InstanceDiff{Attributes: map[string]*tofu.ResourceAttrDiff{}},
Key: "foo",
NewValue: "",
Expected: &tofu.InstanceDiff{
Attributes: func() map[string]*tofu.ResourceAttrDiff {
if computed {
return map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
NewComputed: computed,
},
}
}
return map[string]*tofu.ResourceAttrDiff{}
}(),
},
},
}
}
func TestSetNew(t *testing.T) {
testCases := testDiffCases(t, "", 0, false)
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
m := schemaMap(tc.Schema)
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
err := d.SetNew(tc.Key, tc.NewValue)
switch {
case err != nil && !tc.ExpectedError:
t.Fatalf("bad: %s", err)
case err == nil && tc.ExpectedError:
t.Fatalf("Expected error, got none")
case err != nil && tc.ExpectedError:
return
}
for _, k := range d.UpdatedKeys() {
if err := m.diff(k, m[k], tc.Diff, d, false); err != nil {
t.Fatalf("bad: %s", err)
}
}
if !reflect.DeepEqual(tc.Expected, tc.Diff) {
t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff))
}
})
}
}
func TestSetNewComputed(t *testing.T) {
testCases := testDiffCases(t, "", 0, true)
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
m := schemaMap(tc.Schema)
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
err := d.SetNewComputed(tc.Key)
switch {
case err != nil && !tc.ExpectedError:
t.Fatalf("bad: %s", err)
case err == nil && tc.ExpectedError:
t.Fatalf("Expected error, got none")
case err != nil && tc.ExpectedError:
return
}
for _, k := range d.UpdatedKeys() {
if err := m.diff(k, m[k], tc.Diff, d, false); err != nil {
t.Fatalf("bad: %s", err)
}
}
if !reflect.DeepEqual(tc.Expected, tc.Diff) {
t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff))
}
})
}
}
func TestForceNew(t *testing.T) {
cases := []resourceDiffTestCase{
resourceDiffTestCase{
Name: "basic primitive diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
Expected: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
RequiresNew: true,
},
},
},
},
resourceDiffTestCase{
Name: "no change, should error",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "bar",
}),
ExpectedError: true,
},
resourceDiffTestCase{
Name: "basic primitive, non-computed key",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Required: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
Expected: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
RequiresNew: true,
},
},
},
},
resourceDiffTestCase{
Name: "nested field",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeList,
Required: true,
MaxItems: 1,
Elem: &Resource{
Schema: map[string]*Schema{
"bar": {
Type: TypeString,
Optional: true,
},
"baz": {
Type: TypeString,
Optional: true,
},
},
},
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo.#": "1",
"foo.0.bar": "abc",
"foo.0.baz": "xyz",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": []interface{}{
map[string]interface{}{
"bar": "abcdefg",
"baz": "changed",
},
},
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo.0.bar": &tofu.ResourceAttrDiff{
Old: "abc",
New: "abcdefg",
},
"foo.0.baz": &tofu.ResourceAttrDiff{
Old: "xyz",
New: "changed",
},
},
},
Key: "foo.0.baz",
Expected: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo.0.bar": &tofu.ResourceAttrDiff{
Old: "abc",
New: "abcdefg",
},
"foo.0.baz": &tofu.ResourceAttrDiff{
Old: "xyz",
New: "changed",
RequiresNew: true,
},
},
},
},
resourceDiffTestCase{
Name: "preserve NewRemoved on existing diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "",
NewRemoved: true,
},
},
},
Key: "foo",
Expected: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "",
RequiresNew: true,
NewRemoved: true,
},
},
},
},
resourceDiffTestCase{
Name: "nested field, preserve original diff without zero values",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeList,
Required: true,
MaxItems: 1,
Elem: &Resource{
Schema: map[string]*Schema{
"bar": {
Type: TypeString,
Optional: true,
},
"baz": {
Type: TypeInt,
Optional: true,
},
},
},
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo.#": "1",
"foo.0.bar": "abc",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": []interface{}{
map[string]interface{}{
"bar": "abcdefg",
},
},
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo.0.bar": &tofu.ResourceAttrDiff{
Old: "abc",
New: "abcdefg",
},
},
},
Key: "foo.0.bar",
Expected: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo.0.bar": &tofu.ResourceAttrDiff{
Old: "abc",
New: "abcdefg",
RequiresNew: true,
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
m := schemaMap(tc.Schema)
d := newResourceDiff(m, tc.Config, tc.State, tc.Diff)
err := d.ForceNew(tc.Key)
switch {
case err != nil && !tc.ExpectedError:
t.Fatalf("bad: %s", err)
case err == nil && tc.ExpectedError:
t.Fatalf("Expected error, got none")
case err != nil && tc.ExpectedError:
return
}
for _, k := range d.UpdatedKeys() {
if err := m.diff(k, m[k], tc.Diff, d, false); err != nil {
t.Fatalf("bad: %s", err)
}
}
if !reflect.DeepEqual(tc.Expected, tc.Diff) {
t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff))
}
})
}
}
func TestClear(t *testing.T) {
cases := []resourceDiffTestCase{
resourceDiffTestCase{
Name: "basic primitive diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
Expected: &tofu.InstanceDiff{Attributes: map[string]*tofu.ResourceAttrDiff{}},
},
resourceDiffTestCase{
Name: "non-computed key, should error",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Required: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
ExpectedError: true,
},
resourceDiffTestCase{
Name: "multi-value, one removed",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
"one": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
"one": "two",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
"one": "three",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
"one": &tofu.ResourceAttrDiff{
Old: "two",
New: "three",
},
},
},
Key: "one",
Expected: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
},
resourceDiffTestCase{
Name: "basic sub-block diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeList,
Optional: true,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{
"bar": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
"baz": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
},
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo.0.bar": "bar1",
"foo.0.baz": "baz1",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": []interface{}{
map[string]interface{}{
"bar": "bar2",
"baz": "baz1",
},
},
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo.0.bar": &tofu.ResourceAttrDiff{
Old: "bar1",
New: "bar2",
},
},
},
Key: "foo.0.bar",
Expected: &tofu.InstanceDiff{Attributes: map[string]*tofu.ResourceAttrDiff{}},
},
resourceDiffTestCase{
Name: "sub-block diff only partial clear",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeList,
Optional: true,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{
"bar": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
"baz": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
},
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo.0.bar": "bar1",
"foo.0.baz": "baz1",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": []interface{}{
map[string]interface{}{
"bar": "bar2",
"baz": "baz2",
},
},
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo.0.bar": &tofu.ResourceAttrDiff{
Old: "bar1",
New: "bar2",
},
"foo.0.baz": &tofu.ResourceAttrDiff{
Old: "baz1",
New: "baz2",
},
},
},
Key: "foo.0.bar",
Expected: &tofu.InstanceDiff{Attributes: map[string]*tofu.ResourceAttrDiff{
"foo.0.baz": &tofu.ResourceAttrDiff{
Old: "baz1",
New: "baz2",
},
}},
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
m := schemaMap(tc.Schema)
d := newResourceDiff(m, tc.Config, tc.State, tc.Diff)
err := d.Clear(tc.Key)
switch {
case err != nil && !tc.ExpectedError:
t.Fatalf("bad: %s", err)
case err == nil && tc.ExpectedError:
t.Fatalf("Expected error, got none")
case err != nil && tc.ExpectedError:
return
}
for _, k := range d.UpdatedKeys() {
if err := m.diff(k, m[k], tc.Diff, d, false); err != nil {
t.Fatalf("bad: %s", err)
}
}
if !reflect.DeepEqual(tc.Expected, tc.Diff) {
t.Fatalf("Expected %s, got %s", spew.Sdump(tc.Expected), spew.Sdump(tc.Diff))
}
})
}
}
func TestGetChangedKeysPrefix(t *testing.T) {
cases := []resourceDiffTestCase{
resourceDiffTestCase{
Name: "basic primitive diff",
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"foo": "bar",
},
},
Config: testConfig(t, map[string]interface{}{
"foo": "baz",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"foo": &tofu.ResourceAttrDiff{
Old: "bar",
New: "baz",
},
},
},
Key: "foo",
ExpectedKeys: []string{
"foo",
},
},
resourceDiffTestCase{
Name: "nested field filtering",
Schema: map[string]*Schema{
"testfield": &Schema{
Type: TypeString,
Required: true,
},
"foo": &Schema{
Type: TypeList,
Required: true,
MaxItems: 1,
Elem: &Resource{
Schema: map[string]*Schema{
"bar": {
Type: TypeString,
Optional: true,
},
"baz": {
Type: TypeString,
Optional: true,
},
},
},
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"testfield": "blablah",
"foo.#": "1",
"foo.0.bar": "abc",
"foo.0.baz": "xyz",
},
},
Config: testConfig(t, map[string]interface{}{
"testfield": "modified",
"foo": []interface{}{
map[string]interface{}{
"bar": "abcdefg",
"baz": "changed",
},
},
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"testfield": &tofu.ResourceAttrDiff{
Old: "blablah",
New: "modified",
},
"foo.0.bar": &tofu.ResourceAttrDiff{
Old: "abc",
New: "abcdefg",
},
"foo.0.baz": &tofu.ResourceAttrDiff{
Old: "xyz",
New: "changed",
},
},
},
Key: "foo",
ExpectedKeys: []string{
"foo.0.bar",
"foo.0.baz",
},
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
m := schemaMap(tc.Schema)
d := newResourceDiff(m, tc.Config, tc.State, tc.Diff)
keys := d.GetChangedKeysPrefix(tc.Key)
for _, k := range d.UpdatedKeys() {
if err := m.diff(k, m[k], tc.Diff, d, false); err != nil {
t.Fatalf("bad: %s", err)
}
}
sort.Strings(keys)
if !reflect.DeepEqual(tc.ExpectedKeys, keys) {
t.Fatalf("Expected %s, got %s", spew.Sdump(tc.ExpectedKeys), spew.Sdump(keys))
}
})
}
}
func TestResourceDiffGetOkExists(t *testing.T) {
cases := []struct {
Name string
Schema map[string]*Schema
State *tofu.InstanceState
Config *tofu.ResourceConfig
Diff *tofu.InstanceDiff
Key string
Value interface{}
Ok bool
}{
/*
* Primitives
*/
{
Name: "string-literal-empty",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: nil,
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "",
},
},
},
Key: "availability_zone",
Value: "",
Ok: true,
},
{
Name: "string-computed-empty",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: nil,
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "",
NewComputed: true,
},
},
},
Key: "availability_zone",
Value: "",
Ok: false,
},
{
Name: "string-optional-computed-nil-diff",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: nil,
Diff: nil,
Key: "availability_zone",
Value: "",
Ok: false,
},
/*
* Lists
*/
{
Name: "list-optional",
Schema: map[string]*Schema{
"ports": {
Type: TypeList,
Optional: true,
Elem: &Schema{Type: TypeInt},
},
},
State: nil,
Config: nil,
Diff: nil,
Key: "ports",
Value: []interface{}{},
Ok: false,
},
/*
* Map
*/
{
Name: "map-optional",
Schema: map[string]*Schema{
"ports": {
Type: TypeMap,
Optional: true,
},
},
State: nil,
Config: nil,
Diff: nil,
Key: "ports",
Value: map[string]interface{}{},
Ok: false,
},
/*
* Set
*/
{
Name: "set-optional",
Schema: map[string]*Schema{
"ports": {
Type: TypeSet,
Optional: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int { return a.(int) },
},
},
State: nil,
Config: nil,
Diff: nil,
Key: "ports",
Value: []interface{}{},
Ok: false,
},
{
Name: "set-optional-key",
Schema: map[string]*Schema{
"ports": {
Type: TypeSet,
Optional: true,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int { return a.(int) },
},
},
State: nil,
Config: nil,
Diff: nil,
Key: "ports.0",
Value: 0,
Ok: false,
},
{
Name: "bool-literal-empty",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeBool,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: nil,
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "",
},
},
},
Key: "availability_zone",
Value: false,
Ok: true,
},
{
Name: "bool-literal-set",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeBool,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Config: nil,
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"availability_zone": {
New: "true",
},
},
},
Key: "availability_zone",
Value: true,
Ok: true,
},
{
Name: "value-in-config",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{
"availability_zone": "foo",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{},
},
Key: "availability_zone",
Value: "foo",
Ok: true,
},
{
Name: "new-value-in-config",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: nil,
Config: testConfig(t, map[string]interface{}{
"availability_zone": "foo",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "foo",
},
},
},
Key: "availability_zone",
Value: "foo",
Ok: true,
},
{
Name: "optional-computed-value-in-config",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{
"availability_zone": "bar",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"availability_zone": {
Old: "foo",
New: "bar",
},
},
},
Key: "availability_zone",
Value: "bar",
Ok: true,
},
{
Name: "removed-value",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"availability_zone": {
Old: "foo",
New: "",
NewRemoved: true,
},
},
},
Key: "availability_zone",
Value: "",
Ok: true,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
v, ok := d.GetOkExists(tc.Key)
if s, ok := v.(*Set); ok {
v = s.List()
}
if !reflect.DeepEqual(v, tc.Value) {
t.Fatalf("Bad %s: \n%#v", tc.Name, v)
}
if ok != tc.Ok {
t.Fatalf("%s: expected ok: %t, got: %t", tc.Name, tc.Ok, ok)
}
})
}
}
func TestResourceDiffGetOkExistsSetNew(t *testing.T) {
tc := struct {
Schema map[string]*Schema
State *tofu.InstanceState
Diff *tofu.InstanceDiff
Key string
Value interface{}
Ok bool
}{
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: nil,
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{},
},
Key: "availability_zone",
Value: "foobar",
Ok: true,
}
d := newResourceDiff(tc.Schema, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff)
d.SetNew(tc.Key, tc.Value)
v, ok := d.GetOkExists(tc.Key)
if s, ok := v.(*Set); ok {
v = s.List()
}
if !reflect.DeepEqual(v, tc.Value) {
t.Fatalf("Bad: \n%#v", v)
}
if ok != tc.Ok {
t.Fatalf("expected ok: %t, got: %t", tc.Ok, ok)
}
}
func TestResourceDiffGetOkExistsSetNewComputed(t *testing.T) {
tc := struct {
Schema map[string]*Schema
State *tofu.InstanceState
Diff *tofu.InstanceDiff
Key string
Value interface{}
Ok bool
}{
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{},
},
Key: "availability_zone",
Value: "foobar",
Ok: false,
}
d := newResourceDiff(tc.Schema, testConfig(t, map[string]interface{}{}), tc.State, tc.Diff)
d.SetNewComputed(tc.Key)
_, ok := d.GetOkExists(tc.Key)
if ok != tc.Ok {
t.Fatalf("expected ok: %t, got: %t", tc.Ok, ok)
}
}
func TestResourceDiffNewValueKnown(t *testing.T) {
cases := []struct {
Name string
Schema map[string]*Schema
State *tofu.InstanceState
Config *tofu.ResourceConfig
Diff *tofu.InstanceDiff
Key string
Expected bool
}{
{
Name: "in config, no state",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: nil,
Config: testConfig(t, map[string]interface{}{
"availability_zone": "foo",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"availability_zone": {
Old: "",
New: "foo",
},
},
},
Key: "availability_zone",
Expected: true,
},
{
Name: "in config, has state, no diff",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{
"availability_zone": "foo",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: true,
},
{
Name: "computed attribute, in state, no diff",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: true,
},
{
Name: "optional and computed attribute, in state, no config",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: true,
},
{
Name: "optional and computed attribute, in state, with config",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{
"availability_zone": "foo",
}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: true,
},
{
Name: "computed value, through config reader",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(
t,
map[string]interface{}{
"availability_zone": hcl2shim.UnknownVariableValue,
},
),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: false,
},
{
Name: "computed value, through diff reader",
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(
t,
map[string]interface{}{
"availability_zone": hcl2shim.UnknownVariableValue,
},
),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"availability_zone": {
Old: "foo",
New: "",
NewComputed: true,
},
},
},
Key: "availability_zone",
Expected: false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
actual := d.NewValueKnown(tc.Key)
if tc.Expected != actual {
t.Fatalf("%s: expected ok: %t, got: %t", tc.Name, tc.Expected, actual)
}
})
}
}
func TestResourceDiffNewValueKnownSetNew(t *testing.T) {
tc := struct {
Schema map[string]*Schema
State *tofu.InstanceState
Config *tofu.ResourceConfig
Diff *tofu.InstanceDiff
Key string
Value interface{}
Expected bool
}{
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Optional: true,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(
t,
map[string]interface{}{
"availability_zone": hcl2shim.UnknownVariableValue,
},
),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{
"availability_zone": {
Old: "foo",
New: "",
NewComputed: true,
},
},
},
Key: "availability_zone",
Value: "bar",
Expected: true,
}
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
d.SetNew(tc.Key, tc.Value)
actual := d.NewValueKnown(tc.Key)
if tc.Expected != actual {
t.Fatalf("expected ok: %t, got: %t", tc.Expected, actual)
}
}
func TestResourceDiffNewValueKnownSetNewComputed(t *testing.T) {
tc := struct {
Schema map[string]*Schema
State *tofu.InstanceState
Config *tofu.ResourceConfig
Diff *tofu.InstanceDiff
Key string
Expected bool
}{
Schema: map[string]*Schema{
"availability_zone": {
Type: TypeString,
Computed: true,
},
},
State: &tofu.InstanceState{
Attributes: map[string]string{
"availability_zone": "foo",
},
},
Config: testConfig(t, map[string]interface{}{}),
Diff: &tofu.InstanceDiff{
Attributes: map[string]*tofu.ResourceAttrDiff{},
},
Key: "availability_zone",
Expected: false,
}
d := newResourceDiff(tc.Schema, tc.Config, tc.State, tc.Diff)
d.SetNewComputed(tc.Key)
actual := d.NewValueKnown(tc.Key)
if tc.Expected != actual {
t.Fatalf("expected ok: %t, got: %t", tc.Expected, actual)
}
}