mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
565 lines
13 KiB
Go
565 lines
13 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 jsonplan
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/lang/marks"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
)
|
|
|
|
func TestOmitUnknowns(t *testing.T) {
|
|
tests := []struct {
|
|
Input cty.Value
|
|
Want cty.Value
|
|
}{
|
|
{
|
|
cty.StringVal("hello"),
|
|
cty.StringVal("hello"),
|
|
},
|
|
{
|
|
cty.NullVal(cty.String),
|
|
cty.NullVal(cty.String),
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String),
|
|
cty.NilVal,
|
|
},
|
|
{
|
|
cty.ListValEmpty(cty.String),
|
|
cty.EmptyTupleVal,
|
|
},
|
|
{
|
|
cty.ListVal([]cty.Value{cty.StringVal("hello")}),
|
|
cty.TupleVal([]cty.Value{cty.StringVal("hello")}),
|
|
},
|
|
{
|
|
cty.ListVal([]cty.Value{cty.NullVal(cty.String)}),
|
|
cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}),
|
|
},
|
|
{
|
|
cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
|
|
cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}),
|
|
},
|
|
{
|
|
cty.ListVal([]cty.Value{cty.StringVal("hello")}),
|
|
cty.TupleVal([]cty.Value{cty.StringVal("hello")}),
|
|
},
|
|
//
|
|
{
|
|
cty.ListVal([]cty.Value{
|
|
cty.StringVal("hello"),
|
|
cty.UnknownVal(cty.String)}),
|
|
cty.TupleVal([]cty.Value{
|
|
cty.StringVal("hello"),
|
|
cty.NullVal(cty.String),
|
|
}),
|
|
},
|
|
{
|
|
cty.MapVal(map[string]cty.Value{
|
|
"hello": cty.True,
|
|
"world": cty.UnknownVal(cty.Bool),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"hello": cty.True,
|
|
}),
|
|
},
|
|
{
|
|
cty.TupleVal([]cty.Value{
|
|
cty.StringVal("alpha"),
|
|
cty.UnknownVal(cty.String),
|
|
cty.StringVal("charlie"),
|
|
}),
|
|
cty.TupleVal([]cty.Value{
|
|
cty.StringVal("alpha"),
|
|
cty.NullVal(cty.String),
|
|
cty.StringVal("charlie"),
|
|
}),
|
|
},
|
|
{
|
|
cty.SetVal([]cty.Value{
|
|
cty.StringVal("dev"),
|
|
cty.StringVal("foo"),
|
|
cty.StringVal("stg"),
|
|
cty.UnknownVal(cty.String),
|
|
}),
|
|
cty.TupleVal([]cty.Value{
|
|
cty.StringVal("dev"),
|
|
cty.StringVal("foo"),
|
|
cty.StringVal("stg"),
|
|
cty.NullVal(cty.String),
|
|
}),
|
|
},
|
|
{
|
|
cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.UnknownVal(cty.String),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("known"),
|
|
}),
|
|
}),
|
|
cty.TupleVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("known"),
|
|
}),
|
|
cty.EmptyObjectVal,
|
|
}),
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
got := omitUnknowns(test.Input)
|
|
if !reflect.DeepEqual(got, test.Want) {
|
|
t.Errorf(
|
|
"wrong result\ninput: %#v\ngot: %#v\nwant: %#v",
|
|
test.Input, got, test.Want,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnknownAsBool(t *testing.T) {
|
|
tests := []struct {
|
|
Input cty.Value
|
|
Want cty.Value
|
|
}{
|
|
{
|
|
cty.StringVal("hello"),
|
|
cty.False,
|
|
},
|
|
{
|
|
cty.NullVal(cty.String),
|
|
cty.False,
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String),
|
|
cty.True,
|
|
},
|
|
|
|
{
|
|
cty.NullVal(cty.DynamicPseudoType),
|
|
cty.False,
|
|
},
|
|
{
|
|
cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})),
|
|
cty.False,
|
|
},
|
|
{
|
|
cty.DynamicVal,
|
|
cty.True,
|
|
},
|
|
|
|
{
|
|
cty.ListValEmpty(cty.String),
|
|
cty.EmptyTupleVal,
|
|
},
|
|
{
|
|
cty.ListVal([]cty.Value{cty.StringVal("hello")}),
|
|
cty.TupleVal([]cty.Value{cty.False}),
|
|
},
|
|
{
|
|
cty.ListVal([]cty.Value{cty.NullVal(cty.String)}),
|
|
cty.TupleVal([]cty.Value{cty.False}),
|
|
},
|
|
{
|
|
cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
|
|
cty.TupleVal([]cty.Value{cty.True}),
|
|
},
|
|
{
|
|
cty.SetValEmpty(cty.String),
|
|
cty.EmptyTupleVal,
|
|
},
|
|
{
|
|
cty.SetVal([]cty.Value{cty.StringVal("hello")}),
|
|
cty.TupleVal([]cty.Value{cty.False}),
|
|
},
|
|
{
|
|
cty.SetVal([]cty.Value{cty.NullVal(cty.String)}),
|
|
cty.TupleVal([]cty.Value{cty.False}),
|
|
},
|
|
{
|
|
cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)}),
|
|
cty.TupleVal([]cty.Value{cty.True}),
|
|
},
|
|
{
|
|
cty.EmptyTupleVal,
|
|
cty.EmptyTupleVal,
|
|
},
|
|
{
|
|
cty.TupleVal([]cty.Value{cty.StringVal("hello")}),
|
|
cty.TupleVal([]cty.Value{cty.False}),
|
|
},
|
|
{
|
|
cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}),
|
|
cty.TupleVal([]cty.Value{cty.False}),
|
|
},
|
|
{
|
|
cty.TupleVal([]cty.Value{cty.UnknownVal(cty.String)}),
|
|
cty.TupleVal([]cty.Value{cty.True}),
|
|
},
|
|
{
|
|
cty.MapValEmpty(cty.String),
|
|
cty.EmptyObjectVal,
|
|
},
|
|
{
|
|
cty.MapVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}),
|
|
cty.EmptyObjectVal,
|
|
},
|
|
{
|
|
cty.MapVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}),
|
|
cty.EmptyObjectVal,
|
|
},
|
|
{
|
|
cty.MapVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}),
|
|
cty.ObjectVal(map[string]cty.Value{"greeting": cty.True}),
|
|
},
|
|
{
|
|
cty.EmptyObjectVal,
|
|
cty.EmptyObjectVal,
|
|
},
|
|
{
|
|
cty.ObjectVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}),
|
|
cty.EmptyObjectVal,
|
|
},
|
|
{
|
|
cty.ObjectVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}),
|
|
cty.EmptyObjectVal,
|
|
},
|
|
{
|
|
cty.ObjectVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}),
|
|
cty.ObjectVal(map[string]cty.Value{"greeting": cty.True}),
|
|
},
|
|
{
|
|
cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.UnknownVal(cty.String),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("known"),
|
|
}),
|
|
}),
|
|
cty.TupleVal([]cty.Value{
|
|
cty.EmptyObjectVal,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.True,
|
|
}),
|
|
}),
|
|
},
|
|
{
|
|
cty.SetVal([]cty.Value{
|
|
cty.MapValEmpty(cty.String),
|
|
cty.MapVal(map[string]cty.Value{
|
|
"a": cty.StringVal("known"),
|
|
}),
|
|
cty.MapVal(map[string]cty.Value{
|
|
"a": cty.UnknownVal(cty.String),
|
|
}),
|
|
}),
|
|
cty.TupleVal([]cty.Value{
|
|
cty.EmptyObjectVal,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.True,
|
|
}),
|
|
cty.EmptyObjectVal,
|
|
}),
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
got := unknownAsBool(test.Input)
|
|
if !reflect.DeepEqual(got, test.Want) {
|
|
t.Errorf(
|
|
"wrong result\ninput: %#v\ngot: %#v\nwant: %#v",
|
|
test.Input, got, test.Want,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEncodePaths(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Input cty.PathSet
|
|
Want json.RawMessage
|
|
}{
|
|
"empty set": {
|
|
cty.NewPathSet(),
|
|
json.RawMessage(nil),
|
|
},
|
|
"index path with string and int steps": {
|
|
cty.NewPathSet(cty.IndexStringPath("boop").IndexInt(0)),
|
|
json.RawMessage(`[["boop",0]]`),
|
|
},
|
|
"get attr path with one step": {
|
|
cty.NewPathSet(cty.GetAttrPath("triggers")),
|
|
json.RawMessage(`[["triggers"]]`),
|
|
},
|
|
"multiple paths of different types": {
|
|
// The order of the path sets is not guaranteed, so we sort the
|
|
// result by the number of elements in the path to make the test deterministic.
|
|
cty.NewPathSet(
|
|
cty.GetAttrPath("alpha").GetAttr("beta"), // 2 elements
|
|
cty.GetAttrPath("triggers").IndexString("name").IndexString("test"), // 3 elements
|
|
cty.IndexIntPath(0).IndexInt(1).IndexInt(2).IndexInt(3), // 4 elements
|
|
),
|
|
json.RawMessage(`[[0,1,2,3],["alpha","beta"],["triggers","name","test"]]`),
|
|
},
|
|
}
|
|
|
|
// comp is a custom comparator for comparing JSON arrays. It sorts the
|
|
// arrays based on the number of elements in each path before comparing them.
|
|
// this allows our test cases to be more flexible about the order of the
|
|
// paths in the result. and deterministic on both 32 and 64 bit architectures.
|
|
comp := func(a, b json.RawMessage) (bool, error) {
|
|
if a == nil && b == nil {
|
|
return true, nil // Both are nil, they are equal
|
|
}
|
|
if a == nil || b == nil {
|
|
return false, nil // One is nil and the other is not, they are not equal
|
|
}
|
|
|
|
var pathsA, pathsB [][]interface{}
|
|
err := json.Unmarshal(a, &pathsA)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error unmarshalling first argument: %w", err)
|
|
}
|
|
err = json.Unmarshal(b, &pathsB)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error unmarshalling second argument: %w", err)
|
|
}
|
|
|
|
// Sort the slices based on the number of elements in each path
|
|
sort.Slice(pathsA, func(i, j int) bool {
|
|
return len(pathsA[i]) < len(pathsA[j])
|
|
})
|
|
sort.Slice(pathsB, func(i, j int) bool {
|
|
return len(pathsB[i]) < len(pathsB[j])
|
|
})
|
|
|
|
return cmp.Equal(pathsA, pathsB), nil
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
got, err := encodePaths(test.Input)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
equal, err := comp(got, test.Want)
|
|
if err != nil {
|
|
t.Fatalf("error comparing JSON slices: %s", err)
|
|
}
|
|
if !equal {
|
|
t.Errorf("paths do not match:\n%s", cmp.Diff(got, test.Want))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOutputs(t *testing.T) {
|
|
root := addrs.RootModuleInstance
|
|
|
|
child, diags := addrs.ParseModuleInstanceStr("module.child")
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected errors: %s", diags.Err())
|
|
}
|
|
|
|
tests := map[string]struct {
|
|
changes *plans.Changes
|
|
expected map[string]Change
|
|
}{
|
|
"copies all outputs": {
|
|
changes: &plans.Changes{
|
|
Outputs: []*plans.OutputChangeSrc{
|
|
{
|
|
Addr: root.OutputValue("first"),
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Create,
|
|
},
|
|
},
|
|
{
|
|
Addr: root.OutputValue("second"),
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Create,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: map[string]Change{
|
|
"first": {
|
|
Actions: []string{"create"},
|
|
Before: json.RawMessage("null"),
|
|
After: json.RawMessage("null"),
|
|
AfterUnknown: json.RawMessage("false"),
|
|
BeforeSensitive: json.RawMessage("false"),
|
|
AfterSensitive: json.RawMessage("false"),
|
|
},
|
|
"second": {
|
|
Actions: []string{"create"},
|
|
Before: json.RawMessage("null"),
|
|
After: json.RawMessage("null"),
|
|
AfterUnknown: json.RawMessage("false"),
|
|
BeforeSensitive: json.RawMessage("false"),
|
|
AfterSensitive: json.RawMessage("false"),
|
|
},
|
|
},
|
|
},
|
|
"skips non root modules": {
|
|
changes: &plans.Changes{
|
|
Outputs: []*plans.OutputChangeSrc{
|
|
{
|
|
Addr: root.OutputValue("first"),
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Create,
|
|
},
|
|
},
|
|
{
|
|
Addr: child.OutputValue("second"),
|
|
ChangeSrc: plans.ChangeSrc{
|
|
Action: plans.Create,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: map[string]Change{
|
|
"first": {
|
|
Actions: []string{"create"},
|
|
Before: json.RawMessage("null"),
|
|
After: json.RawMessage("null"),
|
|
AfterUnknown: json.RawMessage("false"),
|
|
BeforeSensitive: json.RawMessage("false"),
|
|
AfterSensitive: json.RawMessage("false"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
changes, err := MarshalOutputChanges(test.changes)
|
|
if err != nil {
|
|
t.Fatalf("unexpected err: %s", err)
|
|
}
|
|
|
|
if !cmp.Equal(changes, test.expected) {
|
|
t.Errorf("wrong result:\n %v\n", cmp.Diff(changes, test.expected))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func deepObjectValue(depth int) cty.Value {
|
|
v := cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a"),
|
|
"b": cty.NumberIntVal(2),
|
|
"c": cty.True,
|
|
"d": cty.UnknownVal(cty.String),
|
|
})
|
|
|
|
result := v
|
|
|
|
for i := 0; i < depth; i++ {
|
|
result = cty.ObjectVal(map[string]cty.Value{
|
|
"a": result,
|
|
"b": result,
|
|
"c": result,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func BenchmarkUnknownAsBool_2(b *testing.B) {
|
|
value := deepObjectValue(2)
|
|
for n := 0; n < b.N; n++ {
|
|
unknownAsBool(value)
|
|
}
|
|
}
|
|
|
|
func BenchmarkUnknownAsBool_3(b *testing.B) {
|
|
value := deepObjectValue(3)
|
|
for n := 0; n < b.N; n++ {
|
|
unknownAsBool(value)
|
|
}
|
|
}
|
|
|
|
func BenchmarkUnknownAsBool_5(b *testing.B) {
|
|
value := deepObjectValue(5)
|
|
for n := 0; n < b.N; n++ {
|
|
unknownAsBool(value)
|
|
}
|
|
}
|
|
|
|
func BenchmarkUnknownAsBool_7(b *testing.B) {
|
|
value := deepObjectValue(7)
|
|
for n := 0; n < b.N; n++ {
|
|
unknownAsBool(value)
|
|
}
|
|
}
|
|
|
|
func BenchmarkUnknownAsBool_9(b *testing.B) {
|
|
value := deepObjectValue(9)
|
|
for n := 0; n < b.N; n++ {
|
|
unknownAsBool(value)
|
|
}
|
|
}
|
|
|
|
// TestGenerateChange covers sensitivity tests for GenerateChange.
|
|
// TestOutputs test cases covered by outputs, but since is invalid to
|
|
// have outputs with sensitivity on the root module, we're creating this test
|
|
// to cover the remaining edge cases.
|
|
func TestGenerateChange(t *testing.T) {
|
|
tests := map[string]struct {
|
|
val1 cty.Value
|
|
val2 cty.Value
|
|
expected *Change
|
|
}{
|
|
"basic change": {
|
|
val1: cty.StringVal("test0"),
|
|
val2: cty.StringVal("test1"),
|
|
expected: &Change{
|
|
Before: json.RawMessage("\"test0\""),
|
|
After: json.RawMessage("\"test1\""),
|
|
AfterUnknown: json.RawMessage("false"),
|
|
BeforeSensitive: json.RawMessage("false"),
|
|
AfterSensitive: json.RawMessage("false"),
|
|
},
|
|
},
|
|
"handles sensitivity": {
|
|
val1: cty.NumberIntVal(3).Mark(marks.Sensitive),
|
|
val2: cty.NumberIntVal(5).Mark(marks.Sensitive),
|
|
expected: &Change{
|
|
Before: json.RawMessage("3"),
|
|
After: json.RawMessage("5"),
|
|
AfterUnknown: json.RawMessage("false"),
|
|
BeforeSensitive: json.RawMessage("true"),
|
|
AfterSensitive: json.RawMessage("true"),
|
|
},
|
|
},
|
|
}
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
change, err := GenerateChange(test.val1, test.val2)
|
|
if err != nil {
|
|
t.Fatalf("unexpected err: %s", err)
|
|
}
|
|
|
|
if !cmp.Equal(change, test.expected) {
|
|
t.Errorf("wrong result:\n %v\n", cmp.Diff(change, test.expected))
|
|
}
|
|
})
|
|
}
|
|
}
|