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