mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
When calculating the unknown values for JSON plan output, we would previously recursively call the `unknownAsBool` function on the current sub-tree twice, if any values were unknown. This was wasteful, but not noticeable for normal Terraform resource shapes. However for deeper nested object values, such as Kubernetes manifests, this was a severe performance problem, causing `terraform show -json` to take several hours to render a plan. This commit reuses the already calculated unknown value for the subtree, and adds benchmark coverage to demonstrate the improvement.
363 lines
7.5 KiB
Go
363 lines
7.5 KiB
Go
package jsonplan
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
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.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.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": {
|
|
cty.NewPathSet(
|
|
cty.GetAttrPath("alpha").GetAttr("beta").GetAttr("gamma"),
|
|
cty.GetAttrPath("triggers").IndexString("name"),
|
|
cty.IndexIntPath(0).IndexInt(1).IndexInt(2).IndexInt(3),
|
|
),
|
|
json.RawMessage(`[["alpha","beta","gamma"],["triggers","name"],[0,1,2,3]]`),
|
|
},
|
|
}
|
|
|
|
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)
|
|
}
|
|
if !cmp.Equal(got, test.Want) {
|
|
t.Errorf("wrong result:\n %v\n", cmp.Diff(got, test.Want))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|