github.com/opentofu/opentofu@v1.7.1/internal/command/format/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 format 7 8 import ( 9 "strings" 10 "testing" 11 12 "github.com/google/go-cmp/cmp" 13 "github.com/hashicorp/hcl/v2" 14 "github.com/hashicorp/hcl/v2/hclsyntax" 15 "github.com/hashicorp/hcl/v2/hcltest" 16 "github.com/mitchellh/colorstring" 17 "github.com/zclconf/go-cty/cty" 18 "github.com/zclconf/go-cty/cty/function" 19 20 viewsjson "github.com/opentofu/opentofu/internal/command/views/json" 21 "github.com/opentofu/opentofu/internal/lang/marks" 22 23 "github.com/opentofu/opentofu/internal/tfdiags" 24 ) 25 26 func TestDiagnostic(t *testing.T) { 27 28 tests := map[string]struct { 29 Diag interface{} 30 Want string 31 }{ 32 "sourceless error": { 33 tfdiags.Sourceless( 34 tfdiags.Error, 35 "A sourceless error", 36 "It has no source references but it does have a pretty long detail that should wrap over multiple lines.", 37 ), 38 `[red]╷[reset] 39 [red]│[reset] [bold][red]Error: [reset][bold]A sourceless error[reset] 40 [red]│[reset] 41 [red]│[reset] It has no source references but it 42 [red]│[reset] does have a pretty long detail that 43 [red]│[reset] should wrap over multiple lines. 44 [red]╵[reset] 45 `, 46 }, 47 "sourceless warning": { 48 tfdiags.Sourceless( 49 tfdiags.Warning, 50 "A sourceless warning", 51 "It has no source references but it does have a pretty long detail that should wrap over multiple lines.", 52 ), 53 `[yellow]╷[reset] 54 [yellow]│[reset] [bold][yellow]Warning: [reset][bold]A sourceless warning[reset] 55 [yellow]│[reset] 56 [yellow]│[reset] It has no source references but it 57 [yellow]│[reset] does have a pretty long detail that 58 [yellow]│[reset] should wrap over multiple lines. 59 [yellow]╵[reset] 60 `, 61 }, 62 "error with source code subject": { 63 &hcl.Diagnostic{ 64 Severity: hcl.DiagError, 65 Summary: "Bad bad bad", 66 Detail: "Whatever shall we do?", 67 Subject: &hcl.Range{ 68 Filename: "test.tf", 69 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 70 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 71 }, 72 }, 73 `[red]╷[reset] 74 [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] 75 [red]│[reset] 76 [red]│[reset] on test.tf line 1: 77 [red]│[reset] 1: test [underline]source[reset] code 78 [red]│[reset] 79 [red]│[reset] Whatever shall we do? 80 [red]╵[reset] 81 `, 82 }, 83 "error with source code subject and known expression": { 84 &hcl.Diagnostic{ 85 Severity: hcl.DiagError, 86 Summary: "Bad bad bad", 87 Detail: "Whatever shall we do?", 88 Subject: &hcl.Range{ 89 Filename: "test.tf", 90 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 91 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 92 }, 93 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 94 hcl.TraverseRoot{Name: "boop"}, 95 hcl.TraverseAttr{Name: "beep"}, 96 }), 97 EvalContext: &hcl.EvalContext{ 98 Variables: map[string]cty.Value{ 99 "boop": cty.ObjectVal(map[string]cty.Value{ 100 "beep": cty.StringVal("blah"), 101 }), 102 }, 103 }, 104 }, 105 `[red]╷[reset] 106 [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] 107 [red]│[reset] 108 [red]│[reset] on test.tf line 1: 109 [red]│[reset] 1: test [underline]source[reset] code 110 [red]│[reset] [dark_gray]├────────────────[reset] 111 [red]│[reset] [dark_gray]│[reset] [bold]boop.beep[reset] is "blah" 112 [red]│[reset] 113 [red]│[reset] Whatever shall we do? 114 [red]╵[reset] 115 `, 116 }, 117 "error with source code subject and expression referring to sensitive value": { 118 &hcl.Diagnostic{ 119 Severity: hcl.DiagError, 120 Summary: "Bad bad bad", 121 Detail: "Whatever shall we do?", 122 Subject: &hcl.Range{ 123 Filename: "test.tf", 124 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 125 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 126 }, 127 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 128 hcl.TraverseRoot{Name: "boop"}, 129 hcl.TraverseAttr{Name: "beep"}, 130 }), 131 EvalContext: &hcl.EvalContext{ 132 Variables: map[string]cty.Value{ 133 "boop": cty.ObjectVal(map[string]cty.Value{ 134 "beep": cty.StringVal("blah").Mark(marks.Sensitive), 135 }), 136 }, 137 }, 138 Extra: diagnosticCausedBySensitive(true), 139 }, 140 `[red]╷[reset] 141 [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] 142 [red]│[reset] 143 [red]│[reset] on test.tf line 1: 144 [red]│[reset] 1: test [underline]source[reset] code 145 [red]│[reset] [dark_gray]├────────────────[reset] 146 [red]│[reset] [dark_gray]│[reset] [bold]boop.beep[reset] has a sensitive value 147 [red]│[reset] 148 [red]│[reset] Whatever shall we do? 149 [red]╵[reset] 150 `, 151 }, 152 "error with source code subject and unknown string expression": { 153 &hcl.Diagnostic{ 154 Severity: hcl.DiagError, 155 Summary: "Bad bad bad", 156 Detail: "Whatever shall we do?", 157 Subject: &hcl.Range{ 158 Filename: "test.tf", 159 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 160 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 161 }, 162 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 163 hcl.TraverseRoot{Name: "boop"}, 164 hcl.TraverseAttr{Name: "beep"}, 165 }), 166 EvalContext: &hcl.EvalContext{ 167 Variables: map[string]cty.Value{ 168 "boop": cty.ObjectVal(map[string]cty.Value{ 169 "beep": cty.UnknownVal(cty.String), 170 }), 171 }, 172 }, 173 Extra: diagnosticCausedByUnknown(true), 174 }, 175 `[red]╷[reset] 176 [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] 177 [red]│[reset] 178 [red]│[reset] on test.tf line 1: 179 [red]│[reset] 1: test [underline]source[reset] code 180 [red]│[reset] [dark_gray]├────────────────[reset] 181 [red]│[reset] [dark_gray]│[reset] [bold]boop.beep[reset] is a string, known only after apply 182 [red]│[reset] 183 [red]│[reset] Whatever shall we do? 184 [red]╵[reset] 185 `, 186 }, 187 "error with source code subject and unknown expression of unknown type": { 188 &hcl.Diagnostic{ 189 Severity: hcl.DiagError, 190 Summary: "Bad bad bad", 191 Detail: "Whatever shall we do?", 192 Subject: &hcl.Range{ 193 Filename: "test.tf", 194 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 195 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 196 }, 197 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 198 hcl.TraverseRoot{Name: "boop"}, 199 hcl.TraverseAttr{Name: "beep"}, 200 }), 201 EvalContext: &hcl.EvalContext{ 202 Variables: map[string]cty.Value{ 203 "boop": cty.ObjectVal(map[string]cty.Value{ 204 "beep": cty.UnknownVal(cty.DynamicPseudoType), 205 }), 206 }, 207 }, 208 Extra: diagnosticCausedByUnknown(true), 209 }, 210 `[red]╷[reset] 211 [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] 212 [red]│[reset] 213 [red]│[reset] on test.tf line 1: 214 [red]│[reset] 1: test [underline]source[reset] code 215 [red]│[reset] [dark_gray]├────────────────[reset] 216 [red]│[reset] [dark_gray]│[reset] [bold]boop.beep[reset] will be known only after apply 217 [red]│[reset] 218 [red]│[reset] Whatever shall we do? 219 [red]╵[reset] 220 `, 221 }, 222 "error with source code subject and function call annotation": { 223 &hcl.Diagnostic{ 224 Severity: hcl.DiagError, 225 Summary: "Bad bad bad", 226 Detail: "Whatever shall we do?", 227 Subject: &hcl.Range{ 228 Filename: "test.tf", 229 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 230 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 231 }, 232 Expression: hcltest.MockExprLiteral(cty.True), 233 EvalContext: &hcl.EvalContext{ 234 Functions: map[string]function.Function{ 235 "beep": function.New(&function.Spec{ 236 Params: []function.Parameter{ 237 { 238 Name: "pos_param_0", 239 Type: cty.String, 240 }, 241 { 242 Name: "pos_param_1", 243 Type: cty.Number, 244 }, 245 }, 246 VarParam: &function.Parameter{ 247 Name: "var_param", 248 Type: cty.Bool, 249 }, 250 }), 251 }, 252 }, 253 // This is simulating what the HCL function call expression 254 // type would generate on evaluation, by implementing the 255 // same interface it uses. 256 Extra: fakeDiagFunctionCallExtra("beep"), 257 }, 258 `[red]╷[reset] 259 [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset] 260 [red]│[reset] 261 [red]│[reset] on test.tf line 1: 262 [red]│[reset] 1: test [underline]source[reset] code 263 [red]│[reset] [dark_gray]├────────────────[reset] 264 [red]│[reset] [dark_gray]│[reset] while calling [bold]beep[reset](pos_param_0, pos_param_1, var_param...) 265 [red]│[reset] 266 [red]│[reset] Whatever shall we do? 267 [red]╵[reset] 268 `, 269 }, 270 } 271 272 sources := map[string][]byte{ 273 "test.tf": []byte(`test source code`), 274 } 275 276 // This empty Colorize just passes through all of the formatting codes 277 // untouched, because it doesn't define any formatting keywords. 278 colorize := &colorstring.Colorize{} 279 280 for name, test := range tests { 281 t.Run(name, func(t *testing.T) { 282 var diags tfdiags.Diagnostics 283 diags = diags.Append(test.Diag) // to normalize it into a tfdiag.Diagnostic 284 diag := diags[0] 285 got := strings.TrimSpace(Diagnostic(diag, sources, colorize, 40)) 286 want := strings.TrimSpace(test.Want) 287 if got != want { 288 t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want) 289 } 290 }) 291 } 292 } 293 294 func TestDiagnosticPlain(t *testing.T) { 295 296 tests := map[string]struct { 297 Diag interface{} 298 Want string 299 }{ 300 "sourceless error": { 301 tfdiags.Sourceless( 302 tfdiags.Error, 303 "A sourceless error", 304 "It has no source references but it does have a pretty long detail that should wrap over multiple lines.", 305 ), 306 ` 307 Error: A sourceless error 308 309 It has no source references but it does 310 have a pretty long detail that should 311 wrap over multiple lines. 312 `, 313 }, 314 "sourceless warning": { 315 tfdiags.Sourceless( 316 tfdiags.Warning, 317 "A sourceless warning", 318 "It has no source references but it does have a pretty long detail that should wrap over multiple lines.", 319 ), 320 ` 321 Warning: A sourceless warning 322 323 It has no source references but it does 324 have a pretty long detail that should 325 wrap over multiple lines. 326 `, 327 }, 328 "error with source code subject": { 329 &hcl.Diagnostic{ 330 Severity: hcl.DiagError, 331 Summary: "Bad bad bad", 332 Detail: "Whatever shall we do?", 333 Subject: &hcl.Range{ 334 Filename: "test.tf", 335 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 336 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 337 }, 338 }, 339 ` 340 Error: Bad bad bad 341 342 on test.tf line 1: 343 1: test source code 344 345 Whatever shall we do? 346 `, 347 }, 348 "error with source code subject and known expression": { 349 &hcl.Diagnostic{ 350 Severity: hcl.DiagError, 351 Summary: "Bad bad bad", 352 Detail: "Whatever shall we do?", 353 Subject: &hcl.Range{ 354 Filename: "test.tf", 355 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 356 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 357 }, 358 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 359 hcl.TraverseRoot{Name: "boop"}, 360 hcl.TraverseAttr{Name: "beep"}, 361 }), 362 EvalContext: &hcl.EvalContext{ 363 Variables: map[string]cty.Value{ 364 "boop": cty.ObjectVal(map[string]cty.Value{ 365 "beep": cty.StringVal("blah"), 366 }), 367 }, 368 }, 369 }, 370 ` 371 Error: Bad bad bad 372 373 on test.tf line 1: 374 1: test source code 375 ├──────────────── 376 │ boop.beep is "blah" 377 378 Whatever shall we do? 379 `, 380 }, 381 "error with source code subject and expression referring to sensitive value": { 382 &hcl.Diagnostic{ 383 Severity: hcl.DiagError, 384 Summary: "Bad bad bad", 385 Detail: "Whatever shall we do?", 386 Subject: &hcl.Range{ 387 Filename: "test.tf", 388 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 389 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 390 }, 391 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 392 hcl.TraverseRoot{Name: "boop"}, 393 hcl.TraverseAttr{Name: "beep"}, 394 }), 395 EvalContext: &hcl.EvalContext{ 396 Variables: map[string]cty.Value{ 397 "boop": cty.ObjectVal(map[string]cty.Value{ 398 "beep": cty.StringVal("blah").Mark(marks.Sensitive), 399 }), 400 }, 401 }, 402 Extra: diagnosticCausedBySensitive(true), 403 }, 404 ` 405 Error: Bad bad bad 406 407 on test.tf line 1: 408 1: test source code 409 ├──────────────── 410 │ boop.beep has a sensitive value 411 412 Whatever shall we do? 413 `, 414 }, 415 "error with source code subject and expression referring to sensitive value when not related to sensitivity": { 416 &hcl.Diagnostic{ 417 Severity: hcl.DiagError, 418 Summary: "Bad bad bad", 419 Detail: "Whatever shall we do?", 420 Subject: &hcl.Range{ 421 Filename: "test.tf", 422 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 423 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 424 }, 425 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 426 hcl.TraverseRoot{Name: "boop"}, 427 hcl.TraverseAttr{Name: "beep"}, 428 }), 429 EvalContext: &hcl.EvalContext{ 430 Variables: map[string]cty.Value{ 431 "boop": cty.ObjectVal(map[string]cty.Value{ 432 "beep": cty.StringVal("blah").Mark(marks.Sensitive), 433 }), 434 }, 435 }, 436 }, 437 ` 438 Error: Bad bad bad 439 440 on test.tf line 1: 441 1: test source code 442 443 Whatever shall we do? 444 `, 445 }, 446 "error with source code subject and unknown string expression": { 447 &hcl.Diagnostic{ 448 Severity: hcl.DiagError, 449 Summary: "Bad bad bad", 450 Detail: "Whatever shall we do?", 451 Subject: &hcl.Range{ 452 Filename: "test.tf", 453 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 454 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 455 }, 456 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 457 hcl.TraverseRoot{Name: "boop"}, 458 hcl.TraverseAttr{Name: "beep"}, 459 }), 460 EvalContext: &hcl.EvalContext{ 461 Variables: map[string]cty.Value{ 462 "boop": cty.ObjectVal(map[string]cty.Value{ 463 "beep": cty.UnknownVal(cty.String), 464 }), 465 }, 466 }, 467 Extra: diagnosticCausedByUnknown(true), 468 }, 469 ` 470 Error: Bad bad bad 471 472 on test.tf line 1: 473 1: test source code 474 ├──────────────── 475 │ boop.beep is a string, known only after apply 476 477 Whatever shall we do? 478 `, 479 }, 480 "error with source code subject and unknown string expression when problem isn't unknown-related": { 481 &hcl.Diagnostic{ 482 Severity: hcl.DiagError, 483 Summary: "Bad bad bad", 484 Detail: "Whatever shall we do?", 485 Subject: &hcl.Range{ 486 Filename: "test.tf", 487 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 488 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 489 }, 490 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 491 hcl.TraverseRoot{Name: "boop"}, 492 hcl.TraverseAttr{Name: "beep"}, 493 }), 494 EvalContext: &hcl.EvalContext{ 495 Variables: map[string]cty.Value{ 496 "boop": cty.ObjectVal(map[string]cty.Value{ 497 "beep": cty.UnknownVal(cty.String), 498 }), 499 }, 500 }, 501 }, 502 ` 503 Error: Bad bad bad 504 505 on test.tf line 1: 506 1: test source code 507 ├──────────────── 508 │ boop.beep is a string 509 510 Whatever shall we do? 511 `, 512 }, 513 "error with source code subject and unknown expression of unknown type": { 514 &hcl.Diagnostic{ 515 Severity: hcl.DiagError, 516 Summary: "Bad bad bad", 517 Detail: "Whatever shall we do?", 518 Subject: &hcl.Range{ 519 Filename: "test.tf", 520 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 521 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 522 }, 523 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 524 hcl.TraverseRoot{Name: "boop"}, 525 hcl.TraverseAttr{Name: "beep"}, 526 }), 527 EvalContext: &hcl.EvalContext{ 528 Variables: map[string]cty.Value{ 529 "boop": cty.ObjectVal(map[string]cty.Value{ 530 "beep": cty.UnknownVal(cty.DynamicPseudoType), 531 }), 532 }, 533 }, 534 Extra: diagnosticCausedByUnknown(true), 535 }, 536 ` 537 Error: Bad bad bad 538 539 on test.tf line 1: 540 1: test source code 541 ├──────────────── 542 │ boop.beep will be known only after apply 543 544 Whatever shall we do? 545 `, 546 }, 547 "error with source code subject and unknown expression of unknown type when problem isn't unknown-related": { 548 &hcl.Diagnostic{ 549 Severity: hcl.DiagError, 550 Summary: "Bad bad bad", 551 Detail: "Whatever shall we do?", 552 Subject: &hcl.Range{ 553 Filename: "test.tf", 554 Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, 555 End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, 556 }, 557 Expression: hcltest.MockExprTraversal(hcl.Traversal{ 558 hcl.TraverseRoot{Name: "boop"}, 559 hcl.TraverseAttr{Name: "beep"}, 560 }), 561 EvalContext: &hcl.EvalContext{ 562 Variables: map[string]cty.Value{ 563 "boop": cty.ObjectVal(map[string]cty.Value{ 564 "beep": cty.UnknownVal(cty.DynamicPseudoType), 565 }), 566 }, 567 }, 568 }, 569 ` 570 Error: Bad bad bad 571 572 on test.tf line 1: 573 1: test source code 574 575 Whatever shall we do? 576 `, 577 }, 578 } 579 580 sources := map[string][]byte{ 581 "test.tf": []byte(`test source code`), 582 } 583 584 for name, test := range tests { 585 t.Run(name, func(t *testing.T) { 586 var diags tfdiags.Diagnostics 587 diags = diags.Append(test.Diag) // to normalize it into a tfdiag.Diagnostic 588 diag := diags[0] 589 got := strings.TrimSpace(DiagnosticPlain(diag, sources, 40)) 590 want := strings.TrimSpace(test.Want) 591 if got != want { 592 t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want) 593 } 594 }) 595 } 596 } 597 598 func TestDiagnosticWarningsCompact(t *testing.T) { 599 var diags tfdiags.Diagnostics 600 diags = diags.Append(tfdiags.SimpleWarning("foo")) 601 diags = diags.Append(tfdiags.SimpleWarning("foo")) 602 diags = diags.Append(tfdiags.SimpleWarning("bar")) 603 diags = diags.Append(&hcl.Diagnostic{ 604 Severity: hcl.DiagWarning, 605 Summary: "source foo", 606 Detail: "...", 607 Subject: &hcl.Range{ 608 Filename: "source.tf", 609 Start: hcl.Pos{Line: 2, Column: 1, Byte: 5}, 610 End: hcl.Pos{Line: 2, Column: 1, Byte: 5}, 611 }, 612 }) 613 diags = diags.Append(&hcl.Diagnostic{ 614 Severity: hcl.DiagWarning, 615 Summary: "source foo", 616 Detail: "...", 617 Subject: &hcl.Range{ 618 Filename: "source.tf", 619 Start: hcl.Pos{Line: 3, Column: 1, Byte: 7}, 620 End: hcl.Pos{Line: 3, Column: 1, Byte: 7}, 621 }, 622 }) 623 diags = diags.Append(&hcl.Diagnostic{ 624 Severity: hcl.DiagWarning, 625 Summary: "source bar", 626 Detail: "...", 627 Subject: &hcl.Range{ 628 Filename: "source2.tf", 629 Start: hcl.Pos{Line: 1, Column: 1, Byte: 1}, 630 End: hcl.Pos{Line: 1, Column: 1, Byte: 1}, 631 }, 632 }) 633 634 // ConsolidateWarnings groups together the ones 635 // that have source location information and that 636 // have the same summary text. 637 diags = diags.ConsolidateWarnings(1) 638 639 // A zero-value Colorize just passes all the formatting 640 // codes back to us, so we can test them literally. 641 got := DiagnosticWarningsCompact(diags, &colorstring.Colorize{}) 642 want := `[bold][yellow]Warnings:[reset] 643 644 - foo 645 - foo 646 - bar 647 - source foo 648 on source.tf line 2 (and 1 more) 649 - source bar 650 on source2.tf line 1 651 ` 652 if got != want { 653 t.Errorf( 654 "wrong result\ngot:\n%s\n\nwant:\n%s\n\ndiff:\n%s", 655 got, want, cmp.Diff(want, got), 656 ) 657 } 658 } 659 660 // Test case via https://github.com/hashicorp/terraform/issues/21359 661 func TestDiagnostic_nonOverlappingHighlightContext(t *testing.T) { 662 var diags tfdiags.Diagnostics 663 664 diags = diags.Append(&hcl.Diagnostic{ 665 Severity: hcl.DiagError, 666 Summary: "Some error", 667 Detail: "...", 668 Subject: &hcl.Range{ 669 Filename: "source.tf", 670 Start: hcl.Pos{Line: 1, Column: 5, Byte: 5}, 671 End: hcl.Pos{Line: 1, Column: 5, Byte: 5}, 672 }, 673 Context: &hcl.Range{ 674 Filename: "source.tf", 675 Start: hcl.Pos{Line: 1, Column: 5, Byte: 5}, 676 End: hcl.Pos{Line: 4, Column: 2, Byte: 60}, 677 }, 678 }) 679 sources := map[string][]byte{ 680 "source.tf": []byte(`x = somefunc("testing", { 681 alpha = "foo" 682 beta = "bar" 683 }) 684 `), 685 } 686 color := &colorstring.Colorize{ 687 Colors: colorstring.DefaultColors, 688 Reset: true, 689 Disable: true, 690 } 691 expected := `╷ 692 │ Error: Some error 693 │ 694 │ on source.tf line 1: 695 │ 1: x = somefunc("testing", { 696 │ 2: alpha = "foo" 697 │ 3: beta = "bar" 698 │ 4: }) 699 │ 700 │ ... 701 ╵ 702 ` 703 output := Diagnostic(diags[0], sources, color, 80) 704 705 if output != expected { 706 t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected) 707 } 708 } 709 710 func TestDiagnostic_emptyOverlapHighlightContext(t *testing.T) { 711 var diags tfdiags.Diagnostics 712 713 diags = diags.Append(&hcl.Diagnostic{ 714 Severity: hcl.DiagError, 715 Summary: "Some error", 716 Detail: "...", 717 Subject: &hcl.Range{ 718 Filename: "source.tf", 719 Start: hcl.Pos{Line: 3, Column: 10, Byte: 38}, 720 End: hcl.Pos{Line: 4, Column: 1, Byte: 39}, 721 }, 722 Context: &hcl.Range{ 723 Filename: "source.tf", 724 Start: hcl.Pos{Line: 2, Column: 13, Byte: 27}, 725 End: hcl.Pos{Line: 4, Column: 1, Byte: 39}, 726 }, 727 }) 728 sources := map[string][]byte{ 729 "source.tf": []byte(`variable "x" { 730 default = { 731 "foo" 732 } 733 `), 734 } 735 color := &colorstring.Colorize{ 736 Colors: colorstring.DefaultColors, 737 Reset: true, 738 Disable: true, 739 } 740 expected := `╷ 741 │ Error: Some error 742 │ 743 │ on source.tf line 3, in variable "x": 744 │ 2: default = { 745 │ 3: "foo" 746 │ 4: } 747 │ 748 │ ... 749 ╵ 750 ` 751 output := Diagnostic(diags[0], sources, color, 80) 752 753 if output != expected { 754 t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected) 755 } 756 } 757 758 func TestDiagnosticPlain_emptyOverlapHighlightContext(t *testing.T) { 759 var diags tfdiags.Diagnostics 760 761 diags = diags.Append(&hcl.Diagnostic{ 762 Severity: hcl.DiagError, 763 Summary: "Some error", 764 Detail: "...", 765 Subject: &hcl.Range{ 766 Filename: "source.tf", 767 Start: hcl.Pos{Line: 3, Column: 10, Byte: 38}, 768 End: hcl.Pos{Line: 4, Column: 1, Byte: 39}, 769 }, 770 Context: &hcl.Range{ 771 Filename: "source.tf", 772 Start: hcl.Pos{Line: 2, Column: 13, Byte: 27}, 773 End: hcl.Pos{Line: 4, Column: 1, Byte: 39}, 774 }, 775 }) 776 sources := map[string][]byte{ 777 "source.tf": []byte(`variable "x" { 778 default = { 779 "foo" 780 } 781 `), 782 } 783 784 expected := ` 785 Error: Some error 786 787 on source.tf line 3, in variable "x": 788 2: default = { 789 3: "foo" 790 4: } 791 792 ... 793 ` 794 output := DiagnosticPlain(diags[0], sources, 80) 795 796 if output != expected { 797 t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected) 798 } 799 } 800 801 func TestDiagnostic_wrapDetailIncludingCommand(t *testing.T) { 802 var diags tfdiags.Diagnostics 803 804 diags = diags.Append(&hcl.Diagnostic{ 805 Severity: hcl.DiagError, 806 Summary: "Everything went wrong", 807 Detail: "This is a very long sentence about whatever went wrong which is supposed to wrap onto multiple lines. Thank-you very much for listening.\n\nTo fix this, run this very long command:\n terraform read-my-mind -please -thanks -but-do-not-wrap-this-line-because-it-is-prefixed-with-spaces\n\nHere is a coda which is also long enough to wrap and so it should eventually make it onto multiple lines. THE END", 808 }) 809 color := &colorstring.Colorize{ 810 Colors: colorstring.DefaultColors, 811 Reset: true, 812 Disable: true, 813 } 814 expected := `╷ 815 │ Error: Everything went wrong 816 │ 817 │ This is a very long sentence about whatever went wrong which is supposed 818 │ to wrap onto multiple lines. Thank-you very much for listening. 819 │ 820 │ To fix this, run this very long command: 821 │ terraform read-my-mind -please -thanks -but-do-not-wrap-this-line-because-it-is-prefixed-with-spaces 822 │ 823 │ Here is a coda which is also long enough to wrap and so it should 824 │ eventually make it onto multiple lines. THE END 825 ╵ 826 ` 827 output := Diagnostic(diags[0], nil, color, 76) 828 829 if output != expected { 830 t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected) 831 } 832 } 833 834 func TestDiagnosticPlain_wrapDetailIncludingCommand(t *testing.T) { 835 var diags tfdiags.Diagnostics 836 837 diags = diags.Append(&hcl.Diagnostic{ 838 Severity: hcl.DiagError, 839 Summary: "Everything went wrong", 840 Detail: "This is a very long sentence about whatever went wrong which is supposed to wrap onto multiple lines. Thank-you very much for listening.\n\nTo fix this, run this very long command:\n terraform read-my-mind -please -thanks -but-do-not-wrap-this-line-because-it-is-prefixed-with-spaces\n\nHere is a coda which is also long enough to wrap and so it should eventually make it onto multiple lines. THE END", 841 }) 842 843 expected := ` 844 Error: Everything went wrong 845 846 This is a very long sentence about whatever went wrong which is supposed to 847 wrap onto multiple lines. Thank-you very much for listening. 848 849 To fix this, run this very long command: 850 terraform read-my-mind -please -thanks -but-do-not-wrap-this-line-because-it-is-prefixed-with-spaces 851 852 Here is a coda which is also long enough to wrap and so it should 853 eventually make it onto multiple lines. THE END 854 ` 855 output := DiagnosticPlain(diags[0], nil, 76) 856 857 if output != expected { 858 t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected) 859 } 860 } 861 862 // Test cases covering invalid JSON diagnostics which should still render 863 // correctly. These JSON diagnostic values cannot be generated from the 864 // json.NewDiagnostic code path, but we may read and display JSON diagnostics 865 // in future from other sources. 866 func TestDiagnosticFromJSON_invalid(t *testing.T) { 867 tests := map[string]struct { 868 Diag *viewsjson.Diagnostic 869 Want string 870 }{ 871 "zero-value end range and highlight end byte": { 872 &viewsjson.Diagnostic{ 873 Severity: viewsjson.DiagnosticSeverityError, 874 Summary: "Bad end", 875 Detail: "It all went wrong.", 876 Range: &viewsjson.DiagnosticRange{ 877 Filename: "ohno.tf", 878 Start: viewsjson.Pos{Line: 1, Column: 23, Byte: 22}, 879 End: viewsjson.Pos{Line: 0, Column: 0, Byte: 0}, 880 }, 881 Snippet: &viewsjson.DiagnosticSnippet{ 882 Code: `resource "foo_bar "baz" {`, 883 StartLine: 1, 884 HighlightStartOffset: 22, 885 HighlightEndOffset: 0, 886 }, 887 }, 888 `[red]╷[reset] 889 [red]│[reset] [bold][red]Error: [reset][bold]Bad end[reset] 890 [red]│[reset] 891 [red]│[reset] on ohno.tf line 1: 892 [red]│[reset] 1: resource "foo_bar "baz[underline]"[reset] { 893 [red]│[reset] 894 [red]│[reset] It all went wrong. 895 [red]╵[reset] 896 `, 897 }, 898 } 899 900 // This empty Colorize just passes through all of the formatting codes 901 // untouched, because it doesn't define any formatting keywords. 902 colorize := &colorstring.Colorize{} 903 904 for name, test := range tests { 905 t.Run(name, func(t *testing.T) { 906 got := strings.TrimSpace(DiagnosticFromJSON(test.Diag, colorize, 40)) 907 want := strings.TrimSpace(test.Want) 908 if got != want { 909 t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want) 910 } 911 }) 912 } 913 } 914 915 // fakeDiagFunctionCallExtra is a fake implementation of the interface that 916 // HCL uses to provide "extra information" associated with diagnostics that 917 // describe errors during a function call. 918 type fakeDiagFunctionCallExtra string 919 920 var _ hclsyntax.FunctionCallDiagExtra = fakeDiagFunctionCallExtra("") 921 922 func (e fakeDiagFunctionCallExtra) CalledFunctionName() string { 923 return string(e) 924 } 925 926 func (e fakeDiagFunctionCallExtra) FunctionCallError() error { 927 return nil 928 } 929 930 // diagnosticCausedByUnknown is a testing helper for exercising our logic 931 // for selectively showing unknown values alongside our source snippets for 932 // diagnostics that are explicitly marked as being caused by unknown values. 933 type diagnosticCausedByUnknown bool 934 935 var _ tfdiags.DiagnosticExtraBecauseUnknown = diagnosticCausedByUnknown(true) 936 937 func (e diagnosticCausedByUnknown) DiagnosticCausedByUnknown() bool { 938 return bool(e) 939 } 940 941 // diagnosticCausedBySensitive is a testing helper for exercising our logic 942 // for selectively showing sensitive values alongside our source snippets for 943 // diagnostics that are explicitly marked as being caused by sensitive values. 944 type diagnosticCausedBySensitive bool 945 946 var _ tfdiags.DiagnosticExtraBecauseSensitive = diagnosticCausedBySensitive(true) 947 948 func (e diagnosticCausedBySensitive) DiagnosticCausedBySensitive() bool { 949 return bool(e) 950 }