github.com/opentofu/opentofu@v1.7.1/internal/tfdiags/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 tfdiags 7 8 import ( 9 "errors" 10 "fmt" 11 "reflect" 12 "strings" 13 "testing" 14 15 "github.com/hashicorp/go-multierror" 16 17 "github.com/davecgh/go-spew/spew" 18 "github.com/hashicorp/hcl/v2" 19 ) 20 21 func TestBuild(t *testing.T) { 22 type diagFlat struct { 23 Severity Severity 24 Summary string 25 Detail string 26 Subject *SourceRange 27 Context *SourceRange 28 } 29 30 tests := map[string]struct { 31 Cons func(Diagnostics) Diagnostics 32 Want []diagFlat 33 }{ 34 "nil": { 35 func(diags Diagnostics) Diagnostics { 36 return diags 37 }, 38 nil, 39 }, 40 "fmt.Errorf": { 41 func(diags Diagnostics) Diagnostics { 42 diags = diags.Append(fmt.Errorf("oh no bad")) 43 return diags 44 }, 45 []diagFlat{ 46 { 47 Severity: Error, 48 Summary: "oh no bad", 49 }, 50 }, 51 }, 52 "errors.New": { 53 func(diags Diagnostics) Diagnostics { 54 diags = diags.Append(errors.New("oh no bad")) 55 return diags 56 }, 57 []diagFlat{ 58 { 59 Severity: Error, 60 Summary: "oh no bad", 61 }, 62 }, 63 }, 64 "hcl.Diagnostic": { 65 func(diags Diagnostics) Diagnostics { 66 diags = diags.Append(&hcl.Diagnostic{ 67 Severity: hcl.DiagError, 68 Summary: "Something bad happened", 69 Detail: "It was really, really bad.", 70 Subject: &hcl.Range{ 71 Filename: "foo.tf", 72 Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, 73 End: hcl.Pos{Line: 2, Column: 3, Byte: 25}, 74 }, 75 Context: &hcl.Range{ 76 Filename: "foo.tf", 77 Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, 78 End: hcl.Pos{Line: 3, Column: 1, Byte: 30}, 79 }, 80 }) 81 return diags 82 }, 83 []diagFlat{ 84 { 85 Severity: Error, 86 Summary: "Something bad happened", 87 Detail: "It was really, really bad.", 88 Subject: &SourceRange{ 89 Filename: "foo.tf", 90 Start: SourcePos{Line: 1, Column: 10, Byte: 9}, 91 End: SourcePos{Line: 2, Column: 3, Byte: 25}, 92 }, 93 Context: &SourceRange{ 94 Filename: "foo.tf", 95 Start: SourcePos{Line: 1, Column: 1, Byte: 0}, 96 End: SourcePos{Line: 3, Column: 1, Byte: 30}, 97 }, 98 }, 99 }, 100 }, 101 "hcl.Diagnostics": { 102 func(diags Diagnostics) Diagnostics { 103 diags = diags.Append(hcl.Diagnostics{ 104 { 105 Severity: hcl.DiagError, 106 Summary: "Something bad happened", 107 Detail: "It was really, really bad.", 108 }, 109 { 110 Severity: hcl.DiagWarning, 111 Summary: "Also, somebody sneezed", 112 Detail: "How rude!", 113 }, 114 }) 115 return diags 116 }, 117 []diagFlat{ 118 { 119 Severity: Error, 120 Summary: "Something bad happened", 121 Detail: "It was really, really bad.", 122 }, 123 { 124 Severity: Warning, 125 Summary: "Also, somebody sneezed", 126 Detail: "How rude!", 127 }, 128 }, 129 }, 130 "multierror.Error": { 131 func(diags Diagnostics) Diagnostics { 132 err := multierror.Append(nil, errors.New("bad thing A")) 133 err = multierror.Append(err, errors.New("bad thing B")) 134 diags = diags.Append(err) 135 return diags 136 }, 137 []diagFlat{ 138 { 139 Severity: Error, 140 Summary: "bad thing A", 141 }, 142 { 143 Severity: Error, 144 Summary: "bad thing B", 145 }, 146 }, 147 }, 148 "concat Diagnostics": { 149 func(diags Diagnostics) Diagnostics { 150 var moreDiags Diagnostics 151 moreDiags = moreDiags.Append(errors.New("bad thing A")) 152 moreDiags = moreDiags.Append(errors.New("bad thing B")) 153 return diags.Append(moreDiags) 154 }, 155 []diagFlat{ 156 { 157 Severity: Error, 158 Summary: "bad thing A", 159 }, 160 { 161 Severity: Error, 162 Summary: "bad thing B", 163 }, 164 }, 165 }, 166 "single Diagnostic": { 167 func(diags Diagnostics) Diagnostics { 168 return diags.Append(SimpleWarning("Don't forget your toothbrush!")) 169 }, 170 []diagFlat{ 171 { 172 Severity: Warning, 173 Summary: "Don't forget your toothbrush!", 174 }, 175 }, 176 }, 177 "multiple appends": { 178 func(diags Diagnostics) Diagnostics { 179 diags = diags.Append(SimpleWarning("Don't forget your toothbrush!")) 180 diags = diags.Append(fmt.Errorf("exploded")) 181 return diags 182 }, 183 []diagFlat{ 184 { 185 Severity: Warning, 186 Summary: "Don't forget your toothbrush!", 187 }, 188 { 189 Severity: Error, 190 Summary: "exploded", 191 }, 192 }, 193 }, 194 } 195 196 for name, test := range tests { 197 t.Run(name, func(t *testing.T) { 198 gotDiags := test.Cons(nil) 199 var got []diagFlat 200 for _, item := range gotDiags { 201 desc := item.Description() 202 source := item.Source() 203 got = append(got, diagFlat{ 204 Severity: item.Severity(), 205 Summary: desc.Summary, 206 Detail: desc.Detail, 207 Subject: source.Subject, 208 Context: source.Context, 209 }) 210 } 211 212 if !reflect.DeepEqual(got, test.Want) { 213 t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(test.Want)) 214 } 215 }) 216 } 217 } 218 219 func TestDiagnosticsErr(t *testing.T) { 220 t.Run("empty", func(t *testing.T) { 221 var diags Diagnostics 222 err := diags.Err() 223 if err != nil { 224 t.Errorf("got non-nil error %#v; want nil", err) 225 } 226 }) 227 t.Run("warning only", func(t *testing.T) { 228 var diags Diagnostics 229 diags = diags.Append(SimpleWarning("bad")) 230 err := diags.Err() 231 if err != nil { 232 t.Errorf("got non-nil error %#v; want nil", err) 233 } 234 }) 235 t.Run("one error", func(t *testing.T) { 236 var diags Diagnostics 237 diags = diags.Append(errors.New("didn't work")) 238 err := diags.Err() 239 if err == nil { 240 t.Fatalf("got nil error %#v; want non-nil", err) 241 } 242 if got, want := err.Error(), "didn't work"; got != want { 243 t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) 244 } 245 }) 246 t.Run("two errors", func(t *testing.T) { 247 var diags Diagnostics 248 diags = diags.Append(errors.New("didn't work")) 249 diags = diags.Append(errors.New("didn't work either")) 250 err := diags.Err() 251 if err == nil { 252 t.Fatalf("got nil error %#v; want non-nil", err) 253 } 254 want := strings.TrimSpace(` 255 2 problems: 256 257 - didn't work 258 - didn't work either 259 `) 260 if got := err.Error(); got != want { 261 t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) 262 } 263 }) 264 t.Run("error and warning", func(t *testing.T) { 265 var diags Diagnostics 266 diags = diags.Append(errors.New("didn't work")) 267 diags = diags.Append(SimpleWarning("didn't work either")) 268 err := diags.Err() 269 if err == nil { 270 t.Fatalf("got nil error %#v; want non-nil", err) 271 } 272 // Since this "as error" mode is just a fallback for 273 // non-diagnostics-aware situations like tests, we don't actually 274 // distinguish warnings and errors here since the point is to just 275 // get the messages rendered. User-facing code should be printing 276 // each diagnostic separately, so won't enter this codepath, 277 want := strings.TrimSpace(` 278 2 problems: 279 280 - didn't work 281 - didn't work either 282 `) 283 if got := err.Error(); got != want { 284 t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) 285 } 286 }) 287 } 288 289 func TestDiagnosticsErrWithWarnings(t *testing.T) { 290 t.Run("empty", func(t *testing.T) { 291 var diags Diagnostics 292 err := diags.ErrWithWarnings() 293 if err != nil { 294 t.Errorf("got non-nil error %#v; want nil", err) 295 } 296 }) 297 t.Run("warning only", func(t *testing.T) { 298 var diags Diagnostics 299 diags = diags.Append(SimpleWarning("bad")) 300 err := diags.ErrWithWarnings() 301 if err == nil { 302 t.Errorf("got nil error; want NonFatalError") 303 return 304 } 305 if _, ok := err.(NonFatalError); !ok { 306 t.Errorf("got %T; want NonFatalError", err) 307 } 308 }) 309 t.Run("one error", func(t *testing.T) { 310 var diags Diagnostics 311 diags = diags.Append(errors.New("didn't work")) 312 err := diags.ErrWithWarnings() 313 if err == nil { 314 t.Fatalf("got nil error %#v; want non-nil", err) 315 } 316 if got, want := err.Error(), "didn't work"; got != want { 317 t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) 318 } 319 }) 320 t.Run("two errors", func(t *testing.T) { 321 var diags Diagnostics 322 diags = diags.Append(errors.New("didn't work")) 323 diags = diags.Append(errors.New("didn't work either")) 324 err := diags.ErrWithWarnings() 325 if err == nil { 326 t.Fatalf("got nil error %#v; want non-nil", err) 327 } 328 want := strings.TrimSpace(` 329 2 problems: 330 331 - didn't work 332 - didn't work either 333 `) 334 if got := err.Error(); got != want { 335 t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) 336 } 337 }) 338 t.Run("error and warning", func(t *testing.T) { 339 var diags Diagnostics 340 diags = diags.Append(errors.New("didn't work")) 341 diags = diags.Append(SimpleWarning("didn't work either")) 342 err := diags.ErrWithWarnings() 343 if err == nil { 344 t.Fatalf("got nil error %#v; want non-nil", err) 345 } 346 // Since this "as error" mode is just a fallback for 347 // non-diagnostics-aware situations like tests, we don't actually 348 // distinguish warnings and errors here since the point is to just 349 // get the messages rendered. User-facing code should be printing 350 // each diagnostic separately, so won't enter this codepath, 351 want := strings.TrimSpace(` 352 2 problems: 353 354 - didn't work 355 - didn't work either 356 `) 357 if got := err.Error(); got != want { 358 t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) 359 } 360 }) 361 } 362 363 func TestDiagnosticsNonFatalErr(t *testing.T) { 364 t.Run("empty", func(t *testing.T) { 365 var diags Diagnostics 366 err := diags.NonFatalErr() 367 if err != nil { 368 t.Errorf("got non-nil error %#v; want nil", err) 369 } 370 }) 371 t.Run("warning only", func(t *testing.T) { 372 var diags Diagnostics 373 diags = diags.Append(SimpleWarning("bad")) 374 err := diags.NonFatalErr() 375 if err == nil { 376 t.Errorf("got nil error; want NonFatalError") 377 return 378 } 379 if _, ok := err.(NonFatalError); !ok { 380 t.Errorf("got %T; want NonFatalError", err) 381 } 382 }) 383 t.Run("one error", func(t *testing.T) { 384 var diags Diagnostics 385 diags = diags.Append(errors.New("didn't work")) 386 err := diags.NonFatalErr() 387 if err == nil { 388 t.Fatalf("got nil error %#v; want non-nil", err) 389 } 390 if got, want := err.Error(), "didn't work"; got != want { 391 t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) 392 } 393 if _, ok := err.(NonFatalError); !ok { 394 t.Errorf("got %T; want NonFatalError", err) 395 } 396 }) 397 t.Run("two errors", func(t *testing.T) { 398 var diags Diagnostics 399 diags = diags.Append(errors.New("didn't work")) 400 diags = diags.Append(errors.New("didn't work either")) 401 err := diags.NonFatalErr() 402 if err == nil { 403 t.Fatalf("got nil error %#v; want non-nil", err) 404 } 405 want := strings.TrimSpace(` 406 2 problems: 407 408 - didn't work 409 - didn't work either 410 `) 411 if got := err.Error(); got != want { 412 t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) 413 } 414 if _, ok := err.(NonFatalError); !ok { 415 t.Errorf("got %T; want NonFatalError", err) 416 } 417 }) 418 t.Run("error and warning", func(t *testing.T) { 419 var diags Diagnostics 420 diags = diags.Append(errors.New("didn't work")) 421 diags = diags.Append(SimpleWarning("didn't work either")) 422 err := diags.NonFatalErr() 423 if err == nil { 424 t.Fatalf("got nil error %#v; want non-nil", err) 425 } 426 // Since this "as error" mode is just a fallback for 427 // non-diagnostics-aware situations like tests, we don't actually 428 // distinguish warnings and errors here since the point is to just 429 // get the messages rendered. User-facing code should be printing 430 // each diagnostic separately, so won't enter this codepath, 431 want := strings.TrimSpace(` 432 2 problems: 433 434 - didn't work 435 - didn't work either 436 `) 437 if got := err.Error(); got != want { 438 t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) 439 } 440 if _, ok := err.(NonFatalError); !ok { 441 t.Errorf("got %T; want NonFatalError", err) 442 } 443 }) 444 }