github.com/kevinklinger/open_terraform@v1.3.6/noninternal/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 address: "original", 189 }, 190 subject: &SourceRange{ 191 Filename: "somewhere_else.tf", 192 }, 193 }, 194 &SourceRange{ 195 Filename: "somewhere_else.tf", 196 }, 197 }, 198 { 199 // Missing path 200 &attributeDiagnostic{ 201 diagnosticBase: diagnosticBase{ 202 summary: "missing path", 203 }, 204 }, 205 nil, 206 }, 207 208 // Nested attributes 209 { 210 AttributeValue( 211 Error, 212 "parent.nested_str", 213 "detail", 214 cty.Path{ 215 cty.GetAttrStep{Name: "parent"}, 216 cty.GetAttrStep{Name: "nested_str"}, 217 }, 218 ), 219 &SourceRange{ 220 Filename: "test.tf", 221 Start: SourcePos{Line: 18, Column: 16, Byte: 145}, 222 End: SourcePos{Line: 18, Column: 23, Byte: 152}, 223 }, 224 }, 225 { 226 AttributeValue( 227 Error, 228 "parent.nested_str_tuple[99]", 229 "detail", 230 cty.Path{ 231 cty.GetAttrStep{Name: "parent"}, 232 cty.GetAttrStep{Name: "nested_str_tuple"}, 233 cty.IndexStep{Key: cty.NumberIntVal(99)}, 234 }, 235 ), 236 &SourceRange{ 237 Filename: "test.tf", 238 Start: SourcePos{Line: 19, Column: 3, Byte: 155}, 239 End: SourcePos{Line: 19, Column: 19, Byte: 171}, 240 }, 241 }, 242 { 243 AttributeValue( 244 Error, 245 "parent.nested_str_tuple[0]", 246 "detail", 247 cty.Path{ 248 cty.GetAttrStep{Name: "parent"}, 249 cty.GetAttrStep{Name: "nested_str_tuple"}, 250 cty.IndexStep{Key: cty.NumberIntVal(0)}, 251 }, 252 ), 253 &SourceRange{ 254 Filename: "test.tf", 255 Start: SourcePos{Line: 19, Column: 23, Byte: 175}, 256 End: SourcePos{Line: 19, Column: 27, Byte: 179}, 257 }, 258 }, 259 { 260 AttributeValue( 261 Error, 262 "parent.nested_str_tuple[2]", 263 "detail", 264 cty.Path{ 265 cty.GetAttrStep{Name: "parent"}, 266 cty.GetAttrStep{Name: "nested_str_tuple"}, 267 cty.IndexStep{Key: cty.NumberIntVal(2)}, 268 }, 269 ), 270 &SourceRange{ 271 Filename: "test.tf", 272 Start: SourcePos{Line: 19, Column: 36, Byte: 188}, 273 End: SourcePos{Line: 19, Column: 42, Byte: 194}, 274 }, 275 }, 276 { 277 AttributeValue( 278 Error, 279 "parent.nested_num_tuple[0]", 280 "detail", 281 cty.Path{ 282 cty.GetAttrStep{Name: "parent"}, 283 cty.GetAttrStep{Name: "nested_num_tuple"}, 284 cty.IndexStep{Key: cty.NumberIntVal(0)}, 285 }, 286 ), 287 &SourceRange{ 288 Filename: "test.tf", 289 Start: SourcePos{Line: 20, Column: 23, Byte: 218}, 290 End: SourcePos{Line: 20, Column: 24, Byte: 219}, 291 }, 292 }, 293 { 294 AttributeValue( 295 Error, 296 "parent.nested_num_tuple[1]", 297 "detail", 298 cty.Path{ 299 cty.GetAttrStep{Name: "parent"}, 300 cty.GetAttrStep{Name: "nested_num_tuple"}, 301 cty.IndexStep{Key: cty.NumberIntVal(1)}, 302 }, 303 ), 304 &SourceRange{ 305 Filename: "test.tf", 306 Start: SourcePos{Line: 20, Column: 26, Byte: 221}, 307 End: SourcePos{Line: 20, Column: 30, Byte: 225}, 308 }, 309 }, 310 { 311 AttributeValue( 312 Error, 313 "parent.nested_map.first_key", 314 "detail", 315 cty.Path{ 316 cty.GetAttrStep{Name: "parent"}, 317 cty.GetAttrStep{Name: "nested_map"}, 318 cty.IndexStep{Key: cty.StringVal("first_key")}, 319 }, 320 ), 321 &SourceRange{ 322 Filename: "test.tf", 323 Start: SourcePos{Line: 22, Column: 19, Byte: 266}, 324 End: SourcePos{Line: 22, Column: 30, Byte: 277}, 325 }, 326 }, 327 { 328 AttributeValue( 329 Error, 330 "parent.nested_map.second_key", 331 "detail", 332 cty.Path{ 333 cty.GetAttrStep{Name: "parent"}, 334 cty.GetAttrStep{Name: "nested_map"}, 335 cty.IndexStep{Key: cty.StringVal("second_key")}, 336 }, 337 ), 338 &SourceRange{ 339 Filename: "test.tf", 340 Start: SourcePos{Line: 23, Column: 19, Byte: 297}, 341 End: SourcePos{Line: 23, Column: 28, Byte: 306}, 342 }, 343 }, 344 { 345 AttributeValue( 346 Error, 347 "parent.nested_map.undefined_key", 348 "detail", 349 cty.Path{ 350 cty.GetAttrStep{Name: "parent"}, 351 cty.GetAttrStep{Name: "nested_map"}, 352 cty.IndexStep{Key: cty.StringVal("undefined_key")}, 353 }, 354 ), 355 &SourceRange{ 356 Filename: "test.tf", 357 Start: SourcePos{Line: 21, Column: 3, Byte: 233}, 358 End: SourcePos{Line: 21, Column: 13, Byte: 243}, 359 }, 360 }, 361 362 // Root attributes of complex types 363 { 364 AttributeValue( 365 Error, 366 "tuple_of_one[0]", 367 "detail", 368 cty.Path{ 369 cty.GetAttrStep{Name: "tuple_of_one"}, 370 cty.IndexStep{Key: cty.NumberIntVal(0)}, 371 }, 372 ), 373 &SourceRange{ 374 Filename: "test.tf", 375 Start: SourcePos{Line: 26, Column: 17, Byte: 330}, 376 End: SourcePos{Line: 26, Column: 22, Byte: 335}, 377 }, 378 }, 379 { 380 AttributeValue( 381 Error, 382 "tuple_of_two[0]", 383 "detail", 384 cty.Path{ 385 cty.GetAttrStep{Name: "tuple_of_two"}, 386 cty.IndexStep{Key: cty.NumberIntVal(0)}, 387 }, 388 ), 389 &SourceRange{ 390 Filename: "test.tf", 391 Start: SourcePos{Line: 27, Column: 17, Byte: 353}, 392 End: SourcePos{Line: 27, Column: 24, Byte: 360}, 393 }, 394 }, 395 { 396 AttributeValue( 397 Error, 398 "tuple_of_two[1]", 399 "detail", 400 cty.Path{ 401 cty.GetAttrStep{Name: "tuple_of_two"}, 402 cty.IndexStep{Key: cty.NumberIntVal(1)}, 403 }, 404 ), 405 &SourceRange{ 406 Filename: "test.tf", 407 Start: SourcePos{Line: 27, Column: 26, Byte: 362}, 408 End: SourcePos{Line: 27, Column: 33, Byte: 369}, 409 }, 410 }, 411 { 412 AttributeValue( 413 Error, 414 "tuple_of_one[null]", 415 "detail", 416 cty.Path{ 417 cty.GetAttrStep{Name: "tuple_of_one"}, 418 cty.IndexStep{Key: cty.NullVal(cty.Number)}, 419 }, 420 ), 421 &SourceRange{ 422 Filename: "test.tf", 423 Start: SourcePos{Line: 26, Column: 1, Byte: 314}, 424 End: SourcePos{Line: 26, Column: 13, Byte: 326}, 425 }, 426 }, 427 { 428 // index out of range 429 AttributeValue( 430 Error, 431 "tuple_of_two[99]", 432 "detail", 433 cty.Path{ 434 cty.GetAttrStep{Name: "tuple_of_two"}, 435 cty.IndexStep{Key: cty.NumberIntVal(99)}, 436 }, 437 ), 438 &SourceRange{ 439 Filename: "test.tf", 440 Start: SourcePos{Line: 27, Column: 1, Byte: 337}, 441 End: SourcePos{Line: 27, Column: 13, Byte: 349}, 442 }, 443 }, 444 { 445 AttributeValue( 446 Error, 447 "root_map.first", 448 "detail", 449 cty.Path{ 450 cty.GetAttrStep{Name: "root_map"}, 451 cty.IndexStep{Key: cty.StringVal("first")}, 452 }, 453 ), 454 &SourceRange{ 455 Filename: "test.tf", 456 Start: SourcePos{Line: 29, Column: 13, Byte: 396}, 457 End: SourcePos{Line: 29, Column: 16, Byte: 399}, 458 }, 459 }, 460 { 461 AttributeValue( 462 Error, 463 "root_map.second", 464 "detail", 465 cty.Path{ 466 cty.GetAttrStep{Name: "root_map"}, 467 cty.IndexStep{Key: cty.StringVal("second")}, 468 }, 469 ), 470 &SourceRange{ 471 Filename: "test.tf", 472 Start: SourcePos{Line: 30, Column: 13, Byte: 413}, 473 End: SourcePos{Line: 30, Column: 16, Byte: 416}, 474 }, 475 }, 476 { 477 AttributeValue( 478 Error, 479 "root_map.undefined_key", 480 "detail", 481 cty.Path{ 482 cty.GetAttrStep{Name: "root_map"}, 483 cty.IndexStep{Key: cty.StringVal("undefined_key")}, 484 }, 485 ), 486 &SourceRange{ 487 Filename: "test.tf", 488 Start: SourcePos{Line: 28, Column: 1, Byte: 371}, 489 End: SourcePos{Line: 28, Column: 9, Byte: 379}, 490 }, 491 }, 492 { 493 AttributeValue( 494 Error, 495 "simple_attr", 496 "detail", 497 cty.Path{ 498 cty.GetAttrStep{Name: "simple_attr"}, 499 }, 500 ), 501 &SourceRange{ 502 Filename: "test.tf", 503 Start: SourcePos{Line: 32, Column: 15, Byte: 434}, 504 End: SourcePos{Line: 32, Column: 20, Byte: 439}, 505 }, 506 }, 507 { 508 // This should never happen as error should always point to an attribute 509 // or index of an attribute, but we should not crash if it does 510 AttributeValue( 511 Error, 512 "key", 513 "index_step", 514 cty.Path{ 515 cty.IndexStep{Key: cty.StringVal("key")}, 516 }, 517 ), 518 emptySrcRng, 519 }, 520 { 521 // This should never happen as error should always point to an attribute 522 // or index of an attribute, but we should not crash if it does 523 AttributeValue( 524 Error, 525 "key.another", 526 "index_step", 527 cty.Path{ 528 cty.IndexStep{Key: cty.StringVal("key")}, 529 cty.IndexStep{Key: cty.StringVal("another")}, 530 }, 531 ), 532 emptySrcRng, 533 }, 534 } 535 536 for i, tc := range testCases { 537 t.Run(fmt.Sprintf("%d:%s", i, tc.Diag.Description()), func(t *testing.T) { 538 var diags Diagnostics 539 540 origAddr := tc.Diag.Description().Address 541 diags = diags.Append(tc.Diag) 542 543 gotDiags := diags.InConfigBody(f.Body, "test.addr") 544 gotRange := gotDiags[0].Source().Subject 545 gotAddr := gotDiags[0].Description().Address 546 547 switch { 548 case origAddr != "": 549 if gotAddr != origAddr { 550 t.Errorf("original diagnostic address modified from %s to %s", origAddr, gotAddr) 551 } 552 case gotAddr != "test.addr": 553 t.Error("missing detail address") 554 } 555 556 for _, problem := range deep.Equal(gotRange, tc.ExpectedRange) { 557 t.Error(problem) 558 } 559 }) 560 } 561 } 562 563 func TestGetAttribute(t *testing.T) { 564 path := cty.Path{ 565 cty.GetAttrStep{Name: "foo"}, 566 cty.IndexStep{Key: cty.NumberIntVal(0)}, 567 cty.GetAttrStep{Name: "bar"}, 568 } 569 570 d := AttributeValue( 571 Error, 572 "foo[0].bar", 573 "detail", 574 path, 575 ) 576 577 p := GetAttribute(d) 578 if !reflect.DeepEqual(path, p) { 579 t.Fatalf("paths don't match:\nexpected: %#v\ngot: %#v", path, p) 580 } 581 }