| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package convert | |
| import ( | |
| "errors" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/google/go-cmp/cmp/cmpopts" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| proto "github.com/hashicorp/terraform/internal/tfplugin6" | |
| "github.com/zclconf/go-cty/cty" | |
| ) | |
| var ignoreUnexported = cmpopts.IgnoreUnexported( | |
| proto.Diagnostic{}, | |
| proto.Schema_Block{}, | |
| proto.Schema_NestedBlock{}, | |
| proto.Schema_Attribute{}, | |
| proto.Schema_Object{}, | |
| proto.ResourceIdentitySchema{}, | |
| proto.ResourceIdentitySchema_IdentityAttribute{}, | |
| ) | |
| func TestProtoDiagnostics(t *testing.T) { | |
| diags := WarnsAndErrsToProto( | |
| []string{ | |
| "warning 1", | |
| "warning 2", | |
| }, | |
| []error{ | |
| errors.New("error 1"), | |
| errors.New("error 2"), | |
| }, | |
| ) | |
| expected := []*proto.Diagnostic{ | |
| { | |
| Severity: proto.Diagnostic_WARNING, | |
| Summary: "warning 1", | |
| }, | |
| { | |
| Severity: proto.Diagnostic_WARNING, | |
| Summary: "warning 2", | |
| }, | |
| { | |
| Severity: proto.Diagnostic_ERROR, | |
| Summary: "error 1", | |
| }, | |
| { | |
| Severity: proto.Diagnostic_ERROR, | |
| Summary: "error 2", | |
| }, | |
| } | |
| if !cmp.Equal(expected, diags, ignoreUnexported) { | |
| t.Fatal(cmp.Diff(expected, diags, ignoreUnexported)) | |
| } | |
| } | |
| func TestDiagnostics(t *testing.T) { | |
| type diagFlat struct { | |
| Severity tfdiags.Severity | |
| Attr []interface{} | |
| Summary string | |
| Detail string | |
| } | |
| tests := map[string]struct { | |
| Cons func([]*proto.Diagnostic) []*proto.Diagnostic | |
| Want []diagFlat | |
| }{ | |
| "nil": { | |
| func(diags []*proto.Diagnostic) []*proto.Diagnostic { | |
| return diags | |
| }, | |
| nil, | |
| }, | |
| "error": { | |
| func(diags []*proto.Diagnostic) []*proto.Diagnostic { | |
| return append(diags, &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_ERROR, | |
| Summary: "simple error", | |
| }) | |
| }, | |
| []diagFlat{ | |
| { | |
| Severity: tfdiags.Error, | |
| Summary: "simple error", | |
| }, | |
| }, | |
| }, | |
| "detailed error": { | |
| func(diags []*proto.Diagnostic) []*proto.Diagnostic { | |
| return append(diags, &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_ERROR, | |
| Summary: "simple error", | |
| Detail: "detailed error", | |
| }) | |
| }, | |
| []diagFlat{ | |
| { | |
| Severity: tfdiags.Error, | |
| Summary: "simple error", | |
| Detail: "detailed error", | |
| }, | |
| }, | |
| }, | |
| "warning": { | |
| func(diags []*proto.Diagnostic) []*proto.Diagnostic { | |
| return append(diags, &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_WARNING, | |
| Summary: "simple warning", | |
| }) | |
| }, | |
| []diagFlat{ | |
| { | |
| Severity: tfdiags.Warning, | |
| Summary: "simple warning", | |
| }, | |
| }, | |
| }, | |
| "detailed warning": { | |
| func(diags []*proto.Diagnostic) []*proto.Diagnostic { | |
| return append(diags, &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_WARNING, | |
| Summary: "simple warning", | |
| Detail: "detailed warning", | |
| }) | |
| }, | |
| []diagFlat{ | |
| { | |
| Severity: tfdiags.Warning, | |
| Summary: "simple warning", | |
| Detail: "detailed warning", | |
| }, | |
| }, | |
| }, | |
| "multi error": { | |
| func(diags []*proto.Diagnostic) []*proto.Diagnostic { | |
| diags = append(diags, &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_ERROR, | |
| Summary: "first error", | |
| }, &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_ERROR, | |
| Summary: "second error", | |
| }) | |
| return diags | |
| }, | |
| []diagFlat{ | |
| { | |
| Severity: tfdiags.Error, | |
| Summary: "first error", | |
| }, | |
| { | |
| Severity: tfdiags.Error, | |
| Summary: "second error", | |
| }, | |
| }, | |
| }, | |
| "warning and error": { | |
| func(diags []*proto.Diagnostic) []*proto.Diagnostic { | |
| diags = append(diags, &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_WARNING, | |
| Summary: "warning", | |
| }, &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_ERROR, | |
| Summary: "error", | |
| }) | |
| return diags | |
| }, | |
| []diagFlat{ | |
| { | |
| Severity: tfdiags.Warning, | |
| Summary: "warning", | |
| }, | |
| { | |
| Severity: tfdiags.Error, | |
| Summary: "error", | |
| }, | |
| }, | |
| }, | |
| "attr error": { | |
| func(diags []*proto.Diagnostic) []*proto.Diagnostic { | |
| diags = append(diags, &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_ERROR, | |
| Summary: "error", | |
| Detail: "error detail", | |
| Attribute: &proto.AttributePath{ | |
| Steps: []*proto.AttributePath_Step{ | |
| { | |
| Selector: &proto.AttributePath_Step_AttributeName{ | |
| AttributeName: "attribute_name", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }) | |
| return diags | |
| }, | |
| []diagFlat{ | |
| { | |
| Severity: tfdiags.Error, | |
| Summary: "error", | |
| Detail: "error detail", | |
| Attr: []interface{}{"attribute_name"}, | |
| }, | |
| }, | |
| }, | |
| "multi attr": { | |
| func(diags []*proto.Diagnostic) []*proto.Diagnostic { | |
| diags = append(diags, | |
| &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_ERROR, | |
| Summary: "error 1", | |
| Detail: "error 1 detail", | |
| Attribute: &proto.AttributePath{ | |
| Steps: []*proto.AttributePath_Step{ | |
| { | |
| Selector: &proto.AttributePath_Step_AttributeName{ | |
| AttributeName: "attr", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_ERROR, | |
| Summary: "error 2", | |
| Detail: "error 2 detail", | |
| Attribute: &proto.AttributePath{ | |
| Steps: []*proto.AttributePath_Step{ | |
| { | |
| Selector: &proto.AttributePath_Step_AttributeName{ | |
| AttributeName: "attr", | |
| }, | |
| }, | |
| { | |
| Selector: &proto.AttributePath_Step_AttributeName{ | |
| AttributeName: "sub", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_WARNING, | |
| Summary: "warning", | |
| Detail: "warning detail", | |
| Attribute: &proto.AttributePath{ | |
| Steps: []*proto.AttributePath_Step{ | |
| { | |
| Selector: &proto.AttributePath_Step_AttributeName{ | |
| AttributeName: "attr", | |
| }, | |
| }, | |
| { | |
| Selector: &proto.AttributePath_Step_ElementKeyInt{ | |
| ElementKeyInt: 1, | |
| }, | |
| }, | |
| { | |
| Selector: &proto.AttributePath_Step_AttributeName{ | |
| AttributeName: "sub", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| &proto.Diagnostic{ | |
| Severity: proto.Diagnostic_ERROR, | |
| Summary: "error 3", | |
| Detail: "error 3 detail", | |
| Attribute: &proto.AttributePath{ | |
| Steps: []*proto.AttributePath_Step{ | |
| { | |
| Selector: &proto.AttributePath_Step_AttributeName{ | |
| AttributeName: "attr", | |
| }, | |
| }, | |
| { | |
| Selector: &proto.AttributePath_Step_ElementKeyString{ | |
| ElementKeyString: "idx", | |
| }, | |
| }, | |
| { | |
| Selector: &proto.AttributePath_Step_AttributeName{ | |
| AttributeName: "sub", | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| ) | |
| return diags | |
| }, | |
| []diagFlat{ | |
| { | |
| Severity: tfdiags.Error, | |
| Summary: "error 1", | |
| Detail: "error 1 detail", | |
| Attr: []interface{}{"attr"}, | |
| }, | |
| { | |
| Severity: tfdiags.Error, | |
| Summary: "error 2", | |
| Detail: "error 2 detail", | |
| Attr: []interface{}{"attr", "sub"}, | |
| }, | |
| { | |
| Severity: tfdiags.Warning, | |
| Summary: "warning", | |
| Detail: "warning detail", | |
| Attr: []interface{}{"attr", 1, "sub"}, | |
| }, | |
| { | |
| Severity: tfdiags.Error, | |
| Summary: "error 3", | |
| Detail: "error 3 detail", | |
| Attr: []interface{}{"attr", "idx", "sub"}, | |
| }, | |
| }, | |
| }, | |
| } | |
| flattenTFDiags := func(ds tfdiags.Diagnostics) []diagFlat { | |
| var flat []diagFlat | |
| for _, item := range ds { | |
| desc := item.Description() | |
| var attr []interface{} | |
| for _, a := range tfdiags.GetAttribute(item) { | |
| switch step := a.(type) { | |
| case cty.GetAttrStep: | |
| attr = append(attr, step.Name) | |
| case cty.IndexStep: | |
| switch step.Key.Type() { | |
| case cty.Number: | |
| i, _ := step.Key.AsBigFloat().Int64() | |
| attr = append(attr, int(i)) | |
| case cty.String: | |
| attr = append(attr, step.Key.AsString()) | |
| } | |
| } | |
| } | |
| flat = append(flat, diagFlat{ | |
| Severity: item.Severity(), | |
| Attr: attr, | |
| Summary: desc.Summary, | |
| Detail: desc.Detail, | |
| }) | |
| } | |
| return flat | |
| } | |
| for name, tc := range tests { | |
| t.Run(name, func(t *testing.T) { | |
| // we take the | |
| tfDiags := ProtoToDiagnostics(tc.Cons(nil)) | |
| flat := flattenTFDiags(tfDiags) | |
| if !cmp.Equal(flat, tc.Want, typeComparer, valueComparer, equateEmpty) { | |
| t.Fatal(cmp.Diff(flat, tc.Want, typeComparer, valueComparer, equateEmpty)) | |
| } | |
| }) | |
| } | |
| } |