// 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/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) } }