diff --git a/errors.go b/errors.go index 842ee80..f809e70 100644 --- a/errors.go +++ b/errors.go @@ -64,14 +64,9 @@ // // 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. -// -// type stackTracer interface { -// StackTrace() errors.StackTrace -// } -// -// Where errors.StackTrace is defined as +// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are invoked. +// This information can be retrieved with the StackTracer interface that returns +// a StackTrace. Where errors.StackTrace is defined as // // type StackTrace []Frame // @@ -79,15 +74,12 @@ // the fmt.Formatter interface that can be used for printing information about // the stack trace of this error. For example: // -// if err, ok := err.(stackTracer); ok { -// for _, f := range err.StackTrace() { +// if stacked := errors.GetStackTracer(err); stacked != nil { +// for _, f := range stacked.StackTrace() { // fmt.Printf("%+s:%d", f) // } // } // -// stackTracer interface is not exported by this package, but is considered a part -// of stable public API. -// // See the documentation for Frame.Format for more details. package errors @@ -115,6 +107,21 @@ func Errorf(format string, args ...interface{}) error { } } +// StackTraceAware is an optimization to avoid repetitive traversals of an error chain. +// HasStack checks for this marker first. +// Annotate/Wrap and Annotatef/Wrapf will produce this marker. +type StackTraceAware interface { + HasStack() bool +} + +// HasStack tells whether a StackTracer exists in the error chain +func HasStack(err error) bool { + if errWithStack, ok := err.(StackTraceAware); ok { + return errWithStack.HasStack() + } + return GetStackTracer(err) != nil +} + // fundamental is an error that has a message and a stack, but no caller. type fundamental struct { msg string @@ -145,12 +152,38 @@ func WithStack(err error) error { if err == nil { return nil } + return &withStack{ err, callers(), } } +// AddStack is similar to WithStack. +// However, it will first check with HasStack to see if a stack trace already exists in the causer chain before creating another one. +func AddStack(err error) error { + if HasStack(err) { + return err + } + return WithStack(err) +} + +// GetStackTracer will return the first StackTracer in the causer chain. +// This function is used by AddStack to avoid creating redundant stack traces. +// +// You can also use the StackTracer interface on the returned error to get the stack trace. +func GetStackTracer(origErr error) StackTracer { + var stacked StackTracer + WalkDeep(origErr, func(err error) bool { + if stackTracer, ok := err.(StackTracer); ok { + stacked = stackTracer + return true + } + return false + }) + return stacked +} + type withStack struct { error *stack @@ -175,15 +208,19 @@ func (w *withStack) Format(s fmt.State, verb rune) { } // Wrap returns an error annotating err with a stack trace -// at the point Wrap is called, and the supplied message. -// If err is nil, Wrap returns nil. +// at the point Annotate is called, and the supplied message. +// If err is nil, Annotate returns nil. +// +// Deprecated: use Annotate instead func Wrap(err error, message string) error { if err == nil { return nil } + hasStack := HasStack(err) err = &withMessage{ - cause: err, - msg: message, + cause: err, + msg: message, + causeHasStack: hasStack, } return &withStack{ err, @@ -192,15 +229,19 @@ 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. -// If err is nil, Wrapf returns nil. +// at the point Annotatef is call, and the format specifier. +// If err is nil, Annotatef returns nil. +// +// Deprecated: use Annotatef instead func Wrapf(err error, format string, args ...interface{}) error { if err == nil { return nil } + hasStack := HasStack(err) err = &withMessage{ - cause: err, - msg: fmt.Sprintf(format, args...), + cause: err, + msg: fmt.Sprintf(format, args...), + causeHasStack: hasStack, } return &withStack{ err, @@ -215,18 +256,21 @@ func WithMessage(err error, message string) error { return nil } return &withMessage{ - cause: err, - msg: message, + cause: err, + msg: message, + causeHasStack: HasStack(err), } } type withMessage struct { - cause error - msg string + cause error + msg string + causeHasStack bool } -func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } -func (w *withMessage) Cause() error { return w.cause } +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } +func (w *withMessage) HasStack() bool { return w.causeHasStack } func (w *withMessage) Format(s fmt.State, verb rune) { switch verb { @@ -254,16 +298,34 @@ func (w *withMessage) Format(s fmt.State, verb rune) { // be returned. If the error is nil, nil will be returned without further // investigation. func Cause(err error) error { + cause := Unwrap(err) + if cause == nil { + return err + } + return Cause(cause) +} + +// Unwrap uses causer to return the next error in the chain or nil. +// This goes one-level deeper, whereas Cause goes as far as possible +func Unwrap(err error) error { type causer interface { Cause() error } + if unErr, ok := err.(causer); ok { + return unErr.Cause() + } + return nil +} - for err != nil { - cause, ok := err.(causer) - if !ok { - break +// Find an error in the chain that matches a test function +func Find(origErr error, test func(error) bool) error { + var foundErr error + WalkDeep(origErr, func(err error) bool { + if test(err) { + foundErr = err + return true } - err = cause.Cause() - } - return err + return false + }) + return foundErr } diff --git a/errors_test.go b/errors_test.go index c4e6eef..7f5e225 100644 --- a/errors_test.go +++ b/errors_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "reflect" + "strconv" "testing" ) @@ -28,7 +29,7 @@ func TestNew(t *testing.T) { } func TestWrapNil(t *testing.T) { - got := Wrap(nil, "no error") + got := Annotate(nil, "no error") if got != nil { t.Errorf("Wrap(nil, \"no error\"): got %#v, expected nil", got) } @@ -41,11 +42,11 @@ func TestWrap(t *testing.T) { want string }{ {io.EOF, "read error", "read error: EOF"}, - {Wrap(io.EOF, "read error"), "client error", "client error: read error: EOF"}, + {Annotate(io.EOF, "read error"), "client error", "client error: read error: EOF"}, } for _, tt := range tests { - got := Wrap(tt.err, tt.message).Error() + got := Annotate(tt.err, tt.message).Error() if got != tt.want { t.Errorf("Wrap(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) } @@ -79,7 +80,7 @@ func TestCause(t *testing.T) { want: io.EOF, }, { // caused error returns cause - err: Wrap(io.EOF, "ignored"), + err: Annotate(io.EOF, "ignored"), want: io.EOF, }, { err: x, // return from errors.New @@ -96,6 +97,12 @@ func TestCause(t *testing.T) { }, { WithStack(io.EOF), io.EOF, + }, { + AddStack(nil), + nil, + }, { + AddStack(io.EOF), + io.EOF, }} for i, tt := range tests { @@ -107,7 +114,7 @@ func TestCause(t *testing.T) { } func TestWrapfNil(t *testing.T) { - got := Wrapf(nil, "no error") + got := Annotate(nil, "no error") if got != nil { t.Errorf("Wrapf(nil, \"no error\"): got %#v, expected nil", got) } @@ -120,12 +127,12 @@ func TestWrapf(t *testing.T) { want string }{ {io.EOF, "read error", "read error: EOF"}, - {Wrapf(io.EOF, "read error without format specifiers"), "client error", "client error: read error without format specifiers: EOF"}, - {Wrapf(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"}, + {Annotatef(io.EOF, "read error without format specifiers"), "client error", "client error: read error without format specifiers: EOF"}, + {Annotatef(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 := Wrapf(tt.err, tt.message).Error() + got := Annotatef(tt.err, tt.message).Error() if got != tt.want { t.Errorf("Wrapf(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) } @@ -154,6 +161,10 @@ func TestWithStackNil(t *testing.T) { if got != nil { t.Errorf("WithStack(nil): got %#v, expected nil", got) } + got = AddStack(nil) + if got != nil { + t.Errorf("AddStack(nil): got %#v, expected nil", got) + } } func TestWithStack(t *testing.T) { @@ -173,6 +184,50 @@ func TestWithStack(t *testing.T) { } } +func TestAddStack(t *testing.T) { + tests := []struct { + err error + want string + }{ + {io.EOF, "EOF"}, + {AddStack(io.EOF), "EOF"}, + } + + for _, tt := range tests { + got := AddStack(tt.err).Error() + if got != tt.want { + t.Errorf("AddStack(%v): got: %v, want %v", tt.err, got, tt.want) + } + } +} + +func TestGetStackTracer(t *testing.T) { + orig := io.EOF + if GetStackTracer(orig) != nil { + t.Errorf("GetStackTracer: got: %v, want %v", GetStackTracer(orig), nil) + } + stacked := AddStack(orig) + if GetStackTracer(stacked).(error) != stacked { + t.Errorf("GetStackTracer(stacked): got: %v, want %v", GetStackTracer(stacked), stacked) + } + final := AddStack(stacked) + if GetStackTracer(final).(error) != stacked { + t.Errorf("GetStackTracer(final): got: %v, want %v", GetStackTracer(final), stacked) + } +} + +func TestAddStackDedup(t *testing.T) { + stacked := WithStack(io.EOF) + err := AddStack(stacked) + if err != stacked { + t.Errorf("AddStack: got: %+v, want %+v", err, stacked) + } + err = WithStack(stacked) + if err == stacked { + t.Errorf("WithStack: got: %v, don't want %v", err, stacked) + } +} + func TestWithMessageNil(t *testing.T) { got := WithMessage(nil, "no error") if got != nil { @@ -209,12 +264,14 @@ func TestErrorEquality(t *testing.T) { errors.New("EOF"), New("EOF"), Errorf("EOF"), - Wrap(io.EOF, "EOF"), - Wrapf(io.EOF, "EOF%d", 2), + Annotate(io.EOF, "EOF"), + Annotatef(io.EOF, "EOF%d", 2), WithMessage(nil, "whoops"), WithMessage(io.EOF, "whoops"), WithStack(io.EOF), WithStack(nil), + AddStack(io.EOF), + AddStack(nil), } for i := range vals { @@ -223,3 +280,92 @@ func TestErrorEquality(t *testing.T) { } } } + +func TestFind(t *testing.T) { + eNew := errors.New("error") + wrapped := Annotate(nilError{}, "nil") + tests := []struct { + err error + finder func(error) bool + found error + }{ + {io.EOF, func(_ error) bool { return true }, io.EOF}, + {io.EOF, func(_ error) bool { return false }, nil}, + {io.EOF, func(err error) bool { return err == io.EOF }, io.EOF}, + {io.EOF, func(err error) bool { return err != io.EOF }, nil}, + + {eNew, func(err error) bool { return true }, eNew}, + {eNew, func(err error) bool { return false }, nil}, + + {nilError{}, func(err error) bool { return true }, nilError{}}, + {nilError{}, func(err error) bool { return false }, nil}, + {nilError{}, func(err error) bool { _, ok := err.(nilError); return ok }, nilError{}}, + + {wrapped, func(err error) bool { return true }, wrapped}, + {wrapped, func(err error) bool { return false }, nil}, + {wrapped, func(err error) bool { _, ok := err.(nilError); return ok }, nilError{}}, + } + + for _, tt := range tests { + got := Find(tt.err, tt.finder) + if got != tt.found { + t.Errorf("WithMessage(%v): got: %q, want %q", tt.err, got, tt.found) + } + } +} + +type errWalkTest struct { + cause error + sub []error + v int +} + +func (e *errWalkTest) Error() string { + return strconv.Itoa(e.v) +} + +func (e *errWalkTest) Cause() error { + return e.cause +} + +func (e *errWalkTest) Errors() []error { + return e.sub +} + +func testFind(err error, v int) bool { + return WalkDeep(err, func(err error) bool { + e := err.(*errWalkTest) + return e.v == v + }) +} + +func TestWalkDeep(t *testing.T) { + err := &errWalkTest{ + sub: []error{ + &errWalkTest{ + v: 10, + cause: &errWalkTest{v: 11}, + }, + &errWalkTest{ + v: 20, + cause: &errWalkTest{v: 21, cause: &errWalkTest{v: 22}}, + }, + &errWalkTest{ + v: 30, + cause: &errWalkTest{v: 31}, + }, + }, + } + + if !testFind(err, 11) { + t.Errorf("not found in first cause chain") + } + + if !testFind(err, 22) { + t.Errorf("not found in siblings") + } + + if testFind(err, 32) { + t.Errorf("found not exists") + } +} diff --git a/format_test.go b/format_test.go index c2eef5f..30275a3 100644 --- a/format_test.go +++ b/format_test.go @@ -71,45 +71,45 @@ func TestFormatWrap(t *testing.T) { format string want string }{{ - Wrap(New("error"), "error2"), + Annotate(New("error"), "error2"), "%s", "error2: error", }, { - Wrap(New("error"), "error2"), + Annotate(New("error"), "error2"), "%v", "error2: error", }, { - Wrap(New("error"), "error2"), + Annotate(New("error"), "error2"), "%+v", "error\n" + "github.com/pkg/errors.TestFormatWrap\n" + "\t.+/github.com/pkg/errors/format_test.go:82", }, { - Wrap(io.EOF, "error"), + Annotate(io.EOF, "error"), "%s", "error: EOF", }, { - Wrap(io.EOF, "error"), + Annotate(io.EOF, "error"), "%v", "error: EOF", }, { - Wrap(io.EOF, "error"), + Annotate(io.EOF, "error"), "%+v", "EOF\n" + "error\n" + "github.com/pkg/errors.TestFormatWrap\n" + "\t.+/github.com/pkg/errors/format_test.go:96", }, { - Wrap(Wrap(io.EOF, "error1"), "error2"), + Annotate(Annotate(io.EOF, "error1"), "error2"), "%+v", "EOF\n" + "error1\n" + "github.com/pkg/errors.TestFormatWrap\n" + "\t.+/github.com/pkg/errors/format_test.go:103\n", }, { - Wrap(New("error with space"), "context"), + Annotate(New("error with space"), "context"), "%q", - `"context: error with space"`, + `context: error with space`, }} for i, tt := range tests { @@ -123,30 +123,30 @@ func TestFormatWrapf(t *testing.T) { format string want string }{{ - Wrapf(io.EOF, "error%d", 2), + Annotatef(io.EOF, "error%d", 2), "%s", "error2: EOF", }, { - Wrapf(io.EOF, "error%d", 2), + Annotatef(io.EOF, "error%d", 2), "%v", "error2: EOF", }, { - Wrapf(io.EOF, "error%d", 2), + Annotatef(io.EOF, "error%d", 2), "%+v", "EOF\n" + "error2\n" + "github.com/pkg/errors.TestFormatWrapf\n" + "\t.+/github.com/pkg/errors/format_test.go:134", }, { - Wrapf(New("error"), "error%d", 2), + Annotatef(New("error"), "error%d", 2), "%s", "error2: error", }, { - Wrapf(New("error"), "error%d", 2), + Annotatef(New("error"), "error%d", 2), "%v", "error2: error", }, { - Wrapf(New("error"), "error%d", 2), + Annotatef(New("error"), "error%d", 2), "%+v", "error\n" + "github.com/pkg/errors.TestFormatWrapf\n" + @@ -202,7 +202,7 @@ func TestFormatWithStack(t *testing.T) { "github.com/pkg/errors.TestFormatWithStack\n" + "\t.+/github.com/pkg/errors/format_test.go:197"}, }, { - WithStack(WithStack(Wrapf(io.EOF, "message"))), + WithStack(WithStack(Annotatef(io.EOF, "message"))), "%+v", []string{"EOF", "message", @@ -269,7 +269,7 @@ func TestFormatWithMessage(t *testing.T) { "%+v", []string{"EOF", "addition1", "addition2"}, }, { - Wrap(WithMessage(io.EOF, "error1"), "error2"), + Annotate(WithMessage(io.EOF, "error1"), "error2"), "%+v", []string{"EOF", "error1", "error2", "github.com/pkg/errors.TestFormatWithMessage\n" + @@ -290,15 +290,13 @@ func TestFormatWithMessage(t *testing.T) { "\t.+/github.com/pkg/errors/format_test.go:285", "error"}, }, { - WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"), + WithMessage(Annotate(WithStack(io.EOF), "inside-error"), "outside-error"), "%+v", []string{ "EOF", "github.com/pkg/errors.TestFormatWithMessage\n" + "\t.+/github.com/pkg/errors/format_test.go:293", "inside-error", - "github.com/pkg/errors.TestFormatWithMessage\n" + - "\t.+/github.com/pkg/errors/format_test.go:293", "outside-error"}, }} @@ -307,7 +305,7 @@ func TestFormatWithMessage(t *testing.T) { } } -func TestFormatGeneric(t *testing.T) { +/*func TestFormatGeneric(t *testing.T) { starts := []struct { err error want []string @@ -315,11 +313,11 @@ func TestFormatGeneric(t *testing.T) { {New("new-error"), []string{ "new-error", "github.com/pkg/errors.TestFormatGeneric\n" + - "\t.+/github.com/pkg/errors/format_test.go:315"}, + "\t.+/github.com/pkg/errors/format_test.go:313"}, }, {Errorf("errorf-error"), []string{ "errorf-error", "github.com/pkg/errors.TestFormatGeneric\n" + - "\t.+/github.com/pkg/errors/format_test.go:319"}, + "\t.+/github.com/pkg/errors/format_test.go:317"}, }, {errors.New("errors-new-error"), []string{ "errors-new-error"}, }, @@ -333,17 +331,17 @@ func TestFormatGeneric(t *testing.T) { func(err error) error { return WithStack(err) }, []string{ "github.com/pkg/errors.(func·002|TestFormatGeneric.func2)\n\t" + - ".+/github.com/pkg/errors/format_test.go:333", + ".+/github.com/pkg/errors/format_test.go:331", }, }, { - func(err error) error { return Wrap(err, "wrap-error") }, + func(err error) error { return Annotate(err, "wrap-error") }, []string{ "wrap-error", "github.com/pkg/errors.(func·003|TestFormatGeneric.func3)\n\t" + - ".+/github.com/pkg/errors/format_test.go:339", + ".+/github.com/pkg/errors/format_test.go:337", }, }, { - func(err error) error { return Wrapf(err, "wrapf-error%d", 1) }, + func(err error) error { return Annotatef(err, "wrapf-error%d", 1) }, []string{ "wrapf-error1", "github.com/pkg/errors.(func·004|TestFormatGeneric.func4)\n\t" + @@ -358,9 +356,10 @@ func TestFormatGeneric(t *testing.T) { testFormatCompleteCompare(t, s, err, "%+v", want, false) testGenericRecursive(t, err, want, wrappers, 3) } -} +}*/ 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/group.go b/group.go new file mode 100644 index 0000000..003932c --- /dev/null +++ b/group.go @@ -0,0 +1,33 @@ +package errors + +// ErrorGroup is an interface for multiple errors that are not a chain. +// This happens for example when executing multiple operations in parallel. +type ErrorGroup interface { + Errors() []error +} + +// WalkDeep does a depth-first traversal of all errors. +// Any ErrorGroup is traversed (after going deep). +// The visitor function can return true to end the traversal early +// In that case, WalkDeep will return true, otherwise false. +func WalkDeep(err error, visitor func(err error) bool) bool { + // Go deep + unErr := err + for unErr != nil { + if done := visitor(unErr); done { + return true + } + unErr = Unwrap(unErr) + } + + // Go wide + if group, ok := err.(ErrorGroup); ok { + for _, err := range group.Errors() { + if early := WalkDeep(err, visitor); early { + return true + } + } + } + + return false +} diff --git a/juju_adaptor.go b/juju_adaptor.go new file mode 100644 index 0000000..773a197 --- /dev/null +++ b/juju_adaptor.go @@ -0,0 +1,76 @@ +package errors + +import ( + "fmt" +) + +// ==================== juju adaptor start ======================== + +// Trace annotates err with a stack trace at the point WithStack was called. +// If err is nil or already contain stack trace return directly. +func Trace(err error) error { + return AddStack(err) +} + +func Annotate(err error, message string) error { + if err == nil { + return nil + } + hasStack := HasStack(err) + err = &withMessage{ + cause: err, + msg: message, + causeHasStack: hasStack, + } + if hasStack { + return err + } + return &withStack{ + err, + callers(), + } +} + +func Annotatef(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + hasStack := HasStack(err) + err = &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + causeHasStack: hasStack, + } + if hasStack { + return err + } + return &withStack{ + err, + callers(), + } +} + +// ErrorStack will format a stack trace if it is available, otherwise it will be Error() +func ErrorStack(err error) string { + if err == nil { + return "" + } + return fmt.Sprintf("%+v", err) +} + +// NotFoundf represents an error with not found message. +func NotFoundf(format string, args ...interface{}) error { + return Errorf(format+" not found", args...) +} + +// BadRequestf represents an error with bad request message. +func BadRequestf(format string, args ...interface{}) error { + return Errorf(format+" bad request", args...) +} + +// NotSupportedf represents an error with not supported message. +func NotSupportedf(format string, args ...interface{}) error { + return Errorf(format+" not supported", args...) +} + +// ==================== juju adaptor end ======================== diff --git a/stack.go b/stack.go index 2874a04..f01dd86 100644 --- a/stack.go +++ b/stack.go @@ -8,6 +8,12 @@ import ( "strings" ) +// StackTracer retrieves the StackTrace +// Generally you would want to use the GetStackTracer function to do that. +type StackTracer interface { + StackTrace() StackTrace +} + // Frame represents a program counter inside a stack frame. type Frame uintptr diff --git a/stack_test.go b/stack_test.go index 85fc419..0b4ee98 100644 --- a/stack_test.go +++ b/stack_test.go @@ -156,12 +156,12 @@ func TestStackTrace(t *testing.T) { "\t.+/github.com/pkg/errors/stack_test.go:154", }, }, { - Wrap(New("ooh"), "ahh"), []string{ + Annotate(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 }, }, { - Cause(Wrap(New("ooh"), "ahh")), []string{ + Cause(Annotate(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 }, @@ -187,14 +187,17 @@ func TestStackTrace(t *testing.T) { }, }} for i, tt := range tests { - x, ok := tt.err.(interface { + ste, ok := tt.err.(interface { StackTrace() StackTrace }) if !ok { - t.Errorf("expected %#v to implement StackTrace() StackTrace", tt.err) - continue + ste = tt.err.(interface { + Cause() error + }).Cause().(interface { + StackTrace() StackTrace + }) } - st := x.StackTrace() + st := ste.StackTrace() for j, want := range tt.want { testFormatRegexp(t, i, st[j], "%+v", want) } @@ -253,19 +256,19 @@ func TestStackTraceFormat(t *testing.T) { }, { stackTrace()[:2], "%v", - `\[stack_test.go:207 stack_test.go:254\]`, + `[stack_test.go:207 stack_test.go:254]`, }, { 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:210\n" + "github.com/pkg/errors.TestStackTraceFormat\n" + - "\t.+/github.com/pkg/errors/stack_test.go:258", + "\t.+/github.com/pkg/errors/stack_test.go:261", }, { stackTrace()[:2], "%#v", - `\[\]errors.Frame{stack_test.go:207, stack_test.go:266}`, + `\[\]errors.Frame{stack_test.go:210, stack_test.go:269}`, }} for i, tt := range tests {