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