Skip to content

Commit

Permalink
feat: join error (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gogomoe authored Jun 13, 2024
1 parent e59a5ad commit c2b8c9a
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 15 deletions.
11 changes: 5 additions & 6 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,21 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: 1.22
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v1.50.1
version: v1.59.0
args: --config .golangci.yaml
unit-test:
name: unit-test
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.19
- name: Set up Go 1.22
uses: actions/setup-go@v2
with:
go-version: 1.19
go-version: 1.22
- name: Check out code
uses: actions/checkout@v2
- name: unit-test
Expand Down
14 changes: 14 additions & 0 deletions eris.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package eris

import (
"errors"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -95,6 +96,19 @@ func Errorf(format string, args ...any) statusError {
}
}

type joinError interface {
Unwrap() []error
}

// Join returns an error that wraps the given errors.
func Join(errs ...error) error {
internal := errors.Join(errs...)
if internal == nil {
return nil
}
return wrap(internal, "join error", DEFAULT_ERROR_CODE_NEW)
}

// Wrap adds additional context to all error types while maintaining the type of the original error. Adds a default error code 'internal'
//
// This method behaves differently for each error type. For root errors, the stack trace is reset to the current
Expand Down
23 changes: 23 additions & 0 deletions eris_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -938,3 +938,26 @@ func TestWrapType(t *testing.T) {
t.Errorf("expected nil error if wrap nil error, but error was %v", erisErr)
}
}

func TestJoinError(t *testing.T) {
err := eris.Join(nil, nil)
if err != nil {
t.Errorf("join nil should be nil")
}
err = eris.Join(nil, fmt.Errorf("external error"))
if err == nil {
t.Errorf("join error should be error")
}
err = eris.Join(fmt.Errorf("err1"), nil, fmt.Errorf("err2"))
if err == nil {
t.Errorf("join errors should be error")
}
type joinError interface {
Unwrap() []error
}
if joinErr, ok := eris.Unwrap(err).(joinError); !ok {
if len(joinErr.Unwrap()) != 2 {
t.Errorf("join 2 errors should be 2 errors")
}
}
}
49 changes: 42 additions & 7 deletions format.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package eris

import (
"fmt"
"strings"
)

