Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issues/443: errors; add equivalent functions from standard library #449

Merged
merged 14 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 12 additions & 8 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,18 @@ 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 {
*errp = wrap(*errp, 3)
}
}

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.
Expand Down Expand Up @@ -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 ""
}
2 changes: 2 additions & 0 deletions errors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
})
}
65 changes: 65 additions & 0 deletions errors/join.go
Original file line number Diff line number Diff line change
@@ -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
}
129 changes: 129 additions & 0 deletions errors/join_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
})
}
26 changes: 26 additions & 0 deletions errors/stdlib.go
Original file line number Diff line number Diff line change
@@ -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...)
}
26 changes: 26 additions & 0 deletions errors/stdlib_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
}
8 changes: 4 additions & 4 deletions log/log_benchmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
})
Expand All @@ -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)
}
}
})
Expand Down Expand Up @@ -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)
}
})

Expand All @@ -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)
}
})

Expand Down
Loading
Loading