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