// 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: "", }, { err: (*wrapped)(nil), fmt: "%+v", want: "", }, { err: simple, fmt: "%T", want: "*xerrors_test.wrapped", }, { err: simple, fmt: "%🤪", want: "%!🤪(*xerrors_test.wrapped)", // For 1.13: // want: "%!🤪(*xerrors_test.wrapped=&{simple })", }, { 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 string(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 []interface{} } func fmtTwice(format string, a ...interface{}) 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\.`) var reLine = regexp.MustCompile(":[0-9]*\n?$") func cleanPath(s string) string { s = rePath.ReplaceAllString(s, "/path.") s = reLine.ReplaceAllString(s, ":xxx") s = strings.Replace(s, "\n ", "", -1) s = strings.Replace(s, " /", "/", -1) 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 ...interface{}) { p.str += fmt.Sprint(a...) } func (p *testPrinter) Printf(format string, a ...interface{}) { p.str += fmt.Sprintf(format, a...) } func (p *testPrinter) Detail() bool { p.str += " /" return true }