github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/command/views/json/diagnostic_test.go (about)

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