github.com/opentofu/opentofu@v1.7.1/internal/plugin/convert/diagnostics_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 convert
     7  
     8  import (
     9  	"errors"
    10  	"testing"
    11  
    12  	"github.com/google/go-cmp/cmp"
    13  	"github.com/google/go-cmp/cmp/cmpopts"
    14  	"github.com/hashicorp/hcl/v2"
    15  	"github.com/hashicorp/hcl/v2/hclsyntax"
    16  	"github.com/opentofu/opentofu/internal/tfdiags"
    17  	proto "github.com/opentofu/opentofu/internal/tfplugin5"
    18  	"github.com/zclconf/go-cty/cty"
    19  )
    20  
    21  var ignoreUnexported = cmpopts.IgnoreUnexported(
    22  	proto.Diagnostic{},
    23  	proto.Schema_Block{},
    24  	proto.Schema_NestedBlock{},
    25  	proto.Schema_Attribute{},
    26  )
    27  
    28  func TestProtoDiagnostics(t *testing.T) {
    29  	diags := WarnsAndErrsToProto(
    30  		[]string{
    31  			"warning 1",
    32  			"warning 2",
    33  		},
    34  		[]error{
    35  			errors.New("error 1"),
    36  			errors.New("error 2"),
    37  		},
    38  	)
    39  
    40  	expected := []*proto.Diagnostic{
    41  		{
    42  			Severity: proto.Diagnostic_WARNING,
    43  			Summary:  "warning 1",
    44  		},
    45  		{
    46  			Severity: proto.Diagnostic_WARNING,
    47  			Summary:  "warning 2",
    48  		},
    49  		{
    50  			Severity: proto.Diagnostic_ERROR,
    51  			Summary:  "error 1",
    52  		},
    53  		{
    54  			Severity: proto.Diagnostic_ERROR,
    55  			Summary:  "error 2",
    56  		},
    57  	}
    58  
    59  	if !cmp.Equal(expected, diags, ignoreUnexported) {
    60  		t.Fatal(cmp.Diff(expected, diags, ignoreUnexported))
    61  	}
    62  }
    63  
    64  func TestDiagnostics(t *testing.T) {
    65  	type diagFlat struct {
    66  		Severity tfdiags.Severity
    67  		Attr     []interface{}
    68  		Summary  string
    69  		Detail   string
    70  	}
    71  
    72  	tests := map[string]struct {
    73  		Cons func([]*proto.Diagnostic) []*proto.Diagnostic
    74  		Want []diagFlat
    75  	}{
    76  		"nil": {
    77  			func(diags []*proto.Diagnostic) []*proto.Diagnostic {
    78  				return diags
    79  			},
    80  			nil,
    81  		},
    82  		"error": {
    83  			func(diags []*proto.Diagnostic) []*proto.Diagnostic {
    84  				return append(diags, &proto.Diagnostic{
    85  					Severity: proto.Diagnostic_ERROR,
    86  					Summary:  "simple error",
    87  				})
    88  			},
    89  			[]diagFlat{
    90  				{
    91  					Severity: tfdiags.Error,
    92  					Summary:  "simple error",
    93  				},
    94  			},
    95  		},
    96  		"detailed error": {
    97  			func(diags []*proto.Diagnostic) []*proto.Diagnostic {
    98  				return append(diags, &proto.Diagnostic{
    99  					Severity: proto.Diagnostic_ERROR,
   100  					Summary:  "simple error",
   101  					Detail:   "detailed error",
   102  				})
   103  			},
   104  			[]diagFlat{
   105  				{
   106  					Severity: tfdiags.Error,
   107  					Summary:  "simple error",
   108  					Detail:   "detailed error",
   109  				},
   110  			},
   111  		},
   112  		"warning": {
   113  			func(diags []*proto.Diagnostic) []*proto.Diagnostic {
   114  				return append(diags, &proto.Diagnostic{
   115  					Severity: proto.Diagnostic_WARNING,
   116  					Summary:  "simple warning",
   117  				})
   118  			},
   119  			[]diagFlat{
   120  				{
   121  					Severity: tfdiags.Warning,
   122  					Summary:  "simple warning",
   123  				},
   124  			},
   125  		},
   126  		"detailed warning": {
   127  			func(diags []*proto.Diagnostic) []*proto.Diagnostic {
   128  				return append(diags, &proto.Diagnostic{
   129  					Severity: proto.Diagnostic_WARNING,
   130  					Summary:  "simple warning",
   131  					Detail:   "detailed warning",
   132  				})
   133  			},
   134  			[]diagFlat{
   135  				{
   136  					Severity: tfdiags.Warning,
   137  					Summary:  "simple warning",
   138  					Detail:   "detailed warning",
   139  				},
   140  			},
   141  		},
   142  		"multi error": {
   143  			func(diags []*proto.Diagnostic) []*proto.Diagnostic {
   144  				diags = append(diags, &proto.Diagnostic{
   145  					Severity: proto.Diagnostic_ERROR,
   146  					Summary:  "first error",
   147  				}, &proto.Diagnostic{
   148  					Severity: proto.Diagnostic_ERROR,
   149  					Summary:  "second error",
   150  				})
   151  				return diags
   152  			},
   153  			[]diagFlat{
   154  				{
   155  					Severity: tfdiags.Error,
   156  					Summary:  "first error",
   157  				},
   158  				{
   159  					Severity: tfdiags.Error,
   160  					Summary:  "second error",
   161  				},
   162  			},
   163  		},
   164  		"warning and error": {
   165  			func(diags []*proto.Diagnostic) []*proto.Diagnostic {
   166  				diags = append(diags, &proto.Diagnostic{
   167  					Severity: proto.Diagnostic_WARNING,
   168  					Summary:  "warning",
   169  				}, &proto.Diagnostic{
   170  					Severity: proto.Diagnostic_ERROR,
   171  					Summary:  "error",
   172  				})
   173  				return diags
   174  			},
   175  			[]diagFlat{
   176  				{
   177  					Severity: tfdiags.Warning,
   178  					Summary:  "warning",
   179  				},
   180  				{
   181  					Severity: tfdiags.Error,
   182  					Summary:  "error",
   183  				},
   184  			},
   185  		},
   186  		"attr error": {
   187  			func(diags []*proto.Diagnostic) []*proto.Diagnostic {
   188  				diags = append(diags, &proto.Diagnostic{
   189  					Severity: proto.Diagnostic_ERROR,
   190  					Summary:  "error",
   191  					Detail:   "error detail",
   192  					Attribute: &proto.AttributePath{
   193  						Steps: []*proto.AttributePath_Step{
   194  							{
   195  								Selector: &proto.AttributePath_Step_AttributeName{
   196  									AttributeName: "attribute_name",
   197  								},
   198  							},
   199  						},
   200  					},
   201  				})
   202  				return diags
   203  			},
   204  			[]diagFlat{
   205  				{
   206  					Severity: tfdiags.Error,
   207  					Summary:  "error",
   208  					Detail:   "error detail",
   209  					Attr:     []interface{}{"attribute_name"},
   210  				},
   211  			},
   212  		},
   213  		"multi attr": {
   214  			func(diags []*proto.Diagnostic) []*proto.Diagnostic {
   215  				diags = append(diags,
   216  					&proto.Diagnostic{
   217  						Severity: proto.Diagnostic_ERROR,
   218  						Summary:  "error 1",
   219  						Detail:   "error 1 detail",
   220  						Attribute: &proto.AttributePath{
   221  							Steps: []*proto.AttributePath_Step{
   222  								{
   223  									Selector: &proto.AttributePath_Step_AttributeName{
   224  										AttributeName: "attr",
   225  									},
   226  								},
   227  							},
   228  						},
   229  					},
   230  					&proto.Diagnostic{
   231  						Severity: proto.Diagnostic_ERROR,
   232  						Summary:  "error 2",
   233  						Detail:   "error 2 detail",
   234  						Attribute: &proto.AttributePath{
   235  							Steps: []*proto.AttributePath_Step{
   236  								{
   237  									Selector: &proto.AttributePath_Step_AttributeName{
   238  										AttributeName: "attr",
   239  									},
   240  								},
   241  								{
   242  									Selector: &proto.AttributePath_Step_AttributeName{
   243  										AttributeName: "sub",
   244  									},
   245  								},
   246  							},
   247  						},
   248  					},
   249  					&proto.Diagnostic{
   250  						Severity: proto.Diagnostic_WARNING,
   251  						Summary:  "warning",
   252  						Detail:   "warning detail",
   253  						Attribute: &proto.AttributePath{
   254  							Steps: []*proto.AttributePath_Step{
   255  								{
   256  									Selector: &proto.AttributePath_Step_AttributeName{
   257  										AttributeName: "attr",
   258  									},
   259  								},
   260  								{
   261  									Selector: &proto.AttributePath_Step_ElementKeyInt{
   262  										ElementKeyInt: 1,
   263  									},
   264  								},
   265  								{
   266  									Selector: &proto.AttributePath_Step_AttributeName{
   267  										AttributeName: "sub",
   268  									},
   269  								},
   270  							},
   271  						},
   272  					},
   273  					&proto.Diagnostic{
   274  						Severity: proto.Diagnostic_ERROR,
   275  						Summary:  "error 3",
   276  						Detail:   "error 3 detail",
   277  						Attribute: &proto.AttributePath{
   278  							Steps: []*proto.AttributePath_Step{
   279  								{
   280  									Selector: &proto.AttributePath_Step_AttributeName{
   281  										AttributeName: "attr",
   282  									},
   283  								},
   284  								{
   285  									Selector: &proto.AttributePath_Step_ElementKeyString{
   286  										ElementKeyString: "idx",
   287  									},
   288  								},
   289  								{
   290  									Selector: &proto.AttributePath_Step_AttributeName{
   291  										AttributeName: "sub",
   292  									},
   293  								},
   294  							},
   295  						},
   296  					},
   297  				)
   298  
   299  				return diags
   300  			},
   301  			[]diagFlat{
   302  				{
   303  					Severity: tfdiags.Error,
   304  					Summary:  "error 1",
   305  					Detail:   "error 1 detail",
   306  					Attr:     []interface{}{"attr"},
   307  				},
   308  				{
   309  					Severity: tfdiags.Error,
   310  					Summary:  "error 2",
   311  					Detail:   "error 2 detail",
   312  					Attr:     []interface{}{"attr", "sub"},
   313  				},
   314  				{
   315  					Severity: tfdiags.Warning,
   316  					Summary:  "warning",
   317  					Detail:   "warning detail",
   318  					Attr:     []interface{}{"attr", 1, "sub"},
   319  				},
   320  				{
   321  					Severity: tfdiags.Error,
   322  					Summary:  "error 3",
   323  					Detail:   "error 3 detail",
   324  					Attr:     []interface{}{"attr", "idx", "sub"},
   325  				},
   326  			},
   327  		},
   328  	}
   329  
   330  	flattenTFDiags := func(ds tfdiags.Diagnostics) []diagFlat {
   331  		var flat []diagFlat
   332  		for _, item := range ds {
   333  			desc := item.Description()
   334  
   335  			var attr []interface{}
   336  
   337  			for _, a := range tfdiags.GetAttribute(item) {
   338  				switch step := a.(type) {
   339  				case cty.GetAttrStep:
   340  					attr = append(attr, step.Name)
   341  				case cty.IndexStep:
   342  					switch step.Key.Type() {
   343  					case cty.Number:
   344  						i, _ := step.Key.AsBigFloat().Int64()
   345  						attr = append(attr, int(i))
   346  					case cty.String:
   347  						attr = append(attr, step.Key.AsString())
   348  					}
   349  				}
   350  			}
   351  
   352  			flat = append(flat, diagFlat{
   353  				Severity: item.Severity(),
   354  				Attr:     attr,
   355  				Summary:  desc.Summary,
   356  				Detail:   desc.Detail,
   357  			})
   358  		}
   359  		return flat
   360  	}
   361  
   362  	for name, tc := range tests {
   363  		t.Run(name, func(t *testing.T) {
   364  			// we take the
   365  			tfDiags := ProtoToDiagnostics(tc.Cons(nil))
   366  
   367  			flat := flattenTFDiags(tfDiags)
   368  
   369  			if !cmp.Equal(flat, tc.Want, typeComparer, valueComparer, equateEmpty) {
   370  				t.Fatal(cmp.Diff(flat, tc.Want, typeComparer, valueComparer, equateEmpty))
   371  			}
   372  		})
   373  	}
   374  }
   375  
   376  // Test that a diagnostic with a present but empty attribute results in a
   377  // whole body diagnostic. We verify this by inspecting the resulting Subject
   378  // from the diagnostic when considered in the context of a config body.
   379  func TestProtoDiagnostics_emptyAttributePath(t *testing.T) {
   380  	protoDiags := []*proto.Diagnostic{
   381  		{
   382  			Severity: proto.Diagnostic_ERROR,
   383  			Summary:  "error 1",
   384  			Detail:   "error 1 detail",
   385  			Attribute: &proto.AttributePath{
   386  				Steps: []*proto.AttributePath_Step{
   387  					// this slice is intentionally left empty
   388  				},
   389  			},
   390  		},
   391  	}
   392  	tfDiags := ProtoToDiagnostics(protoDiags)
   393  
   394  	testConfig := `provider "test" {
   395    foo = "bar"
   396  }`
   397  	f, parseDiags := hclsyntax.ParseConfig([]byte(testConfig), "test.tf", hcl.Pos{Line: 1, Column: 1})
   398  	if parseDiags.HasErrors() {
   399  		t.Fatal(parseDiags)
   400  	}
   401  	diags := tfDiags.InConfigBody(f.Body, "")
   402  
   403  	if len(tfDiags) != 1 {
   404  		t.Fatalf("expected 1 diag, got %d", len(tfDiags))
   405  	}
   406  	got := diags[0].Source().Subject
   407  	want := &tfdiags.SourceRange{
   408  		Filename: "test.tf",
   409  		Start:    tfdiags.SourcePos{Line: 1, Column: 1},
   410  		End:      tfdiags.SourcePos{Line: 1, Column: 1},
   411  	}
   412  
   413  	if !cmp.Equal(got, want, typeComparer, valueComparer) {
   414  		t.Fatal(cmp.Diff(got, want, typeComparer, valueComparer))
   415  	}
   416  }