github.com/mailgun/holster/v4@v4.20.0/errors/format_test.go (about) 1 package errors 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "regexp" 8 "strings" 9 "testing" 10 ) 11 12 func TestFormatNew(t *testing.T) { 13 tests := []struct { 14 error 15 format string 16 want string 17 }{{ 18 New("error"), 19 "%s", 20 "error", 21 }, { 22 New("error"), 23 "%v", 24 "error", 25 }, { 26 New("error"), 27 "%+v", 28 "error\n" + 29 "github.com/mailgun/holster/v4/errors.TestFormatNew\n" + 30 "\t.+/errors/format_test.go:26", 31 }, { 32 New("error"), 33 "%q", 34 `"error"`, 35 }} 36 37 for i, tt := range tests { 38 testFormatRegexp(t, i, tt.error, tt.format, tt.want) 39 } 40 } 41 42 func TestFormatErrorf(t *testing.T) { 43 tests := []struct { 44 error 45 format string 46 want string 47 }{{ 48 Errorf("%s", "error"), 49 "%s", 50 "error", 51 }, { 52 Errorf("%s", "error"), 53 "%v", 54 "error", 55 }, { 56 Errorf("%s", "error"), 57 "%+v", 58 "error\n" + 59 "github.com/mailgun/holster/v4/errors.TestFormatErrorf\n" + 60 "\t.+/errors/format_test.go:56", 61 }} 62 63 for i, tt := range tests { 64 testFormatRegexp(t, i, tt.error, tt.format, tt.want) 65 } 66 } 67 68 func TestFormatWrap(t *testing.T) { 69 tests := []struct { 70 error 71 format string 72 want string 73 }{{ 74 Wrap(New("error"), "error2"), 75 "%s", 76 "error2: error", 77 }, { 78 Wrap(New("error"), "error2"), 79 "%v", 80 "error2: error", 81 }, { 82 Wrap(New("error"), "error2"), 83 "%+v", 84 "error\n" + 85 "github.com/mailgun/holster/v4/errors.TestFormatWrap\n" + 86 "\t.+/errors/format_test.go:82", 87 }, { 88 Wrap(io.EOF, "error"), 89 "%s", 90 "error: EOF", 91 }, { 92 Wrap(io.EOF, "error"), 93 "%v", 94 "error: EOF", 95 }, { 96 Wrap(io.EOF, "error"), 97 "%+v", 98 "EOF\n" + 99 "error\n" + 100 "github.com/mailgun/holster/v4/errors.TestFormatWrap\n" + 101 "\t.+/errors/format_test.go:96", 102 }, { 103 Wrap(Wrap(io.EOF, "error1"), "error2"), 104 "%+v", 105 "EOF\n" + 106 "error1\n" + 107 "github.com/mailgun/holster/v4/errors.TestFormatWrap\n" + 108 "\t.+/errors/format_test.go:103\n", 109 }, { 110 Wrap(New("error with space"), "context"), 111 "%q", 112 `"context: error with space"`, 113 }} 114 115 for i, tt := range tests { 116 testFormatRegexp(t, i, tt.error, tt.format, tt.want) 117 } 118 } 119 120 func TestFormatWrapf(t *testing.T) { 121 tests := []struct { 122 error 123 format string 124 want string 125 }{{ 126 Wrapf(io.EOF, "error%d", 2), 127 "%s", 128 "error2: EOF", 129 }, { 130 Wrapf(io.EOF, "error%d", 2), 131 "%v", 132 "error2: EOF", 133 }, { 134 Wrapf(io.EOF, "error%d", 2), 135 "%+v", 136 "EOF\n" + 137 "error2\n" + 138 "github.com/mailgun/holster/v4/errors.TestFormatWrapf\n" + 139 "\t.+/errors/format_test.go:134", 140 }, { 141 Wrapf(New("error"), "error%d", 2), 142 "%s", 143 "error2: error", 144 }, { 145 Wrapf(New("error"), "error%d", 2), 146 "%v", 147 "error2: error", 148 }, { 149 Wrapf(New("error"), "error%d", 2), 150 "%+v", 151 "error\n" + 152 "github.com/mailgun/holster/v4/errors.TestFormatWrapf\n" + 153 "\t.+/errors/format_test.go:149", 154 }} 155 156 for i, tt := range tests { 157 testFormatRegexp(t, i, tt.error, tt.format, tt.want) 158 } 159 } 160 161 func TestFormatWithStack(t *testing.T) { 162 tests := []struct { 163 error 164 format string 165 want []string 166 }{{ 167 WithStack(io.EOF), 168 "%s", 169 []string{"EOF"}, 170 }, { 171 WithStack(io.EOF), 172 "%v", 173 []string{"EOF"}, 174 }, { 175 WithStack(io.EOF), 176 "%+v", 177 []string{"EOF", 178 "github.com/mailgun/holster/v4/errors.TestFormatWithStack\n" + 179 "\t.+/errors/format_test.go:175"}, 180 }, { 181 WithStack(New("error")), 182 "%s", 183 []string{"error"}, 184 }, { 185 WithStack(New("error")), 186 "%v", 187 []string{"error"}, 188 }, { 189 WithStack(New("error")), 190 "%+v", 191 []string{"error", 192 "github.com/mailgun/holster/v4/errors.TestFormatWithStack\n" + 193 "\t.+/errors/format_test.go:189", 194 "github.com/mailgun/holster/v4/errors.TestFormatWithStack\n" + 195 "\t.+/errors/format_test.go:189"}, 196 }, { 197 WithStack(WithStack(io.EOF)), 198 "%+v", 199 []string{"EOF", 200 "github.com/mailgun/holster/v4/errors.TestFormatWithStack\n" + 201 "\t.+/errors/format_test.go:197", 202 "github.com/mailgun/holster/v4/errors.TestFormatWithStack\n" + 203 "\t.+/errors/format_test.go:197"}, 204 }, { 205 WithStack(WithStack(Wrapf(io.EOF, "message"))), 206 "%+v", 207 []string{"EOF", 208 "message", 209 "github.com/mailgun/holster/v4/errors.TestFormatWithStack\n" + 210 "\t.+/errors/format_test.go:205", 211 "github.com/mailgun/holster/v4/errors.TestFormatWithStack\n" + 212 "\t.+/errors/format_test.go:205", 213 "github.com/mailgun/holster/v4/errors.TestFormatWithStack\n" + 214 "\t.+/errors/format_test.go:205"}, 215 }, { 216 WithStack(Errorf("error%d", 1)), 217 "%+v", 218 []string{"error1", 219 "github.com/mailgun/holster/v4/errors.TestFormatWithStack\n" + 220 "\t.+/errors/format_test.go:216", 221 "github.com/mailgun/holster/v4/errors.TestFormatWithStack\n" + 222 "\t.+/errors/format_test.go:216"}, 223 }} 224 225 for i, tt := range tests { 226 testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) 227 } 228 } 229 230 func TestFormatWithMessage(t *testing.T) { 231 tests := []struct { 232 error 233 format string 234 want []string 235 }{{ 236 WithMessage(New("error"), "error2"), 237 "%s", 238 []string{"error2: error"}, 239 }, { 240 WithMessage(New("error"), "error2"), 241 "%v", 242 []string{"error2: error"}, 243 }, { 244 WithMessage(New("error"), "error2"), 245 "%+v", 246 []string{ 247 "error", 248 "github.com/mailgun/holster/v4/errors.TestFormatWithMessage\n" + 249 "\t.+/errors/format_test.go:244", 250 "error2"}, 251 }, { 252 WithMessage(io.EOF, "addition1"), 253 "%s", 254 []string{"addition1: EOF"}, 255 }, { 256 WithMessage(io.EOF, "addition1"), 257 "%v", 258 []string{"addition1: EOF"}, 259 }, { 260 WithMessage(io.EOF, "addition1"), 261 "%+v", 262 []string{"EOF", "addition1"}, 263 }, { 264 WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), 265 "%v", 266 []string{"addition2: addition1: EOF"}, 267 }, { 268 WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), 269 "%+v", 270 []string{"EOF", "addition1", "addition2"}, 271 }, { 272 Wrap(WithMessage(io.EOF, "error1"), "error2"), 273 "%+v", 274 []string{"EOF", "error1", "error2", 275 "github.com/mailgun/holster/v4/errors.TestFormatWithMessage\n" + 276 "\t.+/errors/format_test.go:272"}, 277 }, { 278 WithMessage(Errorf("error%d", 1), "error2"), 279 "%+v", 280 []string{"error1", 281 "github.com/mailgun/holster/v4/errors.TestFormatWithMessage\n" + 282 "\t.+/errors/format_test.go:278", 283 "error2"}, 284 }, { 285 WithMessage(WithStack(io.EOF), "error"), 286 "%+v", 287 []string{ 288 "EOF", 289 "github.com/mailgun/holster/v4/errors.TestFormatWithMessage\n" + 290 "\t.+/errors/format_test.go:285", 291 "error"}, 292 }, { 293 WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"), 294 "%+v", 295 []string{ 296 "EOF", 297 "github.com/mailgun/holster/v4/errors.TestFormatWithMessage\n" + 298 "\t.+/errors/format_test.go:293", 299 "inside-error", 300 "github.com/mailgun/holster/v4/errors.TestFormatWithMessage\n" + 301 "\t.+/errors/format_test.go:293", 302 "outside-error"}, 303 }} 304 305 for i, tt := range tests { 306 testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) 307 } 308 } 309 310 func TestFormatGeneric(t *testing.T) { 311 t.Skip("TODO: fix https://github.com/mailgun/holster/issues/153") 312 313 starts := []struct { 314 err error 315 want []string 316 }{ 317 {New("new-error"), []string{ 318 "new-error", 319 "github.com/mailgun/holster/v4/errors.TestFormatGeneric\n" + 320 "\t.+/errors/format_test.go:315"}, 321 }, {Errorf("errorf-error"), []string{ 322 "errorf-error", 323 "github.com/mailgun/holster/v4/errors.TestFormatGeneric\n" + 324 "\t.+/errors/format_test.go:319"}, 325 }, {errors.New("errors-new-error"), []string{ 326 "errors-new-error"}, 327 }, 328 } 329 330 wrappers := []wrapper{ 331 { 332 func(err error) error { return WithMessage(err, "with-message") }, 333 []string{"with-message"}, 334 }, { 335 WithStack, 336 []string{ 337 "github.com/mailgun/holster/v4/errors.(func·002|TestFormatGeneric.func2)\n\t" + 338 ".+/errors/format_test.go:333", 339 }, 340 }, { 341 func(err error) error { return Wrap(err, "wrap-error") }, 342 []string{ 343 "wrap-error", 344 "github.com/mailgun/holster/v4/errors.(func·003|TestFormatGeneric.func3)\n\t" + 345 ".+/errors/format_test.go:339", 346 }, 347 }, { 348 func(err error) error { return Wrapf(err, "wrapf-error%d", 1) }, 349 []string{ 350 "wrapf-error1", 351 "github.com/mailgun/holster/v4/errors.(func·004|TestFormatGeneric.func4)\n\t" + 352 ".+/errors/format_test.go:346", 353 }, 354 }, 355 } 356 357 for i := range starts { 358 err := starts[i].err 359 want := starts[i].want 360 testFormatCompleteCompare(t, i, err, "%+v", want, false) 361 testGenericRecursive(t, err, want, wrappers, 3) 362 } 363 } 364 365 func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) { 366 got := fmt.Sprintf(format, arg) 367 gotLines := strings.Split(got, "\n") 368 wantLines := strings.Split(want, "\n") 369 370 if len(wantLines) > len(gotLines) { 371 t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want) 372 return 373 } 374 375 for i, w := range wantLines { 376 match, err := regexp.MatchString(w, gotLines[i]) 377 if err != nil { 378 t.Fatal(err) 379 } 380 if !match { 381 t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want) 382 } 383 } 384 } 385 386 var stackLineR = regexp.MustCompile(`\.`) 387 388 // parseBlocks parses input into a slice, where: 389 // - incase entry contains a newline, its a stacktrace 390 // - incase entry contains no newline, its a solo line. 391 // 392 // Detecting stack boundaries only works incase the WithStack-calls are 393 // to be found on the same line, thats why it is optionally here. 394 // 395 // Example use: 396 // 397 // for _, e := range blocks { 398 // if strings.ContainsAny(e, "\n") { 399 // // Match as stack 400 // } else { 401 // // Match as line 402 // } 403 // } 404 func parseBlocks(input string, detectStackboundaries bool) ([]string, error) { 405 var blocks []string 406 407 stack := "" 408 wasStack := false 409 lines := map[string]bool{} // already found lines 410 411 for _, l := range strings.Split(input, "\n") { 412 isStackLine := stackLineR.MatchString(l) 413 414 switch { 415 case !isStackLine && wasStack: 416 blocks = append(blocks, stack, l) 417 stack = "" 418 lines = map[string]bool{} 419 case isStackLine: 420 if wasStack { 421 // Detecting two stacks after another, possible cause lines match in 422 // our tests due to WithStack(WithStack(io.EOF)) on same line. 423 if detectStackboundaries { 424 if lines[l] { 425 if stack == "" { 426 return nil, errors.New("len of block must not be zero here") 427 } 428 429 blocks = append(blocks, stack) 430 stack = l 431 lines = map[string]bool{l: true} 432 continue 433 } 434 } 435 436 stack = stack + "\n" + l 437 } else { 438 stack = l 439 } 440 lines[l] = true 441 case !isStackLine && !wasStack: 442 blocks = append(blocks, l) 443 default: 444 return nil, errors.New("must not happen") 445 } 446 447 wasStack = isStackLine 448 } 449 450 // Use up stack 451 if stack != "" { 452 blocks = append(blocks, stack) 453 } 454 return blocks, nil 455 } 456 457 func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) { 458 gotStr := fmt.Sprintf(format, arg) 459 460 got, err := parseBlocks(gotStr, detectStackBoundaries) 461 if err != nil { 462 t.Fatal(err) 463 } 464 465 if len(got) != len(want) { 466 t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q", 467 n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr) 468 } 469 470 for i := range got { 471 if strings.ContainsAny(want[i], "\n") { 472 // Match as stack 473 match, err := regexp.MatchString(want[i], got[i]) 474 if err != nil { 475 t.Fatal(err) 476 } 477 if !match { 478 t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n", 479 n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want)) 480 } 481 } else if got[i] != want[i] { 482 // Match as message 483 t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i]) 484 } 485 } 486 } 487 488 type wrapper struct { 489 wrap func(err error) error 490 want []string 491 } 492 493 func prettyBlocks(blocks []string, prefix ...string) string { 494 return " " + strings.Join(blocks, "\n ") 495 } 496 497 func testGenericRecursive(t *testing.T, beforeErr error, beforeWant []string, list []wrapper, maxDepth int) { 498 if len(beforeWant) == 0 { 499 panic("beforeWant must not be empty") 500 } 501 for _, w := range list { 502 if len(w.want) == 0 { 503 panic("want must not be empty") 504 } 505 506 err := w.wrap(beforeErr) 507 508 // Copy required cause append(beforeWant, ..) modified beforeWant subtly. 509 beforeCopy := make([]string, len(beforeWant)) 510 copy(beforeCopy, beforeWant) 511 512 beforeWant := beforeCopy 513 last := len(beforeWant) - 1 514 var want []string 515 516 // Merge two stacks behind each other. 517 if strings.ContainsAny(beforeWant[last], "\n") && strings.ContainsAny(w.want[0], "\n") { 518 want = append(beforeWant[:last], append([]string{beforeWant[last] + "((?s).*)" + w.want[0]}, w.want[1:]...)...) 519 } else { 520 want = append(beforeWant, w.want...) 521 } 522 523 testFormatCompleteCompare(t, maxDepth, err, "%+v", want, false) 524 if maxDepth > 0 { 525 testGenericRecursive(t, err, want, list, maxDepth-1) 526 } 527 } 528 }