github.com/opentofu/opentofu@v1.7.1/internal/tfdiags/contextual_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 tfdiags 7 8 import ( 9 "fmt" 10 "reflect" 11 "testing" 12 13 "github.com/go-test/deep" 14 "github.com/hashicorp/hcl/v2" 15 "github.com/hashicorp/hcl/v2/hclsyntax" 16 "github.com/zclconf/go-cty/cty" 17 ) 18 19 func TestAttributeValue(t *testing.T) { 20 testConfig := ` 21 foo { 22 bar = "hi" 23 } 24 foo { 25 bar = "bar" 26 } 27 bar { 28 bar = "woot" 29 } 30 baz "a" { 31 bar = "beep" 32 } 33 baz "b" { 34 bar = "boop" 35 } 36 parent { 37 nested_str = "hello" 38 nested_str_tuple = ["aa", "bbb", "cccc"] 39 nested_num_tuple = [1, 9863, 22] 40 nested_map = { 41 first_key = "first_value" 42 second_key = "2nd value" 43 } 44 } 45 tuple_of_one = ["one"] 46 tuple_of_two = ["first", "22222"] 47 root_map = { 48 first = "1st" 49 second = "2nd" 50 } 51 simple_attr = "val" 52 ` 53 // TODO: Test ConditionalExpr 54 // TODO: Test ForExpr 55 // TODO: Test FunctionCallExpr 56 // TODO: Test IndexExpr 57 // TODO: Test interpolation 58 // TODO: Test SplatExpr 59 60 f, parseDiags := hclsyntax.ParseConfig([]byte(testConfig), "test.tf", hcl.Pos{Line: 1, Column: 1}) 61 if len(parseDiags) != 0 { 62 t.Fatal(parseDiags) 63 } 64 emptySrcRng := &SourceRange{ 65 Filename: "test.tf", 66 Start: SourcePos{Line: 1, Column: 1, Byte: 0}, 67 End: SourcePos{Line: 1, Column: 1, Byte: 0}, 68 } 69 70 testCases := []struct { 71 Diag Diagnostic 72 ExpectedRange *SourceRange 73 }{ 74 { 75 AttributeValue( 76 Error, 77 "foo[0].bar", 78 "detail", 79 cty.Path{ 80 cty.GetAttrStep{Name: "foo"}, 81 cty.IndexStep{Key: cty.NumberIntVal(0)}, 82 cty.GetAttrStep{Name: "bar"}, 83 }, 84 ), 85 &SourceRange{ 86 Filename: "test.tf", 87 Start: SourcePos{Line: 3, Column: 9, Byte: 15}, 88 End: SourcePos{Line: 3, Column: 13, Byte: 19}, 89 }, 90 }, 91 { 92 AttributeValue( 93 Error, 94 "foo[1].bar", 95 "detail", 96 cty.Path{ 97 cty.GetAttrStep{Name: "foo"}, 98 cty.IndexStep{Key: cty.NumberIntVal(1)}, 99 cty.GetAttrStep{Name: "bar"}, 100 }, 101 ), 102 &SourceRange{ 103 Filename: "test.tf", 104 Start: SourcePos{Line: 6, Column: 9, Byte: 36}, 105 End: SourcePos{Line: 6, Column: 14, Byte: 41}, 106 }, 107 }, 108 { 109 AttributeValue( 110 Error, 111 "foo[99].bar", 112 "detail", 113 cty.Path{ 114 cty.GetAttrStep{Name: "foo"}, 115 cty.IndexStep{Key: cty.NumberIntVal(99)}, 116 cty.GetAttrStep{Name: "bar"}, 117 }, 118 ), 119 emptySrcRng, 120 }, 121 { 122 AttributeValue( 123 Error, 124 "bar.bar", 125 "detail", 126 cty.Path{ 127 cty.GetAttrStep{Name: "bar"}, 128 cty.GetAttrStep{Name: "bar"}, 129 }, 130 ), 131 &SourceRange{ 132 Filename: "test.tf", 133 Start: SourcePos{Line: 9, Column: 9, Byte: 58}, 134 End: SourcePos{Line: 9, Column: 15, Byte: 64}, 135 }, 136 }, 137 { 138 AttributeValue( 139 Error, 140 `baz["a"].bar`, 141 "detail", 142 cty.Path{ 143 cty.GetAttrStep{Name: "baz"}, 144 cty.IndexStep{Key: cty.StringVal("a")}, 145 cty.GetAttrStep{Name: "bar"}, 146 }, 147 ), 148 &SourceRange{ 149 Filename: "test.tf", 150 Start: SourcePos{Line: 12, Column: 9, Byte: 85}, 151 End: SourcePos{Line: 12, Column: 15, Byte: 91}, 152 }, 153 }, 154 { 155 AttributeValue( 156 Error, 157 `baz["b"].bar`, 158 "detail", 159 cty.Path{ 160 cty.GetAttrStep{Name: "baz"}, 161 cty.IndexStep{Key: cty.StringVal("b")}, 162 cty.GetAttrStep{Name: "bar"}, 163 }, 164 ), 165 &SourceRange{ 166 Filename: "test.tf", 167 Start: SourcePos{Line: 15, Column: 9, Byte: 112}, 168 End: SourcePos{Line: 15, Column: 15, Byte: 118}, 169 }, 170 }, 171 { 172 AttributeValue( 173 Error, 174 `baz["not_exists"].bar`, 175 "detail", 176 cty.Path{ 177 cty.GetAttrStep{Name: "baz"}, 178 cty.IndexStep{Key: cty.StringVal("not_exists")}, 179 cty.GetAttrStep{Name: "bar"}, 180 }, 181 ), 182 emptySrcRng, 183 }, 184 { 185 // Attribute value with subject already populated should not be disturbed. 186 // (in a real case, this might've been passed through from a deeper function 187 // in the call stack, for example.) 188 &attributeDiagnostic{ 189 attrPath: cty.Path{cty.GetAttrStep{Name: "foo"}}, 190 diagnosticBase: diagnosticBase{ 191 summary: "preexisting", 192 detail: "detail", 193 address: "original", 194 }, 195 subject: &SourceRange{ 196 Filename: "somewhere_else.tf", 197 }, 198 }, 199 &SourceRange{ 200 Filename: "somewhere_else.tf", 201 }, 202 }, 203 { 204 // Missing path 205 &attributeDiagnostic{ 206 diagnosticBase: diagnosticBase{ 207 summary: "missing path", 208 }, 209 }, 210 nil, 211 }, 212 213 // Nested attributes 214 { 215 AttributeValue( 216 Error, 217 "parent.nested_str", 218 "detail", 219 cty.Path{ 220 cty.GetAttrStep{Name: "parent"}, 221 cty.GetAttrStep{Name: "nested_str"}, 222 }, 223 ), 224 &SourceRange{ 225 Filename: "test.tf", 226 Start: SourcePos{Line: 18, Column: 16, Byte: 145}, 227 End: SourcePos{Line: 18, Column: 23, Byte: 152}, 228 }, 229 }, 230 { 231 AttributeValue( 232 Error, 233 "parent.nested_str_tuple[99]", 234 "detail", 235 cty.Path{ 236 cty.GetAttrStep{Name: "parent"}, 237 cty.GetAttrStep{Name: "nested_str_tuple"}, 238 cty.IndexStep{Key: cty.NumberIntVal(99)}, 239 }, 240 ), 241 &SourceRange{ 242 Filename: "test.tf", 243 Start: SourcePos{Line: 19, Column: 3, Byte: 155}, 244 End: SourcePos{Line: 19, Column: 19, Byte: 171}, 245 }, 246 }, 247 { 248 AttributeValue( 249 Error, 250 "parent.nested_str_tuple[0]", 251 "detail", 252 cty.Path{ 253 cty.GetAttrStep{Name: "parent"}, 254 cty.GetAttrStep{Name: "nested_str_tuple"}, 255 cty.IndexStep{Key: cty.NumberIntVal(0)}, 256 }, 257 ), 258 &SourceRange{ 259 Filename: "test.tf", 260 Start: SourcePos{Line: 19, Column: 23, Byte: 175}, 261 End: SourcePos{Line: 19, Column: 27, Byte: 179}, 262 }, 263 }, 264 { 265 AttributeValue( 266 Error, 267 "parent.nested_str_tuple[2]", 268 "detail", 269 cty.Path{ 270 cty.GetAttrStep{Name: "parent"}, 271 cty.GetAttrStep{Name: "nested_str_tuple"}, 272 cty.IndexStep{Key: cty.NumberIntVal(2)}, 273 }, 274 ), 275 &SourceRange{ 276 Filename: "test.tf", 277 Start: SourcePos{Line: 19, Column: 36, Byte: 188}, 278 End: SourcePos{Line: 19, Column: 42, Byte: 194}, 279 }, 280 }, 281 { 282 AttributeValue( 283 Error, 284 "parent.nested_num_tuple[0]", 285 "detail", 286 cty.Path{ 287 cty.GetAttrStep{Name: "parent"}, 288 cty.GetAttrStep{Name: "nested_num_tuple"}, 289 cty.IndexStep{Key: cty.NumberIntVal(0)}, 290 }, 291 ), 292 &SourceRange{ 293 Filename: "test.tf", 294 Start: SourcePos{Line: 20, Column: 23, Byte: 218}, 295 End: SourcePos{Line: 20, Column: 24, Byte: 219}, 296 }, 297 }, 298 { 299 AttributeValue( 300 Error, 301 "parent.nested_num_tuple[1]", 302 "detail", 303 cty.Path{ 304 cty.GetAttrStep{Name: "parent"}, 305 cty.GetAttrStep{Name: "nested_num_tuple"}, 306 cty.IndexStep{Key: cty.NumberIntVal(1)}, 307 }, 308 ), 309 &SourceRange{ 310 Filename: "test.tf", 311 Start: SourcePos{Line: 20, Column: 26, Byte: 221}, 312 End: SourcePos{Line: 20, Column: 30, Byte: 225}, 313 }, 314 }, 315 { 316 AttributeValue( 317 Error, 318 "parent.nested_map.first_key", 319 "detail", 320 cty.Path{ 321 cty.GetAttrStep{Name: "parent"}, 322 cty.GetAttrStep{Name: "nested_map"}, 323 cty.IndexStep{Key: cty.StringVal("first_key")}, 324 }, 325 ), 326 &SourceRange{ 327 Filename: "test.tf", 328 Start: SourcePos{Line: 22, Column: 19, Byte: 266}, 329 End: SourcePos{Line: 22, Column: 30, Byte: 277}, 330 }, 331 }, 332 { 333 AttributeValue( 334 Error, 335 "parent.nested_map.second_key", 336 "detail", 337 cty.Path{ 338 cty.GetAttrStep{Name: "parent"}, 339 cty.GetAttrStep{Name: "nested_map"}, 340 cty.IndexStep{Key: cty.StringVal("second_key")}, 341 }, 342 ), 343 &SourceRange{ 344 Filename: "test.tf", 345 Start: SourcePos{Line: 23, Column: 19, Byte: 297}, 346 End: SourcePos{Line: 23, Column: 28, Byte: 306}, 347 }, 348 }, 349 { 350 AttributeValue( 351 Error, 352 "parent.nested_map.undefined_key", 353 "detail", 354 cty.Path{ 355 cty.GetAttrStep{Name: "parent"}, 356 cty.GetAttrStep{Name: "nested_map"}, 357 cty.IndexStep{Key: cty.StringVal("undefined_key")}, 358 }, 359 ), 360 &SourceRange{ 361 Filename: "test.tf", 362 Start: SourcePos{Line: 21, Column: 3, Byte: 233}, 363 End: SourcePos{Line: 21, Column: 13, Byte: 243}, 364 }, 365 }, 366 367 // Root attributes of complex types 368 { 369 AttributeValue( 370 Error, 371 "tuple_of_one[0]", 372 "detail", 373 cty.Path{ 374 cty.GetAttrStep{Name: "tuple_of_one"}, 375 cty.IndexStep{Key: cty.NumberIntVal(0)}, 376 }, 377 ), 378 &SourceRange{ 379 Filename: "test.tf", 380 Start: SourcePos{Line: 26, Column: 17, Byte: 330}, 381 End: SourcePos{Line: 26, Column: 22, Byte: 335}, 382 }, 383 }, 384 { 385 AttributeValue( 386 Error, 387 "tuple_of_two[0]", 388 "detail", 389 cty.Path{ 390 cty.GetAttrStep{Name: "tuple_of_two"}, 391 cty.IndexStep{Key: cty.NumberIntVal(0)}, 392 }, 393 ), 394 &SourceRange{ 395 Filename: "test.tf", 396 Start: SourcePos{Line: 27, Column: 17, Byte: 353}, 397 End: SourcePos{Line: 27, Column: 24, Byte: 360}, 398 }, 399 }, 400 { 401 AttributeValue( 402 Error, 403 "tuple_of_two[1]", 404 "detail", 405 cty.Path{ 406 cty.GetAttrStep{Name: "tuple_of_two"}, 407 cty.IndexStep{Key: cty.NumberIntVal(1)}, 408 }, 409 ), 410 &SourceRange{ 411 Filename: "test.tf", 412 Start: SourcePos{Line: 27, Column: 26, Byte: 362}, 413 End: SourcePos{Line: 27, Column: 33, Byte: 369}, 414 }, 415 }, 416 { 417 AttributeValue( 418 Error, 419 "tuple_of_one[null]", 420 "detail", 421 cty.Path{ 422 cty.GetAttrStep{Name: "tuple_of_one"}, 423 cty.IndexStep{Key: cty.NullVal(cty.Number)}, 424 }, 425 ), 426 &SourceRange{ 427 Filename: "test.tf", 428 Start: SourcePos{Line: 26, Column: 1, Byte: 314}, 429 End: SourcePos{Line: 26, Column: 13, Byte: 326}, 430 }, 431 }, 432 { 433 // index out of range 434 AttributeValue( 435 Error, 436 "tuple_of_two[99]", 437 "detail", 438 cty.Path{ 439 cty.GetAttrStep{Name: "tuple_of_two"}, 440 cty.IndexStep{Key: cty.NumberIntVal(99)}, 441 }, 442 ), 443 &SourceRange{ 444 Filename: "test.tf", 445 Start: SourcePos{Line: 27, Column: 1, Byte: 337}, 446 End: SourcePos{Line: 27, Column: 13, Byte: 349}, 447 }, 448 }, 449 { 450 AttributeValue( 451 Error, 452 "root_map.first", 453 "detail", 454 cty.Path{ 455 cty.GetAttrStep{Name: "root_map"}, 456 cty.IndexStep{Key: cty.StringVal("first")}, 457 }, 458 ), 459 &SourceRange{ 460 Filename: "test.tf", 461 Start: SourcePos{Line: 29, Column: 13, Byte: 396}, 462 End: SourcePos{Line: 29, Column: 16, Byte: 399}, 463 }, 464 }, 465 { 466 AttributeValue( 467 Error, 468 "root_map.second", 469 "detail", 470 cty.Path{ 471 cty.GetAttrStep{Name: "root_map"}, 472 cty.IndexStep{Key: cty.StringVal("second")}, 473 }, 474 ), 475 &SourceRange{ 476 Filename: "test.tf", 477 Start: SourcePos{Line: 30, Column: 13, Byte: 413}, 478 End: SourcePos{Line: 30, Column: 16, Byte: 416}, 479 }, 480 }, 481 { 482 AttributeValue( 483 Error, 484 "root_map.undefined_key", 485 "detail", 486 cty.Path{ 487 cty.GetAttrStep{Name: "root_map"}, 488 cty.IndexStep{Key: cty.StringVal("undefined_key")}, 489 }, 490 ), 491 &SourceRange{ 492 Filename: "test.tf", 493 Start: SourcePos{Line: 28, Column: 1, Byte: 371}, 494 End: SourcePos{Line: 28, Column: 9, Byte: 379}, 495 }, 496 }, 497 { 498 AttributeValue( 499 Error, 500 "simple_attr", 501 "detail", 502 cty.Path{ 503 cty.GetAttrStep{Name: "simple_attr"}, 504 }, 505 ), 506 &SourceRange{ 507 Filename: "test.tf", 508 Start: SourcePos{Line: 32, Column: 15, Byte: 434}, 509 End: SourcePos{Line: 32, Column: 20, Byte: 439}, 510 }, 511 }, 512 { 513 // This should never happen as error should always point to an attribute 514 // or index of an attribute, but we should not crash if it does 515 AttributeValue( 516 Error, 517 "key", 518 "index_step", 519 cty.Path{ 520 cty.IndexStep{Key: cty.StringVal("key")}, 521 }, 522 ), 523 emptySrcRng, 524 }, 525 { 526 // This should never happen as error should always point to an attribute 527 // or index of an attribute, but we should not crash if it does 528 AttributeValue( 529 Error, 530 "key.another", 531 "index_step", 532 cty.Path{ 533 cty.IndexStep{Key: cty.StringVal("key")}, 534 cty.IndexStep{Key: cty.StringVal("another")}, 535 }, 536 ), 537 emptySrcRng, 538 }, 539 } 540 541 for i, tc := range testCases { 542 t.Run(fmt.Sprintf("%d:%s", i, tc.Diag.Description()), func(t *testing.T) { 543 var diags Diagnostics 544 545 origAddr := tc.Diag.Description().Address 546 diags = diags.Append(tc.Diag) 547 548 gotDiags := diags.InConfigBody(f.Body, "test.addr") 549 gotRange := gotDiags[0].Source().Subject 550 gotAddr := gotDiags[0].Description().Address 551 552 switch { 553 case origAddr != "": 554 if gotAddr != origAddr { 555 t.Errorf("original diagnostic address modified from %s to %s", origAddr, gotAddr) 556 } 557 case gotAddr != "test.addr": 558 t.Error("missing detail address") 559 } 560 561 for _, problem := range deep.Equal(gotRange, tc.ExpectedRange) { 562 t.Error(problem) 563 } 564 }) 565 } 566 } 567 568 func TestGetAttribute(t *testing.T) { 569 path := cty.Path{ 570 cty.GetAttrStep{Name: "foo"}, 571 cty.IndexStep{Key: cty.NumberIntVal(0)}, 572 cty.GetAttrStep{Name: "bar"}, 573 } 574 575 d := AttributeValue( 576 Error, 577 "foo[0].bar", 578 "detail", 579 path, 580 ) 581 582 p := GetAttribute(d) 583 if !reflect.DeepEqual(path, p) { 584 t.Fatalf("paths don't match:\nexpected: %#v\ngot: %#v", path, p) 585 } 586 }