github.com/opentofu/opentofu@v1.7.1/internal/command/jsonplan/plan_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package jsonplan
     7  
     8  import (
     9  	"encoding/json"
    10  	"fmt"
    11  	"reflect"
    12  	"sort"
    13  	"testing"
    14  
    15  	"github.com/google/go-cmp/cmp"
    16  	"github.com/zclconf/go-cty/cty"
    17  
    18  	"github.com/opentofu/opentofu/internal/addrs"
    19  	"github.com/opentofu/opentofu/internal/plans"
    20  )
    21  
    22  func TestOmitUnknowns(t *testing.T) {
    23  	tests := []struct {
    24  		Input cty.Value
    25  		Want  cty.Value
    26  	}{
    27  		{
    28  			cty.StringVal("hello"),
    29  			cty.StringVal("hello"),
    30  		},
    31  		{
    32  			cty.NullVal(cty.String),
    33  			cty.NullVal(cty.String),
    34  		},
    35  		{
    36  			cty.UnknownVal(cty.String),
    37  			cty.NilVal,
    38  		},
    39  		{
    40  			cty.ListValEmpty(cty.String),
    41  			cty.EmptyTupleVal,
    42  		},
    43  		{
    44  			cty.ListVal([]cty.Value{cty.StringVal("hello")}),
    45  			cty.TupleVal([]cty.Value{cty.StringVal("hello")}),
    46  		},
    47  		{
    48  			cty.ListVal([]cty.Value{cty.NullVal(cty.String)}),
    49  			cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}),
    50  		},
    51  		{
    52  			cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
    53  			cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}),
    54  		},
    55  		{
    56  			cty.ListVal([]cty.Value{cty.StringVal("hello")}),
    57  			cty.TupleVal([]cty.Value{cty.StringVal("hello")}),
    58  		},
    59  		//
    60  		{
    61  			cty.ListVal([]cty.Value{
    62  				cty.StringVal("hello"),
    63  				cty.UnknownVal(cty.String)}),
    64  			cty.TupleVal([]cty.Value{
    65  				cty.StringVal("hello"),
    66  				cty.NullVal(cty.String),
    67  			}),
    68  		},
    69  		{
    70  			cty.MapVal(map[string]cty.Value{
    71  				"hello": cty.True,
    72  				"world": cty.UnknownVal(cty.Bool),
    73  			}),
    74  			cty.ObjectVal(map[string]cty.Value{
    75  				"hello": cty.True,
    76  			}),
    77  		},
    78  		{
    79  			cty.TupleVal([]cty.Value{
    80  				cty.StringVal("alpha"),
    81  				cty.UnknownVal(cty.String),
    82  				cty.StringVal("charlie"),
    83  			}),
    84  			cty.TupleVal([]cty.Value{
    85  				cty.StringVal("alpha"),
    86  				cty.NullVal(cty.String),
    87  				cty.StringVal("charlie"),
    88  			}),
    89  		},
    90  		{
    91  			cty.SetVal([]cty.Value{
    92  				cty.StringVal("dev"),
    93  				cty.StringVal("foo"),
    94  				cty.StringVal("stg"),
    95  				cty.UnknownVal(cty.String),
    96  			}),
    97  			cty.TupleVal([]cty.Value{
    98  				cty.StringVal("dev"),
    99  				cty.StringVal("foo"),
   100  				cty.StringVal("stg"),
   101  				cty.NullVal(cty.String),
   102  			}),
   103  		},
   104  		{
   105  			cty.SetVal([]cty.Value{
   106  				cty.ObjectVal(map[string]cty.Value{
   107  					"a": cty.UnknownVal(cty.String),
   108  				}),
   109  				cty.ObjectVal(map[string]cty.Value{
   110  					"a": cty.StringVal("known"),
   111  				}),
   112  			}),
   113  			cty.TupleVal([]cty.Value{
   114  				cty.ObjectVal(map[string]cty.Value{
   115  					"a": cty.StringVal("known"),
   116  				}),
   117  				cty.EmptyObjectVal,
   118  			}),
   119  		},
   120  	}
   121  
   122  	for _, test := range tests {
   123  		got := omitUnknowns(test.Input)
   124  		if !reflect.DeepEqual(got, test.Want) {
   125  			t.Errorf(
   126  				"wrong result\ninput: %#v\ngot:   %#v\nwant:  %#v",
   127  				test.Input, got, test.Want,
   128  			)
   129  		}
   130  	}
   131  }
   132  
   133  func TestUnknownAsBool(t *testing.T) {
   134  	tests := []struct {
   135  		Input cty.Value
   136  		Want  cty.Value
   137  	}{
   138  		{
   139  			cty.StringVal("hello"),
   140  			cty.False,
   141  		},
   142  		{
   143  			cty.NullVal(cty.String),
   144  			cty.False,
   145  		},
   146  		{
   147  			cty.UnknownVal(cty.String),
   148  			cty.True,
   149  		},
   150  
   151  		{
   152  			cty.NullVal(cty.DynamicPseudoType),
   153  			cty.False,
   154  		},
   155  		{
   156  			cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})),
   157  			cty.False,
   158  		},
   159  		{
   160  			cty.DynamicVal,
   161  			cty.True,
   162  		},
   163  
   164  		{
   165  			cty.ListValEmpty(cty.String),
   166  			cty.EmptyTupleVal,
   167  		},
   168  		{
   169  			cty.ListVal([]cty.Value{cty.StringVal("hello")}),
   170  			cty.TupleVal([]cty.Value{cty.False}),
   171  		},
   172  		{
   173  			cty.ListVal([]cty.Value{cty.NullVal(cty.String)}),
   174  			cty.TupleVal([]cty.Value{cty.False}),
   175  		},
   176  		{
   177  			cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
   178  			cty.TupleVal([]cty.Value{cty.True}),
   179  		},
   180  		{
   181  			cty.SetValEmpty(cty.String),
   182  			cty.EmptyTupleVal,
   183  		},
   184  		{
   185  			cty.SetVal([]cty.Value{cty.StringVal("hello")}),
   186  			cty.TupleVal([]cty.Value{cty.False}),
   187  		},
   188  		{
   189  			cty.SetVal([]cty.Value{cty.NullVal(cty.String)}),
   190  			cty.TupleVal([]cty.Value{cty.False}),
   191  		},
   192  		{
   193  			cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)}),
   194  			cty.TupleVal([]cty.Value{cty.True}),
   195  		},
   196  		{
   197  			cty.EmptyTupleVal,
   198  			cty.EmptyTupleVal,
   199  		},
   200  		{
   201  			cty.TupleVal([]cty.Value{cty.StringVal("hello")}),
   202  			cty.TupleVal([]cty.Value{cty.False}),
   203  		},
   204  		{
   205  			cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}),
   206  			cty.TupleVal([]cty.Value{cty.False}),
   207  		},
   208  		{
   209  			cty.TupleVal([]cty.Value{cty.UnknownVal(cty.String)}),
   210  			cty.TupleVal([]cty.Value{cty.True}),
   211  		},
   212  		{
   213  			cty.MapValEmpty(cty.String),
   214  			cty.EmptyObjectVal,
   215  		},
   216  		{
   217  			cty.MapVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}),
   218  			cty.EmptyObjectVal,
   219  		},
   220  		{
   221  			cty.MapVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}),
   222  			cty.EmptyObjectVal,
   223  		},
   224  		{
   225  			cty.MapVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}),
   226  			cty.ObjectVal(map[string]cty.Value{"greeting": cty.True}),
   227  		},
   228  		{
   229  			cty.EmptyObjectVal,
   230  			cty.EmptyObjectVal,
   231  		},
   232  		{
   233  			cty.ObjectVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}),
   234  			cty.EmptyObjectVal,
   235  		},
   236  		{
   237  			cty.ObjectVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}),
   238  			cty.EmptyObjectVal,
   239  		},
   240  		{
   241  			cty.ObjectVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}),
   242  			cty.ObjectVal(map[string]cty.Value{"greeting": cty.True}),
   243  		},
   244  		{
   245  			cty.SetVal([]cty.Value{
   246  				cty.ObjectVal(map[string]cty.Value{
   247  					"a": cty.UnknownVal(cty.String),
   248  				}),
   249  				cty.ObjectVal(map[string]cty.Value{
   250  					"a": cty.StringVal("known"),
   251  				}),
   252  			}),
   253  			cty.TupleVal([]cty.Value{
   254  				cty.EmptyObjectVal,
   255  				cty.ObjectVal(map[string]cty.Value{
   256  					"a": cty.True,
   257  				}),
   258  			}),
   259  		},
   260  		{
   261  			cty.SetVal([]cty.Value{
   262  				cty.MapValEmpty(cty.String),
   263  				cty.MapVal(map[string]cty.Value{
   264  					"a": cty.StringVal("known"),
   265  				}),
   266  				cty.MapVal(map[string]cty.Value{
   267  					"a": cty.UnknownVal(cty.String),
   268  				}),
   269  			}),
   270  			cty.TupleVal([]cty.Value{
   271  				cty.EmptyObjectVal,
   272  				cty.ObjectVal(map[string]cty.Value{
   273  					"a": cty.True,
   274  				}),
   275  				cty.EmptyObjectVal,
   276  			}),
   277  		},
   278  	}
   279  
   280  	for _, test := range tests {
   281  		got := unknownAsBool(test.Input)
   282  		if !reflect.DeepEqual(got, test.Want) {
   283  			t.Errorf(
   284  				"wrong result\ninput: %#v\ngot:   %#v\nwant:  %#v",
   285  				test.Input, got, test.Want,
   286  			)
   287  		}
   288  	}
   289  }
   290  
   291  func TestEncodePaths(t *testing.T) {
   292  	tests := map[string]struct {
   293  		Input cty.PathSet
   294  		Want  json.RawMessage
   295  	}{
   296  		"empty set": {
   297  			cty.NewPathSet(),
   298  			json.RawMessage(nil),
   299  		},
   300  		"index path with string and int steps": {
   301  			cty.NewPathSet(cty.IndexStringPath("boop").IndexInt(0)),
   302  			json.RawMessage(`[["boop",0]]`),
   303  		},
   304  		"get attr path with one step": {
   305  			cty.NewPathSet(cty.GetAttrPath("triggers")),
   306  			json.RawMessage(`[["triggers"]]`),
   307  		},
   308  		"multiple paths of different types": {
   309  			// The order of the path sets is not guaranteed, so we sort the
   310  			// result by the number of elements in the path to make the test deterministic.
   311  			cty.NewPathSet(
   312  				cty.GetAttrPath("alpha").GetAttr("beta"),                            // 2 elements
   313  				cty.GetAttrPath("triggers").IndexString("name").IndexString("test"), // 3 elements
   314  				cty.IndexIntPath(0).IndexInt(1).IndexInt(2).IndexInt(3),             // 4 elements
   315  			),
   316  			json.RawMessage(`[[0,1,2,3],["alpha","beta"],["triggers","name","test"]]`),
   317  		},
   318  	}
   319  
   320  	// comp is a custom comparator for comparing JSON arrays. It sorts the
   321  	// arrays based on the number of elements in each path before comparing them.
   322  	// this allows our test cases to be more flexible about the order of the
   323  	// paths in the result. and deterministic on both 32 and 64 bit architectures.
   324  	comp := func(a, b json.RawMessage) (bool, error) {
   325  		if a == nil && b == nil {
   326  			return true, nil // Both are nil, they are equal
   327  		}
   328  		if a == nil || b == nil {
   329  			return false, nil // One is nil and the other is not, they are not equal
   330  		}
   331  
   332  		var pathsA, pathsB [][]interface{}
   333  		err := json.Unmarshal(a, &pathsA)
   334  		if err != nil {
   335  			return false, fmt.Errorf("error unmarshalling first argument: %w", err)
   336  		}
   337  		err = json.Unmarshal(b, &pathsB)
   338  		if err != nil {
   339  			return false, fmt.Errorf("error unmarshalling second argument: %w", err)
   340  		}
   341  
   342  		// Sort the slices based on the number of elements in each path
   343  		sort.Slice(pathsA, func(i, j int) bool {
   344  			return len(pathsA[i]) < len(pathsA[j])
   345  		})
   346  		sort.Slice(pathsB, func(i, j int) bool {
   347  			return len(pathsB[i]) < len(pathsB[j])
   348  		})
   349  
   350  		return cmp.Equal(pathsA, pathsB), nil
   351  	}
   352  
   353  	for name, test := range tests {
   354  		t.Run(name, func(t *testing.T) {
   355  			got, err := encodePaths(test.Input)
   356  			if err != nil {
   357  				t.Fatalf("unexpected error: %s", err)
   358  			}
   359  
   360  			equal, err := comp(got, test.Want)
   361  			if err != nil {
   362  				t.Fatalf("error comparing JSON slices: %s", err)
   363  			}
   364  			if !equal {
   365  				t.Errorf("paths do not match:\n%s", cmp.Diff(got, test.Want))
   366  			}
   367  		})
   368  	}
   369  }
   370  
   371  func TestOutputs(t *testing.T) {
   372  	root := addrs.RootModuleInstance
   373  
   374  	child, diags := addrs.ParseModuleInstanceStr("module.child")
   375  	if diags.HasErrors() {
   376  		t.Fatalf("unexpected errors: %s", diags.Err())
   377  	}
   378  
   379  	tests := map[string]struct {
   380  		changes  *plans.Changes
   381  		expected map[string]Change
   382  	}{
   383  		"copies all outputs": {
   384  			changes: &plans.Changes{
   385  				Outputs: []*plans.OutputChangeSrc{
   386  					{
   387  						Addr: root.OutputValue("first"),
   388  						ChangeSrc: plans.ChangeSrc{
   389  							Action: plans.Create,
   390  						},
   391  					},
   392  					{
   393  						Addr: root.OutputValue("second"),
   394  						ChangeSrc: plans.ChangeSrc{
   395  							Action: plans.Create,
   396  						},
   397  					},
   398  				},
   399  			},
   400  			expected: map[string]Change{
   401  				"first": {
   402  					Actions:         []string{"create"},
   403  					Before:          json.RawMessage("null"),
   404  					After:           json.RawMessage("null"),
   405  					AfterUnknown:    json.RawMessage("false"),
   406  					BeforeSensitive: json.RawMessage("false"),
   407  					AfterSensitive:  json.RawMessage("false"),
   408  				},
   409  				"second": {
   410  					Actions:         []string{"create"},
   411  					Before:          json.RawMessage("null"),
   412  					After:           json.RawMessage("null"),
   413  					AfterUnknown:    json.RawMessage("false"),
   414  					BeforeSensitive: json.RawMessage("false"),
   415  					AfterSensitive:  json.RawMessage("false"),
   416  				},
   417  			},
   418  		},
   419  		"skips non root modules": {
   420  			changes: &plans.Changes{
   421  				Outputs: []*plans.OutputChangeSrc{
   422  					{
   423  						Addr: root.OutputValue("first"),
   424  						ChangeSrc: plans.ChangeSrc{
   425  							Action: plans.Create,
   426  						},
   427  					},
   428  					{
   429  						Addr: child.OutputValue("second"),
   430  						ChangeSrc: plans.ChangeSrc{
   431  							Action: plans.Create,
   432  						},
   433  					},
   434  				},
   435  			},
   436  			expected: map[string]Change{
   437  				"first": {
   438  					Actions:         []string{"create"},
   439  					Before:          json.RawMessage("null"),
   440  					After:           json.RawMessage("null"),
   441  					AfterUnknown:    json.RawMessage("false"),
   442  					BeforeSensitive: json.RawMessage("false"),
   443  					AfterSensitive:  json.RawMessage("false"),
   444  				},
   445  			},
   446  		},
   447  	}
   448  	for name, test := range tests {
   449  		t.Run(name, func(t *testing.T) {
   450  			changes, err := MarshalOutputChanges(test.changes)
   451  			if err != nil {
   452  				t.Fatalf("unexpected err: %s", err)
   453  			}
   454  
   455  			if !cmp.Equal(changes, test.expected) {
   456  				t.Errorf("wrong result:\n %v\n", cmp.Diff(changes, test.expected))
   457  			}
   458  		})
   459  	}
   460  }
   461  
   462  func deepObjectValue(depth int) cty.Value {
   463  	v := cty.ObjectVal(map[string]cty.Value{
   464  		"a": cty.StringVal("a"),
   465  		"b": cty.NumberIntVal(2),
   466  		"c": cty.True,
   467  		"d": cty.UnknownVal(cty.String),
   468  	})
   469  
   470  	result := v
   471  
   472  	for i := 0; i < depth; i++ {
   473  		result = cty.ObjectVal(map[string]cty.Value{
   474  			"a": result,
   475  			"b": result,
   476  			"c": result,
   477  		})
   478  	}
   479  
   480  	return result
   481  }
   482  
   483  func BenchmarkUnknownAsBool_2(b *testing.B) {
   484  	value := deepObjectValue(2)
   485  	for n := 0; n < b.N; n++ {
   486  		unknownAsBool(value)
   487  	}
   488  }
   489  
   490  func BenchmarkUnknownAsBool_3(b *testing.B) {
   491  	value := deepObjectValue(3)
   492  	for n := 0; n < b.N; n++ {
   493  		unknownAsBool(value)
   494  	}
   495  }
   496  
   497  func BenchmarkUnknownAsBool_5(b *testing.B) {
   498  	value := deepObjectValue(5)
   499  	for n := 0; n < b.N; n++ {
   500  		unknownAsBool(value)
   501  	}
   502  }
   503  
   504  func BenchmarkUnknownAsBool_7(b *testing.B) {
   505  	value := deepObjectValue(7)
   506  	for n := 0; n < b.N; n++ {
   507  		unknownAsBool(value)
   508  	}
   509  }
   510  
   511  func BenchmarkUnknownAsBool_9(b *testing.B) {
   512  	value := deepObjectValue(9)
   513  	for n := 0; n < b.N; n++ {
   514  		unknownAsBool(value)
   515  	}
   516  }