blob: d3d7bec0700b1737a49faffa226ab7d092572091 [file] [log] [blame]
// 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
}