| // Copyright 2018 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package xerrors_test |
| |
| import ( |
| "fmt" |
| "io" |
| "os" |
| "path" |
| "reflect" |
| "regexp" |
| "strconv" |
| "strings" |
| "testing" |
| |
| "golang.org/x/xerrors" |
| ) |
| |
| func TestErrorf(t *testing.T) { |
| chained := &wrapped{"chained", nil} |
| chain := func(s ...string) (a []string) { |
| for _, s := range s { |
| a = append(a, cleanPath(s)) |
| } |
| return a |
| } |
| testCases := []struct { |
| got error |
| want []string |
| }{{ |
| xerrors.Errorf("no args"), |
| chain("no args/path.TestErrorf/path.go:xxx"), |
| }, { |
| xerrors.Errorf("no args: %s"), |
| chain("no args: %!s(MISSING)/path.TestErrorf/path.go:xxx"), |
| }, { |
| xerrors.Errorf("nounwrap: %s", "simple"), |
| chain(`nounwrap: simple/path.TestErrorf/path.go:xxx`), |
| }, { |
| xerrors.Errorf("nounwrap: %v", "simple"), |
| chain(`nounwrap: simple/path.TestErrorf/path.go:xxx`), |
| }, { |
| xerrors.Errorf("%s failed: %v", "foo", chained), |
| chain("foo failed/path.TestErrorf/path.go:xxx", |
| "chained/somefile.go:xxx"), |
| }, { |
| xerrors.Errorf("no wrap: %s", chained), |
| chain("no wrap/path.TestErrorf/path.go:xxx", |
| "chained/somefile.go:xxx"), |
| }, { |
| xerrors.Errorf("%s failed: %w", "foo", chained), |
| chain("wraps:foo failed/path.TestErrorf/path.go:xxx", |
| "chained/somefile.go:xxx"), |
| }, { |
| xerrors.Errorf("nowrapv: %v", chained), |
| chain("nowrapv/path.TestErrorf/path.go:xxx", |
| "chained/somefile.go:xxx"), |
| }, { |
| xerrors.Errorf("wrapw: %w", chained), |
| chain("wraps:wrapw/path.TestErrorf/path.go:xxx", |
| "chained/somefile.go:xxx"), |
| }, { |
| xerrors.Errorf("wrapw %w middle", chained), |
| chain("wraps:wrapw chained middle/path.TestErrorf/path.go:xxx", |
| "chained/somefile.go:xxx"), |
| }, { |
| xerrors.Errorf("not wrapped: %+v", chained), |
| chain("not wrapped: chained: somefile.go:123/path.TestErrorf/path.go:xxx"), |
| }} |
| for i, tc := range testCases { |
| t.Run(strconv.Itoa(i)+"/"+path.Join(tc.want...), func(t *testing.T) { |
| got := errToParts(tc.got) |
| if !reflect.DeepEqual(got, tc.want) { |
| t.Errorf("Format:\n got: %#v\nwant: %#v", got, tc.want) |
| } |
| |
| gotStr := tc.got.Error() |
| wantStr := fmt.Sprint(tc.got) |
| if gotStr != wantStr { |
| t.Errorf("Error:\n got: %#v\nwant: %#v", got, tc.want) |
| } |
| }) |
| } |
| } |
| |
| func TestErrorFormatter(t *testing.T) { |
| var ( |
| simple = &wrapped{"simple", nil} |
| elephant = &wrapped{ |
| "can't adumbrate elephant", |
| detailed{}, |
| } |
| nonascii = &wrapped{"café", nil} |
| newline = &wrapped{"msg with\nnewline", |
| &wrapped{"and another\none", nil}} |
| fallback = &wrapped{"fallback", os.ErrNotExist} |
| oldAndNew = &wrapped{"new style", formatError("old style")} |
| framed = &withFrameAndMore{ |
| frame: xerrors.Caller(0), |
| } |
| opaque = &wrapped{"outer", |
| xerrors.Opaque(&wrapped{"mid", |
| &wrapped{"inner", nil}})} |
| ) |
| testCases := []struct { |
| err error |
| fmt string |
| want string |
| regexp bool |
| }{{ |
| err: simple, |
| fmt: "%s", |
| want: "simple", |
| }, { |
| err: elephant, |
| fmt: "%s", |
| want: "can't adumbrate elephant: out of peanuts", |
| }, { |
| err: &wrapped{"a", &wrapped{"b", &wrapped{"c", nil}}}, |
| fmt: "%s", |
| want: "a: b: c", |
| }, { |
| err: simple, |
| fmt: "%+v", |
| want: "simple:" + |
| "\n somefile.go:123", |
| }, { |
| err: elephant, |
| fmt: "%+v", |
| want: "can't adumbrate elephant:" + |
| "\n somefile.go:123" + |
| "\n - out of peanuts:" + |
| "\n the elephant is on strike" + |
| "\n and the 12 monkeys" + |
| "\n are laughing", |
| }, { |
| err: &oneNewline{nil}, |
| fmt: "%+v", |
| want: "123", |
| }, { |
| err: &oneNewline{&oneNewline{nil}}, |
| fmt: "%+v", |
| want: "123:" + |
| "\n - 123", |
| }, { |
| err: &newlineAtEnd{nil}, |
| fmt: "%+v", |
| want: "newlineAtEnd:\n detail", |
| }, { |
| err: &newlineAtEnd{&newlineAtEnd{nil}}, |
| fmt: "%+v", |
| want: "newlineAtEnd:" + |
| "\n detail" + |
| "\n - newlineAtEnd:" + |
| "\n detail", |
| }, { |
| err: framed, |
| fmt: "%+v", |
| want: "something:" + |
| "\n golang.org/x/xerrors_test.TestErrorFormatter" + |
| "\n .+/fmt_test.go:101" + |
| "\n something more", |
| regexp: true, |
| }, { |
| err: fmtTwice("Hello World!"), |
| fmt: "%#v", |
| want: "2 times Hello World!", |
| }, { |
| err: fallback, |
| fmt: "%s", |
| want: "fallback: file does not exist", |
| }, { |
| err: fallback, |
| fmt: "%+v", |
| // Note: no colon after the last error, as there are no details. |
| want: "fallback:" + |
| "\n somefile.go:123" + |
| "\n - file does not exist", |
| }, { |
| err: opaque, |
| fmt: "%s", |
| want: "outer: mid: inner", |
| }, { |
| err: opaque, |
| fmt: "%+v", |
| want: "outer:" + |
| "\n somefile.go:123" + |
| "\n - mid:" + |
| "\n somefile.go:123" + |
| "\n - inner:" + |
| "\n somefile.go:123", |
| }, { |
| err: oldAndNew, |
| fmt: "%v", |
| want: "new style: old style", |
| }, { |
| err: oldAndNew, |
| fmt: "%q", |
| want: `"new style: old style"`, |
| }, { |
| err: oldAndNew, |
| fmt: "%+v", |
| // Note the extra indentation. |
| // Colon for old style error is rendered by the fmt.Formatter |
| // implementation of the old-style error. |
| want: "new style:" + |
| "\n somefile.go:123" + |
| "\n - old style:" + |
| "\n otherfile.go:456", |
| }, { |
| err: simple, |
| fmt: "%-12s", |
| want: "simple ", |
| }, { |
| // Don't use formatting flags for detailed view. |
| err: simple, |
| fmt: "%+12v", |
| want: "simple:" + |
| "\n somefile.go:123", |
| }, { |
| err: elephant, |
| fmt: "%+50s", |
| want: " can't adumbrate elephant: out of peanuts", |
| }, { |
| err: nonascii, |
| fmt: "%q", |
| want: `"café"`, |
| }, { |
| err: nonascii, |
| fmt: "%+q", |
| want: `"caf\u00e9"`, |
| }, { |
| err: simple, |
| fmt: "% x", |
| want: "73 69 6d 70 6c 65", |
| }, { |
| err: newline, |
| fmt: "%s", |
| want: "msg with" + |
| "\nnewline: and another" + |
| "\none", |
| }, { |
| err: newline, |
| fmt: "%+v", |
| want: "msg with" + |
| "\n newline:" + |
| "\n somefile.go:123" + |
| "\n - and another" + |
| "\n one:" + |
| "\n somefile.go:123", |
| }, { |
| err: &wrapped{"", &wrapped{"inner message", nil}}, |
| fmt: "%+v", |
| want: "somefile.go:123" + |
| "\n - inner message:" + |
| "\n somefile.go:123", |
| }, { |
| err: spurious(""), |
| fmt: "%s", |
| want: "spurious", |
| }, { |
| err: spurious(""), |
| fmt: "%+v", |
| want: "spurious", |
| }, { |
| err: spurious("extra"), |
| fmt: "%s", |
| want: "spurious", |
| }, { |
| err: spurious("extra"), |
| fmt: "%+v", |
| want: "spurious:\n" + |
| " extra", |
| }, { |
| err: nil, |
| fmt: "%+v", |
| want: "<nil>", |
| }, { |
| err: (*wrapped)(nil), |
| fmt: "%+v", |
| want: "<nil>", |
| }, { |
| err: simple, |
| fmt: "%T", |
| want: "*xerrors_test.wrapped", |
| }, { |
| err: simple, |
| fmt: "%🤪", |
| want: "%!🤪(*xerrors_test.wrapped)", |
| // For 1.13: |
| // want: "%!🤪(*xerrors_test.wrapped=&{simple <nil>})", |
| }, { |
| err: formatError("use fmt.Formatter"), |
| fmt: "%#v", |
| want: "use fmt.Formatter", |
| }, { |
| err: fmtTwice("%s %s", "ok", panicValue{}), |
| fmt: "%s", |
| // Different Go versions produce different results. |
| want: `ok %!s\(PANIC=(String method: )?panic\)/ok %!s\(PANIC=(String method: )?panic\)`, |
| regexp: true, |
| }, { |
| err: fmtTwice("%o %s", panicValue{}, "ok"), |
| fmt: "%s", |
| want: "{} ok/{} ok", |
| }, { |
| err: adapted{"adapted", nil}, |
| fmt: "%+v", |
| want: "adapted:" + |
| "\n detail", |
| }, { |
| err: adapted{"outer", adapted{"mid", adapted{"inner", nil}}}, |
| fmt: "%+v", |
| want: "outer:" + |
| "\n detail" + |
| "\n - mid:" + |
| "\n detail" + |
| "\n - inner:" + |
| "\n detail", |
| }} |
| for i, tc := range testCases { |
| t.Run(fmt.Sprintf("%d/%s", i, tc.fmt), func(t *testing.T) { |
| got := fmt.Sprintf(tc.fmt, tc.err) |
| var ok bool |
| if tc.regexp { |
| var err error |
| ok, err = regexp.MatchString(tc.want+"$", got) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } else { |
| ok = got == tc.want |
| } |
| if !ok { |
| t.Errorf("\n got: %q\nwant: %q", got, tc.want) |
| } |
| }) |
| } |
| } |
| |
| func TestAdaptor(t *testing.T) { |
| testCases := []struct { |
| err error |
| fmt string |
| want string |
| regexp bool |
| }{{ |
| err: adapted{"adapted", nil}, |
| fmt: "%+v", |
| want: "adapted:" + |
| "\n detail", |
| }, { |
| err: adapted{"outer", adapted{"mid", adapted{"inner", nil}}}, |
| fmt: "%+v", |
| want: "outer:" + |
| "\n detail" + |
| "\n - mid:" + |
| "\n detail" + |
| "\n - inner:" + |
| "\n detail", |
| }} |
| for i, tc := range testCases { |
| t.Run(fmt.Sprintf("%d/%s", i, tc.fmt), func(t *testing.T) { |
| got := fmt.Sprintf(tc.fmt, tc.err) |
| if got != tc.want { |
| t.Errorf("\n got: %q\nwant: %q", got, tc.want) |
| } |
| }) |
| } |
| } |
| |
| var _ xerrors.Formatter = wrapped{} |
| |
| type wrapped struct { |
| msg string |
| err error |
| } |
| |
| func (e wrapped) Error() string { return "should call Format" } |
| |
| func (e wrapped) Format(s fmt.State, verb rune) { |
| xerrors.FormatError(&e, s, verb) |
| } |
| |
| func (e wrapped) FormatError(p xerrors.Printer) (next error) { |
| p.Print(e.msg) |
| p.Detail() |
| p.Print("somefile.go:123") |
| return e.err |
| } |
| |
| var _ xerrors.Formatter = detailed{} |
| |
| type detailed struct{} |
| |
| func (e detailed) Error() string { panic("should have called FormatError") } |
| |
| func (detailed) FormatError(p xerrors.Printer) (next error) { |
| p.Printf("out of %s", "peanuts") |
| p.Detail() |
| p.Print("the elephant is on strike\n") |
| p.Printf("and the %d monkeys\nare laughing", 12) |
| return nil |
| } |
| |
| type withFrameAndMore struct { |
| frame xerrors.Frame |
| } |
| |
| func (e *withFrameAndMore) Error() string { return fmt.Sprint(e) } |
| |
| func (e *withFrameAndMore) Format(s fmt.State, v rune) { |
| xerrors.FormatError(e, s, v) |
| } |
| |
| func (e *withFrameAndMore) FormatError(p xerrors.Printer) (next error) { |
| p.Print("something") |
| if p.Detail() { |
| e.frame.Format(p) |
| p.Print("something more") |
| } |
| return nil |
| } |
| |
| type spurious string |
| |
| func (e spurious) Error() string { return fmt.Sprint(e) } |
| |
| // move to 1_12 test file |
| func (e spurious) Format(s fmt.State, verb rune) { |
| xerrors.FormatError(e, s, verb) |
| } |
| |
| func (e spurious) FormatError(p xerrors.Printer) (next error) { |
| p.Print("spurious") |
| p.Detail() // Call detail even if we don't print anything |
| if e == "" { |
| p.Print() |
| } else { |
| p.Print("\n", string(e)) // print extraneous leading newline |
| } |
| return nil |
| } |
| |
| type oneNewline struct { |
| next error |
| } |
| |
| func (e *oneNewline) Error() string { return fmt.Sprint(e) } |
| |
| func (e *oneNewline) Format(s fmt.State, verb rune) { |
| xerrors.FormatError(e, s, verb) |
| } |
| |
| func (e *oneNewline) FormatError(p xerrors.Printer) (next error) { |
| p.Print("1") |
| p.Print("2") |
| p.Print("3") |
| p.Detail() |
| p.Print("\n") |
| return e.next |
| } |
| |
| type newlineAtEnd struct { |
| next error |
| } |
| |
| func (e *newlineAtEnd) Error() string { return fmt.Sprint(e) } |
| |
| func (e *newlineAtEnd) Format(s fmt.State, verb rune) { |
| xerrors.FormatError(e, s, verb) |
| } |
| |
| func (e *newlineAtEnd) FormatError(p xerrors.Printer) (next error) { |
| p.Print("newlineAtEnd") |
| p.Detail() |
| p.Print("detail\n") |
| return e.next |
| } |
| |
| type adapted struct { |
| msg string |
| err error |
| } |
| |
| func (e adapted) Error() string { return e.msg } |
| |
| func (e adapted) Format(s fmt.State, verb rune) { |
| xerrors.FormatError(e, s, verb) |
| } |
| |
| func (e adapted) FormatError(p xerrors.Printer) error { |
| p.Print(e.msg) |
| p.Detail() |
| p.Print("detail") |
| return e.err |
| } |
| |
| // formatError is an error implementing Format instead of xerrors.Formatter. |
| // The implementation mimics the implementation of github.com/pkg/errors. |
| type formatError string |
| |
| func (e formatError) Error() string { return string(e) } |
| |
| func (e formatError) Format(s fmt.State, verb rune) { |
| // Body based on pkg/errors/errors.go |
| switch verb { |
| case 'v': |
| if s.Flag('+') { |
| io.WriteString(s, string(e)) |
| fmt.Fprintf(s, ":\n%s", "otherfile.go:456") |
| return |
| } |
| fallthrough |
| case 's': |
| io.WriteString(s, string(e)) |
| case 'q': |
| fmt.Fprintf(s, "%q", string(e)) |
| } |
| } |
| |
| func (e formatError) GoString() string { |
| panic("should never be called") |
| } |
| |
| type fmtTwiceErr struct { |
| format string |
| args []any |
| } |
| |
| func fmtTwice(format string, a ...any) error { |
| return fmtTwiceErr{format, a} |
| } |
| |
| func (e fmtTwiceErr) Error() string { return fmt.Sprint(e) } |
| |
| func (e fmtTwiceErr) Format(s fmt.State, verb rune) { |
| xerrors.FormatError(e, s, verb) |
| } |
| |
| func (e fmtTwiceErr) FormatError(p xerrors.Printer) (next error) { |
| p.Printf(e.format, e.args...) |
| p.Print("/") |
| p.Printf(e.format, e.args...) |
| return nil |
| } |
| |
| func (e fmtTwiceErr) GoString() string { |
| return "2 times " + fmt.Sprintf(e.format, e.args...) |
| } |
| |
| type panicValue struct{} |
| |
| func (panicValue) String() string { panic("panic") } |
| |
| var rePath = regexp.MustCompile(`( [^ ]+)\/(xerrors_test|fmt_test)\.`) |
| var reLine = regexp.MustCompile(":[0-9]+\n?$") |
| |
| func cleanPath(s string) string { |
| s = rePath.ReplaceAllString(s, "/path.") |
| s = reLine.ReplaceAllString(s, ":xxx") |
| s = strings.ReplaceAll(s, "\n ", "") |
| s = strings.ReplaceAll(s, " /", "/") |
| return s |
| } |
| |
| func errToParts(err error) (a []string) { |
| for err != nil { |
| var p testPrinter |
| if xerrors.Unwrap(err) != nil { |
| p.str += "wraps:" |
| } |
| f, ok := err.(xerrors.Formatter) |
| if !ok { |
| a = append(a, err.Error()) |
| break |
| } |
| err = f.FormatError(&p) |
| a = append(a, cleanPath(p.str)) |
| } |
| return a |
| |
| } |
| |
| type testPrinter struct { |
| str string |
| } |
| |
| func (p *testPrinter) Print(a ...any) { |
| p.str += fmt.Sprint(a...) |
| } |
| |
| func (p *testPrinter) Printf(format string, a ...any) { |
| p.str += fmt.Sprintf(format, a...) |
| } |
| |
| func (p *testPrinter) Detail() bool { |
| p.str += " /" |
| return true |
| } |