github.com/opentofu/opentofu@v1.7.1/internal/tfdiags/contextual_test.go (about)

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