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