// FormatOptions defines output options like omitting stack traces and inverting the error or stack order.
Expand Down Expand Up @@ -103,8 +104,11 @@ func ToCustomString(err error, format StringFormat) string {
if format.Options.InvertOutput {
errSep := false
if format.Options.WithExternal && upErr.ErrExternal != nil {
str += formatExternalStr(upErr.ErrExternal, format.Options.WithTrace)
if (format.Options.WithTrace && len(upErr.ErrRoot.Stack) > 0) || upErr.ErrRoot.Msg != "" {
externalStr := formatExternalStr(upErr.ErrExternal, format.Options.WithTrace)
str += externalStr
if strings.Contains(externalStr, "\n") {
str += "\n"
} else if (format.Options.WithTrace && len(upErr.ErrRoot.Stack) > 0) || upErr.ErrRoot.Msg != "" {
errSep = true
str += format.ErrorSep
}
Expand All @@ -124,10 +128,13 @@ func ToCustomString(err error, format StringFormat) string {
}
str += upErr.ErrRoot.formatStr(format)
if format.Options.WithExternal && upErr.ErrExternal != nil {
if (format.Options.WithTrace && len(upErr.ErrRoot.Stack) > 0) || upErr.ErrRoot.Msg != "" {
externalStr := formatExternalStr(upErr.ErrExternal, format.Options.WithTrace)
if strings.Contains(externalStr, "\n") {
str += "\n"
} else if (format.Options.WithTrace && len(upErr.ErrRoot.Stack) > 0) || upErr.ErrRoot.Msg != "" {
str += format.ErrorSep
}
str += formatExternalStr(upErr.ErrExternal, format.Options.WithTrace)
str += externalStr
}
}

Expand Down Expand Up @@ -250,7 +257,17 @@ func ToCustomJSON(err error, format JSONFormat) map[string]any {

jsonMap := make(map[string]any)
if format.Options.WithExternal && upErr.ErrExternal != nil {
jsonMap["external"] = formatExternalStr(upErr.ErrExternal, format.Options.WithTrace)

join, ok := upErr.ErrExternal.(joinError)
if !ok {
jsonMap["external"] = formatExternalStr(upErr.ErrExternal, format.Options.WithTrace)
} else {
var externals []map[string]any
for _, e := range join.Unwrap() {
externals = append(externals, ToCustomJSON(e, format))
}
jsonMap["externals"] = externals
}
}

if upErr.ErrRoot.Msg != "" || len(upErr.ErrRoot.Stack) > 0 {
Expand Down Expand Up @@ -311,10 +328,28 @@ type UnpackedError struct {

// String formatter for external errors.
func formatExternalStr(err error, withTrace bool) string {
type joinError interface {
Unwrap() []error
}

format := "%v"
if withTrace {
return fmt.Sprintf("%+v", err)
format = "%+v"
}
join, ok := err.(joinError)
if !ok {
return fmt.Sprintf(format, err)
}

var strs []string
for i, e := range join.Unwrap() {
lines := strings.Split(fmt.Sprintf(format, e), "\n")
for no, line := range lines {
lines[no] = fmt.Sprintf("\t%s", line)
}
strs = append(strs, fmt.Sprintf("%d>", i)+strings.Join(lines, "\n"))
}
return fmt.Sprint(err)
return strings.Join(strs, "\n")
}

// ErrRoot represents an error stack and the accompanying message.
Expand Down
193 changes: 191 additions & 2 deletions format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package eris_test
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"regexp"
"testing"

"github.com/risingwavelabs/eris"
Expand Down Expand Up @@ -362,7 +364,7 @@ func TestFormatJSONWithStack(t *testing.T) {
t.Fatalf("%v: expected a 'message' field in the output but didn't find one { %v }", desc, errJSON)
}
if rootMap["message"] != tt.rootOutput["message"] {
t.Errorf("%v: expected { %v } got { %v }", desc, rootMap["message"], tt.rootOutput["message"])
t.Errorf("%v: expected { %v } got { %v }", desc, tt.rootOutput["message"], rootMap["message"])
}
if _, exists := rootMap["stack"]; !exists {
t.Fatalf("%v: expected a 'stack' field in the output but didn't find one { %v }", desc, errJSON)
Expand All @@ -381,7 +383,7 @@ func TestFormatJSONWithStack(t *testing.T) {
t.Fatalf("%v: expected a 'message' field in the output but didn't find one { %v }", desc, errJSON)
}
if wrapMap[i]["message"] != tt.wrapOutput[i]["message"] {
t.Errorf("%v: expected { %v } got { %v }", desc, wrapMap[i]["message"], tt.wrapOutput[i]["message"])
t.Errorf("%v: expected { %v } got { %v }", desc, tt.wrapOutput[i]["message"], wrapMap[i]["message"])
}
if _, exists := wrapMap[i]["stack"]; !exists {
t.Fatalf("%v: expected a 'stack' field in the output but didn't find one { %v }", desc, errJSON)
Expand All @@ -393,3 +395,190 @@ func TestFormatJSONWithStack(t *testing.T) {
})
}
}

func TestFormatJoinError(t *testing.T) {
tests := map[string]struct {
input error
withTrace bool
withExternal bool
stringOutput string
regexOutput *regexp.Regexp
rootOutput map[string]any
wrapOutput []map[string]any
externalsOutput []map[string]any
}{
"without trace and external": {
input: eris.Wrap(eris.Join(
fmt.Errorf("fmt error"),
eris.Wrap(eris.Wrap(fmt.Errorf("external"), "wrap2"), "wrap1"),
eris.New("eris error"),
), "outer wrap"),
withTrace: false,
withExternal: false,
stringOutput: "code(internal) outer wrap: code(unknown) join error",
rootOutput: map[string]any{
"message": "join error",
},
wrapOutput: []map[string]any{
{
"message": "outer wrap",
},
},
},
"without trace": {
input: eris.Wrap(eris.Join(
fmt.Errorf("fmt error"),
eris.Wrap(eris.Wrap(fmt.Errorf("external"), "wrap2"), "wrap1"),
eris.New("eris error"),
), "outer wrap"),
withTrace: false,
withExternal: true,
stringOutput: `code(internal) outer wrap: code(unknown) join error
0> fmt error
1> code(internal) wrap1: code(internal) wrap2: external
2> code(unknown) eris error`,
rootOutput: map[string]any{
"message": "join error",
},
wrapOutput: []map[string]any{
{
"message": "outer wrap",
},
},
externalsOutput: []map[string]any{
{
"external": "fmt error",
}, {
"external": "external",
"root": map[string]any{},
"wrap": []map[string]any{},
}, {
"root": map[string]any{},
},
},
},
"with trace": {
input: eris.Wrap(eris.Join(
fmt.Errorf("fmt error"),
eris.Wrap(eris.Wrap(fmt.Errorf("external"), "wrap2"), "wrap1"),
eris.New("eris error"),
), "outer wrap"),
withTrace: true,
withExternal: true,
regexOutput: regexp.MustCompile(`code\(internal\) outer wrap
eris_test\.TestFormatJoinError:\S+:\d+
code\(unknown\) join error
eris_test\.TestFormatJoinError:\S+:\d+
eris_test\.TestFormatJoinError:\S+:\d+
0> fmt error
1> code\(internal\) wrap1
eris_test\.TestFormatJoinError:\S+:\d+
code\(internal\) wrap2
eris_test\.TestFormatJoinError:\S+:\d+
eris_test\.TestFormatJoinError:\S+:\d+
external
2> code\(unknown\) eris error
eris_test\.TestFormatJoinError:\S+:\d+`),
rootOutput: map[string]any{
"message": "join error",
},
wrapOutput: []map[string]any{
{
"message": "outer wrap",
},
},
externalsOutput: []map[string]any{
{
"external": "fmt error",
}, {
"external": "external",
"root": map[string]any{},
"wrap": []map[string]any{},
}, {
"root": map[string]any{},
},
},
},
}

for desc, tt := range tests {
t.Run(desc, func(t *testing.T) {
errStr := eris.ToCustomString(tt.input, eris.NewDefaultStringFormat(eris.FormatOptions{
WithTrace: tt.withTrace,
WithExternal: tt.withExternal,
}))
if tt.stringOutput != "" && tt.stringOutput != errStr {
t.Errorf("%v: expected { %v } got { %v }", desc, tt.stringOutput, errStr)
}
if tt.regexOutput != nil && !tt.regexOutput.MatchString(errStr) {
t.Errorf("%v: expected match { %v } got { %v }", desc, tt.regexOutput.String(), errStr)
}

errJSON := eris.ToCustomJSON(tt.input, eris.NewDefaultJSONFormat(eris.FormatOptions{
WithTrace: tt.withTrace,
WithExternal: tt.withExternal,
}))

// make sure messages are correct and stack elements exist (actual stack validation is in stack_test.go)
if rootMap, ok := errJSON["root"].(map[string]any); ok {
if _, exists := rootMap["message"]; !exists {
t.Fatalf("%v: expected a 'message' field in the output but didn't find one { %v }", desc, errJSON)
}
if rootMap["message"] != tt.rootOutput["message"] {
t.Errorf("%v: expected { %v } got { %v }", desc, tt.rootOutput["message"], rootMap["message"])
}
if _, exists := rootMap["stack"]; tt.withTrace && !exists {
t.Fatalf("%v: expected a 'stack' field in the output but didn't find one { %v }", desc, errJSON)
}
} else {
t.Errorf("%v: expected root error is malformed { %v }", desc, errJSON)
}

// make sure messages are correct and stack elements exist (actual stack validation is in stack_test.go)
if wrapMap, ok := errJSON["wrap"].([]map[string]any); ok {
if len(tt.wrapOutput) != len(wrapMap) {
t.Fatalf("%v: expected number of wrap layers { %v } doesn't match actual { %v }", desc, len(tt.wrapOutput), len(wrapMap))
}
for i := 0; i < len(wrapMap); i++ {
if _, exists := wrapMap[i]["message"]; !exists {
t.Fatalf("%v: expected a 'message' field in the output but didn't find one { %v }", desc, errJSON)
}
if wrapMap[i]["message"] != tt.wrapOutput[i]["message"] {
t.Errorf("%v: expected { %v } got { %v }", desc, tt.wrapOutput[i]["message"], wrapMap[i]["message"])
}
if _, exists := wrapMap[i]["stack"]; tt.withTrace && !exists {
t.Fatalf("%v: expected a 'stack' field in the output but didn't find one { %v }", desc, errJSON)
}
}
} else {
t.Errorf("%v: expected wrap error is malformed { %v }", desc, errJSON)
}

if externalsMap, ok := errJSON["externals"].([]map[string]any); ok {
if len(tt.externalsOutput) != len(externalsMap) {
t.Fatalf("%v: expected number of externals errors { %v } doesn't match actual { %v }", desc, len(tt.externalsOutput), len(externalsMap))
}
for i, externalsOutputItem := range tt.externalsOutput {
for key, val := range externalsOutputItem {
switch val.(type) {
case string:
if externalsMap[i][key] != val {
t.Errorf("%v: expected externals[%d][%s] { %v } got { %v }", desc, i, key, val, externalsMap[i][key])
}
case map[string]any:
if _, ok := externalsMap[i][key].(map[string]any); !ok {
t.Errorf("%v: expected externals[%d][%s] is object got { %v }", desc, i, key, externalsMap[i][key])
}
case []map[string]any:
if _, ok := externalsMap[i][key].([]map[string]any); !ok {
t.Errorf("%v: expected externals[%d][%s] is object array got { %v }", desc, i, key, externalsMap[i][key])
}
}
}
}
} else if tt.withExternal {
t.Errorf("%v: expected externals error is malformed { %v }", desc, errJSON)
}
})
}
}

0 comments on commit c2b8c9a

Please sign in to comment.