github.com/hugorut/terraform@v1.1.3/src/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/hcltest"
    10  	"github.com/mitchellh/colorstring"
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	viewsjson "github.com/hugorut/terraform/src/command/views/json"
    14  	"github.com/hugorut/terraform/src/lang/marks"
    15  
    16  	"github.com/hugorut/terraform/src/tfdiags"
    17  )
    18  
    19  func TestDiagnostic(t *testing.T) {
    20  
    21  	tests := map[string]struct {
    22  		Diag interface{}
    23  		Want string
    24  	}{
    25  		"sourceless error": {
    26  			tfdiags.Sourceless(
    27  				tfdiags.Error,
    28  				"A sourceless error",
    29  				"It has no source references but it does have a pretty long detail that should wrap over multiple lines.",
    30  			),
    31  			`[red]╷[reset]
    32  [red]│[reset] [bold][red]Error: [reset][bold]A sourceless error[reset]
    33  [red]│[reset]
    34  [red]│[reset] It has no source references but it
    35  [red]│[reset] does have a pretty long detail that
    36  [red]│[reset] should wrap over multiple lines.
    37  [red]╵[reset]
    38  `,
    39  		},
    40  		"sourceless warning": {
    41  			tfdiags.Sourceless(
    42  				tfdiags.Warning,
    43  				"A sourceless warning",
    44  				"It has no source references but it does have a pretty long detail that should wrap over multiple lines.",
    45  			),
    46  			`[yellow]╷[reset]
    47  [yellow]│[reset] [bold][yellow]Warning: [reset][bold]A sourceless warning[reset]
    48  [yellow]│[reset]
    49  [yellow]│[reset] It has no source references but it
    50  [yellow]│[reset] does have a pretty long detail that
    51  [yellow]│[reset] should wrap over multiple lines.
    52  [yellow]╵[reset]
    53  `,
    54  		},
    55  		"error with source code subject": {
    56  			&hcl.Diagnostic{
    57  				Severity: hcl.DiagError,
    58  				Summary:  "Bad bad bad",
    59  				Detail:   "Whatever shall we do?",
    60  				Subject: &hcl.Range{
    61  					Filename: "test.tf",
    62  					Start:    hcl.Pos{Line: 1, Column: 6, Byte: 5},
    63  					End:      hcl.Pos{Line: 1, Column: 12, Byte: 11},
    64  				},
    65  			},
    66  			`[red]╷[reset]
    67  [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset]
    68  [red]│[reset]
    69  [red]│[reset]   on test.tf line 1:
    70  [red]│[reset]    1: test [underline]source[reset] code
    71  [red]│[reset]
    72  [red]│[reset] Whatever shall we do?
    73  [red]╵[reset]
    74  `,
    75  		},
    76  		"error with source code subject and known expression": {
    77  			&hcl.Diagnostic{
    78  				Severity: hcl.DiagError,
    79  				Summary:  "Bad bad bad",
    80  				Detail:   "Whatever shall we do?",
    81  				Subject: &hcl.Range{
    82  					Filename: "test.tf",
    83  					Start:    hcl.Pos{Line: 1, Column: 6, Byte: 5},
    84  					End:      hcl.Pos{Line: 1, Column: 12, Byte: 11},
    85  				},
    86  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
    87  					hcl.TraverseRoot{Name: "boop"},
    88  					hcl.TraverseAttr{Name: "beep"},
    89  				}),
    90  				EvalContext: &hcl.EvalContext{
    91  					Variables: map[string]cty.Value{
    92  						"boop": cty.ObjectVal(map[string]cty.Value{
    93  							"beep": cty.StringVal("blah"),
    94  						}),
    95  					},
    96  				},
    97  			},
    98  			`[red]╷[reset]
    99  [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset]
   100  [red]│[reset]
   101  [red]│[reset]   on test.tf line 1:
   102  [red]│[reset]    1: test [underline]source[reset] code
   103  [red]│[reset]     [dark_gray]├────────────────[reset]
   104  [red]│[reset]     [dark_gray]│[reset] [bold]boop.beep[reset] is "blah"
   105  [red]│[reset]
   106  [red]│[reset] Whatever shall we do?
   107  [red]╵[reset]
   108  `,
   109  		},
   110  		"error with source code subject and expression referring to sensitive value": {
   111  			&hcl.Diagnostic{
   112  				Severity: hcl.DiagError,
   113  				Summary:  "Bad bad bad",
   114  				Detail:   "Whatever shall we do?",
   115  				Subject: &hcl.Range{
   116  					Filename: "test.tf",
   117  					Start:    hcl.Pos{Line: 1, Column: 6, Byte: 5},
   118  					End:      hcl.Pos{Line: 1, Column: 12, Byte: 11},
   119  				},
   120  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   121  					hcl.TraverseRoot{Name: "boop"},
   122  					hcl.TraverseAttr{Name: "beep"},
   123  				}),
   124  				EvalContext: &hcl.EvalContext{
   125  					Variables: map[string]cty.Value{
   126  						"boop": cty.ObjectVal(map[string]cty.Value{
   127  							"beep": cty.StringVal("blah").Mark(marks.Sensitive),
   128  						}),
   129  					},
   130  				},
   131  			},
   132  			`[red]╷[reset]
   133  [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset]
   134  [red]│[reset]
   135  [red]│[reset]   on test.tf line 1:
   136  [red]│[reset]    1: test [underline]source[reset] code
   137  [red]│[reset]     [dark_gray]├────────────────[reset]
   138  [red]│[reset]     [dark_gray]│[reset] [bold]boop.beep[reset] has a sensitive value
   139  [red]│[reset]
   140  [red]│[reset] Whatever shall we do?
   141  [red]╵[reset]
   142  `,
   143  		},
   144  		"error with source code subject and unknown string expression": {
   145  			&hcl.Diagnostic{
   146  				Severity: hcl.DiagError,
   147  				Summary:  "Bad bad bad",
   148  				Detail:   "Whatever shall we do?",
   149  				Subject: &hcl.Range{
   150  					Filename: "test.tf",
   151  					Start:    hcl.Pos{Line: 1, Column: 6, Byte: 5},
   152  					End:      hcl.Pos{Line: 1, Column: 12, Byte: 11},
   153  				},
   154  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   155  					hcl.TraverseRoot{Name: "boop"},
   156  					hcl.TraverseAttr{Name: "beep"},
   157  				}),
   158  				EvalContext: &hcl.EvalContext{
   159  					Variables: map[string]cty.Value{
   160  						"boop": cty.ObjectVal(map[string]cty.Value{
   161  							"beep": cty.UnknownVal(cty.String),
   162  						}),
   163  					},
   164  				},
   165  			},
   166  			`[red]╷[reset]
   167  [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset]
   168  [red]│[reset]
   169  [red]│[reset]   on test.tf line 1:
   170  [red]│[reset]    1: test [underline]source[reset] code
   171  [red]│[reset]     [dark_gray]├────────────────[reset]
   172  [red]│[reset]     [dark_gray]│[reset] [bold]boop.beep[reset] is a string, known only after apply
   173  [red]│[reset]
   174  [red]│[reset] Whatever shall we do?
   175  [red]╵[reset]
   176  `,
   177  		},
   178  		"error with source code subject and unknown expression of unknown type": {
   179  			&hcl.Diagnostic{
   180  				Severity: hcl.DiagError,
   181  				Summary:  "Bad bad bad",
   182  				Detail:   "Whatever shall we do?",
   183  				Subject: &hcl.Range{
   184  					Filename: "test.tf",
   185  					Start:    hcl.Pos{Line: 1, Column: 6, Byte: 5},
   186  					End:      hcl.Pos{Line: 1, Column: 12, Byte: 11},
   187  				},
   188  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   189  					hcl.TraverseRoot{Name: "boop"},
   190  					hcl.TraverseAttr{Name: "beep"},
   191  				}),
   192  				EvalContext: &hcl.EvalContext{
   193  					Variables: map[string]cty.Value{
   194  						"boop": cty.ObjectVal(map[string]cty.Value{
   195  							"beep": cty.UnknownVal(cty.DynamicPseudoType),
   196  						}),
   197  					},
   198  				},
   199  			},
   200  			`[red]╷[reset]
   201  [red]│[reset] [bold][red]Error: [reset][bold]Bad bad bad[reset]
   202  [red]│[reset]
   203  [red]│[reset]   on test.tf line 1:
   204  [red]│[reset]    1: test [underline]source[reset] code
   205  [red]│[reset]     [dark_gray]├────────────────[reset]
   206  [red]│[reset]     [dark_gray]│[reset] [bold]boop.beep[reset] will be known only after apply
   207  [red]│[reset]
   208  [red]│[reset] Whatever shall we do?
   209  [red]╵[reset]
   210  `,
   211  		},
   212  	}
   213  
   214  	sources := map[string][]byte{
   215  		"test.tf": []byte(`test source code`),
   216  	}
   217  
   218  	// This empty Colorize just passes through all of the formatting codes
   219  	// untouched, because it doesn't define any formatting keywords.
   220  	colorize := &colorstring.Colorize{}
   221  
   222  	for name, test := range tests {
   223  		t.Run(name, func(t *testing.T) {
   224  			var diags tfdiags.Diagnostics
   225  			diags = diags.Append(test.Diag) // to normalize it into a tfdiag.Diagnostic
   226  			diag := diags[0]
   227  			got := strings.TrimSpace(Diagnostic(diag, sources, colorize, 40))
   228  			want := strings.TrimSpace(test.Want)
   229  			if got != want {
   230  				t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want)
   231  			}
   232  		})
   233  	}
   234  }
   235  
   236  func TestDiagnosticPlain(t *testing.T) {
   237  
   238  	tests := map[string]struct {
   239  		Diag interface{}
   240  		Want string
   241  	}{
   242  		"sourceless error": {
   243  			tfdiags.Sourceless(
   244  				tfdiags.Error,
   245  				"A sourceless error",
   246  				"It has no source references but it does have a pretty long detail that should wrap over multiple lines.",
   247  			),
   248  			`
   249  Error: A sourceless error
   250  
   251  It has no source references but it does
   252  have a pretty long detail that should
   253  wrap over multiple lines.
   254  `,
   255  		},
   256  		"sourceless warning": {
   257  			tfdiags.Sourceless(
   258  				tfdiags.Warning,
   259  				"A sourceless warning",
   260  				"It has no source references but it does have a pretty long detail that should wrap over multiple lines.",
   261  			),
   262  			`
   263  Warning: A sourceless warning
   264  
   265  It has no source references but it does
   266  have a pretty long detail that should
   267  wrap over multiple lines.
   268  `,
   269  		},
   270  		"error with source code subject": {
   271  			&hcl.Diagnostic{
   272  				Severity: hcl.DiagError,
   273  				Summary:  "Bad bad bad",
   274  				Detail:   "Whatever shall we do?",
   275  				Subject: &hcl.Range{
   276  					Filename: "test.tf",
   277  					Start:    hcl.Pos{Line: 1, Column: 6, Byte: 5},
   278  					End:      hcl.Pos{Line: 1, Column: 12, Byte: 11},
   279  				},
   280  			},
   281  			`
   282  Error: Bad bad bad
   283  
   284    on test.tf line 1:
   285     1: test source code
   286  
   287  Whatever shall we do?
   288  `,
   289  		},
   290  		"error with source code subject and known expression": {
   291  			&hcl.Diagnostic{
   292  				Severity: hcl.DiagError,
   293  				Summary:  "Bad bad bad",
   294  				Detail:   "Whatever shall we do?",
   295  				Subject: &hcl.Range{
   296  					Filename: "test.tf",
   297  					Start:    hcl.Pos{Line: 1, Column: 6, Byte: 5},
   298  					End:      hcl.Pos{Line: 1, Column: 12, Byte: 11},
   299  				},
   300  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   301  					hcl.TraverseRoot{Name: "boop"},
   302  					hcl.TraverseAttr{Name: "beep"},
   303  				}),
   304  				EvalContext: &hcl.EvalContext{
   305  					Variables: map[string]cty.Value{
   306  						"boop": cty.ObjectVal(map[string]cty.Value{
   307  							"beep": cty.StringVal("blah"),
   308  						}),
   309  					},
   310  				},
   311  			},
   312  			`
   313  Error: Bad bad bad
   314  
   315    on test.tf line 1:
   316     1: test source code
   317      ├────────────────
   318      │ boop.beep is "blah"
   319  
   320  Whatever shall we do?
   321  `,
   322  		},
   323  		"error with source code subject and expression referring to sensitive value": {
   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  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   334  					hcl.TraverseRoot{Name: "boop"},
   335  					hcl.TraverseAttr{Name: "beep"},
   336  				}),
   337  				EvalContext: &hcl.EvalContext{
   338  					Variables: map[string]cty.Value{
   339  						"boop": cty.ObjectVal(map[string]cty.Value{
   340  							"beep": cty.StringVal("blah").Mark(marks.Sensitive),
   341  						}),
   342  					},
   343  				},
   344  			},
   345  			`
   346  Error: Bad bad bad
   347  
   348    on test.tf line 1:
   349     1: test source code
   350      ├────────────────
   351      │ boop.beep has a sensitive value
   352  
   353  Whatever shall we do?
   354  `,
   355  		},
   356  		"error with source code subject and unknown string expression": {
   357  			&hcl.Diagnostic{
   358  				Severity: hcl.DiagError,
   359  				Summary:  "Bad bad bad",
   360  				Detail:   "Whatever shall we do?",
   361  				Subject: &hcl.Range{
   362  					Filename: "test.tf",
   363  					Start:    hcl.Pos{Line: 1, Column: 6, Byte: 5},
   364  					End:      hcl.Pos{Line: 1, Column: 12, Byte: 11},
   365  				},
   366  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   367  					hcl.TraverseRoot{Name: "boop"},
   368  					hcl.TraverseAttr{Name: "beep"},
   369  				}),
   370  				EvalContext: &hcl.EvalContext{
   371  					Variables: map[string]cty.Value{
   372  						"boop": cty.ObjectVal(map[string]cty.Value{
   373  							"beep": cty.UnknownVal(cty.String),
   374  						}),
   375  					},
   376  				},
   377  			},
   378  			`
   379  Error: Bad bad bad
   380  
   381    on test.tf line 1:
   382     1: test source code
   383      ├────────────────
   384      │ boop.beep is a string, known only after apply
   385  
   386  Whatever shall we do?
   387  `,
   388  		},
   389  		"error with source code subject and unknown expression of unknown type": {
   390  			&hcl.Diagnostic{
   391  				Severity: hcl.DiagError,
   392  				Summary:  "Bad bad bad",
   393  				Detail:   "Whatever shall we do?",
   394  				Subject: &hcl.Range{
   395  					Filename: "test.tf",
   396  					Start:    hcl.Pos{Line: 1, Column: 6, Byte: 5},
   397  					End:      hcl.Pos{Line: 1, Column: 12, Byte: 11},
   398  				},
   399  				Expression: hcltest.MockExprTraversal(hcl.Traversal{
   400  					hcl.TraverseRoot{Name: "boop"},
   401  					hcl.TraverseAttr{Name: "beep"},
   402  				}),
   403  				EvalContext: &hcl.EvalContext{
   404  					Variables: map[string]cty.Value{
   405  						"boop": cty.ObjectVal(map[string]cty.Value{
   406  							"beep": cty.UnknownVal(cty.DynamicPseudoType),
   407  						}),
   408  					},
   409  				},
   410  			},
   411  			`
   412  Error: Bad bad bad
   413  
   414    on test.tf line 1:
   415     1: test source code
   416      ├────────────────
   417      │ boop.beep will be known only after apply
   418  
   419  Whatever shall we do?
   420  `,
   421  		},
   422  	}
   423  
   424  	sources := map[string][]byte{
   425  		"test.tf": []byte(`test source code`),
   426  	}
   427  
   428  	for name, test := range tests {
   429  		t.Run(name, func(t *testing.T) {
   430  			var diags tfdiags.Diagnostics
   431  			diags = diags.Append(test.Diag) // to normalize it into a tfdiag.Diagnostic
   432  			diag := diags[0]
   433  			got := strings.TrimSpace(DiagnosticPlain(diag, sources, 40))
   434  			want := strings.TrimSpace(test.Want)
   435  			if got != want {
   436  				t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want)
   437  			}
   438  		})
   439  	}
   440  }
   441  
   442  func TestDiagnosticWarningsCompact(t *testing.T) {
   443  	var diags tfdiags.Diagnostics
   444  	diags = diags.Append(tfdiags.SimpleWarning("foo"))
   445  	diags = diags.Append(tfdiags.SimpleWarning("foo"))
   446  	diags = diags.Append(tfdiags.SimpleWarning("bar"))
   447  	diags = diags.Append(&hcl.Diagnostic{
   448  		Severity: hcl.DiagWarning,
   449  		Summary:  "source foo",
   450  		Detail:   "...",
   451  		Subject: &hcl.Range{
   452  			Filename: "source.tf",
   453  			Start:    hcl.Pos{Line: 2, Column: 1, Byte: 5},
   454  			End:      hcl.Pos{Line: 2, Column: 1, Byte: 5},
   455  		},
   456  	})
   457  	diags = diags.Append(&hcl.Diagnostic{
   458  		Severity: hcl.DiagWarning,
   459  		Summary:  "source foo",
   460  		Detail:   "...",
   461  		Subject: &hcl.Range{
   462  			Filename: "source.tf",
   463  			Start:    hcl.Pos{Line: 3, Column: 1, Byte: 7},
   464  			End:      hcl.Pos{Line: 3, Column: 1, Byte: 7},
   465  		},
   466  	})
   467  	diags = diags.Append(&hcl.Diagnostic{
   468  		Severity: hcl.DiagWarning,
   469  		Summary:  "source bar",
   470  		Detail:   "...",
   471  		Subject: &hcl.Range{
   472  			Filename: "source2.tf",
   473  			Start:    hcl.Pos{Line: 1, Column: 1, Byte: 1},
   474  			End:      hcl.Pos{Line: 1, Column: 1, Byte: 1},
   475  		},
   476  	})
   477  
   478  	// ConsolidateWarnings groups together the ones
   479  	// that have source location information and that
   480  	// have the same summary text.
   481  	diags = diags.ConsolidateWarnings(1)
   482  
   483  	// A zero-value Colorize just passes all the formatting
   484  	// codes back to us, so we can test them literally.
   485  	got := DiagnosticWarningsCompact(diags, &colorstring.Colorize{})
   486  	want := `[bold][yellow]Warnings:[reset]
   487  
   488  - foo
   489  - foo
   490  - bar
   491  - source foo
   492    on source.tf line 2 (and 1 more)
   493  - source bar
   494    on source2.tf line 1
   495  `
   496  	if got != want {
   497  		t.Errorf(
   498  			"wrong result\ngot:\n%s\n\nwant:\n%s\n\ndiff:\n%s",
   499  			got, want, cmp.Diff(want, got),
   500  		)
   501  	}
   502  }
   503  
   504  // Test case via https://github.com/hugorut/terraform/issues/21359
   505  func TestDiagnostic_nonOverlappingHighlightContext(t *testing.T) {
   506  	var diags tfdiags.Diagnostics
   507  
   508  	diags = diags.Append(&hcl.Diagnostic{
   509  		Severity: hcl.DiagError,
   510  		Summary:  "Some error",
   511  		Detail:   "...",
   512  		Subject: &hcl.Range{
   513  			Filename: "source.tf",
   514  			Start:    hcl.Pos{Line: 1, Column: 5, Byte: 5},
   515  			End:      hcl.Pos{Line: 1, Column: 5, Byte: 5},
   516  		},
   517  		Context: &hcl.Range{
   518  			Filename: "source.tf",
   519  			Start:    hcl.Pos{Line: 1, Column: 5, Byte: 5},
   520  			End:      hcl.Pos{Line: 4, Column: 2, Byte: 60},
   521  		},
   522  	})
   523  	sources := map[string][]byte{
   524  		"source.tf": []byte(`x = somefunc("testing", {
   525    alpha = "foo"
   526    beta  = "bar"
   527  })
   528  `),
   529  	}
   530  	color := &colorstring.Colorize{
   531  		Colors:  colorstring.DefaultColors,
   532  		Reset:   true,
   533  		Disable: true,
   534  	}
   535  	expected := `╷
   536  │ Error: Some error
   537  │
   538  │   on source.tf line 1:
   539  │    1: x = somefunc("testing", {
   540  │    2:   alpha = "foo"
   541  │    3:   beta  = "bar"
   542  │    4: })
   543  │
   544  │ ...
   545  ╵
   546  `
   547  	output := Diagnostic(diags[0], sources, color, 80)
   548  
   549  	if output != expected {
   550  		t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected)
   551  	}
   552  }
   553  
   554  func TestDiagnostic_emptyOverlapHighlightContext(t *testing.T) {
   555  	var diags tfdiags.Diagnostics
   556  
   557  	diags = diags.Append(&hcl.Diagnostic{
   558  		Severity: hcl.DiagError,
   559  		Summary:  "Some error",
   560  		Detail:   "...",
   561  		Subject: &hcl.Range{
   562  			Filename: "source.tf",
   563  			Start:    hcl.Pos{Line: 3, Column: 10, Byte: 38},
   564  			End:      hcl.Pos{Line: 4, Column: 1, Byte: 39},
   565  		},
   566  		Context: &hcl.Range{
   567  			Filename: "source.tf",
   568  			Start:    hcl.Pos{Line: 2, Column: 13, Byte: 27},
   569  			End:      hcl.Pos{Line: 4, Column: 1, Byte: 39},
   570  		},
   571  	})
   572  	sources := map[string][]byte{
   573  		"source.tf": []byte(`variable "x" {
   574    default = {
   575      "foo"
   576    }
   577  `),
   578  	}
   579  	color := &colorstring.Colorize{
   580  		Colors:  colorstring.DefaultColors,
   581  		Reset:   true,
   582  		Disable: true,
   583  	}
   584  	expected := `╷
   585  │ Error: Some error
   586  │
   587  │   on source.tf line 3, in variable "x":
   588  │    2:   default = {
   589  │    3:     "foo"
   590  │    4:   }
   591  │
   592  │ ...
   593  ╵
   594  `
   595  	output := Diagnostic(diags[0], sources, color, 80)
   596  
   597  	if output != expected {
   598  		t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected)
   599  	}
   600  }
   601  
   602  func TestDiagnosticPlain_emptyOverlapHighlightContext(t *testing.T) {
   603  	var diags tfdiags.Diagnostics
   604  
   605  	diags = diags.Append(&hcl.Diagnostic{
   606  		Severity: hcl.DiagError,
   607  		Summary:  "Some error",
   608  		Detail:   "...",
   609  		Subject: &hcl.Range{
   610  			Filename: "source.tf",
   611  			Start:    hcl.Pos{Line: 3, Column: 10, Byte: 38},
   612  			End:      hcl.Pos{Line: 4, Column: 1, Byte: 39},
   613  		},
   614  		Context: &hcl.Range{
   615  			Filename: "source.tf",
   616  			Start:    hcl.Pos{Line: 2, Column: 13, Byte: 27},
   617  			End:      hcl.Pos{Line: 4, Column: 1, Byte: 39},
   618  		},
   619  	})
   620  	sources := map[string][]byte{
   621  		"source.tf": []byte(`variable "x" {
   622    default = {
   623      "foo"
   624    }
   625  `),
   626  	}
   627  
   628  	expected := `
   629  Error: Some error
   630  
   631    on source.tf line 3, in variable "x":
   632     2:   default = {
   633     3:     "foo"
   634     4:   }
   635  
   636  ...
   637  `
   638  	output := DiagnosticPlain(diags[0], sources, 80)
   639  
   640  	if output != expected {
   641  		t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected)
   642  	}
   643  }
   644  
   645  func TestDiagnostic_wrapDetailIncludingCommand(t *testing.T) {
   646  	var diags tfdiags.Diagnostics
   647  
   648  	diags = diags.Append(&hcl.Diagnostic{
   649  		Severity: hcl.DiagError,
   650  		Summary:  "Everything went wrong",
   651  		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",
   652  	})
   653  	color := &colorstring.Colorize{
   654  		Colors:  colorstring.DefaultColors,
   655  		Reset:   true,
   656  		Disable: true,
   657  	}
   658  	expected := `╷
   659  │ Error: Everything went wrong
   660  │
   661  │ This is a very long sentence about whatever went wrong which is supposed
   662  │ to wrap onto multiple lines. Thank-you very much for listening.
   663  │
   664  │ To fix this, run this very long command:
   665  │   terraform read-my-mind -please -thanks -but-do-not-wrap-this-line-because-it-is-prefixed-with-spaces
   666  │
   667  │ Here is a coda which is also long enough to wrap and so it should
   668  │ eventually make it onto multiple lines. THE END
   669  ╵
   670  `
   671  	output := Diagnostic(diags[0], nil, color, 76)
   672  
   673  	if output != expected {
   674  		t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected)
   675  	}
   676  }
   677  
   678  func TestDiagnosticPlain_wrapDetailIncludingCommand(t *testing.T) {
   679  	var diags tfdiags.Diagnostics
   680  
   681  	diags = diags.Append(&hcl.Diagnostic{
   682  		Severity: hcl.DiagError,
   683  		Summary:  "Everything went wrong",
   684  		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",
   685  	})
   686  
   687  	expected := `
   688  Error: Everything went wrong
   689  
   690  This is a very long sentence about whatever went wrong which is supposed to
   691  wrap onto multiple lines. Thank-you very much for listening.
   692  
   693  To fix this, run this very long command:
   694    terraform read-my-mind -please -thanks -but-do-not-wrap-this-line-because-it-is-prefixed-with-spaces
   695  
   696  Here is a coda which is also long enough to wrap and so it should
   697  eventually make it onto multiple lines. THE END
   698  `
   699  	output := DiagnosticPlain(diags[0], nil, 76)
   700  
   701  	if output != expected {
   702  		t.Fatalf("unexpected output: got:\n%s\nwant\n%s\n", output, expected)
   703  	}
   704  }
   705  
   706  // Test cases covering invalid JSON diagnostics which should still render
   707  // correctly. These JSON diagnostic values cannot be generated from the
   708  // json.NewDiagnostic code path, but we may read and display JSON diagnostics
   709  // in future from other sources.
   710  func TestDiagnosticFromJSON_invalid(t *testing.T) {
   711  	tests := map[string]struct {
   712  		Diag *viewsjson.Diagnostic
   713  		Want string
   714  	}{
   715  		"zero-value end range and highlight end byte": {
   716  			&viewsjson.Diagnostic{
   717  				Severity: viewsjson.DiagnosticSeverityError,
   718  				Summary:  "Bad end",
   719  				Detail:   "It all went wrong.",
   720  				Range: &viewsjson.DiagnosticRange{
   721  					Filename: "ohno.tf",
   722  					Start:    viewsjson.Pos{Line: 1, Column: 23, Byte: 22},
   723  					End:      viewsjson.Pos{Line: 0, Column: 0, Byte: 0},
   724  				},
   725  				Snippet: &viewsjson.DiagnosticSnippet{
   726  					Code:                 `resource "foo_bar "baz" {`,
   727  					StartLine:            1,
   728  					HighlightStartOffset: 22,
   729  					HighlightEndOffset:   0,
   730  				},
   731  			},
   732  			`[red]╷[reset]
   733  [red]│[reset] [bold][red]Error: [reset][bold]Bad end[reset]
   734  [red]│[reset]
   735  [red]│[reset]   on ohno.tf line 1:
   736  [red]│[reset]    1: resource "foo_bar "baz[underline]"[reset] {
   737  [red]│[reset]
   738  [red]│[reset] It all went wrong.
   739  [red]╵[reset]
   740  `,
   741  		},
   742  	}
   743  
   744  	// This empty Colorize just passes through all of the formatting codes
   745  	// untouched, because it doesn't define any formatting keywords.
   746  	colorize := &colorstring.Colorize{}
   747  
   748  	for name, test := range tests {
   749  		t.Run(name, func(t *testing.T) {
   750  			got := strings.TrimSpace(DiagnosticFromJSON(test.Diag, colorize, 40))
   751  			want := strings.TrimSpace(test.Want)
   752  			if got != want {
   753  				t.Errorf("wrong result\ngot:\n%s\n\nwant:\n%s\n\n", got, want)
   754  			}
   755  		})
   756  	}
   757  }