errors/fmt: improve error printing in detail mode
- Add a colon after a non-detailed message if
it has details or of it is followed by another
message.
Follows the design draft.
This is more readable and more consistent
with non-detailed errors.
- Gobble newline after last detail
This results in a better API experience when
printing multiple sections consecutively.
- Allow Detail to be called more than once.
Change-Id: Ic19179cdf1479fded8b657d569cb66bd2e54467e
Reviewed-on: https://go-review.googlesource.com/c/144199
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/errors/fmt/errors.go b/errors/fmt/errors.go
index da0e3db..5e68180 100644
--- a/errors/fmt/errors.go
+++ b/errors/fmt/errors.go
@@ -5,6 +5,7 @@
package fmt
import (
+ "bytes"
"strings"
"golang.org/x/exp/errors"
@@ -87,8 +88,8 @@
func fmtError(p *pp, verb rune, err error) (handled bool) {
var (
- sep = ": " // separator before next error
- w = p // print buffer where error text is written
+ sep = " " // separator before next error
+ w = p // print buffer where error text is written
)
switch {
// Note that this switch must match the preference order
@@ -156,7 +157,15 @@
if err == nil {
break
}
+ if !w.fmt.inDetail || !p.fmt.plusV {
+ w.buf.WriteByte(':')
+ }
+ // Strip last newline of detail.
+ if bytes.HasSuffix([]byte(w.buf), detailSep) {
+ w.buf = w.buf[:len(w.buf)-len(detailSep)]
+ }
w.buf.WriteString(sep)
+ w.fmt.inDetail = false
}
if w != p {
@@ -165,9 +174,10 @@
return true
}
+var detailSep = []byte("\n ")
+
// errPPState wraps a pp to implement State with indentation. It is used
// for errors implementing fmt.Formatter.
-
type errPPState pp
func (p *errPPState) Width() (wid int, ok bool) { return (*pp)(p).Width() }
@@ -181,7 +191,7 @@
for i, c := range b {
if c == '\n' {
p.buf.Write(b[k:i])
- p.buf.Write([]byte("\n "))
+ p.buf.Write(detailSep)
k = i + 1
}
}
@@ -215,8 +225,11 @@
}
func (p *errPP) Detail() bool {
- p.fmt.indent = p.fmt.plusV
+ inDetail := p.fmt.inDetail
p.fmt.inDetail = true
- (*errPPState)(p).Write([]byte("\n"))
+ p.fmt.indent = p.fmt.plusV
+ if p.fmt.plusV && !inDetail {
+ (*errPPState)(p).Write([]byte(":\n"))
+ }
return p.fmt.plusV
}
diff --git a/errors/fmt/errors_test.go b/errors/fmt/errors_test.go
index 427a527..57b7f2b 100644
--- a/errors/fmt/errors_test.go
+++ b/errors/fmt/errors_test.go
@@ -49,7 +49,7 @@
"chained/somefile.go:xxx"),
}, {
fmt.Errorf("not wrapped: %+v", chained),
- chain("not wrapped: chained somefile.go:123/path.TestErrorf/path.go:xxx"),
+ 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) {
@@ -80,7 +80,10 @@
&wrapped{"and another\none", nil}}
fallback = &wrapped{"fallback", os.ErrNotExist}
oldAndNew = &wrapped{"new style", formatError("old style")}
- opaque = &wrapped{"outer",
+ framed = &withFrameAndMore{
+ frame: errors.Caller(0),
+ }
+ opaque = &wrapped{"outer",
errors.Opaque(&wrapped{"mid",
&wrapped{"inner", nil}})}
)
@@ -97,24 +100,29 @@
fmt: "%s",
want: "can't adumbrate elephant: out of peanuts",
}, {
- err: simple,
- fmt: "%+v",
- want: "simple\n somefile.go:123",
+ 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" +
+ want: "can't adumbrate elephant:" +
"\n somefile.go:123" +
- "\n--- out of peanuts" +
+ "\n--- out of peanuts:" +
"\n the elephant is on strike" +
"\n and the 12 monkeys" +
"\n are laughing",
}, {
err: transition,
fmt: "%+v",
- want: "elephant still on strike" +
+ want: "elephant still on strike:" +
"\n somefile.go:123" +
- "\n--- out of peanuts" +
+ "\n--- out of peanuts:" +
"\n the elephant is on strike" +
"\n and the 12 monkeys" +
"\n are laughing",
@@ -123,6 +131,13 @@
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 /Users/mpvl/dev/go/text/src/golang.org/x/exp/errors/fmt/errors_test.go:84" +
+ "\n something more",
+ }, {
err: fmtTwice("Hello World!"),
fmt: "%#v",
want: "2 times Hello World!",
@@ -131,9 +146,12 @@
fmt: "%s",
want: "fallback: file does not exist",
}, {
- err: fallback,
- fmt: "%+v",
- want: "fallback\n somefile.go:123\n--- 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",
@@ -141,11 +159,11 @@
}, {
err: opaque,
fmt: "%+v",
- want: "outer" +
+ want: "outer:" +
"\n somefile.go:123" +
- "\n--- mid" +
+ "\n--- mid:" +
"\n somefile.go:123" +
- "\n--- inner" +
+ "\n--- inner:" +
"\n somefile.go:123",
}, {
err: oldAndNew,
@@ -159,16 +177,22 @@
err: oldAndNew,
fmt: "%+v",
// Note the extra indentation.
- want: "new style\n somefile.go:123\n--- old style\n otherfile.go:456",
+ // 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: simple,
+ fmt: "%+12v",
+ want: "simple:" +
+ "\n somefile.go:123",
}, {
err: elephant,
fmt: "%+50s",
@@ -186,9 +210,11 @@
fmt: "% x",
want: "73 69 6d 70 6c 65",
}, {
- err: newline,
- fmt: "%s",
- want: "msg with\nnewline: and another\none",
+ err: newline,
+ fmt: "%s",
+ want: "msg with" +
+ "\nnewline: and another" +
+ "\none",
}, {
err: nil,
fmt: "%+v",
@@ -277,6 +303,21 @@
return nil
}
+type withFrameAndMore struct {
+ frame errors.Frame
+}
+
+func (e *withFrameAndMore) Error() string { return fmt.Sprint(e) }
+
+func (e *withFrameAndMore) Format(p errors.Printer) (next error) {
+ p.Print("something")
+ if p.Detail() {
+ e.frame.Format(p)
+ p.Print("something more")
+ }
+ return nil
+}
+
// formatError is an error implementing Format instead of errors.Formatter.
// The implementation mimics the implementation of github.com/pkg/errors,
// including that
@@ -290,7 +331,7 @@
case 'v':
if s.Flag('+') {
io.WriteString(s, string(e))
- fmt.Fprintf(s, "\n%s", "otherfile.go:456")
+ fmt.Fprintf(s, ":\n%s", "otherfile.go:456")
return
}
fallthrough
@@ -332,7 +373,7 @@
func (panicValue) String() string { panic("panic") }
var rePath = regexp.MustCompile(`( [^ ]*)fmt.*test\.`)
-var reLine = regexp.MustCompile(":[0-9]*$")
+var reLine = regexp.MustCompile(":[0-9]*\n?$")
func cleanPath(s string) string {
s = rePath.ReplaceAllString(s, "/path.")
diff --git a/errors/fmt/format_example_test.go b/errors/fmt/format_example_test.go
index 7609ffc..c549cc8 100644
--- a/errors/fmt/format_example_test.go
+++ b/errors/fmt/format_example_test.go
@@ -28,13 +28,13 @@
// foo: bar(nameserver 139): baz flopped
//
// Detailed error:
- // foo
+ // foo:
// golang.org/x/exp/errors/fmt_test.foo
// golang.org/x/exp/errors/fmt/format_example_test.go:17
- // --- bar(nameserver 139)
+ // --- bar(nameserver 139):
// golang.org/x/exp/errors/fmt_test.bar
// golang.org/x/exp/errors/fmt/format_example_test.go:16
- // --- baz flopped
+ // --- baz flopped:
// golang.org/x/exp/errors/fmt_test.baz
// golang.org/x/exp/errors/fmt/format_example_test.go:15
}
diff --git a/errors/frame.go b/errors/frame.go
index 10d2e20..a5369e5 100644
--- a/errors/frame.go
+++ b/errors/frame.go
@@ -50,7 +50,7 @@
p.Printf("%s\n ", function)
}
if file != "" {
- p.Printf("%s:%d", file, line)
+ p.Printf("%s:%d\n", file, line)
}
}
}
diff --git a/errors/wrap_test.go b/errors/wrap_test.go
index dbf6e31..9481401 100644
--- a/errors/wrap_test.go
+++ b/errors/wrap_test.go
@@ -128,7 +128,7 @@
}
got = fmt.Errorf("foo: %+v", errors.Opaque(errorD{}))
- want = "foo: errorD\n detail"
+ want = "foo: errorD:\n detail"
if got.Error() != want {
t.Errorf("error with Format: got %v; want %v", got, want)
}