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 }