diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1929a7a5..8d5c202f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 1 strategy: matrix: - go-version: ['>=1.22.2'] + go-version: ['>=1.22.4'] platform: [ubuntu-22.04] runs-on: ${{ matrix.platform }} steps: @@ -68,7 +68,7 @@ jobs: timeout-minutes: 7 strategy: matrix: - go-version: ['>=1.22.2'] + go-version: ['>=1.22.4'] platform: [ubuntu-22.04] runs-on: ${{ matrix.platform }} steps: @@ -102,7 +102,7 @@ jobs: timeout-minutes: 8 strategy: matrix: - go-version: ['>=1.22.2'] + go-version: ['>=1.22.4'] platform: [ubuntu-22.04] runs-on: ${{ matrix.platform }} steps: @@ -208,7 +208,7 @@ jobs: timeout-minutes: 3 strategy: matrix: - go-version: ['>=1.22.2'] + go-version: ['>=1.22.4'] platform: [ubuntu-22.04] runs-on: ${{ matrix.platform }} steps: @@ -265,7 +265,7 @@ jobs: # timeout-minutes: 2 # strategy: # matrix: - # go-version: ['>=1.22.2'] + # go-version: ['>=1.22.4'] # platform: [ubuntu-22.04] # runs-on: ${{ matrix.platform }} # steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index c088d13f..1f7b0dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ Most recent version is listed first. +# v0.0.98 +- ong/errors: add equivalent functions from standard library: https://github.com/komuw/ong/pull/449 + +# v0.0.97 +- ong/middleware: disable reload protector middleware : https://github.com/komuw/ong/pull/448 + That middleware is not working as intended. This PR mitigates until we can implement a proper fix. + # v0.0.96 - ong/acme: verify the requested acme challenge token: https://github.com/komuw/ong/pull/440 This is a bug fix for v0.0.95 diff --git a/errors/errors.go b/errors/errors.go index a6b1014f..95177e9d 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -46,7 +46,7 @@ func Wrap(err error) error { return wrap(err, 3) } -// Dwrap adds stack traces to the error. +// Dwrap(aka deferred wrap) adds stack traces to the error. // It does nothing when *errp == nil. func Dwrap(errp *error) { if *errp != nil { @@ -54,9 +54,10 @@ func Dwrap(errp *error) { } } -func wrap(err error, skip int) error { - if _, ok := err.(*stackError); ok { - return err +func wrap(err error, skip int) *stackError { + c, ok := err.(*stackError) + if ok { + return c } // limit stack size to 64 call depth. @@ -107,9 +108,12 @@ func (e *stackError) Format(f fmt.State, verb rune) { // StackTrace returns the stack trace contained in err, if any, else an empty string. func StackTrace(err error) string { - sterr, ok := err.(*stackError) - if !ok { - return "" + if sterr, ok := err.(*stackError); ok { + return sterr.getStackTrace() } - return sterr.getStackTrace() + if sterr, ok := err.(*joinError); ok { + return sterr.getStackTrace() + } + + return "" } diff --git a/errors/errors_test.go b/errors/errors_test.go index e5d3361a..5aeda172 100644 --- a/errors/errors_test.go +++ b/errors/errors_test.go @@ -190,5 +190,7 @@ func TestStackError(t *testing.T) { attest.True(t, stdErrors.Is(err, os.ErrNotExist)) attest.NotZero(t, stdErrors.Unwrap(err)) attest.True(t, stdErrors.As(err, &targetErr)) + + _ = wrap(err, 2) // This is here to quiet golangci-lint which complains that wrap is always called with an argument of 3. }) } diff --git a/errors/join.go b/errors/join.go new file mode 100644 index 00000000..ffe0fd85 --- /dev/null +++ b/errors/join.go @@ -0,0 +1,65 @@ +package errors + +// Some of the code here is inspired(or taken from) by: +// (a) https://github.com/golang/go/blob/go1.20.14/src/errors/join.go whose license(BSD 3-Clause) can be found here: https://github.com/golang/go/blob/go1.20.14/LICENSE + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. +// Join returns nil if every value in errs is nil. +// 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. +// +// A non-nil error returned by Join implements the Unwrap() error method. +// +// It only returns the stack trace of the first error. Unwrap also only returns the first error. +// +// Note that this function is equivalent to the one in standard library only in spirit. +// This is not a direct replacement of the standard library one. +func Join(errs ...error) error { + n := 0 + for _, err := range errs { + if err != nil { + n++ + } + } + if n == 0 { + return nil + } + + e := &joinError{errs: make([]error, 0, n)} + for _, err := range errs { + if err != nil { + ef := wrap(err, 3) + e.errs = append(e.errs, ef) + if e.stackError == nil { + e.stackError = ef + } + } + } + + return e +} + +type joinError struct { + *stackError + errs []error +} + +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 { + if len(e.errs) > 0 { + return e.errs[0] + } + return nil +} diff --git a/errors/join_test.go b/errors/join_test.go new file mode 100644 index 00000000..5e6aaa71 --- /dev/null +++ b/errors/join_test.go @@ -0,0 +1,129 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package errors + +import ( + "reflect" + "testing" + + "go.akshayshah.org/attest" +) + +// Some of the code here is inspired(or taken from) by: +// (a) https://github.com/golang/go/blob/go1.20.14/src/errors/join.go whose license(BSD 3-Clause) can be found here: https://github.com/golang/go/blob/go1.20.14/LICENSE + +func TestJoinReturnsNil(t *testing.T) { + if err := Join(); err != nil { + t.Errorf("errors.Join() = %v, want nil", err) + } + if err := Join(nil); err != nil { + t.Errorf("errors.Join(nil) = %v, want nil", err) + } + if err := Join(nil, nil); err != nil { + t.Errorf("errors.Join(nil, nil) = %v, want nil", err) + } +} + +func TestJoin(t *testing.T) { + err1 := New("err1") + err2 := New("err2") + for _, test := range []struct { + errs []error + want error + }{ + { + errs: []error{err1}, + want: err1, + }, + { + errs: []error{err1, err2}, + want: err1, + }, + { + errs: []error{err2, err1, nil}, + want: err2, + }, + { + errs: []error{nil, err2, err1}, + want: err2, + }, + } { + got := Join(test.errs...).(interface{ Unwrap() error }).Unwrap() + if !reflect.DeepEqual(got, test.want) { + t.Errorf("Join(%v) got = %v; want %v", test.errs, got, test.want) + } + // if len(got) != cap(got) { + // t.Errorf("Join(%v) returns errors with len=%v, cap=%v; want len==cap", test.errs, len(got), cap(got)) + // } + } +} + +func TestJoinErrorMethod(t *testing.T) { + err1 := New("err1") + err2 := New("err2") + for _, test := range []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", + }} { + got := Join(test.errs...).Error() + if got != test.want { + t.Errorf("Join(%v).Error() = %q; want %q", test.errs, got, test.want) + } + } +} + +func TestJoinStackTrace(t *testing.T) { + t.Parallel() + + t.Run("errors.Join", func(t *testing.T) { + t.Parallel() + + err1 := New("hello") + err2 := hello() + + { + err3 := Join(err1, err2) + + sterr, ok := err3.(*joinError) + attest.True(t, ok) + attest.Equal(t, sterr.Error(), "hello\nerror in foo") + + stackTrace := sterr.getStackTrace() + for _, v := range []string{ + "ong/errors/join_test.go:92", // Join only shows stack trace of first error. ie, err1 + } { + attest.Subsequence(t, stackTrace, v, attest.Sprintf("\n\t%s: not found in stackTrace: %s", v, stackTrace)) + } + } + + { + err3 := Join(err2, err1) + + sterr, ok := err3.(*joinError) + attest.True(t, ok) + attest.Equal(t, sterr.Error(), "error in foo\nhello") + + stackTrace := sterr.getStackTrace() + for _, v := range []string{ + // Join only shows stack trace of first error. ie, err2 + "ong/errors/errors_test.go:30", + "ong/errors/errors_test.go:23", + "ong/errors/errors_test.go:17", + "ong/errors/join_test.go:93", + } { + attest.Subsequence(t, stackTrace, v, attest.Sprintf("\n\t%s: not found in stackTrace: %s", v, stackTrace)) + } + } + }) +} diff --git a/errors/stdlib.go b/errors/stdlib.go new file mode 100644 index 00000000..29ca6d9e --- /dev/null +++ b/errors/stdlib.go @@ -0,0 +1,26 @@ +package errors + +import ( + stdErrors "errors" + "fmt" +) + +// As is a pass through to the same func from the standard library errors package. +func As(err error, target any) bool { + return stdErrors.As(err, target) +} + +// Is is a pass through to the same func from the standard library errors package. +func Is(err, target error) bool { + return stdErrors.Is(err, target) +} + +// Unwrap is a pass through to the same func from the standard library errors package. +func Unwrap(err error) error { + return stdErrors.Unwrap(err) +} + +// Errorf is a pass through to the same func from the standard library fmt package. +func Errorf(format string, a ...any) error { + return fmt.Errorf(format, a...) +} diff --git a/errors/stdlib_test.go b/errors/stdlib_test.go new file mode 100644 index 00000000..559bb592 --- /dev/null +++ b/errors/stdlib_test.go @@ -0,0 +1,26 @@ +package errors + +import ( + "io/fs" + "os" + "testing" + + "go.akshayshah.org/attest" +) + +func TestStdLib(t *testing.T) { + t.Parallel() + + t.Run("stdlib pass throughs", func(t *testing.T) { + t.Parallel() + + err := prepFile() + var targetErr *fs.PathError + + _, ok := err.(*stackError) + attest.True(t, ok) + attest.True(t, Is(err, os.ErrNotExist)) + attest.NotZero(t, Unwrap(err)) + attest.True(t, As(err, &targetErr)) + }) +} diff --git a/log/log_benchmarks_test.go b/log/log_benchmarks_test.go index 824d42ec..28237492 100644 --- a/log/log_benchmarks_test.go +++ b/log/log_benchmarks_test.go @@ -216,7 +216,7 @@ func BenchmarkAverageCase(b *testing.B) { for range b.N { l.Info(sl[0], slAny...) if rand.IntN(100) >= 99 { - l.Error("some-error", logErr) + l.Error("some-error", "err", logErr) } } }) @@ -228,7 +228,7 @@ func BenchmarkAverageCase(b *testing.B) { for range b.N { l.Info(sl[0], slAny...) if rand.IntN(100) >= 99 { - l.Error("some-error", logErr) + l.Error("some-error", "err", logErr) } } }) @@ -285,7 +285,7 @@ func BenchmarkWorstCase(b *testing.B) { b.ResetTimer() for range b.N { l.Info(sl[0], slAny...) - l.Error("some-error", logErr) + l.Error("some-error", "err", logErr) } }) @@ -295,7 +295,7 @@ func BenchmarkWorstCase(b *testing.B) { b.ResetTimer() for range b.N { l.Info(sl[0], slAny...) - l.Error("some-error", logErr) + l.Error("some-error", "err", logErr) } }) diff --git a/log/log_test.go b/log/log_test.go index fc73222f..dcbab1e7 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -120,7 +120,7 @@ func TestLogger(t *testing.T) { maxMsgs := 3 l := New(context.Background(), w, maxMsgs) msg := "oops, Houston we got 99 problems." - l.Error(msg, errors.New(msg)) + l.Error(msg, "err", errors.New(msg)) attest.Subsequence(t, w.String(), msg) @@ -165,7 +165,7 @@ func TestLogger(t *testing.T) { infoMsg := "hello world" l.Info(infoMsg) - l.Error("some-err", errors.New("this-ting-is-bad")) + l.Error("some-err", "err", errors.New("this-ting-is-bad")) attest.Subsequence(t, w.String(), logIDFieldName) attest.Subsequence(t, w.String(), "level") @@ -200,7 +200,7 @@ func TestLogger(t *testing.T) { l.Info(infoMsg) } errMsg := "oops, Houston we got 99 problems." - l.Error("somer-error", errors.New(errMsg)) + l.Error("somer-error", "err", errors.New(errMsg)) attest.False(t, strings.Contains(w.String(), "hello world : 1")) attest.False(t, strings.Contains(w.String(), "hello world : 2")) @@ -219,7 +219,7 @@ func TestLogger(t *testing.T) { l1 := New(context.Background(), w, maxMsgs) _, ok := l1.Handler().(*handler) attest.True(t, ok) - l1.Error("hey1", errors.New("cool1")) + l1.Error("hey1", "err", errors.New("cool1")) attest.Subsequence(t, w.String(), "hey1") attest.Subsequence(t, w.String(), logIDFieldName) @@ -230,7 +230,7 @@ func TestLogger(t *testing.T) { w.Reset() // clear buffer. l2 := l1.With("category", "load_balancers") - l2.Error("hey2", errors.New("cool2")) + l2.Error("hey2", "err", errors.New("cool2")) attest.Subsequence(t, w.String(), "hey2") attest.False(t, strings.Contains(w.String(), "hey1")) // hey1 is not loggged here. attest.Subsequence(t, w.String(), logIDFieldName) @@ -278,7 +278,7 @@ func TestLogger(t *testing.T) { { logger2 := l.With("name", "Kim") errMsg := "oops, Houston we got 99 problems." - logger2.Error("some-error", errors.New(errMsg)) + logger2.Error("some-error", "err", errors.New(errMsg)) attest.False(t, strings.Contains(w.String(), "hello world : 0")) attest.False(t, strings.Contains(w.String(), "hello world : 1"))