1// Copyright 2018 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package xerrors_test 6 7import ( 8 "fmt" 9 "io" 10 "os" 11 "path" 12 "reflect" 13 "regexp" 14 "strconv" 15 "strings" 16 "testing" 17 18 "golang.org/x/xerrors" 19) 20 21func TestErrorf(t *testing.T) { 22 chained := &wrapped{"chained", nil} 23 chain := func(s ...string) (a []string) { 24 for _, s := range s { 25 a = append(a, cleanPath(s)) 26 } 27 return a 28 } 29 testCases := []struct { 30 got error 31 want []string 32 }{{ 33 xerrors.Errorf("no args"), 34 chain("no args/path.TestErrorf/path.go:xxx"), 35 }, { 36 xerrors.Errorf("no args: %s"), 37 chain("no args: %!s(MISSING)/path.TestErrorf/path.go:xxx"), 38 }, { 39 xerrors.Errorf("nounwrap: %s", "simple"), 40 chain(`nounwrap: simple/path.TestErrorf/path.go:xxx`), 41 }, { 42 xerrors.Errorf("nounwrap: %v", "simple"), 43 chain(`nounwrap: simple/path.TestErrorf/path.go:xxx`), 44 }, { 45 xerrors.Errorf("%s failed: %v", "foo", chained), 46 chain("foo failed/path.TestErrorf/path.go:xxx", 47 "chained/somefile.go:xxx"), 48 }, { 49 xerrors.Errorf("no wrap: %s", chained), 50 chain("no wrap/path.TestErrorf/path.go:xxx", 51 "chained/somefile.go:xxx"), 52 }, { 53 xerrors.Errorf("%s failed: %w", "foo", chained), 54 chain("wraps:foo failed/path.TestErrorf/path.go:xxx", 55 "chained/somefile.go:xxx"), 56 }, { 57 xerrors.Errorf("nowrapv: %v", chained), 58 chain("nowrapv/path.TestErrorf/path.go:xxx", 59 "chained/somefile.go:xxx"), 60 }, { 61 xerrors.Errorf("wrapw: %w", chained), 62 chain("wraps:wrapw/path.TestErrorf/path.go:xxx", 63 "chained/somefile.go:xxx"), 64 }, { 65 xerrors.Errorf("wrapw %w middle", chained), 66 chain("wraps:wrapw chained middle/path.TestErrorf/path.go:xxx", 67 "chained/somefile.go:xxx"), 68 }, { 69 xerrors.Errorf("not wrapped: %+v", chained), 70 chain("not wrapped: chained: somefile.go:123/path.TestErrorf/path.go:xxx"), 71 }} 72 for i, tc := range testCases { 73 t.Run(strconv.Itoa(i)+"/"+path.Join(tc.want...), func(t *testing.T) { 74 got := errToParts(tc.got) 75 if !reflect.DeepEqual(got, tc.want) { 76 t.Errorf("Format:\n got: %#v\nwant: %#v", got, tc.want) 77 } 78 79 gotStr := tc.got.Error() 80 wantStr := fmt.Sprint(tc.got) 81 if gotStr != wantStr { 82 t.Errorf("Error:\n got: %#v\nwant: %#v", got, tc.want) 83 } 84 }) 85 } 86} 87 88func TestErrorFormatter(t *testing.T) { 89 var ( 90 simple = &wrapped{"simple", nil} 91 elephant = &wrapped{ 92 "can't adumbrate elephant", 93 detailed{}, 94 } 95 nonascii = &wrapped{"café", nil} 96 newline = &wrapped{"msg with\nnewline", 97 &wrapped{"and another\none", nil}} 98 fallback = &wrapped{"fallback", os.ErrNotExist} 99 oldAndNew = &wrapped{"new style", formatError("old style")} 100 framed = &withFrameAndMore{ 101 frame: xerrors.Caller(0), 102 } 103 opaque = &wrapped{"outer", 104 xerrors.Opaque(&wrapped{"mid", 105 &wrapped{"inner", nil}})} 106 ) 107 testCases := []struct { 108 err error 109 fmt string 110 want string 111 regexp bool 112 }{{ 113 err: simple, 114 fmt: "%s", 115 want: "simple", 116 }, { 117 err: elephant, 118 fmt: "%s", 119 want: "can't adumbrate elephant: out of peanuts", 120 }, { 121 err: &wrapped{"a", &wrapped{"b", &wrapped{"c", nil}}}, 122 fmt: "%s", 123 want: "a: b: c", 124 }, { 125 err: simple, 126 fmt: "%+v", 127 want: "simple:" + 128 "\n somefile.go:123", 129 }, { 130 err: elephant, 131 fmt: "%+v", 132 want: "can't adumbrate elephant:" + 133 "\n somefile.go:123" + 134 "\n - out of peanuts:" + 135 "\n the elephant is on strike" + 136 "\n and the 12 monkeys" + 137 "\n are laughing", 138 }, { 139 err: &oneNewline{nil}, 140 fmt: "%+v", 141 want: "123", 142 }, { 143 err: &oneNewline{&oneNewline{nil}}, 144 fmt: "%+v", 145 want: "123:" + 146 "\n - 123", 147 }, { 148 err: &newlineAtEnd{nil}, 149 fmt: "%+v", 150 want: "newlineAtEnd:\n detail", 151 }, { 152 err: &newlineAtEnd{&newlineAtEnd{nil}}, 153 fmt: "%+v", 154 want: "newlineAtEnd:" + 155 "\n detail" + 156 "\n - newlineAtEnd:" + 157 "\n detail", 158 }, { 159 err: framed, 160 fmt: "%+v", 161 want: "something:" + 162 "\n golang.org/x/xerrors_test.TestErrorFormatter" + 163 "\n .+/fmt_test.go:101" + 164 "\n something more", 165 regexp: true, 166 }, { 167 err: fmtTwice("Hello World!"), 168 fmt: "%#v", 169 want: "2 times Hello World!", 170 }, { 171 err: fallback, 172 fmt: "%s", 173 want: "fallback: file does not exist", 174 }, { 175 err: fallback, 176 fmt: "%+v", 177 // Note: no colon after the last error, as there are no details. 178 want: "fallback:" + 179 "\n somefile.go:123" + 180 "\n - file does not exist", 181 }, { 182 err: opaque, 183 fmt: "%s", 184 want: "outer: mid: inner", 185 }, { 186 err: opaque, 187 fmt: "%+v", 188 want: "outer:" + 189 "\n somefile.go:123" + 190 "\n - mid:" + 191 "\n somefile.go:123" + 192 "\n - inner:" + 193 "\n somefile.go:123", 194 }, { 195 err: oldAndNew, 196 fmt: "%v", 197 want: "new style: old style", 198 }, { 199 err: oldAndNew, 200 fmt: "%q", 201 want: `"new style: old style"`, 202 }, { 203 err: oldAndNew, 204 fmt: "%+v", 205 // Note the extra indentation. 206 // Colon for old style error is rendered by the fmt.Formatter 207 // implementation of the old-style error. 208 want: "new style:" + 209 "\n somefile.go:123" + 210 "\n - old style:" + 211 "\n otherfile.go:456", 212 }, { 213 err: simple, 214 fmt: "%-12s", 215 want: "simple ", 216 }, { 217 // Don't use formatting flags for detailed view. 218 err: simple, 219 fmt: "%+12v", 220 want: "simple:" + 221 "\n somefile.go:123", 222 }, { 223 err: elephant, 224 fmt: "%+50s", 225 want: " can't adumbrate elephant: out of peanuts", 226 }, { 227 err: nonascii, 228 fmt: "%q", 229 want: `"café"`, 230 }, { 231 err: nonascii, 232 fmt: "%+q", 233 want: `"caf\u00e9"`, 234 }, { 235 err: simple, 236 fmt: "% x", 237 want: "73 69 6d 70 6c 65", 238 }, { 239 err: newline, 240 fmt: "%s", 241 want: "msg with" + 242 "\nnewline: and another" + 243 "\none", 244 }, { 245 err: newline, 246 fmt: "%+v", 247 want: "msg with" + 248 "\n newline:" + 249 "\n somefile.go:123" + 250 "\n - and another" + 251 "\n one:" + 252 "\n somefile.go:123", 253 }, { 254 err: &wrapped{"", &wrapped{"inner message", nil}}, 255 fmt: "%+v", 256 want: "somefile.go:123" + 257 "\n - inner message:" + 258 "\n somefile.go:123", 259 }, { 260 err: spurious(""), 261 fmt: "%s", 262 want: "spurious", 263 }, { 264 err: spurious(""), 265 fmt: "%+v", 266 want: "spurious", 267 }, { 268 err: spurious("extra"), 269 fmt: "%s", 270 want: "spurious", 271 }, { 272 err: spurious("extra"), 273 fmt: "%+v", 274 want: "spurious:\n" + 275 " extra", 276 }, { 277 err: nil, 278 fmt: "%+v", 279 want: "<nil>", 280 }, { 281 err: (*wrapped)(nil), 282 fmt: "%+v", 283 want: "<nil>", 284 }, { 285 err: simple, 286 fmt: "%T", 287 want: "*xerrors_test.wrapped", 288 }, { 289 err: simple, 290 fmt: "%", 291 want: "%!(*xerrors_test.wrapped)", 292 // For 1.13: 293 // want: "%!(*xerrors_test.wrapped=&{simple <nil>})", 294 }, { 295 err: formatError("use fmt.Formatter"), 296 fmt: "%#v", 297 want: "use fmt.Formatter", 298 }, { 299 err: fmtTwice("%s %s", "ok", panicValue{}), 300 fmt: "%s", 301 // Different Go versions produce different results. 302 want: `ok %!s\(PANIC=(String method: )?panic\)/ok %!s\(PANIC=(String method: )?panic\)`, 303 regexp: true, 304 }, { 305 err: fmtTwice("%o %s", panicValue{}, "ok"), 306 fmt: "%s", 307 want: "{} ok/{} ok", 308 }, { 309 err: adapted{"adapted", nil}, 310 fmt: "%+v", 311 want: "adapted:" + 312 "\n detail", 313 }, { 314 err: adapted{"outer", adapted{"mid", adapted{"inner", nil}}}, 315 fmt: "%+v", 316 want: "outer:" + 317 "\n detail" + 318 "\n - mid:" + 319 "\n detail" + 320 "\n - inner:" + 321 "\n detail", 322 }} 323 for i, tc := range testCases { 324 t.Run(fmt.Sprintf("%d/%s", i, tc.fmt), func(t *testing.T) { 325 got := fmt.Sprintf(tc.fmt, tc.err) 326 var ok bool 327 if tc.regexp { 328 var err error 329 ok, err = regexp.MatchString(tc.want+"$", got) 330 if err != nil { 331 t.Fatal(err) 332 } 333 } else { 334 ok = got == tc.want 335 } 336 if !ok { 337 t.Errorf("\n got: %q\nwant: %q", got, tc.want) 338 } 339 }) 340 } 341} 342 343func TestAdaptor(t *testing.T) { 344 testCases := []struct { 345 err error 346 fmt string 347 want string 348 regexp bool 349 }{{ 350 err: adapted{"adapted", nil}, 351 fmt: "%+v", 352 want: "adapted:" + 353 "\n detail", 354 }, { 355 err: adapted{"outer", adapted{"mid", adapted{"inner", nil}}}, 356 fmt: "%+v", 357 want: "outer:" + 358 "\n detail" + 359 "\n - mid:" + 360 "\n detail" + 361 "\n - inner:" + 362 "\n detail", 363 }} 364 for i, tc := range testCases { 365 t.Run(fmt.Sprintf("%d/%s", i, tc.fmt), func(t *testing.T) { 366 got := fmt.Sprintf(tc.fmt, tc.err) 367 if got != tc.want { 368 t.Errorf("\n got: %q\nwant: %q", got, tc.want) 369 } 370 }) 371 } 372} 373 374var _ xerrors.Formatter = wrapped{} 375 376type wrapped struct { 377 msg string 378 err error 379} 380 381func (e wrapped) Error() string { return "should call Format" } 382 383func (e wrapped) Format(s fmt.State, verb rune) { 384 xerrors.FormatError(&e, s, verb) 385} 386 387func (e wrapped) FormatError(p xerrors.Printer) (next error) { 388 p.Print(e.msg) 389 p.Detail() 390 p.Print("somefile.go:123") 391 return e.err 392} 393 394var _ xerrors.Formatter = detailed{} 395 396type detailed struct{} 397 398func (e detailed) Error() string { return fmt.Sprint(e) } 399 400func (detailed) FormatError(p xerrors.Printer) (next error) { 401 p.Printf("out of %s", "peanuts") 402 p.Detail() 403 p.Print("the elephant is on strike\n") 404 p.Printf("and the %d monkeys\nare laughing", 12) 405 return nil 406} 407 408type withFrameAndMore struct { 409 frame xerrors.Frame 410} 411 412func (e *withFrameAndMore) Error() string { return fmt.Sprint(e) } 413 414func (e *withFrameAndMore) Format(s fmt.State, v rune) { 415 xerrors.FormatError(e, s, v) 416} 417 418func (e *withFrameAndMore) FormatError(p xerrors.Printer) (next error) { 419 p.Print("something") 420 if p.Detail() { 421 e.frame.Format(p) 422 p.Print("something more") 423 } 424 return nil 425} 426 427type spurious string 428 429func (e spurious) Error() string { return fmt.Sprint(e) } 430 431// move to 1_12 test file 432func (e spurious) Format(s fmt.State, verb rune) { 433 xerrors.FormatError(e, s, verb) 434} 435 436func (e spurious) FormatError(p xerrors.Printer) (next error) { 437 p.Print("spurious") 438 p.Detail() // Call detail even if we don't print anything 439 if e == "" { 440 p.Print() 441 } else { 442 p.Print("\n", string(e)) // print extraneous leading newline 443 } 444 return nil 445} 446 447type oneNewline struct { 448 next error 449} 450 451func (e *oneNewline) Error() string { return fmt.Sprint(e) } 452 453func (e *oneNewline) Format(s fmt.State, verb rune) { 454 xerrors.FormatError(e, s, verb) 455} 456 457func (e *oneNewline) FormatError(p xerrors.Printer) (next error) { 458 p.Print("1") 459 p.Print("2") 460 p.Print("3") 461 p.Detail() 462 p.Print("\n") 463 return e.next 464} 465 466type newlineAtEnd struct { 467 next error 468} 469 470func (e *newlineAtEnd) Error() string { return fmt.Sprint(e) } 471 472func (e *newlineAtEnd) Format(s fmt.State, verb rune) { 473 xerrors.FormatError(e, s, verb) 474} 475 476func (e *newlineAtEnd) FormatError(p xerrors.Printer) (next error) { 477 p.Print("newlineAtEnd") 478 p.Detail() 479 p.Print("detail\n") 480 return e.next 481} 482 483type adapted struct { 484 msg string 485 err error 486} 487 488func (e adapted) Error() string { return string(e.msg) } 489 490func (e adapted) Format(s fmt.State, verb rune) { 491 xerrors.FormatError(e, s, verb) 492} 493 494func (e adapted) FormatError(p xerrors.Printer) error { 495 p.Print(e.msg) 496 p.Detail() 497 p.Print("detail") 498 return e.err 499} 500 501// formatError is an error implementing Format instead of xerrors.Formatter. 502// The implementation mimics the implementation of github.com/pkg/errors. 503type formatError string 504 505func (e formatError) Error() string { return string(e) } 506 507func (e formatError) Format(s fmt.State, verb rune) { 508 // Body based on pkg/errors/errors.go 509 switch verb { 510 case 'v': 511 if s.Flag('+') { 512 io.WriteString(s, string(e)) 513 fmt.Fprintf(s, ":\n%s", "otherfile.go:456") 514 return 515 } 516 fallthrough 517 case 's': 518 io.WriteString(s, string(e)) 519 case 'q': 520 fmt.Fprintf(s, "%q", string(e)) 521 } 522} 523 524func (e formatError) GoString() string { 525 panic("should never be called") 526} 527 528type fmtTwiceErr struct { 529 format string 530 args []interface{} 531} 532 533func fmtTwice(format string, a ...interface{}) error { 534 return fmtTwiceErr{format, a} 535} 536 537func (e fmtTwiceErr) Error() string { return fmt.Sprint(e) } 538 539func (e fmtTwiceErr) Format(s fmt.State, verb rune) { 540 xerrors.FormatError(e, s, verb) 541} 542 543func (e fmtTwiceErr) FormatError(p xerrors.Printer) (next error) { 544 p.Printf(e.format, e.args...) 545 p.Print("/") 546 p.Printf(e.format, e.args...) 547 return nil 548} 549 550func (e fmtTwiceErr) GoString() string { 551 return "2 times " + fmt.Sprintf(e.format, e.args...) 552} 553 554type panicValue struct{} 555 556func (panicValue) String() string { panic("panic") } 557 558var rePath = regexp.MustCompile(`( [^ ]*)xerrors.*test\.`) 559var reLine = regexp.MustCompile(":[0-9]*\n?$") 560 561func cleanPath(s string) string { 562 s = rePath.ReplaceAllString(s, "/path.") 563 s = reLine.ReplaceAllString(s, ":xxx") 564 s = strings.Replace(s, "\n ", "", -1) 565 s = strings.Replace(s, " /", "/", -1) 566 return s 567} 568 569func errToParts(err error) (a []string) { 570 for err != nil { 571 var p testPrinter 572 if xerrors.Unwrap(err) != nil { 573 p.str += "wraps:" 574 } 575 f, ok := err.(xerrors.Formatter) 576 if !ok { 577 a = append(a, err.Error()) 578 break 579 } 580 err = f.FormatError(&p) 581 a = append(a, cleanPath(p.str)) 582 } 583 return a 584 585} 586 587type testPrinter struct { 588 str string 589} 590 591func (p *testPrinter) Print(a ...interface{}) { 592 p.str += fmt.Sprint(a...) 593} 594 595func (p *testPrinter) Printf(format string, a ...interface{}) { 596 p.str += fmt.Sprintf(format, a...) 597} 598 599func (p *testPrinter) Detail() bool { 600 p.str += " /" 601 return true 602} 603