From d486efaad56c7a59f6dbe1b6892839cc9184cc42 Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Sun, 16 Jul 2023 17:03:57 +0300 Subject: [PATCH 1/4] feature: joining errors --- errors.go | 20 ++++++++++- errors_test.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++++- go.mod | 2 +- joining.go | 71 +++++++++++++++++++++++++++++++++++++ joining_test.go | 82 +++++++++++++++++++++++++++++++++++++++++++ logging.go | 16 +++++++-- logging_test.go | 34 ++++++++++++++++++ options.go | 5 +++ 8 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 joining.go create mode 100644 joining_test.go diff --git a/errors.go b/errors.go index 5fbaf94..1a70136 100644 --- a/errors.go +++ b/errors.go @@ -69,7 +69,25 @@ func As[T any](err error) (T, bool) { return t, true } } - err = Unwrap(err) + switch x := err.(type) { + case interface{ Unwrap() error }: + err = x.Unwrap() + if err == nil { + var z T + return z, false + } + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + if t, ok := As[T](err); ok { + return t, ok + } + } + var z T + return z, false + default: + var z T + return z, false + } } var z T diff --git a/errors_test.go b/errors_test.go index ad2a517..42163b4 100644 --- a/errors_test.go +++ b/errors_test.go @@ -2,6 +2,7 @@ package errors_test import ( "encoding/json" + stderrors "errors" "fmt" "io/fs" "os" @@ -111,7 +112,7 @@ func TestStackTrace(t *testing.T) { err: wrap(errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.wrap\n" + - "\t.+/errors/errors_test.go:160", + "\t.+/errors/errors_test.go:200", "github.com/muonsoft/errors_test.TestStackTrace\n" + "\t.+/errors/errors_test.go:111", }, @@ -140,6 +141,46 @@ func TestStackTrace(t *testing.T) { "\t.+/errors/errors_test.go:137", }, }, + { + name: "join one std error", + err: errors.Join(fmt.Errorf("ooh")), + want: []string{ + "github.com/muonsoft/errors_test.TestStackTrace\n" + + "\t.+/errors/errors_test.go:145", + }, + }, + { + name: "join two std errors", + err: errors.Join( + fmt.Errorf("ooh"), + fmt.Errorf("ooh"), + ), + want: []string{ + "github.com/muonsoft/errors_test.TestStackTrace\n" + + "\t.+/errors/errors_test.go:153", + }, + }, + { + name: "join one stacked error", + err: errors.Join( + errors.Errorf("ooh"), + ), + want: []string{ + "github.com/muonsoft/errors_test.TestStackTrace\n" + + "\t.+/errors/errors_test.go:165", + }, + }, + { + name: "join two stacked errors", + err: errors.Join( + errors.Errorf("ooh"), + errors.Errorf("ooh"), + ), + want: []string{ + "github.com/muonsoft/errors_test.TestStackTrace\n" + + "\t.+/errors/errors_test.go:174", + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -203,6 +244,11 @@ func TestFields(t *testing.T) { err: errors.Wrap(errors.Errorf("error"), errors.String("key", "value")), expected: "value", }, + { + name: "stringer interface", + err: errors.Wrap(errors.Errorf("error"), errors.Stringer("key", stringer{s: "value"})), + expected: "value", + }, { name: "strings", err: errors.Wrap(errors.Errorf("error"), errors.Strings("key", []string{"value"})), @@ -394,6 +440,43 @@ func TestAs(t *testing.T) { true, errFileNotFound, }, + { + "wrapped wrapped error", + errors.Wrap(wrapped{"error", errorT{"T"}}), + func(err error) (any, bool) { + return errors.As[errorT](err) + }, + true, + errorT{"T"}, + }, + { + "wrapped joined error", + errors.Wrap( + errors.Join( + wrapped{"error", errorT{"T"}}, + wrapped{"error", errorT{"T"}}, + ), + ), + func(err error) (any, bool) { + return errors.As[errorT](err) + }, + true, + errorT{"T"}, + }, + { + "wrapped std joined error", + errors.Wrap( + stderrors.Join( + wrapped{"error", errorT{"T"}}, + wrapped{"error", errorT{"T"}}, + ), + ), + func(err error) (any, bool) { + return errors.As[errorT](err) + }, + true, + errorT{"T"}, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -555,3 +638,11 @@ type wrapped struct { func (e wrapped) Error() string { return e.msg } func (e wrapped) Unwrap() error { return e.err } + +type stringer struct { + s string +} + +func (s stringer) String() string { + return s.s +} diff --git a/go.mod b/go.mod index cedc942..82c9dbc 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/muonsoft/errors -go 1.18 +go 1.20 require github.com/sirupsen/logrus v1.8.1 diff --git a/joining.go b/joining.go new file mode 100644 index 0000000..17cd604 --- /dev/null +++ b/joining.go @@ -0,0 +1,71 @@ +package errors + +// Join returns an error that wraps the given errors with a stack trace +// at the point Join is called. Any nil error values are discarded. +// Join returns nil if errs contains no non-nil values. +// The error formats as the concatenation of the strings obtained +// by calling the Error method of each element of errs, with a newline +// between each string. +// If there is only one error in chain, then it's stack trace will be +// preserved if present. +func Join(errs ...error) error { + n := 0 + for _, err := range errs { + if err != nil { + n++ + } + } + if n == 0 { + return nil + } + if n == 1 { + for _, err := range errs { + if err != nil { + if isWrapper(err) { + return err + } + + return &stacked{ + wrapped: &wrapped{wrapped: err}, + stack: newStack(0), + } + } + } + } + + e := &joinError{errs: make([]error, 0, n)} + + for _, err := range errs { + if err != nil { + e.errs = append(e.errs, err) + } + } + + return &stacked{ + wrapped: &wrapped{wrapped: e}, + stack: newStack(0), + } +} + +type joinError struct { + errs []error +} + +// todo: add marshal json? + +func (e *joinError) Error() string { + var b []byte + + for i, err := range e.errs { + if i > 0 { + b = append(b, '\n') + } + b = append(b, err.Error()...) + } + + return string(b) +} + +func (e *joinError) Unwrap() []error { + return e.errs +} diff --git a/joining_test.go b/joining_test.go new file mode 100644 index 0000000..e93e4c4 --- /dev/null +++ b/joining_test.go @@ -0,0 +1,82 @@ +package errors_test + +import ( + "fmt" + "testing" + + "github.com/muonsoft/errors" +) + +func TestJoin_ReturnsNil(t *testing.T) { + if err := errors.Join(); err != nil { + t.Errorf("errors.Join() = %v, want nil", err) + } + if err := errors.Join(nil); err != nil { + t.Errorf("errors.Join(nil) = %v, want nil", err) + } + if err := errors.Join(nil, nil); err != nil { + t.Errorf("errors.Join(nil, nil) = %v, want nil", err) + } +} + +func TestJoin(t *testing.T) { + err1 := errors.New("err1") + err2 := errors.New("err2") + tests := []struct { + errs []error + want []error + }{ + { + errs: []error{err1}, + want: []error{err1}, + }, + { + errs: []error{err1, err2}, + want: []error{err1, err2}, + }, + { + errs: []error{err1, nil, err2}, + want: []error{err1, err2}, + }, + } + for _, test := range tests { + t.Run(fmt.Sprintf("%v", test.errs), func(t *testing.T) { + got := errors.Join(test.errs...) + for _, want := range test.want { + if !errors.Is(got, want) { + t.Errorf("want err %v in chain", want) + } + } + }) + } +} + +func TestJoin_ErrorMethod(t *testing.T) { + err1 := errors.New("err1") + err2 := errors.New("err2") + tests := []struct { + errs []error + want string + }{ + { + errs: []error{err1}, + want: "err1", + }, + { + errs: []error{err1, err2}, + want: "err1\nerr2", + }, + { + errs: []error{err1, nil, err2}, + want: "err1\nerr2", + }, + } + for _, test := range tests { + t.Run(fmt.Sprintf("%v", test.errs), func(t *testing.T) { + got := errors.Join(test.errs...).Error() + if got != test.want { + t.Errorf("Join().Error() = %q; want %q", got, test.want) + } + }) + } +} diff --git a/logging.go b/logging.go index 45d3d67..72878d6 100644 --- a/logging.go +++ b/logging.go @@ -43,12 +43,24 @@ func Log(err error, logger Logger) { if s, ok := e.(stackTracer); ok { logger.SetStackTrace(s.StackTrace()) } + } + logFields(err, logger) + + logger.Log(err.Error()) +} + +func logFields(err error, logger Logger) { + for e := err; e != nil; e = errors.Unwrap(e) { if w, ok := e.(LoggableError); ok { w.LogFields(logger) } - } - logger.Log(err.Error()) + if joined, ok := e.(interface{ Unwrap() []error }); ok { + for _, u := range joined.Unwrap() { + logFields(u, logger) + } + } + } } type BoolField struct { diff --git a/logging_test.go b/logging_test.go index 2d46dfb..03f3cb0 100644 --- a/logging_test.go +++ b/logging_test.go @@ -1,6 +1,7 @@ package errors_test import ( + stderrors "errors" "testing" "github.com/muonsoft/errors" @@ -45,3 +46,36 @@ func TestLog_errorWithStack(t *testing.T) { logger.AssertField(t, "deepKey", "deepValue") logger.AssertField(t, "deepestKey", "deepestValue") } + +func TestLog_joinedErrors(t *testing.T) { + logger := errorstest.NewLogger() + + err := errors.Wrap( + errors.Join( + errors.Wrap( + errors.Errorf("error 1", errors.String("key1", "value1")), + errors.String("key2", "value2"), + ), + errors.Errorf("error 2", errors.String("key3", "value3")), + stderrors.Join( + errors.Errorf("error 3", errors.String("key4", "value4")), + errors.Errorf("error 4", errors.String("key5", "value5")), + ), + ), + ) + errors.Log(err, logger) + + logger.AssertMessage(t, "error 1\nerror 2\nerror 3\nerror 4") + logger.AssertStackTrace(t, errorstest.StackTrace{ + { + Function: "github.com/muonsoft/errors_test.TestLog_joinedErrors", + File: ".+errors/logging_test.go", + Line: 54, + }, + }) + logger.AssertField(t, "key1", "value1") + logger.AssertField(t, "key2", "value2") + logger.AssertField(t, "key3", "value3") + logger.AssertField(t, "key4", "value4") + logger.AssertField(t, "key5", "value5") +} diff --git a/options.go b/options.go index ed38df8..a8405bd 100644 --- a/options.go +++ b/options.go @@ -2,6 +2,7 @@ package errors import ( "encoding/json" + "fmt" "time" ) @@ -60,6 +61,10 @@ func String(key string, value string) Option { } } +func Stringer(key string, value fmt.Stringer) Option { + return String(key, value.String()) +} + func Strings(key string, values []string) Option { return func(options *Options) { options.AddField(StringsField{Key: key, Values: values}) From 7fed68b36f2791ef38b14de11b8ca9ad06ef05a4 Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Sat, 5 Aug 2023 10:27:33 +0300 Subject: [PATCH 2/4] feature: joining errors --- errors.go | 8 ++++++- errors_test.go | 40 +++++++++++++++---------------- joining.go | 17 ++++++++++++- json_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++-- logging_test.go | 2 +- 5 files changed, 106 insertions(+), 25 deletions(-) diff --git a/errors.go b/errors.go index 1a70136..7b1de8c 100644 --- a/errors.go +++ b/errors.go @@ -248,7 +248,13 @@ func (e *stacked) Format(s fmt.State, verb rune) { func (e *stacked) MarshalJSON() ([]byte, error) { data := mapWriter{"error": e.Error()} data.SetStackTrace(e.StackTrace()) - e.LogFields(data) + + var err error + for err = e; err != nil; err = Unwrap(err) { + if loggable, ok := err.(LoggableError); ok { + loggable.LogFields(data) + } + } return json.Marshal(data) } diff --git a/errors_test.go b/errors_test.go index 42163b4..d5f9ac9 100644 --- a/errors_test.go +++ b/errors_test.go @@ -24,7 +24,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("ooh"), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:23", + "\t.+/errors/errors_test.go:24", }, }, { @@ -32,7 +32,7 @@ func TestStackTrace(t *testing.T) { err: errors.Wrap(errors.Errorf("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:31", + "\t.+/errors/errors_test.go:32", }, }, { @@ -40,7 +40,7 @@ func TestStackTrace(t *testing.T) { err: errors.Wrap(errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:39", + "\t.+/errors/errors_test.go:40", }, }, { @@ -48,7 +48,7 @@ func TestStackTrace(t *testing.T) { err: errors.Wrap(errors.Wrap(errors.New("ooh"))), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:47", + "\t.+/errors/errors_test.go:48", }, }, { @@ -56,7 +56,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("ooh"), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:55", + "\t.+/errors/errors_test.go:56", }, }, { @@ -64,7 +64,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%v", errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:63", + "\t.+/errors/errors_test.go:64", }, }, { @@ -72,7 +72,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%w", errors.Wrap(errors.New("ooh"))), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:71", + "\t.+/errors/errors_test.go:72", }, }, { @@ -80,7 +80,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%%w %v", errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:79", + "\t.+/errors/errors_test.go:80", }, }, { @@ -88,7 +88,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%s: %w", "prefix", errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:87", + "\t.+/errors/errors_test.go:88", }, }, { @@ -96,7 +96,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%w", errors.Errorf("%w", errors.New("ooh"))), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:95", + "\t.+/errors/errors_test.go:96", }, }, { @@ -104,7 +104,7 @@ func TestStackTrace(t *testing.T) { err: errors.Errorf("%w", fmt.Errorf("%w", errors.New("ooh"))), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:103", + "\t.+/errors/errors_test.go:104", }, }, { @@ -112,9 +112,9 @@ func TestStackTrace(t *testing.T) { err: wrap(errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.wrap\n" + - "\t.+/errors/errors_test.go:200", + "\t.+/errors/errors_test.go:201", "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:111", + "\t.+/errors/errors_test.go:112", }, }, { @@ -122,7 +122,7 @@ func TestStackTrace(t *testing.T) { err: wrapSkipCaller(errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:121", + "\t.+/errors/errors_test.go:122", }, }, { @@ -130,7 +130,7 @@ func TestStackTrace(t *testing.T) { err: wrapSkipCallers(errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:129", + "\t.+/errors/errors_test.go:130", }, }, { @@ -138,7 +138,7 @@ func TestStackTrace(t *testing.T) { err: errorfSkipCaller("ooh"), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:137", + "\t.+/errors/errors_test.go:138", }, }, { @@ -146,7 +146,7 @@ func TestStackTrace(t *testing.T) { err: errors.Join(fmt.Errorf("ooh")), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:145", + "\t.+/errors/errors_test.go:146", }, }, { @@ -157,7 +157,7 @@ func TestStackTrace(t *testing.T) { ), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:153", + "\t.+/errors/errors_test.go:154", }, }, { @@ -167,7 +167,7 @@ func TestStackTrace(t *testing.T) { ), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:165", + "\t.+/errors/errors_test.go:166", }, }, { @@ -178,7 +178,7 @@ func TestStackTrace(t *testing.T) { ), want: []string{ "github.com/muonsoft/errors_test.TestStackTrace\n" + - "\t.+/errors/errors_test.go:174", + "\t.+/errors/errors_test.go:175", }, }, } diff --git a/joining.go b/joining.go index 17cd604..53af66b 100644 --- a/joining.go +++ b/joining.go @@ -51,7 +51,9 @@ type joinError struct { errs []error } -// todo: add marshal json? +func (e *joinError) LogFields(logger FieldLogger) { + logFieldsFromErrors(logger, e.errs) +} func (e *joinError) Error() string { var b []byte @@ -69,3 +71,16 @@ func (e *joinError) Error() string { func (e *joinError) Unwrap() []error { return e.errs } + +func logFieldsFromErrors(logger FieldLogger, errs []error) { + for _, err := range errs { + for w := err; w != nil; w = Unwrap(w) { + if j, ok := w.(interface{ Unwrap() []error }); ok { + logFieldsFromErrors(logger, j.Unwrap()) + } + if loggable, ok := w.(LoggableError); ok { + loggable.LogFields(logger) + } + } + } +} diff --git a/json_test.go b/json_test.go index 7246db8..269a3c0 100644 --- a/json_test.go +++ b/json_test.go @@ -2,6 +2,7 @@ package errors_test import ( "encoding/json" + stderrors "errors" "testing" "github.com/muonsoft/errors" @@ -27,7 +28,7 @@ func TestStackedError_MarshalJSON(t *testing.T) { { Function: "github.com/muonsoft/errors_test.TestStackedError_MarshalJSON", File: ".+/errors/json_test.go", - Line: 12, + Line: 13, }, }) if jsonError.Key != "value" { @@ -57,7 +58,7 @@ func TestWrappedError_MarshalJSON(t *testing.T) { { Function: "github.com/muonsoft/errors_test.TestWrappedError_MarshalJSON", File: ".+/errors/json_test.go", - Line: 40, + Line: 41, }, }) if jsonError.Key != "value" { @@ -68,6 +69,65 @@ func TestWrappedError_MarshalJSON(t *testing.T) { } } +func TestJoinedError_MarshalJSON(t *testing.T) { + err := errors.Join( + errors.Join( + errors.Wrap( + errors.Errorf("error 1", errors.String("key1", "value1")), + errors.String("key2", "value2"), + ), + errors.Errorf("error 2", errors.String("key3", "value3")), + stderrors.Join( + errors.Errorf("error 3", errors.String("key4", "value4")), + errors.Errorf("error 4", errors.String("key5", "value5")), + ), + ), + ) + jsonData, e := json.Marshal(err) + if e != nil { + t.Fatalf("expected %#v to be marshalable into json: %v", err, e) + } + var jsonError struct { + Error string `json:"error"` + StackTrace errorstest.StackTrace `json:"stackTrace"` + Key1 string `json:"key1"` + Key2 string `json:"key2"` + Key3 string `json:"key3"` + Key4 string `json:"key4"` + Key5 string `json:"key5"` + } + e = json.Unmarshal(jsonData, &jsonError) + if e != nil { + t.Fatalf("failed to unmarshal json: %v", e) + } + + if jsonError.Error != "error 1\nerror 2\nerror 3\nerror 4" { + t.Errorf("expected %#v to have error key", err) + } + assertStackRegexp(t, jsonError.StackTrace, errorstest.StackTrace{ + { + Function: "github.com/muonsoft/errors_test.TestJoinedError_MarshalJSON", + File: ".+/errors/json_test.go", + Line: 74, + }, + }) + if jsonError.Key1 != "value1" { + t.Errorf(`expected %#v to have key "key1"`, err) + } + if jsonError.Key2 != "value2" { + t.Errorf(`expected %#v to have key "key2"`, err) + } + if jsonError.Key3 != "value3" { + t.Errorf(`expected %#v to have key "key3"`, err) + } + if jsonError.Key4 != "value4" { + t.Errorf(`expected %#v to have key "key4"`, err) + } + if jsonError.Key5 != "value5" { + t.Errorf(`expected %#v to have key "key5"`, err) + } +} + type JSONError struct { Error string `json:"error"` StackTrace errorstest.StackTrace `json:"stackTrace"` diff --git a/logging_test.go b/logging_test.go index 03f3cb0..f486f84 100644 --- a/logging_test.go +++ b/logging_test.go @@ -39,7 +39,7 @@ func TestLog_errorWithStack(t *testing.T) { { Function: "github.com/muonsoft/errors_test.TestLog_errorWithStack", File: ".+errors/logging_test.go", - Line: 29, + Line: 30, }, }) logger.AssertField(t, "key", "value") From a9df4e70b224aed27e8f6a547047c1de144d9ccd Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Sat, 5 Aug 2023 21:24:10 +0300 Subject: [PATCH 3/4] feature: joining errors --- errors.go | 36 +++++++++++++++++++++--------------- errors_test.go | 14 +++++++++++++- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/errors.go b/errors.go index 7b1de8c..8c5942e 100644 --- a/errors.go +++ b/errors.go @@ -124,8 +124,8 @@ func Errorf(message string, argsAndOptions ...interface{}) error { opts := newOptions(options...) err := fmt.Errorf(message, args...) - argError := getArgError(message, args) - if isWrapper(argError) { + argErrors := getArgErrors(message, args) + if len(argErrors) == 1 && isWrapper(argErrors[0]) { return &wrapped{wrapped: err, fields: opts.fields} } @@ -144,12 +144,16 @@ func Wrap(err error, options ...Option) error { if err == nil { return nil } - opts := newOptions(options...) - if isWrapper(err) { - return &wrapped{wrapped: err, fields: opts.fields} + if len(options) == 0 { + return err + } + + return &wrapped{wrapped: err, fields: newOptions(options...).fields} } + opts := newOptions(options...) + return &stacked{ wrapped: &wrapped{wrapped: err, fields: opts.fields}, stack: newStack(opts.skipCallers), @@ -278,28 +282,30 @@ func splitArgsAndOptions(argsAndOptions []interface{}) ([]interface{}, []Option) return args, options } -func getArgError(message string, args []interface{}) error { - index := getErrorIndex(message) +func getArgErrors(message string, args []interface{}) []error { + indices := getErrorIndices(message) + errs := make([]error, 0, len(indices)) - if index >= 0 && index < len(args) { - if err, ok := args[index].(error); ok { - return err + for _, i := range indices { + if err, ok := args[i].(error); ok { + errs = append(errs, err) } } - return nil + return errs } -func getErrorIndex(message string) int { - i := -1 +func getErrorIndices(message string) []int { + indices := make([]int, 0, 1) isFormat := false + i := -1 for _, s := range message { if isFormat { if s != '%' { i++ if s == 'w' { - return i + indices = append(indices, i) } } isFormat = false @@ -308,7 +314,7 @@ func getErrorIndex(message string) int { } } - return -1 + return indices } type mapWriter map[string]interface{} diff --git a/errors_test.go b/errors_test.go index d5f9ac9..e39a2be 100644 --- a/errors_test.go +++ b/errors_test.go @@ -112,7 +112,7 @@ func TestStackTrace(t *testing.T) { err: wrap(errors.New("ooh")), want: []string{ "github.com/muonsoft/errors_test.wrap\n" + - "\t.+/errors/errors_test.go:201", + "\t.+/errors/errors_test.go:213", "github.com/muonsoft/errors_test.TestStackTrace\n" + "\t.+/errors/errors_test.go:112", }, @@ -181,6 +181,18 @@ func TestStackTrace(t *testing.T) { "\t.+/errors/errors_test.go:175", }, }, + { + name: "Errorf() with multiple errors", + err: errors.Errorf( + "first: %w; second: %w", + errors.Errorf("ooh"), + errors.Errorf("ooh"), + ), + want: []string{ + "github.com/muonsoft/errors_test.TestStackTrace\n" + + "\t.+/errors/errors_test.go:186", + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { From 7634999d5f573633b489173da2f59d0bf1834eb3 Mon Sep 17 00:00:00 2001 From: Igor Lazarev Date: Wed, 9 Aug 2023 17:42:02 +0300 Subject: [PATCH 4/4] fix linter settings --- .github/workflows/tests.yml | 4 ++-- .golangci.yml | 11 ++++++----- stack.go | 20 ++++++++++---------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a392357..8a4f09c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: ^1.13 + go-version: ^1.20 id: go - name: Checkout code @@ -25,7 +25,7 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v2 with: - version: v1.46 + version: v1.52 - name: Run tests run: go test -v $(go list ./... | grep -v vendor) diff --git a/.golangci.yml b/.golangci.yml index 5776037..17b24e2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,8 +7,6 @@ linters: - bidichk - bodyclose - contextcheck - - cyclop - - deadcode - depguard - dogsled - dupl @@ -16,7 +14,6 @@ linters: - exportloopref - forbidigo - funlen - - gci - gocognit - goconst - gocritic @@ -46,7 +43,6 @@ linters: - predeclared - promlinter - revive - - structcheck - stylecheck - tenv - testpackage @@ -56,7 +52,6 @@ linters: - unconvert - unparam - unused - - varcheck - whitespace issues: @@ -84,3 +79,9 @@ issues: - errcheck - goconst - gocritic + +linters-settings: + revive: + rules: + - name: var-naming + disabled: true diff --git a/stack.go b/stack.go index 50a7b8b..735977c 100644 --- a/stack.go +++ b/stack.go @@ -57,16 +57,16 @@ func (f Frame) Name() string { // Format 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': @@ -124,12 +124,12 @@ type StackTrace []Frame // 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':