From acb2999c01f84e84a037f23069d7abeeed4f4a16 Mon Sep 17 00:00:00 2001 From: Masayoshi Mizutani Date: Sat, 3 Aug 2024 09:18:54 +0900 Subject: [PATCH] Add Unstack method --- README.md | 11 ++++++++++- errors.go | 12 ++++++++++++ errors_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ stack.go | 34 ++++++++++++++++++++++++---------- 4 files changed, 90 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3afa768..642ae79 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,16 @@ if err := someAction("no_such_file.txt"); err != nil { exit status 1 ``` -NOTE: If the error is wrapped by `goerr` multiply, `%+v` will print the stack trace of the deepest error. +**NOTE**: If the error is wrapped by `goerr` multiply, `%+v` will print the stack trace of the deepest error. + +**Tips**: If you want not to print the stack trace for current stack frame, you can use `Unstack` method. Also, `UnstackN` method removes the top multiple stack frames. + +```go +if err := someAction("no_such_file.txt"); err != nil { + // Unstack() removes the current stack frame from the error message. + return goerr.Wrap(err, "failed to someAction").Unstack() +} +``` ### Add/Extract contextual variables diff --git a/errors.go b/errors.go index d7b4cbd..cfe5fb6 100644 --- a/errors.go +++ b/errors.go @@ -158,6 +158,18 @@ func (x *Error) With(key string, value any) *Error { return x } +// Unstack trims stack trace by 1. It can be used for internal helper or utility functions. +func (x *Error) Unstack() *Error { + x.st = unstack(x.st, 1) + return x +} + +// UnstackN trims stack trace by n. It can be used for internal helper or utility functions. +func (x *Error) UnstackN(n int) *Error { + x.st = unstack(x.st, n) + return x +} + // Is returns true if target is goerr.Error and Error.id of two errors are matched. It's for errors.Is. If Error.id is empty, it always returns false. func (x *Error) Is(target error) bool { var err *Error diff --git a/errors_test.go b/errors_test.go index 561c98b..e36e544 100644 --- a/errors_test.go +++ b/errors_test.go @@ -177,3 +177,47 @@ func TestLoggerWithNil(t *testing.T) { t.Errorf("Expected log output to contain '\"error\":null', got '%s'", out.String()) } } + +func TestUnstack(t *testing.T) { + t.Run("original stack", func(t *testing.T) { + err := oops() + st := err.Stacks() + if st == nil { + t.Error("Expected stack trace to be nil") + } + if len(st) == 0 { + t.Error("Expected stack trace length to be 0") + } + if st[0].Func != "github.com/m-mizutani/goerr_test.oops" { + t.Errorf("Not expected stack trace func name (github.com/m-mizutani/goerr_test.oops): %s", st[0].Func) + } + }) + + t.Run("unstacked", func(t *testing.T) { + err := oops().Unstack() + st1 := err.Stacks() + if st1 == nil { + t.Error("Expected stack trace to be non-nil") + } + if len(st1) == 0 { + t.Error("Expected stack trace length to be non-zero") + } + if st1[0].Func != "github.com/m-mizutani/goerr_test.TestUnstack.func2" { + t.Errorf("Not expected stack trace func name (github.com/m-mizutani/goerr_test.TestUnstack.func2): %s", st1[0].Func) + } + }) + + t.Run("unstackN with 2", func(t *testing.T) { + err := oops().UnstackN(2) + st2 := err.Stacks() + if st2 == nil { + t.Error("Expected stack trace to be non-nil") + } + if len(st2) == 0 { + t.Error("Expected stack trace length to be non-zero") + } + if st2[0].Func != "testing.tRunner" { + t.Errorf("Not expected stack trace func name (testing.tRunner): %s", st2[0].Func) + } + }) +} diff --git a/stack.go b/stack.go index ebdf478..7eaa72d 100644 --- a/stack.go +++ b/stack.go @@ -105,16 +105,16 @@ func (f frame) name() string { // Format of frame formats the frame according to the fmt.Formatter interface. // -// %s source file -// %d source line -// %n function name -// %v equivalent to %s:%d +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d // // Format accepts flags that alter the printing of some verbs, as follows: // -// %+s function name and path of source file relative to the compile time -// GOPATH separated by \n\t (\n\t) -// %+v equivalent to %+s:%d +// %+s function name and path of source file relative to the compile time +// GOPATH separated by \n\t (\n\t) +// %+v equivalent to %+s:%d func (f frame) Format(s fmt.State, verb rune) { switch verb { case 's': @@ -149,12 +149,12 @@ func (f frame) MarshalText() ([]byte, error) { // Format formats the stack of Frames according to the fmt.Formatter interface. // -// %s lists source files for each Frame in the stack -// %v lists the source file and line number for each Frame in the stack +// %s lists source files for each Frame in the stack +// %v lists the source file and line number for each Frame in the stack // // Format accepts flags that alter the printing of some verbs, as follows: // -// %+v Prints filename, function, and line number for each Frame in the stack. +// %+v Prints filename, function, and line number for each Frame in the stack. func (st StackTrace) Format(s fmt.State, verb rune) { switch verb { case 'v': @@ -208,6 +208,20 @@ func (s *stack) StackTrace() StackTrace { return frames } +func unstack(st *stack, n int) *stack { + switch { + case n <= 0: + var ret stack + return &ret + case n >= len(*st): + return st + + default: + ret := (*st)[n:] + return &ret + } +} + func callers() *stack { const depth = 32 var pcs [depth]uintptr