Skip to content

Commit

Permalink
issues/443: errors; add equivalent functions from standard library (#449
Browse files Browse the repository at this point in the history
)

- With this, you only need to inport ong/errors without needing to import standard lib
- Fixes: #443
  • Loading branch information
komuw authored Jun 11, 2024
1 parent 94b4f3f commit e6ff616
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 23 deletions.
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

0 comments on commit e6ff616

Please sign in to comment.