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 }