diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f6fc4468 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,14 @@ +name: ci + +on: + workflow_dispatch: + push: + pull_request: + +jobs: + dummy: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 15e5a192..81e650b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,12 @@ +arch: + - amd64 + language: go go_import_path: github.com/pkg/errors go: - - 1.4.x - - 1.5.x - - 1.6.x - - 1.7.x - - 1.8.x - - 1.9.x - - 1.10.x + - 1.14 + - 1.15 - tip script: - - go test -v ./... + - make check diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ce9d7cde --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +PKGS := github.com/pkg/errors +SRCDIRS := $(shell go list -f '{{.Dir}}' $(PKGS)) +GO := go + +check: test vet gofmt misspell unconvert staticcheck ineffassign unparam + +test: + $(GO) test $(PKGS) + +vet: | test + $(GO) vet $(PKGS) + +staticcheck: + $(GO) get honnef.co/go/tools/cmd/staticcheck + staticcheck -checks all $(PKGS) + +misspell: + $(GO) get github.com/client9/misspell/cmd/misspell + misspell \ + -locale GB \ + -error \ + *.md *.go + +unconvert: + $(GO) get github.com/mdempsky/unconvert + unconvert -v $(PKGS) + +ineffassign: + $(GO) get github.com/gordonklaus/ineffassign + find $(SRCDIRS) -name '*.go' | xargs ineffassign + +pedantic: check errcheck + +unparam: + $(GO) get mvdan.cc/unparam + unparam ./... + +errcheck: + $(GO) get github.com/kisielk/errcheck + errcheck $(PKGS) + +gofmt: + @echo Checking code is gofmted + @test -z "$(shell gofmt -s -l -d -e $(SRCDIRS) | tee /dev/stderr)" diff --git a/README.md b/README.md index 6483ba2a..54dfdcb1 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,18 @@ default: [Read the package documentation for more information](https://godoc.org/github.com/pkg/errors). +## Roadmap + +With the upcoming [Go2 error proposals](https://go.googlesource.com/proposal/+/master/design/go2draft.md) this package is moving into maintenance mode. The roadmap for a 1.0 release is as follows: + +- 0.9. Remove pre Go 1.9 and Go 1.10 support, address outstanding pull requests (if possible) +- 1.0. Final release. + ## Contributing -We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high. +Because of the Go2 errors changes, this package is not accepting proposals for new functionality. With that said, we welcome pull requests, bug fixes and issue reports. -Before proposing a change, please discuss your change by raising an issue. +Before sending a PR, please discuss your change by raising an issue. ## License diff --git a/bench_test.go b/bench_test.go index 903b5f2d..c906870e 100644 --- a/bench_test.go +++ b/bench_test.go @@ -25,7 +25,7 @@ func yesErrors(at, depth int) error { // GlobalE is an exported global to store the result of benchmark results, // preventing the compiler from optimising the benchmark functions away. -var GlobalE error +var GlobalE interface{} func BenchmarkErrors(b *testing.B) { type run struct { @@ -61,3 +61,50 @@ func BenchmarkErrors(b *testing.B) { }) } } + +func BenchmarkStackFormatting(b *testing.B) { + type run struct { + stack int + format string + } + runs := []run{ + {10, "%s"}, + {10, "%v"}, + {10, "%+v"}, + {30, "%s"}, + {30, "%v"}, + {30, "%+v"}, + {60, "%s"}, + {60, "%v"}, + {60, "%+v"}, + } + + var stackStr string + for _, r := range runs { + name := fmt.Sprintf("%s-stack-%d", r.format, r.stack) + b.Run(name, func(b *testing.B) { + err := yesErrors(0, r.stack) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + stackStr = fmt.Sprintf(r.format, err) + } + b.StopTimer() + }) + } + + for _, r := range runs { + name := fmt.Sprintf("%s-stacktrace-%d", r.format, r.stack) + b.Run(name, func(b *testing.B) { + err := yesErrors(0, r.stack) + st := err.(*fundamental).stack.StackTrace() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + stackStr = fmt.Sprintf(r.format, st) + } + b.StopTimer() + }) + } + GlobalE = stackStr +} diff --git a/errors.go b/errors.go index 842ee804..161aea25 100644 --- a/errors.go +++ b/errors.go @@ -6,7 +6,7 @@ // return err // } // -// which applied recursively up the call stack results in error reports +// which when applied recursively up the call stack results in error reports // without context or debugging information. The errors package allows // programmers to add context to the failure path in their code in a way // that does not destroy the original value of the error. @@ -15,16 +15,17 @@ // // The errors.Wrap function returns a new error that adds context to the // original error by recording a stack trace at the point Wrap is called, -// and the supplied message. For example +// together with the supplied message. For example // // _, err := ioutil.ReadAll(r) // if err != nil { // return errors.Wrap(err, "read failed") // } // -// If additional control is required the errors.WithStack and errors.WithMessage -// functions destructure errors.Wrap into its component operations of annotating -// an error with a stack trace and an a message, respectively. +// If additional control is required, the errors.WithStack and +// errors.WithMessage functions destructure errors.Wrap into its component +// operations: annotating an error with a stack trace and with a message, +// respectively. // // Retrieving the cause of an error // @@ -38,7 +39,7 @@ // } // // can be inspected by errors.Cause. errors.Cause will recursively retrieve -// the topmost error which does not implement causer, which is assumed to be +// the topmost error that does not implement causer, which is assumed to be // the original cause. For example: // // switch err := errors.Cause(err).(type) { @@ -48,16 +49,16 @@ // // unknown error // } // -// causer interface is not exported by this package, but is considered a part -// of stable public API. +// Although the causer interface is not exported by this package, it is +// considered a part of its stable public interface. // // Formatted printing of errors // // All error values returned from this package implement fmt.Formatter and can -// be formatted by the fmt package. The following verbs are supported +// be formatted by the fmt package. The following verbs are supported: // // %s print the error. If the error has a Cause it will be -// printed recursively +// printed recursively. // %v see %s // %+v extended format. Each Frame of the error's StackTrace will // be printed in detail. @@ -65,13 +66,13 @@ // Retrieving the stack trace of an error or wrapper // // New, Errorf, Wrap, and Wrapf record a stack trace at the point they are -// invoked. This information can be retrieved with the following interface. +// invoked. This information can be retrieved with the following interface: // // type stackTracer interface { // StackTrace() errors.StackTrace // } // -// Where errors.StackTrace is defined as +// The returned errors.StackTrace type is defined as // // type StackTrace []Frame // @@ -81,12 +82,12 @@ // // if err, ok := err.(stackTracer); ok { // for _, f := range err.StackTrace() { -// fmt.Printf("%+s:%d", f) +// fmt.Printf("%+s:%d\n", f, f) // } // } // -// stackTracer interface is not exported by this package, but is considered a part -// of stable public API. +// Although the stackTracer interface is not exported by this package, it is +// considered a part of its stable public interface. // // See the documentation for Frame.Format for more details. package errors @@ -158,6 +159,9 @@ type withStack struct { func (w *withStack) Cause() error { return w.error } +// Unwrap provides compatibility for Go 1.13 error chains. +func (w *withStack) Unwrap() error { return w.error } + func (w *withStack) Format(s fmt.State, verb rune) { switch verb { case 'v': @@ -192,7 +196,7 @@ func Wrap(err error, message string) error { } // Wrapf returns an error annotating err with a stack trace -// at the point Wrapf is call, and the format specifier. +// at the point Wrapf is called, and the format specifier. // If err is nil, Wrapf returns nil. func Wrapf(err error, format string, args ...interface{}) error { if err == nil { @@ -220,6 +224,18 @@ func WithMessage(err error, message string) error { } } +// WithMessagef annotates err with the format specifier. +// If err is nil, WithMessagef returns nil. +func WithMessagef(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } +} + type withMessage struct { cause error msg string @@ -228,6 +244,9 @@ type withMessage struct { func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } func (w *withMessage) Cause() error { return w.cause } +// Unwrap provides compatibility for Go 1.13 error chains. +func (w *withMessage) Unwrap() error { return w.cause } + func (w *withMessage) Format(s fmt.State, verb rune) { switch verb { case 'v': diff --git a/errors_test.go b/errors_test.go index c4e6eef6..2089b2f7 100644 --- a/errors_test.go +++ b/errors_test.go @@ -198,6 +198,32 @@ func TestWithMessage(t *testing.T) { } } +func TestWithMessagefNil(t *testing.T) { + got := WithMessagef(nil, "no error") + if got != nil { + t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got) + } +} + +func TestWithMessagef(t *testing.T) { + tests := []struct { + err error + message string + want string + }{ + {io.EOF, "read error", "read error: EOF"}, + {WithMessagef(io.EOF, "read error without format specifier"), "client error", "client error: read error without format specifier: EOF"}, + {WithMessagef(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"}, + } + + for _, tt := range tests { + got := WithMessagef(tt.err, tt.message).Error() + if got != tt.want { + t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want) + } + } +} + // errors.New, etc values are not expected to be compared by value // but the change in errors#27 made them incomparable. Assert that // various kinds of errors have a functional equality operator, even diff --git a/example_test.go b/example_test.go index c1fc13e3..7d0e286f 100644 --- a/example_test.go +++ b/example_test.go @@ -195,7 +195,7 @@ func Example_stackTrace() { func ExampleCause_printf() { err := errors.Wrap(func() error { return func() error { - return errors.Errorf("hello %s", fmt.Sprintf("world")) + return errors.New("hello world") }() }(), "failed") diff --git a/format_test.go b/format_test.go index c2eef5f0..cb1df821 100644 --- a/format_test.go +++ b/format_test.go @@ -360,7 +360,32 @@ func TestFormatGeneric(t *testing.T) { } } +func wrappedNew(message string) error { // This function will be mid-stack inlined in go 1.12+ + return New(message) +} + +func TestFormatWrappedNew(t *testing.T) { + tests := []struct { + error + format string + want string + }{{ + wrappedNew("error"), + "%+v", + "error\n" + + "github.com/pkg/errors.wrappedNew\n" + + "\t.+/github.com/pkg/errors/format_test.go:364\n" + + "github.com/pkg/errors.TestFormatWrappedNew\n" + + "\t.+/github.com/pkg/errors/format_test.go:373", + }} + + for i, tt := range tests { + testFormatRegexp(t, i, tt.error, tt.format, tt.want) + } +} + func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) { + t.Helper() got := fmt.Sprintf(format, arg) gotLines := strings.SplitN(got, "\n", -1) wantLines := strings.SplitN(want, "\n", -1) diff --git a/go113.go b/go113.go new file mode 100644 index 00000000..be0d10d0 --- /dev/null +++ b/go113.go @@ -0,0 +1,38 @@ +// +build go1.13 + +package errors + +import ( + stderrors "errors" +) + +// Is reports whether any error in err's chain matches target. +// +// The chain consists of err itself followed by the sequence of errors obtained by +// repeatedly calling Unwrap. +// +// An error is considered to match a target if it is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +func Is(err, target error) bool { return stderrors.Is(err, target) } + +// As finds the first error in err's chain that matches target, and if so, sets +// target to that error value and returns true. +// +// The chain consists of err itself followed by the sequence of errors obtained by +// repeatedly calling Unwrap. +// +// An error matches target if the error's concrete value is assignable to the value +// pointed to by target, or if the error has a method As(interface{}) bool such that +// As(target) returns true. In the latter case, the As method is responsible for +// setting target. +// +// As will panic if target is not a non-nil pointer to either a type that implements +// error, or to any interface type. As returns false if err is nil. +func As(err error, target interface{}) bool { return stderrors.As(err, target) } + +// Unwrap returns the result of calling the Unwrap method on err, if err's +// type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +func Unwrap(err error) error { + return stderrors.Unwrap(err) +} diff --git a/go113_test.go b/go113_test.go new file mode 100644 index 00000000..4ea37e61 --- /dev/null +++ b/go113_test.go @@ -0,0 +1,178 @@ +// +build go1.13 + +package errors + +import ( + stderrors "errors" + "fmt" + "reflect" + "testing" +) + +func TestErrorChainCompat(t *testing.T) { + err := stderrors.New("error that gets wrapped") + wrapped := Wrap(err, "wrapped up") + if !stderrors.Is(wrapped, err) { + t.Errorf("Wrap does not support Go 1.13 error chains") + } +} + +func TestIs(t *testing.T) { + err := New("test") + + type args struct { + err error + target error + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "with stack", + args: args{ + err: WithStack(err), + target: err, + }, + want: true, + }, + { + name: "with message", + args: args{ + err: WithMessage(err, "test"), + target: err, + }, + want: true, + }, + { + name: "with message format", + args: args{ + err: WithMessagef(err, "%s", "test"), + target: err, + }, + want: true, + }, + { + name: "std errors compatibility", + args: args{ + err: fmt.Errorf("wrap it: %w", err), + target: err, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Is(tt.args.err, tt.args.target); got != tt.want { + t.Errorf("Is() = %v, want %v", got, tt.want) + } + }) + } +} + +type customErr struct { + msg string +} + +func (c customErr) Error() string { return c.msg } + +func TestAs(t *testing.T) { + var err = customErr{msg: "test message"} + + type args struct { + err error + target interface{} + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "with stack", + args: args{ + err: WithStack(err), + target: new(customErr), + }, + want: true, + }, + { + name: "with message", + args: args{ + err: WithMessage(err, "test"), + target: new(customErr), + }, + want: true, + }, + { + name: "with message format", + args: args{ + err: WithMessagef(err, "%s", "test"), + target: new(customErr), + }, + want: true, + }, + { + name: "std errors compatibility", + args: args{ + err: fmt.Errorf("wrap it: %w", err), + target: new(customErr), + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := As(tt.args.err, tt.args.target); got != tt.want { + t.Errorf("As() = %v, want %v", got, tt.want) + } + + ce := tt.args.target.(*customErr) + if !reflect.DeepEqual(err, *ce) { + t.Errorf("set target error failed, target error is %v", *ce) + } + }) + } +} + +func TestUnwrap(t *testing.T) { + err := New("test") + + type args struct { + err error + } + tests := []struct { + name string + args args + want error + }{ + { + name: "with stack", + args: args{err: WithStack(err)}, + want: err, + }, + { + name: "with message", + args: args{err: WithMessage(err, "test")}, + want: err, + }, + { + name: "with message format", + args: args{err: WithMessagef(err, "%s", "test")}, + want: err, + }, + { + name: "std errors compatibility", + args: args{err: fmt.Errorf("wrap: %w", err)}, + want: err, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := Unwrap(tt.args.err); !reflect.DeepEqual(err, tt.want) { + t.Errorf("Unwrap() error = %v, want %v", err, tt.want) + } + }) + } +} diff --git a/json_test.go b/json_test.go new file mode 100644 index 00000000..ad1adec9 --- /dev/null +++ b/json_test.go @@ -0,0 +1,51 @@ +package errors + +import ( + "encoding/json" + "regexp" + "testing" +) + +func TestFrameMarshalText(t *testing.T) { + var tests = []struct { + Frame + want string + }{{ + initpc, + `^github.com/pkg/errors\.init(\.ializers)? .+/github\.com/pkg/errors/stack_test.go:\d+$`, + }, { + 0, + `^unknown$`, + }} + for i, tt := range tests { + got, err := tt.Frame.MarshalText() + if err != nil { + t.Fatal(err) + } + if !regexp.MustCompile(tt.want).Match(got) { + t.Errorf("test %d: MarshalJSON:\n got %q\n want %q", i+1, string(got), tt.want) + } + } +} + +func TestFrameMarshalJSON(t *testing.T) { + var tests = []struct { + Frame + want string + }{{ + initpc, + `^"github\.com/pkg/errors\.init(\.ializers)? .+/github\.com/pkg/errors/stack_test.go:\d+"$`, + }, { + 0, + `^"unknown"$`, + }} + for i, tt := range tests { + got, err := json.Marshal(tt.Frame) + if err != nil { + t.Fatal(err) + } + if !regexp.MustCompile(tt.want).Match(got) { + t.Errorf("test %d: MarshalJSON:\n got %q\n want %q", i+1, string(got), tt.want) + } + } +} diff --git a/stack.go b/stack.go index 2874a048..779a8348 100644 --- a/stack.go +++ b/stack.go @@ -5,10 +5,13 @@ import ( "io" "path" "runtime" + "strconv" "strings" ) // Frame represents a program counter inside a stack frame. +// For historical reasons if Frame is interpreted as a uintptr +// its value represents the program counter + 1. type Frame uintptr // pc returns the program counter for this frame; @@ -37,6 +40,15 @@ func (f Frame) line() int { return line } +// name returns the name of this function, if known. +func (f Frame) name() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + return fn.Name() +} + // Format formats the frame according to the fmt.Formatter interface. // // %s source file @@ -54,22 +66,16 @@ func (f Frame) Format(s fmt.State, verb rune) { case 's': switch { case s.Flag('+'): - pc := f.pc() - fn := runtime.FuncForPC(pc) - if fn == nil { - io.WriteString(s, "unknown") - } else { - file, _ := fn.FileLine(pc) - fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) - } + io.WriteString(s, f.name()) + io.WriteString(s, "\n\t") + io.WriteString(s, f.file()) default: io.WriteString(s, path.Base(f.file())) } case 'd': - fmt.Fprintf(s, "%d", f.line()) + io.WriteString(s, strconv.Itoa(f.line())) case 'n': - name := runtime.FuncForPC(f.pc()).Name() - io.WriteString(s, funcname(name)) + io.WriteString(s, funcname(f.name())) case 'v': f.Format(s, 's') io.WriteString(s, ":") @@ -77,6 +83,16 @@ func (f Frame) Format(s fmt.State, verb rune) { } } +// MarshalText formats a stacktrace Frame as a text string. The output is the +// same as that of fmt.Sprintf("%+v", f), but without newlines or tabs. +func (f Frame) MarshalText() ([]byte, error) { + name := f.name() + if name == "unknown" { + return []byte(name), nil + } + return []byte(fmt.Sprintf("%s %s:%d", name, f.file(), f.line())), nil +} + // StackTrace is stack of Frames from innermost (newest) to outermost (oldest). type StackTrace []Frame @@ -94,16 +110,30 @@ func (st StackTrace) Format(s fmt.State, verb rune) { switch { case s.Flag('+'): for _, f := range st { - fmt.Fprintf(s, "\n%+v", f) + io.WriteString(s, "\n") + f.Format(s, verb) } case s.Flag('#'): fmt.Fprintf(s, "%#v", []Frame(st)) default: - fmt.Fprintf(s, "%v", []Frame(st)) + st.formatSlice(s, verb) } case 's': - fmt.Fprintf(s, "%s", []Frame(st)) + st.formatSlice(s, verb) + } +} + +// formatSlice will format this StackTrace into the given buffer as a slice of +// Frame, only valid when called with '%s' or '%v'. +func (st StackTrace) formatSlice(s fmt.State, verb rune) { + io.WriteString(s, "[") + for i, f := range st { + if i > 0 { + io.WriteString(s, " ") + } + f.Format(s, verb) } + io.WriteString(s, "]") } // stack represents a stack of program counters. diff --git a/stack_test.go b/stack_test.go index 85fc4195..aa10a72e 100644 --- a/stack_test.go +++ b/stack_test.go @@ -6,51 +6,18 @@ import ( "testing" ) -var initpc, _, _, _ = runtime.Caller(0) - -func TestFrameLine(t *testing.T) { - var tests = []struct { - Frame - want int - }{{ - Frame(initpc), - 9, - }, { - func() Frame { - var pc, _, _, _ = runtime.Caller(0) - return Frame(pc) - }(), - 20, - }, { - func() Frame { - var pc, _, _, _ = runtime.Caller(1) - return Frame(pc) - }(), - 28, - }, { - Frame(0), // invalid PC - 0, - }} - - for _, tt := range tests { - got := tt.Frame.line() - want := tt.want - if want != got { - t.Errorf("Frame(%v): want: %v, got: %v", uintptr(tt.Frame), want, got) - } - } -} +var initpc = caller() type X struct{} +// val returns a Frame pointing to itself. func (x X) val() Frame { - var pc, _, _, _ = runtime.Caller(0) - return Frame(pc) + return caller() } +// ptr returns a Frame pointing to itself. func (x *X) ptr() Frame { - var pc, _, _, _ = runtime.Caller(0) - return Frame(pc) + return caller() } func TestFrameFormat(t *testing.T) { @@ -59,32 +26,32 @@ func TestFrameFormat(t *testing.T) { format string want string }{{ - Frame(initpc), + initpc, "%s", "stack_test.go", }, { - Frame(initpc), + initpc, "%+s", "github.com/pkg/errors.init\n" + "\t.+/github.com/pkg/errors/stack_test.go", }, { - Frame(0), + 0, "%s", "unknown", }, { - Frame(0), + 0, "%+s", "unknown", }, { - Frame(initpc), + initpc, "%d", "9", }, { - Frame(0), + 0, "%d", "0", }, { - Frame(initpc), + initpc, "%n", "init", }, { @@ -102,20 +69,20 @@ func TestFrameFormat(t *testing.T) { "%n", "X.val", }, { - Frame(0), + 0, "%n", "", }, { - Frame(initpc), + initpc, "%v", "stack_test.go:9", }, { - Frame(initpc), + initpc, "%+v", "github.com/pkg/errors.init\n" + "\t.+/github.com/pkg/errors/stack_test.go:9", }, { - Frame(0), + 0, "%v", "unknown:0", }} @@ -153,37 +120,37 @@ func TestStackTrace(t *testing.T) { }{{ New("ooh"), []string{ "github.com/pkg/errors.TestStackTrace\n" + - "\t.+/github.com/pkg/errors/stack_test.go:154", + "\t.+/github.com/pkg/errors/stack_test.go:121", }, }, { Wrap(New("ooh"), "ahh"), []string{ "github.com/pkg/errors.TestStackTrace\n" + - "\t.+/github.com/pkg/errors/stack_test.go:159", // this is the stack of Wrap, not New + "\t.+/github.com/pkg/errors/stack_test.go:126", // this is the stack of Wrap, not New }, }, { Cause(Wrap(New("ooh"), "ahh")), []string{ "github.com/pkg/errors.TestStackTrace\n" + - "\t.+/github.com/pkg/errors/stack_test.go:164", // this is the stack of New + "\t.+/github.com/pkg/errors/stack_test.go:131", // this is the stack of New }, }, { func() error { return New("ooh") }(), []string{ - `github.com/pkg/errors.(func·009|TestStackTrace.func1)` + - "\n\t.+/github.com/pkg/errors/stack_test.go:169", // this is the stack of New + `github.com/pkg/errors.TestStackTrace.func1` + + "\n\t.+/github.com/pkg/errors/stack_test.go:136", // this is the stack of New "github.com/pkg/errors.TestStackTrace\n" + - "\t.+/github.com/pkg/errors/stack_test.go:169", // this is the stack of New's caller + "\t.+/github.com/pkg/errors/stack_test.go:136", // this is the stack of New's caller }, }, { Cause(func() error { return func() error { - return Errorf("hello %s", fmt.Sprintf("world")) + return Errorf("hello %s", fmt.Sprintf("world: %s", "ooh")) }() }()), []string{ - `github.com/pkg/errors.(func·010|TestStackTrace.func2.1)` + - "\n\t.+/github.com/pkg/errors/stack_test.go:178", // this is the stack of Errorf - `github.com/pkg/errors.(func·011|TestStackTrace.func2)` + - "\n\t.+/github.com/pkg/errors/stack_test.go:179", // this is the stack of Errorf's caller + `github.com/pkg/errors.TestStackTrace.func2.1` + + "\n\t.+/github.com/pkg/errors/stack_test.go:145", // this is the stack of Errorf + `github.com/pkg/errors.TestStackTrace.func2` + + "\n\t.+/github.com/pkg/errors/stack_test.go:146", // this is the stack of Errorf's caller "github.com/pkg/errors.TestStackTrace\n" + - "\t.+/github.com/pkg/errors/stack_test.go:180", // this is the stack of Errorf's caller's caller + "\t.+/github.com/pkg/errors/stack_test.go:147", // this is the stack of Errorf's caller's caller }, }} for i, tt := range tests { @@ -253,22 +220,31 @@ func TestStackTraceFormat(t *testing.T) { }, { stackTrace()[:2], "%v", - `\[stack_test.go:207 stack_test.go:254\]`, + `\[stack_test.go:174 stack_test.go:221\]`, }, { stackTrace()[:2], "%+v", "\n" + "github.com/pkg/errors.stackTrace\n" + - "\t.+/github.com/pkg/errors/stack_test.go:207\n" + + "\t.+/github.com/pkg/errors/stack_test.go:174\n" + "github.com/pkg/errors.TestStackTraceFormat\n" + - "\t.+/github.com/pkg/errors/stack_test.go:258", + "\t.+/github.com/pkg/errors/stack_test.go:225", }, { stackTrace()[:2], "%#v", - `\[\]errors.Frame{stack_test.go:207, stack_test.go:266}`, + `\[\]errors.Frame{stack_test.go:174, stack_test.go:233}`, }} for i, tt := range tests { testFormatRegexp(t, i, tt.StackTrace, tt.format, tt.want) } } + +// a version of runtime.Caller that returns a Frame, not a uintptr. +func caller() Frame { + var pcs [3]uintptr + n := runtime.Callers(2, pcs[:]) + frames := runtime.CallersFrames(pcs[:n]) + frame, _ := frames.Next() + return Frame(frame.PC) +}