| // 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 fmt_test |
| |
| import ( |
| "io" |
| "os" |
| "path" |
| "reflect" |
| "regexp" |
| "strconv" |
| "strings" |
| "testing" |
| |
| "golang.org/x/exp/errors" |
| "golang.org/x/exp/errors/fmt" |
| ) |
| |
| 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 |
| }{{ |
| fmt.Errorf("no args"), |
| chain("no args/path.TestErrorf/path.go:xxx"), |
| }, { |
| fmt.Errorf("no args: %s"), |
| chain("no args: %!s(MISSING)/path.TestErrorf/path.go:xxx"), |
| }, { |
| fmt.Errorf("nounwrap: %s", "simple"), |
| chain(`nounwrap: simple/path.TestErrorf/path.go:xxx`), |
| }, { |
| fmt.Errorf("nounwrap: %v", "simple"), |
| chain(`nounwrap: simple/path.TestErrorf/path.go:xxx`), |
| }, { |
| fmt.Errorf("%s failed: %v", "foo", chained), |
| chain("foo failed/path.TestErrorf/path.go:xxx", |
| "chained/somefile.go:xxx"), |
| }, { |
| fmt.Errorf("no wrap: %s", chained), |
| chain("no wrap/path.TestErrorf/path.go:xxx", |
| "chained/somefile.go:xxx"), |
| }, { |
| fmt.Errorf("%s failed: %w", "foo", chained), |
| chain("wraps:foo failed/path.TestErrorf/path.go:xxx", |
| "chained/somefile.go:xxx"), |
| }, { |
| fmt.Errorf("nowrapv: %v", chained), |
| chain("nowrapv/path.TestErrorf/path.go:xxx", |
| "chained/somefile.go:xxx"), |
| }, { |
| fmt.Errorf("wrapw: %w", chained), |
| chain("wraps:wrapw/path.TestErrorf/path.go:xxx", |
| "chained/somefile.go:xxx"), |
| }, { |
| fmt.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: errors.Caller(0), |
| } |
| opaque = &wrapped{"outer", |
| errors.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: simple, |
| fmt: "%#v", |
| want: "&fmt_test.wrapped{msg:\"simple\", err:error(nil)}", |
| }, { |
| err: framed, |
| fmt: "%+v", |
| want: "something:" + |
| "\n golang.org/x/exp/errors/fmt_test.TestErrorFormatter" + |
| "\n .+/golang.org/x/exp/errors/fmt/errors_test.go:97" + |
| "\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: 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: "*fmt_test.wrapped", |
| }, { |
| err: simple, |
| fmt: "%🤪", |
| want: "%!🤪(*fmt_test.wrapped=&{simple <nil>})", |
| }, { |
| err: formatError("use fmt.Formatter"), |
| fmt: "%#v", |
| want: "use fmt.Formatter", |
| }, { |
| err: wrapped{"using errors.Formatter", |
| formatError("use fmt.Formatter")}, |
| fmt: "%#v", |
| want: "fmt_test.wrapped{msg:\"using errors.Formatter\", err:\"use fmt.Formatter\"}", |
| }, { |
| err: fmtTwice("%s %s", "ok", panicValue{}), |
| fmt: "%s", |
| want: "ok %!s(PANIC=panic)/ok %!s(PANIC=panic)", |
| }, { |
| err: fmtTwice("%o %s", panicValue{}, "ok"), |
| fmt: "%s", |
| want: "{} ok/{} ok", |
| }} |
| 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) |
| } |
| }) |
| } |
| } |
| |
| var _ errors.Formatter = wrapped{} |
| |
| type wrapped struct { |
| msg string |
| err error |
| } |
| |
| func (e wrapped) Error() string { return "should call Format" } |
| |
| func (e wrapped) FormatError(p errors.Printer) (next error) { |
| p.Print(e.msg) |
| p.Detail() |
| p.Print("somefile.go:123") |
| return e.err |
| } |
| |
| var _ errors.Formatter = detailed{} |
| |
| type detailed struct{} |
| |
| func (e detailed) Error() string { return fmt.Sprint(e) } |
| |
| func (detailed) FormatError(p errors.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 errors.Frame |
| } |
| |
| func (e *withFrameAndMore) Error() string { return fmt.Sprint(e) } |
| |
| func (e *withFrameAndMore) FormatError(p errors.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) } |
| |
| func (e spurious) FormatError(p errors.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 |
| } |
| |
| // formatError is an error implementing Format instead of errors.Formatter. |
| // The implementation mimics the implementation of github.com/pkg/errors, |
| // including that |
| 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) FormatError(p errors.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(`( [^ ]*)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.Replace(s, "\n ", "", -1) |
| s = strings.Replace(s, " /", "/", -1) |
| return s |
| } |
| |
| func errToParts(err error) (a []string) { |
| for err != nil { |
| var p testPrinter |
| if errors.Unwrap(err) != nil { |
| p.str += "wraps:" |
| } |
| f, ok := err.(errors.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 |
| } |