github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/plugin/convert/diagnostics_test.go (about)

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