github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/views/json/diagnostic_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package json 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "os" 11 "path" 12 "strings" 13 "testing" 14 15 "github.com/google/go-cmp/cmp" 16 "github.com/hashicorp/hcl/v2" 17 "github.com/hashicorp/hcl/v2/hcltest" 18 "github.com/terramate-io/tf/lang/marks" 19 "github.com/terramate-io/tf/tfdiags" 20 "github.com/zclconf/go-cty/cty" 21 ) 22 23 func TestNewDiagnostic(t *testing.T) { 24 // Common HCL for diags with source ranges. This does not have any real 25 // semantic errors, but we can synthesize fake HCL errors which will 26 // exercise the diagnostic rendering code using this 27 sources := map[string][]byte{ 28 "test.tf": []byte(`resource "test_resource" "test" { 29 foo = var.boop["hello!"] 30 bar = { 31 baz = maybe 32 } 33 } 34 `), 35 "short.tf": []byte("bad source code"), 36 "odd-comment.tf": []byte("foo\n\n#\n"), 37 "values.tf": []byte(`[ 38 var.a, 39 var.b, 40 var.c, 41 var.d, 42 var.e, 43 var.f, 44 var.g, 45 var.h, 46 var.i, 47 var.j, 48 var.k, 49 ] 50 `), 51 } 52 testCases := map[string]struct { 53 diag interface{} // allow various kinds of diags 54 want *Diagnostic 55 }{ 56 "sourceless warning": { 57 tfdiags.Sourceless( 58 tfdiags.Warning, 59 "Oh no", 60 "Something is broken", 61 ), 62 &Diagnostic{ 63 Severity: "warning", 64 Summary: "Oh no", 65 Detail: "Something is broken", 66 }, 67 }, 68 "error with source code unavailable": { 69 &hcl.Diagnostic{ 70 Severity: hcl.DiagError, 71 Summary: "Bad news", 72 Detail: "It went wrong", 73 Subject: &hcl.Range{ 74 Filename: "modules/oops/missing.tf", 75 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 76 End: hcl.Pos{Line: 2, Column: 12, Byte: 33}, 77 }, 78 }, 79 &Diagnostic{ 80 Severity: "error", 81 Summary: "Bad news", 82 Detail: "It went wrong", 83 Range: &DiagnosticRange{ 84 Filename: "modules/oops/missing.tf", 85 Start: Pos{ 86 Line: 1, 87 Column: 6, 88 Byte: 5, 89 }, 90 End: Pos{ 91 Line: 2, 92 Column: 12, 93 Byte: 33, 94 }, 95 }, 96 }, 97 }, 98 "error with source code subject": { 99 &hcl.Diagnostic{ 100 Severity: hcl.DiagError, 101 Summary: "Tiny explosion", 102 Detail: "Unexpected detonation while parsing", 103 Subject: &hcl.Range{ 104 Filename: "test.tf", 105 Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 106 End: hcl.Pos{Line: 1, Column: 25, Byte: 24}, 107 }, 108 }, 109 &Diagnostic{ 110 Severity: "error", 111 Summary: "Tiny explosion", 112 Detail: "Unexpected detonation while parsing", 113 Range: &DiagnosticRange{ 114 Filename: "test.tf", 115 Start: Pos{ 116 Line: 1, 117 Column: 10, 118 Byte: 9, 119 }, 120 End: Pos{ 121 Line: 1, 122 Column: 25, 123 Byte: 24, 124 }, 125 }, 126 Snippet: &DiagnosticSnippet{ 127 Context: strPtr(`resource "test_resource" "test"`), 128 Code: `resource "test_resource" "test" {`, 129 StartLine: 1, 130 HighlightStartOffset: 9, 131 HighlightEndOffset: 24, 132 Values: []DiagnosticExpressionValue{}, 133 }, 134 }, 135 }, 136 "error with source code subject but no context": { 137 &hcl.Diagnostic{ 138 Severity: hcl.DiagError, 139 Summary: "Nonsense input", 140 Detail: "What you wrote makes no sense", 141 Subject: &hcl.Range{ 142 Filename: "short.tf", 143 Start: hcl.Pos{Line: 1, Column: 5, Byte: 4}, 144 End: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 145 }, 146 }, 147 &Diagnostic{ 148 Severity: "error", 149 Summary: "Nonsense input", 150 Detail: "What you wrote makes no sense", 151 Range: &DiagnosticRange{ 152 Filename: "short.tf", 153 Start: Pos{ 154 Line: 1, 155 Column: 5, 156 Byte: 4, 157 }, 158 End: Pos{ 159 Line: 1, 160 Column: 10, 161 Byte: 9, 162 }, 163 }, 164 Snippet: &DiagnosticSnippet{ 165 Context: nil, 166 Code: (`bad source code`), 167 StartLine: (1), 168 HighlightStartOffset: (4), 169 HighlightEndOffset: (9), 170 Values: []DiagnosticExpressionValue{}, 171 }, 172 }, 173 }, 174 "error with multi-line snippet": { 175 &hcl.Diagnostic{ 176 Severity: hcl.DiagError, 177 Summary: "In this house we respect booleans", 178 Detail: "True or false, there is no maybe", 179 Subject: &hcl.Range{ 180 Filename: "test.tf", 181 Start: hcl.Pos{Line: 4, Column: 11, Byte: 81}, 182 End: hcl.Pos{Line: 4, Column: 16, Byte: 86}, 183 }, 184 Context: &hcl.Range{ 185 Filename: "test.tf", 186 Start: hcl.Pos{Line: 3, Column: 3, Byte: 63}, 187 End: hcl.Pos{Line: 5, Column: 4, Byte: 90}, 188 }, 189 }, 190 &Diagnostic{ 191 Severity: "error", 192 Summary: "In this house we respect booleans", 193 Detail: "True or false, there is no maybe", 194 Range: &DiagnosticRange{ 195 Filename: "test.tf", 196 Start: Pos{ 197 Line: 4, 198 Column: 11, 199 Byte: 81, 200 }, 201 End: Pos{ 202 Line: 4, 203 Column: 16, 204 Byte: 86, 205 }, 206 }, 207 Snippet: &DiagnosticSnippet{ 208 Context: strPtr(`resource "test_resource" "test"`), 209 Code: " bar = {\n baz = maybe\n }", 210 StartLine: 3, 211 HighlightStartOffset: 20, 212 HighlightEndOffset: 25, 213 Values: []DiagnosticExpressionValue{}, 214 }, 215 }, 216 }, 217 "error with empty highlight range at end of source code": { 218 &hcl.Diagnostic{ 219 Severity: hcl.DiagError, 220 Summary: "You forgot something", 221 Detail: "Please finish your thought", 222 Subject: &hcl.Range{ 223 Filename: "short.tf", 224 Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, 225 End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, 226 }, 227 }, 228 &Diagnostic{ 229 Severity: "error", 230 Summary: "You forgot something", 231 Detail: "Please finish your thought", 232 Range: &DiagnosticRange{ 233 Filename: "short.tf", 234 Start: Pos{ 235 Line: 1, 236 Column: 16, 237 Byte: 15, 238 }, 239 End: Pos{ 240 Line: 1, 241 Column: 17, 242 Byte: 16, 243 }, 244 }, 245 Snippet: &DiagnosticSnippet{ 246 Code: ("bad source code"), 247 StartLine: (1), 248 HighlightStartOffset: (15), 249 HighlightEndOffset: (15), 250 Values: []DiagnosticExpressionValue{}, 251 }, 252 }, 253 }, 254 "error with unset highlight end position": { 255 &hcl.Diagnostic{ 256 Severity: hcl.DiagError, 257 Summary: "There is no end", 258 Detail: "But there is a beginning", 259 Subject: &hcl.Range{ 260 Filename: "test.tf", 261 Start: hcl.Pos{Line: 1, Column: 16, Byte: 15}, 262 End: hcl.Pos{Line: 0, Column: 0, Byte: 0}, 263 }, 264 }, 265 &Diagnostic{ 266 Severity: "error", 267 Summary: "There is no end", 268 Detail: "But there is a beginning", 269 Range: &DiagnosticRange{ 270 Filename: "test.tf", 271 Start: Pos{ 272 Line: 1, 273 Column: 16, 274 Byte: 15, 275 }, 276 End: Pos{ 277 Line: 1, 278 Column: 17, 279 Byte: 16, 280 }, 281 }, 282 Snippet: &DiagnosticSnippet{ 283 Context: strPtr(`resource "test_resource" "test"`), 284 Code: `resource "test_resource" "test" {`, 285 StartLine: 1, 286 HighlightStartOffset: 15, 287 HighlightEndOffset: 16, 288 Values: []DiagnosticExpressionValue{}, 289 }, 290 }, 291 }, 292 "error whose range starts at a newline": { 293 &hcl.Diagnostic{ 294 Severity: hcl.DiagError, 295 Summary: "Invalid newline", 296 Detail: "How awkward!", 297 Subject: &hcl.Range{ 298 Filename: "odd-comment.tf", 299 Start: hcl.Pos{Line: 2, Column: 5, Byte: 4}, 300 End: hcl.Pos{Line: 3, Column: 1, Byte: 6}, 301 }, 302 }, 303 &Diagnostic{ 304 Severity: "error", 305 Summary: "Invalid newline", 306 Detail: "How awkward!", 307 Range: &DiagnosticRange{ 308 Filename: "odd-comment.tf", 309 Start: Pos{ 310 Line: 2, 311 Column: 5, 312 Byte: 4, 313 }, 314 End: Pos{ 315 Line: 3, 316 Column: 1, 317 Byte: 6, 318 }, 319 }, 320 Snippet: &DiagnosticSnippet{ 321 Code: `#`, 322 StartLine: 2, 323 Values: []DiagnosticExpressionValue{}, 324 325 // Due to the range starting at a newline on a blank 326 // line, we end up stripping off the initial newline 327 // to produce only a one-line snippet. That would 328 // therefore cause the start offset to naturally be 329 // -1, just before the Code we returned, but then we 330 // force it to zero so that the result will still be 331 // in range for a byte-oriented slice of Code. 332 HighlightStartOffset: 0, 333 HighlightEndOffset: 1, 334 }, 335 }, 336 }, 337 "error with source code subject and known expression": { 338 &hcl.Diagnostic{ 339 Severity: hcl.DiagError, 340 Summary: "Wrong noises", 341 Detail: "Biological sounds are not allowed", 342 Subject: &hcl.Range{ 343 Filename: "test.tf", 344 Start: hcl.Pos{Line: 2, Column: 9, Byte: 42}, 345 End: hcl.Pos{Line: 2, Column: 26, Byte: 59}, 346 }, 347 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 348 hcl.TraverseRoot{Name: "var"}, 349 hcl.TraverseAttr{Name: "boop"}, 350 hcl.TraverseIndex{Key: cty.StringVal("hello!")}, 351 }), 352 EvalContext: &hcl.EvalContext{ 353 Variables: map[string]cty.Value{ 354 "var": cty.ObjectVal(map[string]cty.Value{ 355 "boop": cty.MapVal(map[string]cty.Value{ 356 "hello!": cty.StringVal("bleurgh"), 357 }), 358 }), 359 }, 360 }, 361 }, 362 &Diagnostic{ 363 Severity: "error", 364 Summary: "Wrong noises", 365 Detail: "Biological sounds are not allowed", 366 Range: &DiagnosticRange{ 367 Filename: "test.tf", 368 Start: Pos{ 369 Line: 2, 370 Column: 9, 371 Byte: 42, 372 }, 373 End: Pos{ 374 Line: 2, 375 Column: 26, 376 Byte: 59, 377 }, 378 }, 379 Snippet: &DiagnosticSnippet{ 380 Context: strPtr(`resource "test_resource" "test"`), 381 Code: (` foo = var.boop["hello!"]`), 382 StartLine: (2), 383 HighlightStartOffset: (8), 384 HighlightEndOffset: (25), 385 Values: []DiagnosticExpressionValue{ 386 { 387 Traversal: `var.boop["hello!"]`, 388 Statement: `is "bleurgh"`, 389 }, 390 }, 391 }, 392 }, 393 }, 394 "error with source code subject and expression referring to sensitive value": { 395 &hcl.Diagnostic{ 396 Severity: hcl.DiagError, 397 Summary: "Wrong noises", 398 Detail: "Biological sounds are not allowed", 399 Subject: &hcl.Range{ 400 Filename: "test.tf", 401 Start: hcl.Pos{Line: 2, Column: 9, Byte: 42}, 402 End: hcl.Pos{Line: 2, Column: 26, Byte: 59}, 403 }, 404 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 405 hcl.TraverseRoot{Name: "var"}, 406 hcl.TraverseAttr{Name: "boop"}, 407 hcl.TraverseIndex{Key: cty.StringVal("hello!")}, 408 }), 409 EvalContext: &hcl.EvalContext{ 410 Variables: map[string]cty.Value{ 411 "var": cty.ObjectVal(map[string]cty.Value{ 412 "boop": cty.MapVal(map[string]cty.Value{ 413 "hello!": cty.StringVal("bleurgh").Mark(marks.Sensitive), 414 }), 415 }), 416 }, 417 }, 418 Extra: diagnosticCausedBySensitive(true), 419 }, 420 &Diagnostic{ 421 Severity: "error", 422 Summary: "Wrong noises", 423 Detail: "Biological sounds are not allowed", 424 Range: &DiagnosticRange{ 425 Filename: "test.tf", 426 Start: Pos{ 427 Line: 2, 428 Column: 9, 429 Byte: 42, 430 }, 431 End: Pos{ 432 Line: 2, 433 Column: 26, 434 Byte: 59, 435 }, 436 }, 437 Snippet: &DiagnosticSnippet{ 438 Context: strPtr(`resource "test_resource" "test"`), 439 Code: (` foo = var.boop["hello!"]`), 440 StartLine: (2), 441 HighlightStartOffset: (8), 442 HighlightEndOffset: (25), 443 Values: []DiagnosticExpressionValue{ 444 { 445 Traversal: `var.boop["hello!"]`, 446 Statement: `has a sensitive value`, 447 }, 448 }, 449 }, 450 }, 451 }, 452 "error with source code subject and expression referring to sensitive value when not caused by sensitive values": { 453 &hcl.Diagnostic{ 454 Severity: hcl.DiagError, 455 Summary: "Wrong noises", 456 Detail: "Biological sounds are not allowed", 457 Subject: &hcl.Range{ 458 Filename: "test.tf", 459 Start: hcl.Pos{Line: 2, Column: 9, Byte: 42}, 460 End: hcl.Pos{Line: 2, Column: 26, Byte: 59}, 461 }, 462 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 463 hcl.TraverseRoot{Name: "var"}, 464 hcl.TraverseAttr{Name: "boop"}, 465 hcl.TraverseIndex{Key: cty.StringVal("hello!")}, 466 }), 467 EvalContext: &hcl.EvalContext{ 468 Variables: map[string]cty.Value{ 469 "var": cty.ObjectVal(map[string]cty.Value{ 470 "boop": cty.MapVal(map[string]cty.Value{ 471 "hello!": cty.StringVal("bleurgh").Mark(marks.Sensitive), 472 }), 473 }), 474 }, 475 }, 476 }, 477 &Diagnostic{ 478 Severity: "error", 479 Summary: "Wrong noises", 480 Detail: "Biological sounds are not allowed", 481 Range: &DiagnosticRange{ 482 Filename: "test.tf", 483 Start: Pos{ 484 Line: 2, 485 Column: 9, 486 Byte: 42, 487 }, 488 End: Pos{ 489 Line: 2, 490 Column: 26, 491 Byte: 59, 492 }, 493 }, 494 Snippet: &DiagnosticSnippet{ 495 Context: strPtr(`resource "test_resource" "test"`), 496 Code: (` foo = var.boop["hello!"]`), 497 StartLine: (2), 498 HighlightStartOffset: (8), 499 HighlightEndOffset: (25), 500 Values: []DiagnosticExpressionValue{ 501 // The sensitive value is filtered out because this is 502 // not a sensitive-value-related diagnostic message. 503 }, 504 }, 505 }, 506 }, 507 "error with source code subject and expression referring to a collection containing a sensitive value": { 508 &hcl.Diagnostic{ 509 Severity: hcl.DiagError, 510 Summary: "Wrong noises", 511 Detail: "Biological sounds are not allowed", 512 Subject: &hcl.Range{ 513 Filename: "test.tf", 514 Start: hcl.Pos{Line: 2, Column: 9, Byte: 42}, 515 End: hcl.Pos{Line: 2, Column: 26, Byte: 59}, 516 }, 517 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 518 hcl.TraverseRoot{Name: "var"}, 519 hcl.TraverseAttr{Name: "boop"}, 520 }), 521 EvalContext: &hcl.EvalContext{ 522 Variables: map[string]cty.Value{ 523 "var": cty.ObjectVal(map[string]cty.Value{ 524 "boop": cty.MapVal(map[string]cty.Value{ 525 "hello!": cty.StringVal("bleurgh").Mark(marks.Sensitive), 526 }), 527 }), 528 }, 529 }, 530 }, 531 &Diagnostic{ 532 Severity: "error", 533 Summary: "Wrong noises", 534 Detail: "Biological sounds are not allowed", 535 Range: &DiagnosticRange{ 536 Filename: "test.tf", 537 Start: Pos{ 538 Line: 2, 539 Column: 9, 540 Byte: 42, 541 }, 542 End: Pos{ 543 Line: 2, 544 Column: 26, 545 Byte: 59, 546 }, 547 }, 548 Snippet: &DiagnosticSnippet{ 549 Context: strPtr(`resource "test_resource" "test"`), 550 Code: (` foo = var.boop["hello!"]`), 551 StartLine: (2), 552 HighlightStartOffset: (8), 553 HighlightEndOffset: (25), 554 Values: []DiagnosticExpressionValue{ 555 { 556 Traversal: `var.boop`, 557 Statement: `is map of string with 1 element`, 558 }, 559 }, 560 }, 561 }, 562 }, 563 "error with source code subject and unknown string expression": { 564 &hcl.Diagnostic{ 565 Severity: hcl.DiagError, 566 Summary: "Wrong noises", 567 Detail: "Biological sounds are not allowed", 568 Subject: &hcl.Range{ 569 Filename: "test.tf", 570 Start: hcl.Pos{Line: 2, Column: 9, Byte: 42}, 571 End: hcl.Pos{Line: 2, Column: 26, Byte: 59}, 572 }, 573 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 574 hcl.TraverseRoot{Name: "var"}, 575 hcl.TraverseAttr{Name: "boop"}, 576 hcl.TraverseIndex{Key: cty.StringVal("hello!")}, 577 }), 578 EvalContext: &hcl.EvalContext{ 579 Variables: map[string]cty.Value{ 580 "var": cty.ObjectVal(map[string]cty.Value{ 581 "boop": cty.MapVal(map[string]cty.Value{ 582 "hello!": cty.UnknownVal(cty.String), 583 }), 584 }), 585 }, 586 }, 587 Extra: diagnosticCausedByUnknown(true), 588 }, 589 &Diagnostic{ 590 Severity: "error", 591 Summary: "Wrong noises", 592 Detail: "Biological sounds are not allowed", 593 Range: &DiagnosticRange{ 594 Filename: "test.tf", 595 Start: Pos{ 596 Line: 2, 597 Column: 9, 598 Byte: 42, 599 }, 600 End: Pos{ 601 Line: 2, 602 Column: 26, 603 Byte: 59, 604 }, 605 }, 606 Snippet: &DiagnosticSnippet{ 607 Context: strPtr(`resource "test_resource" "test"`), 608 Code: (` foo = var.boop["hello!"]`), 609 StartLine: (2), 610 HighlightStartOffset: (8), 611 HighlightEndOffset: (25), 612 Values: []DiagnosticExpressionValue{ 613 { 614 Traversal: `var.boop["hello!"]`, 615 Statement: `is a string, known only after apply`, 616 }, 617 }, 618 }, 619 }, 620 }, 621 "error with source code subject and unknown expression of unknown type": { 622 &hcl.Diagnostic{ 623 Severity: hcl.DiagError, 624 Summary: "Wrong noises", 625 Detail: "Biological sounds are not allowed", 626 Subject: &hcl.Range{ 627 Filename: "test.tf", 628 Start: hcl.Pos{Line: 2, Column: 9, Byte: 42}, 629 End: hcl.Pos{Line: 2, Column: 26, Byte: 59}, 630 }, 631 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 632 hcl.TraverseRoot{Name: "var"}, 633 hcl.TraverseAttr{Name: "boop"}, 634 hcl.TraverseIndex{Key: cty.StringVal("hello!")}, 635 }), 636 EvalContext: &hcl.EvalContext{ 637 Variables: map[string]cty.Value{ 638 "var": cty.ObjectVal(map[string]cty.Value{ 639 "boop": cty.MapVal(map[string]cty.Value{ 640 "hello!": cty.UnknownVal(cty.DynamicPseudoType), 641 }), 642 }), 643 }, 644 }, 645 Extra: diagnosticCausedByUnknown(true), 646 }, 647 &Diagnostic{ 648 Severity: "error", 649 Summary: "Wrong noises", 650 Detail: "Biological sounds are not allowed", 651 Range: &DiagnosticRange{ 652 Filename: "test.tf", 653 Start: Pos{ 654 Line: 2, 655 Column: 9, 656 Byte: 42, 657 }, 658 End: Pos{ 659 Line: 2, 660 Column: 26, 661 Byte: 59, 662 }, 663 }, 664 Snippet: &DiagnosticSnippet{ 665 Context: strPtr(`resource "test_resource" "test"`), 666 Code: (` foo = var.boop["hello!"]`), 667 StartLine: (2), 668 HighlightStartOffset: (8), 669 HighlightEndOffset: (25), 670 Values: []DiagnosticExpressionValue{ 671 { 672 Traversal: `var.boop["hello!"]`, 673 Statement: `will be known only after apply`, 674 }, 675 }, 676 }, 677 }, 678 }, 679 "error with source code subject and unknown expression of unknown type when not caused by unknown values": { 680 &hcl.Diagnostic{ 681 Severity: hcl.DiagError, 682 Summary: "Wrong noises", 683 Detail: "Biological sounds are not allowed", 684 Subject: &hcl.Range{ 685 Filename: "test.tf", 686 Start: hcl.Pos{Line: 2, Column: 9, Byte: 42}, 687 End: hcl.Pos{Line: 2, Column: 26, Byte: 59}, 688 }, 689 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 690 hcl.TraverseRoot{Name: "var"}, 691 hcl.TraverseAttr{Name: "boop"}, 692 hcl.TraverseIndex{Key: cty.StringVal("hello!")}, 693 }), 694 EvalContext: &hcl.EvalContext{ 695 Variables: map[string]cty.Value{ 696 "var": cty.ObjectVal(map[string]cty.Value{ 697 "boop": cty.MapVal(map[string]cty.Value{ 698 "hello!": cty.UnknownVal(cty.DynamicPseudoType), 699 }), 700 }), 701 }, 702 }, 703 }, 704 &Diagnostic{ 705 Severity: "error", 706 Summary: "Wrong noises", 707 Detail: "Biological sounds are not allowed", 708 Range: &DiagnosticRange{ 709 Filename: "test.tf", 710 Start: Pos{ 711 Line: 2, 712 Column: 9, 713 Byte: 42, 714 }, 715 End: Pos{ 716 Line: 2, 717 Column: 26, 718 Byte: 59, 719 }, 720 }, 721 Snippet: &DiagnosticSnippet{ 722 Context: strPtr(`resource "test_resource" "test"`), 723 Code: (` foo = var.boop["hello!"]`), 724 StartLine: (2), 725 HighlightStartOffset: (8), 726 HighlightEndOffset: (25), 727 Values: []DiagnosticExpressionValue{ 728 // The unknown value is filtered out because this is 729 // not an unknown-value-related diagnostic message. 730 }, 731 }, 732 }, 733 }, 734 "error with source code subject with multiple expression values": { 735 &hcl.Diagnostic{ 736 Severity: hcl.DiagError, 737 Summary: "Catastrophic failure", 738 Detail: "Basically, everything went wrong", 739 Subject: &hcl.Range{ 740 Filename: "values.tf", 741 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 742 End: hcl.Pos{Line: 13, Column: 2, Byte: 102}, 743 }, 744 Expression: hcltest.MockExprList([]hcl.Expression{ 745 hcltest.MockExprTraversalSrc("var.a"), 746 hcltest.MockExprTraversalSrc("var.b"), 747 hcltest.MockExprTraversalSrc("var.c"), 748 hcltest.MockExprTraversalSrc("var.d"), 749 hcltest.MockExprTraversalSrc("var.e"), 750 hcltest.MockExprTraversalSrc("var.f"), 751 hcltest.MockExprTraversalSrc("var.g"), 752 hcltest.MockExprTraversalSrc("var.h"), 753 hcltest.MockExprTraversalSrc("var.i"), 754 hcltest.MockExprTraversalSrc("var.j"), 755 hcltest.MockExprTraversalSrc("var.k"), 756 }), 757 EvalContext: &hcl.EvalContext{ 758 Variables: map[string]cty.Value{ 759 "var": cty.ObjectVal(map[string]cty.Value{ 760 "a": cty.True, 761 "b": cty.NumberFloatVal(123.45), 762 "c": cty.NullVal(cty.String), 763 "d": cty.StringVal("secret").Mark(marks.Sensitive), 764 "e": cty.False, 765 "f": cty.ListValEmpty(cty.String), 766 "g": cty.MapVal(map[string]cty.Value{ 767 "boop": cty.StringVal("beep"), 768 }), 769 "h": cty.ListVal([]cty.Value{ 770 cty.StringVal("boop"), 771 cty.StringVal("beep"), 772 cty.StringVal("blorp"), 773 }), 774 "i": cty.EmptyObjectVal, 775 "j": cty.ObjectVal(map[string]cty.Value{ 776 "foo": cty.StringVal("bar"), 777 }), 778 "k": cty.ObjectVal(map[string]cty.Value{ 779 "a": cty.True, 780 "b": cty.False, 781 }), 782 }), 783 }, 784 }, 785 Extra: diagnosticCausedBySensitive(true), 786 }, 787 &Diagnostic{ 788 Severity: "error", 789 Summary: "Catastrophic failure", 790 Detail: "Basically, everything went wrong", 791 Range: &DiagnosticRange{ 792 Filename: "values.tf", 793 Start: Pos{ 794 Line: 1, 795 Column: 1, 796 Byte: 0, 797 }, 798 End: Pos{ 799 Line: 13, 800 Column: 2, 801 Byte: 102, 802 }, 803 }, 804 Snippet: &DiagnosticSnippet{ 805 Code: `[ 806 var.a, 807 var.b, 808 var.c, 809 var.d, 810 var.e, 811 var.f, 812 var.g, 813 var.h, 814 var.i, 815 var.j, 816 var.k, 817 ]`, 818 StartLine: (1), 819 HighlightStartOffset: (0), 820 HighlightEndOffset: (102), 821 Values: []DiagnosticExpressionValue{ 822 { 823 Traversal: `var.a`, 824 Statement: `is true`, 825 }, 826 { 827 Traversal: `var.b`, 828 Statement: `is 123.45`, 829 }, 830 { 831 Traversal: `var.c`, 832 Statement: `is null`, 833 }, 834 { 835 Traversal: `var.d`, 836 Statement: `has a sensitive value`, 837 }, 838 { 839 Traversal: `var.e`, 840 Statement: `is false`, 841 }, 842 { 843 Traversal: `var.f`, 844 Statement: `is empty list of string`, 845 }, 846 { 847 Traversal: `var.g`, 848 Statement: `is map of string with 1 element`, 849 }, 850 { 851 Traversal: `var.h`, 852 Statement: `is list of string with 3 elements`, 853 }, 854 { 855 Traversal: `var.i`, 856 Statement: `is object with no attributes`, 857 }, 858 { 859 Traversal: `var.j`, 860 Statement: `is object with 1 attribute "foo"`, 861 }, 862 { 863 Traversal: `var.k`, 864 Statement: `is object with 2 attributes`, 865 }, 866 }, 867 }, 868 }, 869 }, 870 } 871 872 for name, tc := range testCases { 873 t.Run(name, func(t *testing.T) { 874 // Convert the diag into a tfdiags.Diagnostic 875 var diags tfdiags.Diagnostics 876 diags = diags.Append(tc.diag) 877 878 got := NewDiagnostic(diags[0], sources) 879 if !cmp.Equal(tc.want, got) { 880 t.Fatalf("wrong result\n:%s", cmp.Diff(tc.want, got)) 881 } 882 }) 883 884 t.Run(fmt.Sprintf("golden test for %s", name), func(t *testing.T) { 885 // Convert the diag into a tfdiags.Diagnostic 886 var diags tfdiags.Diagnostics 887 diags = diags.Append(tc.diag) 888 889 got := NewDiagnostic(diags[0], sources) 890 891 // Render the diagnostic to indented JSON 892 gotBytes, err := json.MarshalIndent(got, "", " ") 893 if err != nil { 894 t.Fatal(err) 895 } 896 897 // Compare against the golden reference 898 filename := path.Join( 899 "testdata", 900 "diagnostic", 901 fmt.Sprintf("%s.json", strings.ReplaceAll(name, " ", "-")), 902 ) 903 904 // Generate golden reference by uncommenting the next two lines: 905 // gotBytes = append(gotBytes, '\n') 906 // os.WriteFile(filename, gotBytes, 0644) 907 908 wantFile, err := os.Open(filename) 909 if err != nil { 910 t.Fatalf("failed to open golden file: %s", err) 911 } 912 defer wantFile.Close() 913 wantBytes, err := ioutil.ReadAll(wantFile) 914 if err != nil { 915 t.Fatalf("failed to read output file: %s", err) 916 } 917 918 // Don't care about leading or trailing whitespace 919 gotString := strings.TrimSpace(string(gotBytes)) 920 wantString := strings.TrimSpace(string(wantBytes)) 921 922 if !cmp.Equal(wantString, gotString) { 923 t.Fatalf("wrong result\n:%s", cmp.Diff(wantString, gotString)) 924 } 925 }) 926 } 927 } 928 929 // Helper function to make constructing literal Diagnostics easier. There 930 // are fields which are pointer-to-string to ensure that the rendered JSON 931 // results in `null` for an empty value, rather than `""`. 932 func strPtr(s string) *string { return &s } 933 934 // diagnosticCausedByUnknown is a testing helper for exercising our logic 935 // for selectively showing unknown values alongside our source snippets for 936 // diagnostics that are explicitly marked as being caused by unknown values. 937 type diagnosticCausedByUnknown bool 938 939 var _ tfdiags.DiagnosticExtraBecauseUnknown = diagnosticCausedByUnknown(true) 940 941 func (e diagnosticCausedByUnknown) DiagnosticCausedByUnknown() bool { 942 return bool(e) 943 } 944 945 // diagnosticCausedBySensitive is a testing helper for exercising our logic 946 // for selectively showing sensitive values alongside our source snippets for 947 // diagnostics that are explicitly marked as being caused by sensitive values. 948 type diagnosticCausedBySensitive bool 949 950 var _ tfdiags.DiagnosticExtraBecauseSensitive = diagnosticCausedBySensitive(true) 951 952 func (e diagnosticCausedBySensitive) DiagnosticCausedBySensitive() bool { 953 return bool(e) 954 }