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  }