github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/tfdiags/contextual_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package tfdiags
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  	"testing"
    10  
    11  	"github.com/go-test/deep"
    12  	"github.com/hashicorp/hcl/v2"
    13  	"github.com/hashicorp/hcl/v2/hclsyntax"
    14  	"github.com/zclconf/go-cty/cty"
    15  )
    16  
    17  func TestAttributeValue(t *testing.T) {
    18  	testConfig := `
    19  foo {
    20    bar = "hi"
    21  }
    22  foo {
    23    bar = "bar"
    24  }
    25  bar {
    26    bar = "woot"
    27  }
    28  baz "a" {
    29    bar = "beep"
    30  }
    31  baz "b" {
    32    bar = "boop"
    33  }
    34  parent {
    35    nested_str = "hello"
    36    nested_str_tuple = ["aa", "bbb", "cccc"]
    37    nested_num_tuple = [1, 9863, 22]
    38    nested_map = {
    39      first_key  = "first_value"
    40      second_key = "2nd value"
    41    }
    42  }
    43  tuple_of_one = ["one"]
    44  tuple_of_two = ["first", "22222"]
    45  root_map = {
    46    first  = "1st"
    47    second = "2nd"
    48  }
    49  simple_attr = "val"
    50  `
    51  	// TODO: Test ConditionalExpr
    52  	// TODO: Test ForExpr
    53  	// TODO: Test FunctionCallExpr
    54  	// TODO: Test IndexExpr
    55  	// TODO: Test interpolation
    56  	// TODO: Test SplatExpr
    57  
    58  	f, parseDiags := hclsyntax.ParseConfig([]byte(testConfig), "test.tf", hcl.Pos{Line: 1, Column: 1})
    59  	if len(parseDiags) != 0 {
    60  		t.Fatal(parseDiags)
    61  	}
    62  	emptySrcRng := &SourceRange{
    63  		Filename: "test.tf",
    64  		Start:    SourcePos{Line: 1, Column: 1, Byte: 0},
    65  		End:      SourcePos{Line: 1, Column: 1, Byte: 0},
    66  	}
    67  
    68  	testCases := []struct {
    69  		Diag          Diagnostic
    70  		ExpectedRange *SourceRange
    71  	}{
    72  		{
    73  			AttributeValue(
    74  				Error,
    75  				"foo[0].bar",
    76  				"detail",
    77  				cty.Path{
    78  					cty.GetAttrStep{Name: "foo"},
    79  					cty.IndexStep{Key: cty.NumberIntVal(0)},
    80  					cty.GetAttrStep{Name: "bar"},
    81  				},
    82  			),
    83  			&SourceRange{
    84  				Filename: "test.tf",
    85  				Start:    SourcePos{Line: 3, Column: 9, Byte: 15},
    86  				End:      SourcePos{Line: 3, Column: 13, Byte: 19},
    87  			},
    88  		},
    89  		{
    90  			AttributeValue(
    91  				Error,
    92  				"foo[1].bar",
    93  				"detail",
    94  				cty.Path{
    95  					cty.GetAttrStep{Name: "foo"},
    96  					cty.IndexStep{Key: cty.NumberIntVal(1)},
    97  					cty.GetAttrStep{Name: "bar"},
    98  				},
    99  			),
   100  			&SourceRange{
   101  				Filename: "test.tf",
   102  				Start:    SourcePos{Line: 6, Column: 9, Byte: 36},
   103  				End:      SourcePos{Line: 6, Column: 14, Byte: 41},
   104  			},
   105  		},
   106  		{
   107  			AttributeValue(
   108  				Error,
   109  				"foo[99].bar",
   110  				"detail",
   111  				cty.Path{
   112  					cty.GetAttrStep{Name: "foo"},
   113  					cty.IndexStep{Key: cty.NumberIntVal(99)},
   114  					cty.GetAttrStep{Name: "bar"},
   115  				},
   116  			),
   117  			emptySrcRng,
   118  		},
   119  		{
   120  			AttributeValue(
   121  				Error,
   122  				"bar.bar",
   123  				"detail",
   124  				cty.Path{
   125  					cty.GetAttrStep{Name: "bar"},
   126  					cty.GetAttrStep{Name: "bar"},
   127  				},
   128  			),
   129  			&SourceRange{
   130  				Filename: "test.tf",
   131  				Start:    SourcePos{Line: 9, Column: 9, Byte: 58},
   132  				End:      SourcePos{Line: 9, Column: 15, Byte: 64},
   133  			},
   134  		},
   135  		{
   136  			AttributeValue(
   137  				Error,
   138  				`baz["a"].bar`,
   139  				"detail",
   140  				cty.Path{
   141  					cty.GetAttrStep{Name: "baz"},
   142  					cty.IndexStep{Key: cty.StringVal("a")},
   143  					cty.GetAttrStep{Name: "bar"},
   144  				},
   145  			),
   146  			&SourceRange{
   147  				Filename: "test.tf",
   148  				Start:    SourcePos{Line: 12, Column: 9, Byte: 85},
   149  				End:      SourcePos{Line: 12, Column: 15, Byte: 91},
   150  			},
   151  		},
   152  		{
   153  			AttributeValue(
   154  				Error,
   155  				`baz["b"].bar`,
   156  				"detail",
   157  				cty.Path{
   158  					cty.GetAttrStep{Name: "baz"},
   159  					cty.IndexStep{Key: cty.StringVal("b")},
   160  					cty.GetAttrStep{Name: "bar"},
   161  				},
   162  			),
   163  			&SourceRange{
   164  				Filename: "test.tf",
   165  				Start:    SourcePos{Line: 15, Column: 9, Byte: 112},
   166  				End:      SourcePos{Line: 15, Column: 15, Byte: 118},
   167  			},
   168  		},
   169  		{
   170  			AttributeValue(
   171  				Error,
   172  				`baz["not_exists"].bar`,
   173  				"detail",
   174  				cty.Path{
   175  					cty.GetAttrStep{Name: "baz"},
   176  					cty.IndexStep{Key: cty.StringVal("not_exists")},
   177  					cty.GetAttrStep{Name: "bar"},
   178  				},
   179  			),
   180  			emptySrcRng,
   181  		},
   182  		{
   183  			// Attribute value with subject already populated should not be disturbed.
   184  			// (in a real case, this might've been passed through from a deeper function
   185  			// in the call stack, for example.)
   186  			&attributeDiagnostic{
   187  				attrPath: cty.Path{cty.GetAttrStep{Name: "foo"}},
   188  				diagnosticBase: diagnosticBase{
   189  					summary: "preexisting",
   190  					detail:  "detail",
   191  					address: "original",
   192  				},
   193  				subject: &SourceRange{
   194  					Filename: "somewhere_else.tf",
   195  				},
   196  			},
   197  			&SourceRange{
   198  				Filename: "somewhere_else.tf",
   199  			},
   200  		},
   201  		{
   202  			// Missing path
   203  			&attributeDiagnostic{
   204  				diagnosticBase: diagnosticBase{
   205  					summary: "missing path",
   206  				},
   207  			},
   208  			nil,
   209  		},
   210  
   211  		// Nested attributes
   212  		{
   213  			AttributeValue(
   214  				Error,
   215  				"parent.nested_str",
   216  				"detail",
   217  				cty.Path{
   218  					cty.GetAttrStep{Name: "parent"},
   219  					cty.GetAttrStep{Name: "nested_str"},
   220  				},
   221  			),
   222  			&SourceRange{
   223  				Filename: "test.tf",
   224  				Start:    SourcePos{Line: 18, Column: 16, Byte: 145},
   225  				End:      SourcePos{Line: 18, Column: 23, Byte: 152},
   226  			},
   227  		},
   228  		{
   229  			AttributeValue(
   230  				Error,
   231  				"parent.nested_str_tuple[99]",
   232  				"detail",
   233  				cty.Path{
   234  					cty.GetAttrStep{Name: "parent"},
   235  					cty.GetAttrStep{Name: "nested_str_tuple"},
   236  					cty.IndexStep{Key: cty.NumberIntVal(99)},
   237  				},
   238  			),
   239  			&SourceRange{
   240  				Filename: "test.tf",
   241  				Start:    SourcePos{Line: 19, Column: 3, Byte: 155},
   242  				End:      SourcePos{Line: 19, Column: 19, Byte: 171},
   243  			},
   244  		},
   245  		{
   246  			AttributeValue(
   247  				Error,
   248  				"parent.nested_str_tuple[0]",
   249  				"detail",
   250  				cty.Path{
   251  					cty.GetAttrStep{Name: "parent"},
   252  					cty.GetAttrStep{Name: "nested_str_tuple"},
   253  					cty.IndexStep{Key: cty.NumberIntVal(0)},
   254  				},
   255  			),
   256  			&SourceRange{
   257  				Filename: "test.tf",
   258  				Start:    SourcePos{Line: 19, Column: 23, Byte: 175},
   259  				End:      SourcePos{Line: 19, Column: 27, Byte: 179},
   260  			},
   261  		},
   262  		{
   263  			AttributeValue(
   264  				Error,
   265  				"parent.nested_str_tuple[2]",
   266  				"detail",
   267  				cty.Path{
   268  					cty.GetAttrStep{Name: "parent"},
   269  					cty.GetAttrStep{Name: "nested_str_tuple"},
   270  					cty.IndexStep{Key: cty.NumberIntVal(2)},
   271  				},
   272  			),
   273  			&SourceRange{
   274  				Filename: "test.tf",
   275  				Start:    SourcePos{Line: 19, Column: 36, Byte: 188},
   276  				End:      SourcePos{Line: 19, Column: 42, Byte: 194},
   277  			},
   278  		},
   279  		{
   280  			AttributeValue(
   281  				Error,
   282  				"parent.nested_num_tuple[0]",
   283  				"detail",
   284  				cty.Path{
   285  					cty.GetAttrStep{Name: "parent"},
   286  					cty.GetAttrStep{Name: "nested_num_tuple"},
   287  					cty.IndexStep{Key: cty.NumberIntVal(0)},
   288  				},
   289  			),
   290  			&SourceRange{
   291  				Filename: "test.tf",
   292  				Start:    SourcePos{Line: 20, Column: 23, Byte: 218},
   293  				End:      SourcePos{Line: 20, Column: 24, Byte: 219},
   294  			},
   295  		},
   296  		{
   297  			AttributeValue(
   298  				Error,
   299  				"parent.nested_num_tuple[1]",
   300  				"detail",
   301  				cty.Path{
   302  					cty.GetAttrStep{Name: "parent"},
   303  					cty.GetAttrStep{Name: "nested_num_tuple"},
   304  					cty.IndexStep{Key: cty.NumberIntVal(1)},
   305  				},
   306  			),
   307  			&SourceRange{
   308  				Filename: "test.tf",
   309  				Start:    SourcePos{Line: 20, Column: 26, Byte: 221},
   310  				End:      SourcePos{Line: 20, Column: 30, Byte: 225},
   311  			},
   312  		},
   313  		{
   314  			AttributeValue(
   315  				Error,
   316  				"parent.nested_map.first_key",
   317  				"detail",
   318  				cty.Path{
   319  					cty.GetAttrStep{Name: "parent"},
   320  					cty.GetAttrStep{Name: "nested_map"},
   321  					cty.IndexStep{Key: cty.StringVal("first_key")},
   322  				},
   323  			),
   324  			&SourceRange{
   325  				Filename: "test.tf",
   326  				Start:    SourcePos{Line: 22, Column: 19, Byte: 266},
   327  				End:      SourcePos{Line: 22, Column: 30, Byte: 277},
   328  			},
   329  		},
   330  		{
   331  			AttributeValue(
   332  				Error,
   333  				"parent.nested_map.second_key",
   334  				"detail",
   335  				cty.Path{
   336  					cty.GetAttrStep{Name: "parent"},
   337  					cty.GetAttrStep{Name: "nested_map"},
   338  					cty.IndexStep{Key: cty.StringVal("second_key")},
   339  				},
   340  			),
   341  			&SourceRange{
   342  				Filename: "test.tf",
   343  				Start:    SourcePos{Line: 23, Column: 19, Byte: 297},
   344  				End:      SourcePos{Line: 23, Column: 28, Byte: 306},
   345  			},
   346  		},
   347  		{
   348  			AttributeValue(
   349  				Error,
   350  				"parent.nested_map.undefined_key",
   351  				"detail",
   352  				cty.Path{
   353  					cty.GetAttrStep{Name: "parent"},
   354  					cty.GetAttrStep{Name: "nested_map"},
   355  					cty.IndexStep{Key: cty.StringVal("undefined_key")},
   356  				},
   357  			),
   358  			&SourceRange{
   359  				Filename: "test.tf",
   360  				Start:    SourcePos{Line: 21, Column: 3, Byte: 233},
   361  				End:      SourcePos{Line: 21, Column: 13, Byte: 243},
   362  			},
   363  		},
   364  
   365  		// Root attributes of complex types
   366  		{
   367  			AttributeValue(
   368  				Error,
   369  				"tuple_of_one[0]",
   370  				"detail",
   371  				cty.Path{
   372  					cty.GetAttrStep{Name: "tuple_of_one"},
   373  					cty.IndexStep{Key: cty.NumberIntVal(0)},
   374  				},
   375  			),
   376  			&SourceRange{
   377  				Filename: "test.tf",
   378  				Start:    SourcePos{Line: 26, Column: 17, Byte: 330},
   379  				End:      SourcePos{Line: 26, Column: 22, Byte: 335},
   380  			},
   381  		},
   382  		{
   383  			AttributeValue(
   384  				Error,
   385  				"tuple_of_two[0]",
   386  				"detail",
   387  				cty.Path{
   388  					cty.GetAttrStep{Name: "tuple_of_two"},
   389  					cty.IndexStep{Key: cty.NumberIntVal(0)},
   390  				},
   391  			),
   392  			&SourceRange{
   393  				Filename: "test.tf",
   394  				Start:    SourcePos{Line: 27, Column: 17, Byte: 353},
   395  				End:      SourcePos{Line: 27, Column: 24, Byte: 360},
   396  			},
   397  		},
   398  		{
   399  			AttributeValue(
   400  				Error,
   401  				"tuple_of_two[1]",
   402  				"detail",
   403  				cty.Path{
   404  					cty.GetAttrStep{Name: "tuple_of_two"},
   405  					cty.IndexStep{Key: cty.NumberIntVal(1)},
   406  				},
   407  			),
   408  			&SourceRange{
   409  				Filename: "test.tf",
   410  				Start:    SourcePos{Line: 27, Column: 26, Byte: 362},
   411  				End:      SourcePos{Line: 27, Column: 33, Byte: 369},
   412  			},
   413  		},
   414  		{
   415  			AttributeValue(
   416  				Error,
   417  				"tuple_of_one[null]",
   418  				"detail",
   419  				cty.Path{
   420  					cty.GetAttrStep{Name: "tuple_of_one"},
   421  					cty.IndexStep{Key: cty.NullVal(cty.Number)},
   422  				},
   423  			),
   424  			&SourceRange{
   425  				Filename: "test.tf",
   426  				Start:    SourcePos{Line: 26, Column: 1, Byte: 314},
   427  				End:      SourcePos{Line: 26, Column: 13, Byte: 326},
   428  			},
   429  		},
   430  		{
   431  			// index out of range
   432  			AttributeValue(
   433  				Error,
   434  				"tuple_of_two[99]",
   435  				"detail",
   436  				cty.Path{
   437  					cty.GetAttrStep{Name: "tuple_of_two"},
   438  					cty.IndexStep{Key: cty.NumberIntVal(99)},
   439  				},
   440  			),
   441  			&SourceRange{
   442  				Filename: "test.tf",
   443  				Start:    SourcePos{Line: 27, Column: 1, Byte: 337},
   444  				End:      SourcePos{Line: 27, Column: 13, Byte: 349},
   445  			},
   446  		},
   447  		{
   448  			AttributeValue(
   449  				Error,
   450  				"root_map.first",
   451  				"detail",
   452  				cty.Path{
   453  					cty.GetAttrStep{Name: "root_map"},
   454  					cty.IndexStep{Key: cty.StringVal("first")},
   455  				},
   456  			),
   457  			&SourceRange{
   458  				Filename: "test.tf",
   459  				Start:    SourcePos{Line: 29, Column: 13, Byte: 396},
   460  				End:      SourcePos{Line: 29, Column: 16, Byte: 399},
   461  			},
   462  		},
   463  		{
   464  			AttributeValue(
   465  				Error,
   466  				"root_map.second",
   467  				"detail",
   468  				cty.Path{
   469  					cty.GetAttrStep{Name: "root_map"},
   470  					cty.IndexStep{Key: cty.StringVal("second")},
   471  				},
   472  			),
   473  			&SourceRange{
   474  				Filename: "test.tf",
   475  				Start:    SourcePos{Line: 30, Column: 13, Byte: 413},
   476  				End:      SourcePos{Line: 30, Column: 16, Byte: 416},
   477  			},
   478  		},
   479  		{
   480  			AttributeValue(
   481  				Error,
   482  				"root_map.undefined_key",
   483  				"detail",
   484  				cty.Path{
   485  					cty.GetAttrStep{Name: "root_map"},
   486  					cty.IndexStep{Key: cty.StringVal("undefined_key")},
   487  				},
   488  			),
   489  			&SourceRange{
   490  				Filename: "test.tf",
   491  				Start:    SourcePos{Line: 28, Column: 1, Byte: 371},
   492  				End:      SourcePos{Line: 28, Column: 9, Byte: 379},
   493  			},
   494  		},
   495  		{
   496  			AttributeValue(
   497  				Error,
   498  				"simple_attr",
   499  				"detail",
   500  				cty.Path{
   501  					cty.GetAttrStep{Name: "simple_attr"},
   502  				},
   503  			),
   504  			&SourceRange{
   505  				Filename: "test.tf",
   506  				Start:    SourcePos{Line: 32, Column: 15, Byte: 434},
   507  				End:      SourcePos{Line: 32, Column: 20, Byte: 439},
   508  			},
   509  		},
   510  		{
   511  			// This should never happen as error should always point to an attribute
   512  			// or index of an attribute, but we should not crash if it does
   513  			AttributeValue(
   514  				Error,
   515  				"key",
   516  				"index_step",
   517  				cty.Path{
   518  					cty.IndexStep{Key: cty.StringVal("key")},
   519  				},
   520  			),
   521  			emptySrcRng,
   522  		},
   523  		{
   524  			// This should never happen as error should always point to an attribute
   525  			// or index of an attribute, but we should not crash if it does
   526  			AttributeValue(
   527  				Error,
   528  				"key.another",
   529  				"index_step",
   530  				cty.Path{
   531  					cty.IndexStep{Key: cty.StringVal("key")},
   532  					cty.IndexStep{Key: cty.StringVal("another")},
   533  				},
   534  			),
   535  			emptySrcRng,
   536  		},
   537  	}
   538  
   539  	for i, tc := range testCases {
   540  		t.Run(fmt.Sprintf("%d:%s", i, tc.Diag.Description()), func(t *testing.T) {
   541  			var diags Diagnostics
   542  
   543  			origAddr := tc.Diag.Description().Address
   544  			diags = diags.Append(tc.Diag)
   545  
   546  			gotDiags := diags.InConfigBody(f.Body, "test.addr")
   547  			gotRange := gotDiags[0].Source().Subject
   548  			gotAddr := gotDiags[0].Description().Address
   549  
   550  			switch {
   551  			case origAddr != "":
   552  				if gotAddr != origAddr {
   553  					t.Errorf("original diagnostic address modified from %s to %s", origAddr, gotAddr)
   554  				}
   555  			case gotAddr != "test.addr":
   556  				t.Error("missing detail address")
   557  			}
   558  
   559  			for _, problem := range deep.Equal(gotRange, tc.ExpectedRange) {
   560  				t.Error(problem)
   561  			}
   562  		})
   563  	}
   564  }
   565  
   566  func TestGetAttribute(t *testing.T) {
   567  	path := cty.Path{
   568  		cty.GetAttrStep{Name: "foo"},
   569  		cty.IndexStep{Key: cty.NumberIntVal(0)},
   570  		cty.GetAttrStep{Name: "bar"},
   571  	}
   572  
   573  	d := AttributeValue(
   574  		Error,
   575  		"foo[0].bar",
   576  		"detail",
   577  		path,
   578  	)
   579  
   580  	p := GetAttribute(d)
   581  	if !reflect.DeepEqual(path, p) {
   582  		t.Fatalf("paths don't match:\nexpected: %#v\ngot: %#v", path, p)
   583  	}
   584  }