github.com/pulumi/terraform@v1.4.0/pkg/command/format/diagnostic_test.go (about)

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