├── .github └── workflows │ ├── .editorconfig │ ├── checks.yml │ └── ci.yml ├── .gitignore ├── go.mod ├── .editorconfig ├── .fossa.yml ├── .plzconfig ├── utils └── keyval │ ├── to_map_test.go │ └── to_map.go ├── tests ├── errors_std_test.go ├── example_test.go ├── wrap_test.go ├── errors_test.go └── format_test.go ├── wrap_go1_13.go ├── LICENSE ├── go.sum ├── wrap.go ├── errors_test.go ├── BUILD ├── errors_with_message_test.go ├── stack.go ├── .golangci.yml ├── error_details.go ├── error_details_test.go ├── errors_multi.go ├── stack_test.go ├── match ├── match.go └── match_test.go ├── pleasew ├── wrap_go1_12.go ├── LICENSE_THIRD_PARTY ├── CHANGELOG.md ├── errors_new_test.go ├── example_test.go ├── format_test.go ├── wrap_test.go ├── README.md ├── errors_with_stack_test.go ├── errors_wrap_test.go └── errors.go /.github/workflows/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.yml] 2 | indent_size = 2 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp/ 2 | /vendor/ 3 | 4 | # Please output directory and local configuration 5 | plz-out 6 | .plzconfig.local 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module emperror.dev/errors 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/pkg/errors v0.9.1 7 | go.uber.org/multierr v1.6.0 8 | ) 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.go] 12 | indent_style = tab 13 | -------------------------------------------------------------------------------- /.fossa.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | cli: 3 | server: https://app.fossa.com 4 | fetcher: custom 5 | project: emperror.dev/errors 6 | analyze: 7 | modules: 8 | - name: emperror.dev/errors 9 | type: go 10 | target: emperror.dev/errors 11 | strategy: gomodules 12 | path: . 13 | options: 14 | allow-unresolved: true 15 | -------------------------------------------------------------------------------- /.plzconfig: -------------------------------------------------------------------------------- 1 | [please] 2 | version = 15.5.0 3 | 4 | [go] 5 | importpath = emperror.dev/errors 6 | 7 | [buildconfig] 8 | golangci-lint-version = 1.31.0 9 | gotestsum-version = 0.5.3 10 | 11 | [alias "lint"] 12 | desc = Runs linters for this repo 13 | cmd = run ///pleasings2//tools/go:golangci-lint -- run 14 | 15 | [alias "gotest"] 16 | desc = Runs tests for this repo 17 | cmd = run ///pleasings2//tools/go:gotestsum -- --no-summary=skipped --format short -- -race -covermode=atomic -coverprofile=plz-out/log/coverage.txt ./... 18 | 19 | [alias "release"] 20 | desc = Release a new version 21 | cmd = run ///pleasings2//tools/misc:releaser -- 22 | -------------------------------------------------------------------------------- /utils/keyval/to_map_test.go: -------------------------------------------------------------------------------- 1 | package keyval 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestToMap(t *testing.T) { 9 | err := errors.New("error") 10 | actual := ToMap([]interface{}{"key", "value", "error", err}) 11 | 12 | expected := map[string]interface{}{ 13 | "key": "value", 14 | "error": err, 15 | } 16 | 17 | if len(expected) != len(actual) { 18 | t.Fatalf("expected log fields to be equal\ngot: %v\nwant: %v", actual, expected) 19 | } 20 | 21 | for key, value := range expected { 22 | if actual[key] != value { 23 | t.Errorf("expected log fields to be equal\ngot: %v\nwant: %v", actual, expected) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | name: FOSSA 11 | runs-on: ubuntu-latest 12 | env: 13 | GOFLAGS: -mod=readonly 14 | 15 | steps: 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.15 20 | 21 | - name: Set up GOPATH 22 | run: echo "::set-env name=GOPATH::$(go env GOPATH)" 23 | 24 | - name: Checkout code 25 | uses: actions/checkout@v2 26 | 27 | - name: Set up Please 28 | uses: sagikazarmark/setup-please-action@v0 29 | 30 | - name: Build 31 | run: go build ./... 32 | 33 | - name: Analyze 34 | run: plz run ///pleasings2//tools/misc:fossa -- analyze 35 | env: 36 | FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} 37 | -------------------------------------------------------------------------------- /tests/errors_std_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package errors_test 6 | 7 | import ( 8 | "emperror.dev/errors" 9 | 10 | "testing" 11 | ) 12 | 13 | func TestNewEqual(t *testing.T) { 14 | // Different allocations should not be equal. 15 | if errors.NewPlain("abc") == errors.NewPlain("abc") { 16 | t.Errorf(`New("abc") == New("abc")`) 17 | } 18 | if errors.NewPlain("abc") == errors.NewPlain("xyz") { 19 | t.Errorf(`New("abc") == New("xyz")`) 20 | } 21 | 22 | // Same allocation should be equal to itself (not crash). 23 | err := errors.NewPlain("jkl") 24 | if err != err { 25 | t.Errorf(`err != err`) 26 | } 27 | } 28 | 29 | func TestErrorMethod(t *testing.T) { 30 | err := errors.NewPlain("abc") 31 | if err.Error() != "abc" { 32 | t.Errorf(`New("abc").Error() = %q, want %q`, err.Error(), "abc") 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /wrap_go1_13.go: -------------------------------------------------------------------------------- 1 | //go:build go1.13 2 | // +build go1.13 3 | 4 | package errors 5 | 6 | import ( 7 | "errors" 8 | ) 9 | 10 | // Is reports whether any error in err's chain matches target. 11 | // 12 | // An error is considered to match a target if it is equal to that target or if 13 | // it implements a method Is(error) bool such that Is(target) returns true. 14 | func Is(err, target error) bool { 15 | return errors.Is(err, target) 16 | } 17 | 18 | // As finds the first error in err's chain that matches the type to which target 19 | // points, and if so, sets the target to its value and returns true. An error 20 | // matches a type if it is assignable to the target type, or if it has a method 21 | // As(interface{}) bool such that As(target) returns true. As will panic if target 22 | // is not a non-nil pointer to a type which implements error or is of interface type. 23 | // 24 | // The As method should set the target to its value and return true if err 25 | // matches the type to which target points. 26 | func As(err error, target interface{}) bool { 27 | return errors.As(err, target) 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Márk Sági-Kazár 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /utils/keyval/to_map.go: -------------------------------------------------------------------------------- 1 | package keyval 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // ToMap creates a map of key-value pairs from a variadic key-value pair slice. 9 | // 10 | // The implementation bellow is from go-kit's JSON logger. 11 | func ToMap(kvs []interface{}) map[string]interface{} { 12 | m := map[string]interface{}{} 13 | 14 | if len(kvs) == 0 { 15 | return m 16 | } 17 | 18 | if len(kvs)%2 == 1 { 19 | kvs = append(kvs, nil) 20 | } 21 | 22 | for i := 0; i < len(kvs); i += 2 { 23 | merge(m, kvs[i], kvs[i+1]) 24 | } 25 | 26 | return m 27 | } 28 | 29 | func merge(dst map[string]interface{}, k, v interface{}) { 30 | var key string 31 | 32 | switch x := k.(type) { 33 | case string: 34 | key = x 35 | case fmt.Stringer: 36 | key = safeString(x) 37 | default: 38 | key = fmt.Sprint(x) 39 | } 40 | 41 | dst[key] = v 42 | } 43 | 44 | func safeString(str fmt.Stringer) (s string) { 45 | defer func() { 46 | if panicVal := recover(); panicVal != nil { 47 | if v := reflect.ValueOf(str); v.Kind() == reflect.Ptr && v.IsNil() { 48 | s = "NULL" 49 | } else { 50 | panic(panicVal) 51 | } 52 | } 53 | }() 54 | 55 | s = str.String() 56 | 57 | return 58 | } 59 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 5 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 10 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 11 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 12 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 13 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 14 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 15 | -------------------------------------------------------------------------------- /wrap.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // Unwrap returns the result of calling the Unwrap method on err, if err implements 4 | // Unwrap. Otherwise, Unwrap returns nil. 5 | // 6 | // It supports both Go 1.13 Unwrap and github.com/pkg/errors.Causer interfaces 7 | // (the former takes precedence). 8 | func Unwrap(err error) error { 9 | switch e := err.(type) { 10 | case interface{ Unwrap() error }: 11 | return e.Unwrap() 12 | 13 | case interface{ Cause() error }: 14 | return e.Cause() 15 | } 16 | 17 | return nil 18 | } 19 | 20 | // UnwrapEach loops through an error chain and calls a function for each of them. 21 | // 22 | // The provided function can return false to break the loop before it reaches the end of the chain. 23 | // 24 | // It supports both Go 1.13 errors.Wrapper and github.com/pkg/errors.Causer interfaces 25 | // (the former takes precedence). 26 | func UnwrapEach(err error, fn func(err error) bool) { 27 | for err != nil { 28 | continueLoop := fn(err) 29 | if !continueLoop { 30 | break 31 | } 32 | 33 | err = Unwrap(err) 34 | } 35 | } 36 | 37 | // Cause returns the last error (root cause) in an err's chain. 38 | // If err has no chain, it is returned directly. 39 | // 40 | // It supports both Go 1.13 errors.Wrapper and github.com/pkg/errors.Causer interfaces 41 | // (the former takes precedence). 42 | func Cause(err error) error { 43 | for { 44 | cause := Unwrap(err) 45 | if cause == nil { 46 | break 47 | } 48 | 49 | err = cause 50 | } 51 | 52 | return err 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | go: ['1.11', '1.12', '1.13', '1.14', '1.15', '1.16', '1.17', '1.18'] 16 | env: 17 | GOFLAGS: -mod=readonly 18 | GOPROXY: https://proxy.golang.org 19 | 20 | steps: 21 | - name: Set up Go 22 | uses: actions/setup-go@v2 23 | with: 24 | go-version: ${{ matrix.go }} 25 | 26 | - name: Checkout code 27 | uses: actions/checkout@v2 28 | 29 | - name: Set up Please 30 | uses: sagikazarmark/setup-please-action@v0 31 | 32 | - name: Test 33 | run: | 34 | plz test //... 35 | plz gotest 36 | 37 | - name: Upload coverage 38 | uses: codecov/codecov-action@v1 39 | if: always() 40 | with: 41 | token: ${{ secrets.CODECOV_TOKEN }} 42 | file: plz-out/log/coverage.txt 43 | 44 | lint: 45 | name: Lint 46 | runs-on: ubuntu-latest 47 | env: 48 | GOFLAGS: -mod=readonly 49 | 50 | steps: 51 | - name: Set up Go 52 | uses: actions/setup-go@v2 53 | with: 54 | go-version: 1.15 55 | 56 | - name: Checkout code 57 | uses: actions/checkout@v2 58 | 59 | - name: Set up Please 60 | uses: sagikazarmark/setup-please-action@v0 61 | 62 | - name: Lint 63 | run: plz lint 64 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func checkErrorMessage(t *testing.T, err error, message string) { 8 | t.Helper() 9 | 10 | if got, want := err.Error(), message; got != want { 11 | t.Errorf("error message does not match the expectd\nactual: %s\nexpected: %s", got, want) 12 | } 13 | } 14 | 15 | func checkFormat(t *testing.T, err error, formats map[string][]string) { 16 | t.Helper() 17 | 18 | i := 1 19 | 20 | for format, want := range formats { 21 | testFormatCompleteCompare(t, i, err, format, want, true) 22 | 23 | i++ 24 | } 25 | } 26 | 27 | func checkErrorNil(t *testing.T, err error) { 28 | t.Helper() 29 | 30 | if err != nil { 31 | t.Errorf("nil error is expected to result in nil\nactual: %#v", err) 32 | } 33 | } 34 | 35 | func checkUnwrap(t *testing.T, err error, origErr error) { 36 | t.Helper() 37 | 38 | if err, ok := err.(interface{ Unwrap() error }); ok { 39 | if got, want := err.Unwrap(), origErr; got != want { 40 | t.Errorf("error does not match the expected one\nactual: %#v\nexpected: %#v", got, want) 41 | } 42 | } else { 43 | t.Fatal("error does not implement the wrapper (interface{ Unwrap() error}) interface") 44 | } 45 | 46 | if err, ok := err.(interface{ Cause() error }); ok { 47 | if got, want := err.Cause(), origErr; got != want { 48 | t.Errorf("error does not match the expected one\nactual: %#v\nexpected: %#v", got, want) 49 | } 50 | } else { 51 | t.Fatal("error does not implement the causer (interface{ Cause() error}) interface") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | github_repo( 2 | name = "pleasings2", 3 | repo = "sagikazarmark/mypleasings", 4 | revision = "f8a12721c6f929db3e227e07c152d428ac47ab1b", 5 | ) 6 | 7 | sh_cmd( 8 | name = "download_tests", 9 | shell = "/usr/bin/env bash", 10 | labels = ["manual"], 11 | cmd = """ 12 | mkdir -p tests 13 | # curl https://raw.githubusercontent.com/pkg/errors/master/errors_test.go | sed \\\\$'s|"testing"|"testing"\\n\\n\\t. "emperror.dev/errors"|; s|github.com/pkg/errors|emperror.dev/errors/tests|g' > tests/errors_test.go 14 | curl https://raw.githubusercontent.com/pkg/errors/master/errors_test.go | sed \\\\$'s|"errors"|. "emperror.dev/errors"|; s|/github.com/pkg/errors||g; s|github.com/pkg/errors|emperror.dev/errors/tests|g; s|errors\.New|NewPlain|g; s|x := New("error")|x := NewPlain("error")|g' > tests/errors_test.go 15 | curl https://raw.githubusercontent.com/pkg/errors/master/example_test.go | sed 's|"github.com/pkg/errors"|"emperror.dev/errors"|' > tests/example_test.go 16 | curl https://raw.githubusercontent.com/pkg/errors/master/format_test.go | sed \\\\$'s|"errors"|. "emperror.dev/errors"|; s|/github.com/pkg/errors||g; s|github.com/pkg/errors|emperror.dev/errors/tests|g; s|errors\.New|NewPlain|g' > tests/format_test.go 17 | 18 | curl https://raw.githubusercontent.com/golang/go/master/src/errors/errors_test.go | sed \\\\$'s|"errors"|"emperror.dev/errors"|; s|errors\.New|errors.NewPlain|g; s|"fmt"||g' | head -35 > tests/errors_std_test.go 19 | echo -e "// +build go1.13\n\n\\\$(curl https://raw.githubusercontent.com/golang/go/master/src/errors/wrap_test.go)" | sed \\\\$'s|"errors"|"emperror.dev/errors"|; s|errors\.New|errors.NewPlain|g' > tests/wrap_test.go 20 | """, 21 | ) 22 | -------------------------------------------------------------------------------- /errors_with_message_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWithMessage(t *testing.T) { 8 | origErr := NewPlain("something went wrong") 9 | err := WithMessage(origErr, "error") 10 | 11 | t.Parallel() 12 | 13 | t.Run("error_message", func(t *testing.T) { 14 | checkErrorMessage(t, err, "error: something went wrong") 15 | }) 16 | 17 | t.Run("unwrap", func(t *testing.T) { 18 | checkUnwrap(t, err, origErr) 19 | }) 20 | 21 | t.Run("format", func(t *testing.T) { 22 | checkFormat(t, err, map[string][]string{ 23 | "%s": {"error: something went wrong"}, 24 | "%q": {`error: something went wrong`}, // TODO: quotes? 25 | "%v": {"error: something went wrong"}, 26 | "%+v": {"something went wrong", "error"}, 27 | }) 28 | }) 29 | 30 | t.Run("nil", func(t *testing.T) { 31 | checkErrorNil(t, WithMessage(nil, "error")) 32 | }) 33 | } 34 | 35 | func TestWithMessagef(t *testing.T) { 36 | origErr := NewPlain("something went wrong") 37 | err := WithMessagef(origErr, "%s", "error") 38 | 39 | t.Parallel() 40 | 41 | t.Run("error_message", func(t *testing.T) { 42 | checkErrorMessage(t, err, "error: something went wrong") 43 | }) 44 | 45 | t.Run("unwrap", func(t *testing.T) { 46 | checkUnwrap(t, err, origErr) 47 | }) 48 | 49 | t.Run("format", func(t *testing.T) { 50 | checkFormat(t, err, map[string][]string{ 51 | "%s": {"error: something went wrong"}, 52 | "%q": {`error: something went wrong`}, // TODO: quotes? 53 | "%v": {"error: something went wrong"}, 54 | "%+v": {"something went wrong", "error"}, 55 | }) 56 | }) 57 | 58 | t.Run("nil", func(t *testing.T) { 59 | checkErrorNil(t, WithMessagef(nil, "%s", "error")) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /stack.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Dave Cheney . All rights reserved. 2 | // Use of this source code (or at least parts of it) is governed by a BSD-style 3 | // license that can be found in the LICENSE_THIRD_PARTY file. 4 | 5 | package errors 6 | 7 | import ( 8 | "fmt" 9 | "runtime" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // StackTrace is stack of Frames from innermost (newest) to outermost (oldest). 15 | // 16 | // It is an alias of the same type in github.com/pkg/errors. 17 | type StackTrace = errors.StackTrace 18 | 19 | // Frame represents a program counter inside a stack frame. 20 | // For historical reasons if Frame is interpreted as a uintptr 21 | // its value represents the program counter + 1. 22 | // 23 | // It is an alias of the same type in github.com/pkg/errors. 24 | type Frame = errors.Frame 25 | 26 | // stack represents a stack of program counters. 27 | // 28 | // It is a duplicate of the same (sadly unexported) type in github.com/pkg/errors. 29 | type stack []uintptr 30 | 31 | // nolint: gocritic 32 | func (s *stack) Format(st fmt.State, verb rune) { 33 | switch verb { 34 | case 'v': 35 | switch { 36 | case st.Flag('+'): 37 | for _, pc := range *s { 38 | f := Frame(pc) 39 | fmt.Fprintf(st, "\n%+v", f) 40 | } 41 | } 42 | } 43 | } 44 | 45 | func (s *stack) StackTrace() StackTrace { 46 | f := make([]Frame, len(*s)) 47 | for i := 0; i < len(f); i++ { 48 | f[i] = Frame((*s)[i]) 49 | } 50 | 51 | return f 52 | } 53 | 54 | // callers is based on the function with the same name in github.com/pkg/errors, 55 | // but accepts a custom depth (useful to customize the error constructor caller depth). 56 | func callers(depth int) *stack { 57 | const maxDepth = 32 58 | 59 | var pcs [maxDepth]uintptr 60 | 61 | n := runtime.Callers(2+depth, pcs[:]) 62 | st := make(stack, n) 63 | copy(st, pcs[:n]) 64 | 65 | return &st 66 | } 67 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - tests 4 | skip-files: 5 | - format_test.go 6 | - wrap_go1_12.go 7 | - wrap_go1_12_test.go 8 | - wrap_test.go 9 | 10 | linters-settings: 11 | gci: 12 | local-prefixes: emperror.dev/errors 13 | goimports: 14 | local-prefixes: emperror.dev/errors 15 | golint: 16 | min-confidence: 0 17 | 18 | linters: 19 | disable-all: true 20 | enable: 21 | - bodyclose 22 | - deadcode 23 | - dogsled 24 | - dupl 25 | - errcheck 26 | - exhaustive 27 | - exportloopref 28 | - gci 29 | - gochecknoglobals 30 | - gochecknoinits 31 | - gocognit 32 | - goconst 33 | - gocritic 34 | - gocyclo 35 | - godot 36 | - gofmt 37 | - gofumpt 38 | - goimports 39 | - golint 40 | - goprintffuncname 41 | - gosec 42 | - gosimple 43 | - govet 44 | - ineffassign 45 | - lll 46 | - misspell 47 | - nakedret 48 | - nlreturn 49 | - noctx 50 | # - nolintlint 51 | - prealloc 52 | - rowserrcheck 53 | - scopelint 54 | - sqlclosecheck 55 | - staticcheck 56 | - structcheck 57 | - stylecheck 58 | - typecheck 59 | - unconvert 60 | - unparam 61 | - unused 62 | - varcheck 63 | - whitespace 64 | 65 | # unused 66 | # - depguard 67 | # - goheader 68 | # - gomodguard 69 | 70 | # don't enable: 71 | # - asciicheck 72 | # - funlen 73 | # - godox 74 | # - goerr113 75 | # - gomnd 76 | # - interfacer 77 | # - maligned 78 | # - nestif 79 | # - testpackage 80 | # - wsl 81 | 82 | issues: 83 | exclude-rules: 84 | - text: "package comment should not have leading space" 85 | linters: 86 | - golint 87 | -------------------------------------------------------------------------------- /error_details.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // WithDetails annotates err with with arbitrary key-value pairs. 8 | func WithDetails(err error, details ...interface{}) error { 9 | if err == nil { 10 | return nil 11 | } 12 | 13 | if len(details) == 0 { 14 | return err 15 | } 16 | 17 | if len(details)%2 != 0 { 18 | details = append(details, nil) 19 | } 20 | 21 | var w *withDetails 22 | if !As(err, &w) { 23 | w = &withDetails{ 24 | error: err, 25 | } 26 | 27 | err = w 28 | } 29 | 30 | // Limiting the capacity of the stored keyvals ensures that a new 31 | // backing array is created if the slice must grow in With. 32 | // Using the extra capacity without copying risks a data race. 33 | d := append(w.details, details...) // nolint:gocritic 34 | w.details = d[:len(d):len(d)] 35 | 36 | return err 37 | } 38 | 39 | // GetDetails extracts the key-value pairs from err's chain. 40 | func GetDetails(err error) []interface{} { 41 | var details []interface{} 42 | 43 | // Usually there is only one error with details (when using the WithDetails API), 44 | // but errors themselves can also implement the details interface exposing their attributes. 45 | UnwrapEach(err, func(err error) bool { 46 | if derr, ok := err.(interface{ Details() []interface{} }); ok { 47 | details = append(derr.Details(), details...) 48 | } 49 | 50 | return true 51 | }) 52 | 53 | return details 54 | } 55 | 56 | // withDetails annotates an error with arbitrary key-value pairs. 57 | type withDetails struct { 58 | error error 59 | details []interface{} 60 | } 61 | 62 | func (w *withDetails) Error() string { return w.error.Error() } 63 | func (w *withDetails) Cause() error { return w.error } 64 | func (w *withDetails) Unwrap() error { return w.error } 65 | 66 | // Details returns the appended details. 67 | func (w *withDetails) Details() []interface{} { 68 | return w.details 69 | } 70 | 71 | func (w *withDetails) Format(s fmt.State, verb rune) { 72 | switch verb { 73 | case 'v': 74 | if s.Flag('+') { 75 | _, _ = fmt.Fprintf(s, "%+v", w.error) 76 | 77 | return 78 | } 79 | 80 | _, _ = fmt.Fprintf(s, "%v", w.error) 81 | 82 | case 's': 83 | _, _ = fmt.Fprintf(s, "%s", w.error) 84 | 85 | case 'q': 86 | _, _ = fmt.Fprintf(s, "%q", w.error) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /error_details_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestWithDetails(t *testing.T) { 9 | origErr := NewPlain("something went wrong") 10 | details := []interface{}{"key", "value"} 11 | err := WithDetails(origErr, details...) 12 | 13 | t.Parallel() 14 | 15 | t.Run("error_message", func(t *testing.T) { 16 | checkErrorMessage(t, err, "something went wrong") 17 | }) 18 | 19 | t.Run("unwrap", func(t *testing.T) { 20 | checkUnwrap(t, err, origErr) 21 | }) 22 | 23 | t.Run("format", func(t *testing.T) { 24 | checkFormat(t, err, map[string][]string{ 25 | "%s": {"something went wrong"}, 26 | "%q": {`"something went wrong"`}, 27 | "%v": {"something went wrong"}, 28 | "%+v": {"something went wrong"}, 29 | }) 30 | }) 31 | 32 | t.Run("nil", func(t *testing.T) { 33 | checkErrorNil(t, WithDetails(nil, "key", "value")) 34 | }) 35 | 36 | t.Run("details", func(t *testing.T) { 37 | d := err.(*withDetails).Details() 38 | 39 | for i, detail := range d { 40 | if got, want := detail, details[i]; got != want { 41 | t.Errorf("error detail does not match the expected one\nactual: %+v\nexpected: %+v", got, want) 42 | } 43 | } 44 | }) 45 | 46 | t.Run("details_missing_value", func(t *testing.T) { 47 | details := []interface{}{"key", nil} 48 | err := WithDetails(origErr, "key") 49 | 50 | d := err.(*withDetails).Details() 51 | 52 | for i, detail := range d { 53 | if got, want := detail, details[i]; got != want { 54 | t.Errorf("error detail does not match the expected one\nactual: %+v\nexpected: %+v", got, want) 55 | } 56 | } 57 | }) 58 | } 59 | 60 | func TestGetDetails(t *testing.T) { 61 | err := WithDetails( 62 | WithMessage( 63 | WithDetails( 64 | Wrap( 65 | WithDetails( 66 | New("error"), 67 | "key", "value", 68 | ), 69 | "wrapped error", 70 | ), 71 | "key2", "value2", 72 | ), 73 | "another wrapped error", 74 | ), 75 | "key3", "value3", 76 | ) 77 | 78 | expected := []interface{}{ 79 | "key", "value", 80 | "key2", "value2", 81 | "key3", "value3", 82 | } 83 | 84 | actual := GetDetails(err) 85 | 86 | if got, want := actual, expected; !reflect.DeepEqual(got, want) { 87 | t.Errorf("context does not match the expected one\nactual: %v\nexpected: %v", got, want) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /errors_multi.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "go.uber.org/multierr" 5 | ) 6 | 7 | // Combine combines the passed errors into a single error. 8 | // 9 | // If zero arguments were passed or if all items are nil, a nil error is 10 | // returned. 11 | // 12 | // If only a single error was passed, it is returned as-is. 13 | // 14 | // Combine omits nil errors so this function may be used to combine 15 | // together errors from operations that fail independently of each other. 16 | // 17 | // errors.Combine( 18 | // reader.Close(), 19 | // writer.Close(), 20 | // pipe.Close(), 21 | // ) 22 | // 23 | // If any of the passed errors is already an aggregated error, it will be flattened along 24 | // with the other errors. 25 | // 26 | // errors.Combine(errors.Combine(err1, err2), err3) 27 | // // is the same as 28 | // errors.Combine(err1, err2, err3) 29 | // 30 | // The returned error formats into a readable multi-line error message if 31 | // formatted with %+v. 32 | // 33 | // fmt.Sprintf("%+v", errors.Combine(err1, err2)) 34 | func Combine(errors ...error) error { 35 | return multierr.Combine(errors...) 36 | } 37 | 38 | // Append appends the given errors together. Either value may be nil. 39 | // 40 | // This function is a specialization of Combine for the common case where 41 | // there are only two errors. 42 | // 43 | // err = errors.Append(reader.Close(), writer.Close()) 44 | // 45 | // The following pattern may also be used to record failure of deferred 46 | // operations without losing information about the original error. 47 | // 48 | // func doSomething(..) (err error) { 49 | // f := acquireResource() 50 | // defer func() { 51 | // err = errors.Append(err, f.Close()) 52 | // }() 53 | func Append(left error, right error) error { 54 | return multierr.Append(left, right) 55 | } 56 | 57 | // GetErrors returns a slice containing zero or more errors that the supplied 58 | // error is composed of. If the error is nil, the returned slice is empty. 59 | // 60 | // err := errors.Append(r.Close(), w.Close()) 61 | // errors := errors.GetErrors(err) 62 | // 63 | // If the error is not composed of other errors, the returned slice contains 64 | // just the error that was passed in. 65 | // 66 | // Callers of this function are free to modify the returned slice. 67 | func GetErrors(err error) []error { 68 | return multierr.Errors(err) 69 | } 70 | -------------------------------------------------------------------------------- /stack_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Dave Cheney . All rights reserved. 2 | // Use of this source code (or at least parts of it) is governed by a BSD-style 3 | // license that can be found in the LICENSE_THIRD_PARTY file. 4 | 5 | package errors 6 | 7 | import ( 8 | "runtime" 9 | "testing" 10 | ) 11 | 12 | func TestStackTrace(t *testing.T) { 13 | tests := []struct { 14 | stack *stack 15 | want []string 16 | }{ 17 | { 18 | callers(0), 19 | []string{ 20 | "emperror.dev/errors.TestStackTrace\n" + 21 | "\t.+/stack_test.go:18", 22 | }, 23 | }, 24 | } 25 | for i, tt := range tests { 26 | st := tt.stack.StackTrace() 27 | 28 | for j, want := range tt.want { 29 | testFormatRegexp(t, i, st[j], "%+v", want) 30 | } 31 | } 32 | } 33 | 34 | func stackTrace() StackTrace { 35 | const depth = 8 36 | 37 | var pcs [depth]uintptr 38 | 39 | n := runtime.Callers(1, pcs[:]) 40 | 41 | var st stack = pcs[0:n] 42 | 43 | return st.StackTrace() 44 | } 45 | 46 | func TestStackTraceFormat(t *testing.T) { 47 | tests := []struct { 48 | StackTrace 49 | format string 50 | want string 51 | }{{ 52 | nil, 53 | "%s", 54 | `\[\]`, 55 | }, { 56 | nil, 57 | "%v", 58 | `\[\]`, 59 | }, { 60 | nil, 61 | "%+v", 62 | "", 63 | }, { 64 | nil, 65 | "%#v", 66 | `\[\]errors.Frame\(nil\)`, 67 | }, { 68 | make(StackTrace, 0), 69 | "%s", 70 | `\[\]`, 71 | }, { 72 | make(StackTrace, 0), 73 | "%v", 74 | `\[\]`, 75 | }, { 76 | make(StackTrace, 0), 77 | "%+v", 78 | "", 79 | }, { 80 | make(StackTrace, 0), 81 | "%#v", 82 | `\[\]errors.Frame{}`, 83 | }, { 84 | stackTrace()[:2], 85 | "%s", 86 | `\[stack_test.go stack_test.go\]`, 87 | }, { 88 | stackTrace()[:2], 89 | "%v", 90 | `\[stack_test.go:39 stack_test.go:88\]`, 91 | }, { 92 | stackTrace()[:2], 93 | "%+v", 94 | "\n" + 95 | "emperror.dev/errors.stackTrace\n" + 96 | "\t.+/stack_test.go:39\n" + 97 | "emperror.dev/errors.TestStackTraceFormat\n" + 98 | "\t.+/stack_test.go:92", 99 | }, { 100 | stackTrace()[:2], 101 | "%#v", 102 | `\[\]errors.Frame{stack_test.go:39, stack_test.go:100}`, 103 | }} 104 | 105 | for i, tt := range tests { 106 | testFormatRegexp(t, i, tt.StackTrace, tt.format, tt.want) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /match/match.go: -------------------------------------------------------------------------------- 1 | package match 2 | 3 | import ( 4 | "reflect" 5 | 6 | "emperror.dev/errors" 7 | ) 8 | 9 | // ErrorMatcher checks if an error matches a certain condition. 10 | type ErrorMatcher interface { 11 | // MatchError checks if err matches a certain condition. 12 | MatchError(err error) bool 13 | } 14 | 15 | // ErrorMatcherFunc turns a plain function into an ErrorMatcher if it's definition matches the interface. 16 | type ErrorMatcherFunc func(err error) bool 17 | 18 | // MatchError calls the underlying function to check if err matches a certain condition. 19 | func (fn ErrorMatcherFunc) MatchError(err error) bool { 20 | return fn(err) 21 | } 22 | 23 | // Any matches an error if any of the underlying matchers match it. 24 | type Any []ErrorMatcher 25 | 26 | // MatchError calls underlying matchers with err. 27 | // If any of them matches err it returns true, otherwise false. 28 | func (m Any) MatchError(err error) bool { 29 | for _, matcher := range m { 30 | if matcher.MatchError(err) { 31 | return true 32 | } 33 | } 34 | 35 | return false 36 | } 37 | 38 | // All matches an error if all of the underlying matchers match it. 39 | type All []ErrorMatcher 40 | 41 | // MatchError calls underlying matchers with err. 42 | // If all of them matches err it returns true, otherwise false. 43 | func (m All) MatchError(err error) bool { 44 | for _, matcher := range m { 45 | if !matcher.MatchError(err) { 46 | return false 47 | } 48 | } 49 | 50 | return true 51 | } 52 | 53 | // Is returns an error matcher that determines matching by calling errors.Is. 54 | func Is(target error) ErrorMatcher { 55 | return ErrorMatcherFunc(func(err error) bool { 56 | return errors.Is(err, target) 57 | }) 58 | } 59 | 60 | // As returns an error matcher that determines matching by calling errors.As. 61 | func As(target interface{}) ErrorMatcher { 62 | if target == nil { 63 | panic("errors: target cannot be nil") 64 | } 65 | 66 | val := reflect.ValueOf(target) 67 | typ := val.Type() 68 | 69 | if typ.Kind() != reflect.Ptr || val.IsNil() { 70 | panic("errors: target must be a non-nil pointer") 71 | } 72 | 73 | if e := typ.Elem(); e.Kind() != reflect.Interface && !e.Implements(errorType) { 74 | panic("errors: *target must be interface or implement error") 75 | } 76 | 77 | tar := reflect.New(typ).Interface() 78 | 79 | return ErrorMatcherFunc(func(err error) bool { 80 | target := tar 81 | 82 | return errors.As(err, &target) 83 | }) 84 | } 85 | 86 | var errorType = reflect.TypeOf((*error)(nil)).Elem() 87 | -------------------------------------------------------------------------------- /pleasew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -u 3 | 4 | RED="\x1B[31m" 5 | GREEN="\x1B[32m" 6 | YELLOW="\x1B[33m" 7 | RESET="\x1B[0m" 8 | 9 | DEFAULT_URL_BASE="https://get.please.build" 10 | # We might already have it downloaded... 11 | LOCATION=`grep -i "^location" .plzconfig 2>/dev/null | cut -d '=' -f 2 | tr -d ' '` 12 | if [ -z "$LOCATION" ]; then 13 | if [ -z "$HOME" ]; then 14 | echo -e >&2 "${RED}\$HOME not set, not sure where to look for Please.${RESET}" 15 | exit 1 16 | fi 17 | LOCATION="${HOME}/.please" 18 | else 19 | # It can contain a literal ~, need to explicitly handle that. 20 | LOCATION="${LOCATION/\~/$HOME}" 21 | fi 22 | # If this exists at any version, let it handle any update. 23 | TARGET="${LOCATION}/please" 24 | if [ -f "$TARGET" ]; then 25 | exec "$TARGET" "$@" 26 | fi 27 | 28 | URL_BASE="`grep -i "^downloadlocation" .plzconfig | cut -d '=' -f 2 | tr -d ' '`" 29 | if [ -z "$URL_BASE" ]; then 30 | URL_BASE=$DEFAULT_URL_BASE 31 | fi 32 | URL_BASE="${URL_BASE%/}" 33 | 34 | VERSION="`grep -i "^version[^a-z]" .plzconfig`" 35 | VERSION="${VERSION#*=}" # Strip until after first = 36 | VERSION="${VERSION/ /}" # Remove all spaces 37 | VERSION="${VERSION#>=}" # Strip any initial >= 38 | if [ -z "$VERSION" ]; then 39 | echo -e >&2 "${YELLOW}Can't determine version, will use latest.${RESET}" 40 | VERSION=`curl -fsSL ${URL_BASE}/latest_version` 41 | fi 42 | 43 | # Find the os / arch to download. You can do this quite nicely with go env 44 | # but we use this script on machines that don't necessarily have Go itself. 45 | OS=`uname` 46 | if [ "$OS" = "Linux" ]; then 47 | GOOS="linux" 48 | elif [ "$OS" = "Darwin" ]; then 49 | GOOS="darwin" 50 | else 51 | echo -e >&2 "${RED}Unknown operating system $OS${RESET}" 52 | exit 1 53 | fi 54 | # Don't have any builds other than amd64 at the moment. 55 | ARCH="amd64" 56 | 57 | PLEASE_URL="${URL_BASE}/${GOOS}_${ARCH}/${VERSION}/please_${VERSION}.tar.xz" 58 | DIR="${LOCATION}/${VERSION}" 59 | # Potentially we could reuse this but it's easier not to really. 60 | if [ ! -d "$DIR" ]; then 61 | rm -rf "$DIR" 62 | fi 63 | echo -e >&2 "${GREEN}Downloading Please ${VERSION} to ${DIR}...${RESET}" 64 | mkdir -p "$DIR" 65 | curl -fsSL "${PLEASE_URL}" | tar -xJpf- --strip-components=1 -C "$DIR" 66 | # Link it all back up a dir 67 | for x in `ls "$DIR"`; do 68 | ln -sf "${DIR}/${x}" "$LOCATION" 69 | done 70 | ln -sf "${DIR}/please" "${LOCATION}/plz" 71 | echo -e >&2 "${GREEN}Should be good to go now, running plz...${RESET}" 72 | exec "$TARGET" "$@" 73 | -------------------------------------------------------------------------------- /wrap_go1_12.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code (or at least parts of it) is governed by a BSD-style 3 | // license that can be found in the LICENSE_THIRD_PARTY file. 4 | // +build !go1.13 5 | 6 | package errors 7 | 8 | import ( 9 | "reflect" 10 | ) 11 | 12 | // Is reports whether any error in err's chain matches target. 13 | // 14 | // An error is considered to match a target if it is equal to that target or if 15 | // it implements a method Is(error) bool such that Is(target) returns true. 16 | func Is(err, target error) bool { 17 | if target == nil { 18 | return err == target 19 | } 20 | 21 | isComparable := reflect.TypeOf(target).Comparable() 22 | for { 23 | if isComparable && err == target { 24 | return true 25 | } 26 | if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { 27 | return true 28 | } 29 | // TODO: consider supporing target.Is(err). This would allow 30 | // user-definable predicates, but also may allow for coping with sloppy 31 | // APIs, thereby making it easier to get away with them. 32 | if err = Unwrap(err); err == nil { 33 | return false 34 | } 35 | } 36 | } 37 | 38 | // As finds the first error in err's chain that matches the type to which target 39 | // points, and if so, sets the target to its value and returns true. An error 40 | // matches a type if it is assignable to the target type, or if it has a method 41 | // As(interface{}) bool such that As(target) returns true. As will panic if target 42 | // is not a non-nil pointer to a type which implements error or is of interface type. 43 | // 44 | // The As method should set the target to its value and return true if err 45 | // matches the type to which target points. 46 | func As(err error, target interface{}) bool { 47 | if target == nil { 48 | panic("errors: target cannot be nil") 49 | } 50 | val := reflect.ValueOf(target) 51 | typ := val.Type() 52 | if typ.Kind() != reflect.Ptr || val.IsNil() { 53 | panic("errors: target must be a non-nil pointer") 54 | } 55 | if e := typ.Elem(); e.Kind() != reflect.Interface && !e.Implements(errorType) { 56 | panic("errors: *target must be interface or implement error") 57 | } 58 | targetType := typ.Elem() 59 | for err != nil { 60 | if reflect.TypeOf(err).AssignableTo(targetType) { 61 | val.Elem().Set(reflect.ValueOf(err)) 62 | return true 63 | } 64 | if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) { 65 | return true 66 | } 67 | err = Unwrap(err) 68 | } 69 | return false 70 | } 71 | 72 | var errorType = reflect.TypeOf((*error)(nil)).Elem() 73 | -------------------------------------------------------------------------------- /LICENSE_THIRD_PARTY: -------------------------------------------------------------------------------- 1 | Certain parts of this library are inspired by (or entirely copied from) various third party libraries. 2 | This file contains their licenses. 3 | 4 | github.com/pkg/errors: 5 | Copyright (c) 2015, Dave Cheney 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | 30 | github.com/golang/xerrors: 31 | Copyright (c) 2019 The Go Authors. All rights reserved. 32 | 33 | Redistribution and use in source and binary forms, with or without 34 | modification, are permitted provided that the following conditions are 35 | met: 36 | 37 | * Redistributions of source code must retain the above copyright 38 | notice, this list of conditions and the following disclaimer. 39 | * Redistributions in binary form must reproduce the above 40 | copyright notice, this list of conditions and the following disclaimer 41 | in the documentation and/or other materials provided with the 42 | distribution. 43 | * Neither the name of Google Inc. nor the names of its 44 | contributors may be used to endorse or promote products derived from 45 | this software without specific prior written permission. 46 | 47 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 48 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 49 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 50 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 51 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 52 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 53 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 54 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 55 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 56 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 57 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 7 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 8 | 9 | 10 | ## [Unreleased] 11 | 12 | 13 | ## [0.8.1] - 2022-02-23 14 | 15 | ### Fixed 16 | 17 | - Reduced memory allocation when collecting stack information 18 | 19 | 20 | ## [0.8.0] - 2020-09-14 21 | 22 | ### Changed 23 | 24 | - Update dependencies 25 | 26 | 27 | ## [0.7.0] - 2020-01-13 28 | 29 | ### Changed 30 | 31 | - Updated dependencies ([github.com/pkg/errors](https://github.com/pkg/errors)) 32 | - Use `WithMessage` and `WithMessagef` from [github.com/pkg/errors](https://github.com/pkg/errors) 33 | 34 | 35 | ## [0.6.0] - 2020-01-09 36 | 37 | ### Changed 38 | 39 | - Updated dependencies 40 | 41 | 42 | ## [0.5.2] - 2020-01-06 43 | 44 | ### Changed 45 | 46 | - `match`: exported `ErrorMatcherFunc` 47 | 48 | 49 | ## [0.5.1] - 2020-01-06 50 | 51 | ### Fixed 52 | 53 | - `match`: race condition in `As` 54 | 55 | 56 | ## [0.5.0] - 2020-01-06 57 | 58 | ### Added 59 | 60 | - `match` package for matching errors 61 | 62 | 63 | ## [0.4.3] - 2019-09-05 64 | 65 | ### Added 66 | 67 | - `Sentinel` error type for creating [constant error](https://dave.cheney.net/2016/04/07/constant-errors) 68 | 69 | 70 | ## [0.4.2] - 2019-07-19 71 | 72 | ### Added 73 | 74 | - `NewWithDetails` function to create a new error with details attached 75 | 76 | 77 | ## [0.4.1] - 2019-07-17 78 | 79 | ### Added 80 | 81 | - `utils/keyval` package to work with key-value pairs. 82 | 83 | 84 | ## [0.4.0] - 2019-07-17 85 | 86 | ### Added 87 | 88 | - Error details 89 | 90 | 91 | ## [0.3.0] - 2019-07-14 92 | 93 | ### Added 94 | 95 | - Multi error 96 | - `UnwrapEach` function 97 | 98 | 99 | ## [0.2.0] - 2019-07-12 100 | 101 | ### Added 102 | 103 | - `*If` functions that only annotate an error with a stack trace if there isn't one already in the error chain 104 | 105 | 106 | ## [0.1.0] - 2019-07-12 107 | 108 | - Initial release 109 | 110 | 111 | [Unreleased]: https://github.com/emperror/errors/compare/v0.8.1...HEAD 112 | [0.8.1]: https://github.com/emperror/errors/compare/v0.8.0...v0.8.1 113 | [0.8.0]: https://github.com/emperror/errors/compare/v0.7.0...v0.8.0 114 | [0.7.0]: https://github.com/emperror/errors/compare/v0.6.0...v0.7.0 115 | [0.6.0]: https://github.com/emperror/errors/compare/v0.5.2...v0.6.0 116 | [0.5.2]: https://github.com/emperror/errors/compare/v0.5.1...v0.5.2 117 | [0.5.1]: https://github.com/emperror/errors/compare/v0.5.0...v0.5.1 118 | [0.5.0]: https://github.com/emperror/errors/compare/v0.4.3...v0.5.0 119 | [0.4.3]: https://github.com/emperror/errors/compare/v0.4.2...v0.4.3 120 | [0.4.2]: https://github.com/emperror/errors/compare/v0.4.1...v0.4.2 121 | [0.4.1]: https://github.com/emperror/errors/compare/v0.4.0...v0.4.1 122 | [0.4.0]: https://github.com/emperror/errors/compare/v0.3.0...v0.4.0 123 | [0.3.0]: https://github.com/emperror/errors/compare/v0.2.0...v0.3.0 124 | [0.2.0]: https://github.com/emperror/errors/compare/v0.1.0...v0.2.0 125 | [0.1.0]: https://github.com/emperror/errors/compare/v0.0.0...v0.1.0 126 | -------------------------------------------------------------------------------- /errors_new_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewPlain(t *testing.T) { 8 | err := NewPlain("error") 9 | 10 | t.Parallel() 11 | 12 | t.Run("error_message", func(t *testing.T) { 13 | checkErrorMessage(t, err, "error") 14 | }) 15 | 16 | t.Run("format", func(t *testing.T) { 17 | checkFormat(t, err, map[string][]string{ 18 | "%s": {"error"}, 19 | "%q": {`"error"`}, 20 | "%v": {"error"}, 21 | "%+v": {"error"}, 22 | }) 23 | }) 24 | } 25 | 26 | func TestNew(t *testing.T) { 27 | err := New("error") 28 | origErr := err.(*withStack).error 29 | 30 | t.Parallel() 31 | 32 | t.Run("error_message", func(t *testing.T) { 33 | checkErrorMessage(t, err, "error") 34 | }) 35 | 36 | t.Run("unwrap", func(t *testing.T) { 37 | checkUnwrap(t, err, origErr) 38 | }) 39 | 40 | t.Run("format", func(t *testing.T) { 41 | checkFormat(t, err, map[string][]string{ 42 | "%s": {"error"}, 43 | "%q": {`"error"`}, 44 | "%v": {"error"}, 45 | "%+v": {"error", "emperror.dev/errors.TestNew\n\t.+/errors_new_test.go:27"}, 46 | }) 47 | }) 48 | } 49 | 50 | func TestNewWithDetails(t *testing.T) { 51 | details := []interface{}{"key", "value"} 52 | err := NewWithDetails("error", details...) 53 | origErr := err.(*withDetails).error 54 | 55 | t.Parallel() 56 | 57 | t.Run("error_message", func(t *testing.T) { 58 | checkErrorMessage(t, err, "error") 59 | }) 60 | 61 | t.Run("unwrap", func(t *testing.T) { 62 | checkUnwrap(t, err, origErr) 63 | }) 64 | 65 | t.Run("format", func(t *testing.T) { 66 | checkFormat(t, err, map[string][]string{ 67 | "%s": {"error"}, 68 | "%q": {`"error"`}, 69 | "%v": {"error"}, 70 | "%+v": {"error", "emperror.dev/errors.TestNewWithDetails\n\t.+/errors_new_test.go:52"}, 71 | }) 72 | }) 73 | 74 | t.Run("details", func(t *testing.T) { 75 | d := err.(*withDetails).Details() 76 | 77 | for i, detail := range d { 78 | if got, want := detail, details[i]; got != want { 79 | t.Errorf("error detail does not match the expected one\nactual: %+v\nexpected: %+v", got, want) 80 | } 81 | } 82 | }) 83 | 84 | t.Run("details_missing_value", func(t *testing.T) { 85 | details := []interface{}{"key", nil} 86 | err := NewWithDetails("error", "key") 87 | 88 | d := err.(*withDetails).Details() 89 | 90 | for i, detail := range d { 91 | if got, want := detail, details[i]; got != want { 92 | t.Errorf("error detail does not match the expected one\nactual: %+v\nexpected: %+v", got, want) 93 | } 94 | } 95 | }) 96 | } 97 | 98 | func TestErrorf(t *testing.T) { 99 | err := Errorf("error: %s", "something went wrong") 100 | origErr := err.(*withStack).error 101 | 102 | t.Parallel() 103 | 104 | t.Run("error_message", func(t *testing.T) { 105 | checkErrorMessage(t, err, "error: something went wrong") 106 | }) 107 | 108 | t.Run("unwrap", func(t *testing.T) { 109 | checkUnwrap(t, err, origErr) 110 | }) 111 | 112 | t.Run("format", func(t *testing.T) { 113 | checkFormat(t, err, map[string][]string{ 114 | "%s": {"error: something went wrong"}, 115 | "%q": {`"error: something went wrong"`}, 116 | "%v": {"error: something went wrong"}, 117 | "%+v": {"error: something went wrong", "emperror.dev/errors.TestErrorf\n\t.+/errors_new_test.go:99"}, 118 | }) 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package errors_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "emperror.dev/errors" 8 | ) 9 | 10 | // nolint: unused 11 | func ExampleSentinel() { 12 | const ErrSomething = errors.Sentinel("something went wrong") 13 | 14 | // Output: 15 | } 16 | 17 | func ExampleAs() { 18 | if _, err := os.Open("non-existing"); err != nil { 19 | var pathError *os.PathError 20 | if errors.As(err, &pathError) { 21 | fmt.Println("Failed at path:", pathError.Path) 22 | } else { 23 | fmt.Println(err) 24 | } 25 | } 26 | 27 | // Output: 28 | // Failed at path: non-existing 29 | } 30 | 31 | func ExampleIs() { 32 | ErrSomething := errors.NewPlain("something") 33 | 34 | if err := errors.Wrap(ErrSomething, "error"); err != nil { 35 | if errors.Is(err, ErrSomething) { 36 | fmt.Println("something went wrong") 37 | } else { 38 | fmt.Println(err) 39 | } 40 | } 41 | 42 | // Output: 43 | // something went wrong 44 | } 45 | 46 | func ExampleUnwrap() { 47 | ErrSomething := errors.NewPlain("something") 48 | 49 | if err := errors.WithMessage(ErrSomething, "error"); err != nil { 50 | if errors.Unwrap(err) == ErrSomething { 51 | fmt.Println("something went wrong") 52 | } else { 53 | fmt.Println(err) 54 | } 55 | } 56 | 57 | // Output: 58 | // something went wrong 59 | } 60 | 61 | func ExampleCause() { 62 | ErrSomething := errors.NewPlain("something") 63 | 64 | if err := errors.Wrap(ErrSomething, "error"); err != nil { 65 | if errors.Cause(err) == ErrSomething { 66 | fmt.Println("something went wrong") 67 | } else { 68 | fmt.Println(err) 69 | } 70 | } 71 | 72 | // Output: 73 | // something went wrong 74 | } 75 | 76 | func ExampleCombine() { 77 | err := errors.Combine( 78 | errors.NewPlain("call 1 failed"), 79 | nil, // successful request 80 | errors.NewPlain("call 3 failed"), 81 | nil, // successful request 82 | errors.NewPlain("call 5 failed"), 83 | ) 84 | fmt.Printf("%+v", err) 85 | // Output: 86 | // the following errors occurred: 87 | // - call 1 failed 88 | // - call 3 failed 89 | // - call 5 failed 90 | } 91 | 92 | func ExampleCombine_loop() { 93 | var errs []error 94 | 95 | for i := 1; i < 6; i++ { 96 | if i%2 == 0 { 97 | continue 98 | } 99 | 100 | err := errors.NewPlain(fmt.Sprintf("call %d failed", i)) 101 | errs = append(errs, err) 102 | } 103 | 104 | err := errors.Combine(errs...) 105 | 106 | fmt.Printf("%+v", err) 107 | // Output: 108 | // the following errors occurred: 109 | // - call 1 failed 110 | // - call 3 failed 111 | // - call 5 failed 112 | } 113 | 114 | func ExampleAppend() { 115 | var err error 116 | err = errors.Append(err, errors.NewPlain("call 1 failed")) 117 | err = errors.Append(err, errors.NewPlain("call 2 failed")) 118 | fmt.Println(err) 119 | // Output: 120 | // call 1 failed; call 2 failed 121 | } 122 | 123 | func ExampleGetErrors() { 124 | err := errors.Combine( 125 | nil, // successful request 126 | errors.NewPlain("call 2 failed"), 127 | errors.NewPlain("call 3 failed"), 128 | ) 129 | err = errors.Append(err, nil) // successful request 130 | err = errors.Append(err, errors.NewPlain("call 5 failed")) 131 | 132 | errs := errors.GetErrors(err) 133 | for _, err := range errs { 134 | fmt.Println(err) 135 | } 136 | // Output: 137 | // call 2 failed 138 | // call 3 failed 139 | // call 5 failed 140 | } 141 | -------------------------------------------------------------------------------- /match/match_test.go: -------------------------------------------------------------------------------- 1 | package match 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestAny(t *testing.T) { 9 | t.Run("empty", func(t *testing.T) { 10 | matcher := Any{} 11 | 12 | if matcher.MatchError(errors.New("error")) { 13 | t.Error("empty any matcher is not supposed to match an error") 14 | } 15 | }) 16 | 17 | t.Run("not_empty", func(t *testing.T) { 18 | matcher := Any{ 19 | ErrorMatcherFunc(func(err error) bool { return false }), 20 | ErrorMatcherFunc(func(err error) bool { return true }), 21 | } 22 | 23 | if !matcher.MatchError(errors.New("error")) { 24 | t.Error("not-empty any matcher is supposed to match an error if any of the matchers match it") 25 | } 26 | }) 27 | } 28 | 29 | func TestAll(t *testing.T) { 30 | t.Run("empty", func(t *testing.T) { 31 | matcher := All{} 32 | 33 | if !matcher.MatchError(errors.New("error")) { 34 | t.Error("empty all matcher is not supposed to match an error") 35 | } 36 | }) 37 | 38 | t.Run("not_empty", func(t *testing.T) { 39 | matcher := All{ 40 | ErrorMatcherFunc(func(err error) bool { return false }), 41 | ErrorMatcherFunc(func(err error) bool { return true }), 42 | } 43 | 44 | if matcher.MatchError(errors.New("error")) { 45 | t.Error("not-empty all matcher is not supposed to match an error if not all of the matchers match it") 46 | } 47 | }) 48 | } 49 | 50 | func TestIs(t *testing.T) { 51 | err := errors.New("error") 52 | 53 | matcher := Is(err) 54 | 55 | if !matcher.MatchError(err) { 56 | t.Error("is matcher is supposed to match an error if errors.Is returns true") 57 | } 58 | } 59 | 60 | type asErrorStub struct{} 61 | 62 | func (asErrorStub) Error() string { 63 | return "error" 64 | } 65 | 66 | func (asErrorStub) IsError() bool { 67 | return true 68 | } 69 | 70 | func TestAs(t *testing.T) { 71 | var matchErr interface { 72 | IsError() bool 73 | } 74 | 75 | matcher := As(&matchErr) 76 | 77 | if !matcher.MatchError(asErrorStub{}) { 78 | t.Error("is matcher is supposed to match an error if errors.Is returns true") 79 | } 80 | } 81 | 82 | func TestAs_Race(t *testing.T) { 83 | var matchErr interface { 84 | IsError() bool 85 | } 86 | 87 | matcher := As(&matchErr) 88 | 89 | go func() { 90 | if !matcher.MatchError(asErrorStub{}) { 91 | t.Error("is matcher is supposed to match an error if errors.Is returns true") 92 | } 93 | }() 94 | 95 | go func() { 96 | if !matcher.MatchError(asErrorStub{}) { 97 | t.Error("is matcher is supposed to match an error if errors.Is returns true") 98 | } 99 | }() 100 | 101 | go func() { 102 | if !matcher.MatchError(asErrorStub{}) { 103 | t.Error("is matcher is supposed to match an error if errors.Is returns true") 104 | } 105 | }() 106 | } 107 | 108 | func TestAs_Validation(t *testing.T) { 109 | t.Run("nil", func(t *testing.T) { 110 | defer func() { 111 | _ = recover() 112 | }() 113 | 114 | As(nil) 115 | 116 | t.Error("did not panic") 117 | }) 118 | 119 | t.Run("non-pointer", func(t *testing.T) { 120 | defer func() { 121 | _ = recover() 122 | }() 123 | 124 | var s struct{} 125 | 126 | As(s) 127 | 128 | t.Error("did not panic") 129 | }) 130 | 131 | t.Run("non-error", func(t *testing.T) { 132 | defer func() { 133 | _ = recover() 134 | }() 135 | 136 | var s struct{} 137 | 138 | As(&s) 139 | 140 | t.Error("did not panic") 141 | }) 142 | } 143 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Dave Cheney . All rights reserved. 2 | // Use of this source code (or at least parts of it) is governed by a BSD-style 3 | // license that can be found in the LICENSE_THIRD_PARTY file. 4 | 5 | package errors 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "regexp" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) { 16 | t.Helper() 17 | 18 | got := fmt.Sprintf(format, arg) 19 | gotLines := strings.SplitN(got, "\n", -1) 20 | wantLines := strings.SplitN(want, "\n", -1) 21 | 22 | if len(wantLines) > len(gotLines) { 23 | t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want) 24 | return 25 | } 26 | 27 | for i, w := range wantLines { 28 | match, err := regexp.MatchString(w, gotLines[i]) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | if !match { 33 | t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want) 34 | } 35 | } 36 | } 37 | 38 | var stackLineR = regexp.MustCompile(`\.`) 39 | 40 | // parseBlocks parses input into a slice, where: 41 | // - incase entry contains a newline, its a stacktrace 42 | // - incase entry contains no newline, its a solo line. 43 | // 44 | // Detecting stack boundaries only works incase the WithStack-calls are 45 | // to be found on the same line, thats why it is optionally here. 46 | // 47 | // Example use: 48 | // 49 | // for _, e := range blocks { 50 | // if strings.ContainsAny(e, "\n") { 51 | // // Match as stack 52 | // } else { 53 | // // Match as line 54 | // } 55 | // } 56 | // 57 | func parseBlocks(input string, detectStackboundaries bool) ([]string, error) { 58 | var blocks []string 59 | 60 | stack := "" 61 | wasStack := false 62 | lines := map[string]bool{} // already found lines 63 | 64 | for _, l := range strings.Split(input, "\n") { 65 | isStackLine := stackLineR.MatchString(l) 66 | 67 | switch { 68 | case !isStackLine && wasStack: 69 | blocks = append(blocks, stack, l) 70 | stack = "" 71 | lines = map[string]bool{} 72 | case isStackLine: 73 | if wasStack { 74 | // Detecting two stacks after another, possible cause lines match in 75 | // our tests due to WithStack(WithStack(io.EOF)) on same line. 76 | if detectStackboundaries { 77 | if lines[l] { 78 | if len(stack) == 0 { 79 | return nil, errors.New("len of block must not be zero here") 80 | } 81 | 82 | blocks = append(blocks, stack) 83 | stack = l 84 | lines = map[string]bool{l: true} 85 | continue 86 | } 87 | } 88 | 89 | stack = stack + "\n" + l 90 | } else { 91 | stack = l 92 | } 93 | lines[l] = true 94 | case !isStackLine && !wasStack: 95 | blocks = append(blocks, l) 96 | default: 97 | return nil, errors.New("must not happen") 98 | } 99 | 100 | wasStack = isStackLine 101 | } 102 | 103 | // Use up stack 104 | if stack != "" { 105 | blocks = append(blocks, stack) 106 | } 107 | return blocks, nil 108 | } 109 | 110 | func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) { 111 | t.Helper() 112 | 113 | gotStr := fmt.Sprintf(format, arg) 114 | 115 | got, err := parseBlocks(gotStr, detectStackBoundaries) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | if len(got) != len(want) { 121 | t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q", 122 | n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr) 123 | } 124 | 125 | for i := range got { 126 | if strings.ContainsAny(want[i], "\n") { 127 | // Match as stack 128 | match, err := regexp.MatchString(want[i], got[i]) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | if !match { 133 | t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n", 134 | n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want)) 135 | } 136 | } else { 137 | // Match as message 138 | if got[i] != want[i] { 139 | t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i]) 140 | } 141 | } 142 | } 143 | } 144 | 145 | func prettyBlocks(blocks []string) string { 146 | var out []string 147 | 148 | for _, b := range blocks { 149 | out = append(out, fmt.Sprintf("%v", b)) 150 | } 151 | 152 | return " " + strings.Join(out, "\n ") 153 | } 154 | -------------------------------------------------------------------------------- /wrap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Go Authors. All rights reserved. 2 | // Use of this source code (or at least parts of it) is governed by a BSD-style 3 | // license that can be found in the LICENSE_THIRD_PARTY file. 4 | 5 | package errors_test 6 | 7 | import ( 8 | "io" 9 | "reflect" 10 | "testing" 11 | 12 | "emperror.dev/errors" 13 | ) 14 | 15 | func TestUnwrap(t *testing.T) { 16 | err1 := errors.NewPlain("1") 17 | erra := wrapped{"wrap 2", err1} 18 | 19 | testCases := []struct { 20 | err error 21 | want error 22 | }{ 23 | {nil, nil}, 24 | {wrapped{"wrapped", nil}, nil}, 25 | {caused{"wrapped", nil}, nil}, 26 | {err1, nil}, 27 | {erra, err1}, 28 | {wrapped{"wrap 3", erra}, erra}, 29 | {caused{"wrap 3", erra}, erra}, 30 | } 31 | for _, tc := range testCases { 32 | if got := errors.Unwrap(tc.err); got != tc.want { 33 | t.Errorf("Unwrap(%v) = %v, want %v", tc.err, got, tc.want) 34 | } 35 | } 36 | } 37 | 38 | type wrapped struct { 39 | msg string 40 | err error 41 | } 42 | 43 | func (e wrapped) Error() string { return e.msg } 44 | 45 | func (e wrapped) Unwrap() error { return e.err } 46 | 47 | type caused struct { 48 | msg string 49 | err error 50 | } 51 | 52 | func (e caused) Error() string { return e.msg } 53 | 54 | func (e caused) Cause() error { return e.err } 55 | 56 | func TestUnwrapEach(t *testing.T) { 57 | err := errors.WithMessage( 58 | errors.WithMessage( 59 | errors.WithMessage( 60 | errors.New("level 0"), 61 | "level 1", 62 | ), 63 | "level 2", 64 | ), 65 | "level 3", 66 | ) 67 | 68 | var i int 69 | fn := func(err error) bool { 70 | i++ 71 | 72 | return true 73 | } 74 | 75 | errors.UnwrapEach(err, fn) 76 | 77 | if got, want := i, 5; got != want { 78 | t.Errorf("error chain length does not match the expected one\nactual: %d\nexpected: %d", got, want) 79 | } 80 | } 81 | 82 | func TestUnwrapEach_BreakTheLoop(t *testing.T) { 83 | err := errors.WithMessage( 84 | errors.WithMessage( 85 | errors.WithMessage( 86 | errors.New("level 0"), 87 | "level 1", 88 | ), 89 | "level 2", 90 | ), 91 | "level 3", 92 | ) 93 | 94 | var i int 95 | fn := func(err error) bool { 96 | i++ 97 | 98 | return !(i > 2) 99 | } 100 | 101 | errors.UnwrapEach(err, fn) 102 | 103 | if got, want := i, 3; got != want { 104 | t.Errorf("error chain length does not match the expected one\nactual: %d\nexpected: %d", got, want) 105 | } 106 | } 107 | 108 | func TestUnwrapEach_NilError(t *testing.T) { 109 | var i int 110 | fn := func(err error) bool { 111 | i++ 112 | 113 | return !(i > 2) 114 | } 115 | 116 | errors.UnwrapEach(nil, fn) 117 | 118 | if got, want := i, 0; got != want { 119 | t.Errorf("error chain length does not match the expected one\nactual: %d\nexpected: %d", got, want) 120 | } 121 | } 122 | 123 | type nilError struct{} 124 | 125 | func (nilError) Error() string { return "nil error" } 126 | 127 | func TestCause(t *testing.T) { 128 | x := errors.NewPlain("error") 129 | tests := []struct { 130 | err error 131 | want error 132 | }{ 133 | { 134 | // nil error is nil 135 | err: nil, 136 | want: nil, 137 | }, 138 | { 139 | // explicit nil error is nil 140 | err: (error)(nil), 141 | want: nil, 142 | }, 143 | { 144 | // typed nil is nil 145 | err: (*nilError)(nil), 146 | want: (*nilError)(nil), 147 | }, 148 | { 149 | // uncaused error is unaffected 150 | err: io.EOF, 151 | want: io.EOF, 152 | }, 153 | { 154 | // wrapped error returns cause 155 | err: wrapped{"ignored", io.EOF}, 156 | want: io.EOF, 157 | }, 158 | { 159 | // caused error returns cause 160 | err: caused{"ignored", io.EOF}, 161 | want: io.EOF, 162 | }, 163 | { 164 | err: x, // return from errors.New 165 | want: x, 166 | }, 167 | { 168 | errors.WithMessage(nil, "whoops"), 169 | nil, 170 | }, 171 | { 172 | errors.WithMessage(io.EOF, "whoops"), 173 | io.EOF, 174 | }, 175 | { 176 | errors.WithMessagef(nil, "%s", "whoops"), 177 | nil, 178 | }, 179 | { 180 | errors.WithMessagef(io.EOF, "%s", "whoops"), 181 | io.EOF, 182 | }, 183 | { 184 | errors.WithStack(nil), 185 | nil, 186 | }, 187 | { 188 | errors.WithStack(io.EOF), 189 | io.EOF, 190 | }, 191 | { 192 | errors.WithStackDepth(nil, 0), 193 | nil, 194 | }, 195 | { 196 | errors.WithStackDepth(io.EOF, 0), 197 | io.EOF, 198 | }, 199 | } 200 | 201 | for i, tt := range tests { 202 | got := errors.Cause(tt.err) 203 | if !reflect.DeepEqual(got, tt.want) { 204 | t.Errorf("test %d: got %#v, want %#v", i+1, got, tt.want) 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Emperror: Errors [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go#error-handling) 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/emperror/errors/CI?style=flat-square)](https://github.com/emperror/errors/actions?query=workflow%3ACI) 4 | [![Codecov](https://img.shields.io/codecov/c/github/emperror/errors?style=flat-square)](https://codecov.io/gh/emperror/errors) 5 | [![Go Report Card](https://goreportcard.com/badge/emperror.dev/errors?style=flat-square)](https://goreportcard.com/report/emperror.dev/errors) 6 | ![Go Version](https://img.shields.io/badge/go%20version-%3E=1.12-61CFDD.svg?style=flat-square) 7 | [![PkgGoDev](https://pkg.go.dev/badge/mod/emperror.dev/errors)](https://pkg.go.dev/mod/emperror.dev/errors) 8 | [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B8125%2Femperror.dev%2Ferrors.svg?type=shield)](https://app.fossa.com/projects/custom%2B8125%2Femperror.dev%2Ferrors?ref=badge_shield) 9 | 10 | 11 | **Drop-in replacement for the standard library `errors` package and [github.com/pkg/errors](https://github.com/pkg/errors).** 12 | 13 | This is a single, lightweight library merging the features of standard library `errors` package 14 | and [github.com/pkg/errors](https://github.com/pkg/errors). It also backports a few features 15 | (like Go 1.13 error handling related features). 16 | 17 | Standard library features: 18 | - `New` creates an error with stack trace 19 | - `Unwrap` supports both Go 1.13 wrapper (`interface { Unwrap() error }`) and **pkg/errors** causer (`interface { Cause() error }`) interface 20 | - Backported `Is` and `As` functions 21 | 22 | [github.com/pkg/errors](https://github.com/pkg/errors) features: 23 | - `New`, `Errorf`, `WithMessage`, `WithMessagef`, `WithStack`, `Wrap`, `Wrapf` functions behave the same way as in the original library 24 | - `Cause` supports both Go 1.13 wrapper (`interface { Unwrap() error }`) and **pkg/errors** causer (`interface { Cause() error }`) interface 25 | 26 | Additional features: 27 | - `NewPlain` creates a new error without any attached context, like stack trace 28 | - `Sentinel` is a shorthand type for creating [constant error](https://dave.cheney.net/2016/04/07/constant-errors) 29 | - `WithStackDepth` allows attaching stack trace with a custom caller depth 30 | - `WithStackDepthIf`, `WithStackIf`, `WrapIf`, `WrapIff` only annotate errors with a stack trace if there isn't one already in the error chain 31 | - Multi error aggregating multiple errors into a single value 32 | - `NewWithDetails`, `WithDetails` and `Wrap*WithDetails` functions to add key-value pairs to an error 33 | - Match errors using the `match` package 34 | 35 | 36 | ## Installation 37 | 38 | ```bash 39 | go get emperror.dev/errors 40 | ``` 41 | 42 | 43 | ## Usage 44 | 45 | ```go 46 | package main 47 | 48 | import "emperror.dev/errors" 49 | 50 | // ErrSomethingWentWrong is a sentinel error which can be useful within a single API layer. 51 | const ErrSomethingWentWrong = errors.Sentinel("something went wrong") 52 | 53 | // ErrMyError is an error that can be returned from a public API. 54 | type ErrMyError struct { 55 | Msg string 56 | } 57 | 58 | func (e ErrMyError) Error() string { 59 | return e.Msg 60 | } 61 | 62 | func foo() error { 63 | // Attach stack trace to the sentinel error. 64 | return errors.WithStack(ErrSomethingWentWrong) 65 | } 66 | 67 | func bar() error { 68 | return errors.Wrap(ErrMyError{"something went wrong"}, "error") 69 | } 70 | 71 | func main() { 72 | if err := foo(); err != nil { 73 | if errors.Cause(err) == ErrSomethingWentWrong { // or errors.Is(ErrSomethingWentWrong) 74 | // handle error 75 | } 76 | } 77 | 78 | if err := bar(); err != nil { 79 | if errors.As(err, &ErrMyError{}) { 80 | // handle error 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | Match errors: 87 | 88 | ```go 89 | package main 90 | 91 | import ( 92 | "emperror.dev/errors" 93 | "emperror.dev/errors/match" 94 | ) 95 | 96 | // ErrSomethingWentWrong is a sentinel error which can be useful within a single API layer. 97 | const ErrSomethingWentWrong = errors.Sentinel("something went wrong") 98 | 99 | type clientError interface{ 100 | ClientError() bool 101 | } 102 | 103 | func foo() error { 104 | // Attach stack trace to the sentinel error. 105 | return errors.WithStack(ErrSomethingWentWrong) 106 | } 107 | 108 | func main() { 109 | var ce clientError 110 | matcher := match.Any{match.As(&ce), match.Is(ErrSomethingWentWrong)} 111 | 112 | if err := foo(); err != nil { 113 | if matcher.MatchError(err) { 114 | // you can use matchers to write complex conditions for handling (or not) an error 115 | // used in emperror 116 | } 117 | } 118 | } 119 | ``` 120 | 121 | 122 | ## Development 123 | 124 | Contributions are welcome! :) 125 | 126 | 1. Clone the repository 127 | 1. Make changes on a new branch 128 | 1. Run the test suite: 129 | ```bash 130 | ./pleasew build 131 | ./pleasew test 132 | ./pleasew gotest 133 | ./pleasew lint 134 | ``` 135 | 1. Commit, push and open a PR 136 | 137 | 138 | ## License 139 | 140 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 141 | 142 | Certain parts of this library are inspired by (or entirely copied from) various third party libraries. 143 | Their licenses can be found in the [Third Party License File](LICENSE_THIRD_PARTY). 144 | 145 | [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B8125%2Femperror.dev%2Ferrors.svg?type=large)](https://app.fossa.com/projects/custom%2B8125%2Femperror.dev%2Ferrors?ref=badge_large) 146 | -------------------------------------------------------------------------------- /tests/example_test.go: -------------------------------------------------------------------------------- 1 | package errors_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "emperror.dev/errors" 7 | ) 8 | 9 | func ExampleNew() { 10 | err := errors.New("whoops") 11 | fmt.Println(err) 12 | 13 | // Output: whoops 14 | } 15 | 16 | func ExampleNew_printf() { 17 | err := errors.New("whoops") 18 | fmt.Printf("%+v", err) 19 | 20 | // Example output: 21 | // whoops 22 | // github.com/pkg/errors_test.ExampleNew_printf 23 | // /home/dfc/src/github.com/pkg/errors/example_test.go:17 24 | // testing.runExample 25 | // /home/dfc/go/src/testing/example.go:114 26 | // testing.RunExamples 27 | // /home/dfc/go/src/testing/example.go:38 28 | // testing.(*M).Run 29 | // /home/dfc/go/src/testing/testing.go:744 30 | // main.main 31 | // /github.com/pkg/errors/_test/_testmain.go:106 32 | // runtime.main 33 | // /home/dfc/go/src/runtime/proc.go:183 34 | // runtime.goexit 35 | // /home/dfc/go/src/runtime/asm_amd64.s:2059 36 | } 37 | 38 | func ExampleWithMessage() { 39 | cause := errors.New("whoops") 40 | err := errors.WithMessage(cause, "oh noes") 41 | fmt.Println(err) 42 | 43 | // Output: oh noes: whoops 44 | } 45 | 46 | func ExampleWithStack() { 47 | cause := errors.New("whoops") 48 | err := errors.WithStack(cause) 49 | fmt.Println(err) 50 | 51 | // Output: whoops 52 | } 53 | 54 | func ExampleWithStack_printf() { 55 | cause := errors.New("whoops") 56 | err := errors.WithStack(cause) 57 | fmt.Printf("%+v", err) 58 | 59 | // Example Output: 60 | // whoops 61 | // github.com/pkg/errors_test.ExampleWithStack_printf 62 | // /home/fabstu/go/src/github.com/pkg/errors/example_test.go:55 63 | // testing.runExample 64 | // /usr/lib/go/src/testing/example.go:114 65 | // testing.RunExamples 66 | // /usr/lib/go/src/testing/example.go:38 67 | // testing.(*M).Run 68 | // /usr/lib/go/src/testing/testing.go:744 69 | // main.main 70 | // github.com/pkg/errors/_test/_testmain.go:106 71 | // runtime.main 72 | // /usr/lib/go/src/runtime/proc.go:183 73 | // runtime.goexit 74 | // /usr/lib/go/src/runtime/asm_amd64.s:2086 75 | // github.com/pkg/errors_test.ExampleWithStack_printf 76 | // /home/fabstu/go/src/github.com/pkg/errors/example_test.go:56 77 | // testing.runExample 78 | // /usr/lib/go/src/testing/example.go:114 79 | // testing.RunExamples 80 | // /usr/lib/go/src/testing/example.go:38 81 | // testing.(*M).Run 82 | // /usr/lib/go/src/testing/testing.go:744 83 | // main.main 84 | // github.com/pkg/errors/_test/_testmain.go:106 85 | // runtime.main 86 | // /usr/lib/go/src/runtime/proc.go:183 87 | // runtime.goexit 88 | // /usr/lib/go/src/runtime/asm_amd64.s:2086 89 | } 90 | 91 | func ExampleWrap() { 92 | cause := errors.New("whoops") 93 | err := errors.Wrap(cause, "oh noes") 94 | fmt.Println(err) 95 | 96 | // Output: oh noes: whoops 97 | } 98 | 99 | func fn() error { 100 | e1 := errors.New("error") 101 | e2 := errors.Wrap(e1, "inner") 102 | e3 := errors.Wrap(e2, "middle") 103 | return errors.Wrap(e3, "outer") 104 | } 105 | 106 | func ExampleCause() { 107 | err := fn() 108 | fmt.Println(err) 109 | fmt.Println(errors.Cause(err)) 110 | 111 | // Output: outer: middle: inner: error 112 | // error 113 | } 114 | 115 | func ExampleWrap_extended() { 116 | err := fn() 117 | fmt.Printf("%+v\n", err) 118 | 119 | // Example output: 120 | // error 121 | // github.com/pkg/errors_test.fn 122 | // /home/dfc/src/github.com/pkg/errors/example_test.go:47 123 | // github.com/pkg/errors_test.ExampleCause_printf 124 | // /home/dfc/src/github.com/pkg/errors/example_test.go:63 125 | // testing.runExample 126 | // /home/dfc/go/src/testing/example.go:114 127 | // testing.RunExamples 128 | // /home/dfc/go/src/testing/example.go:38 129 | // testing.(*M).Run 130 | // /home/dfc/go/src/testing/testing.go:744 131 | // main.main 132 | // /github.com/pkg/errors/_test/_testmain.go:104 133 | // runtime.main 134 | // /home/dfc/go/src/runtime/proc.go:183 135 | // runtime.goexit 136 | // /home/dfc/go/src/runtime/asm_amd64.s:2059 137 | // github.com/pkg/errors_test.fn 138 | // /home/dfc/src/github.com/pkg/errors/example_test.go:48: inner 139 | // github.com/pkg/errors_test.fn 140 | // /home/dfc/src/github.com/pkg/errors/example_test.go:49: middle 141 | // github.com/pkg/errors_test.fn 142 | // /home/dfc/src/github.com/pkg/errors/example_test.go:50: outer 143 | } 144 | 145 | func ExampleWrapf() { 146 | cause := errors.New("whoops") 147 | err := errors.Wrapf(cause, "oh noes #%d", 2) 148 | fmt.Println(err) 149 | 150 | // Output: oh noes #2: whoops 151 | } 152 | 153 | func ExampleErrorf_extended() { 154 | err := errors.Errorf("whoops: %s", "foo") 155 | fmt.Printf("%+v", err) 156 | 157 | // Example output: 158 | // whoops: foo 159 | // github.com/pkg/errors_test.ExampleErrorf 160 | // /home/dfc/src/github.com/pkg/errors/example_test.go:101 161 | // testing.runExample 162 | // /home/dfc/go/src/testing/example.go:114 163 | // testing.RunExamples 164 | // /home/dfc/go/src/testing/example.go:38 165 | // testing.(*M).Run 166 | // /home/dfc/go/src/testing/testing.go:744 167 | // main.main 168 | // /github.com/pkg/errors/_test/_testmain.go:102 169 | // runtime.main 170 | // /home/dfc/go/src/runtime/proc.go:183 171 | // runtime.goexit 172 | // /home/dfc/go/src/runtime/asm_amd64.s:2059 173 | } 174 | 175 | func Example_stackTrace() { 176 | type stackTracer interface { 177 | StackTrace() errors.StackTrace 178 | } 179 | 180 | err, ok := errors.Cause(fn()).(stackTracer) 181 | if !ok { 182 | panic("oops, err does not implement stackTracer") 183 | } 184 | 185 | st := err.StackTrace() 186 | fmt.Printf("%+v", st[0:2]) // top two frames 187 | 188 | // Example output: 189 | // github.com/pkg/errors_test.fn 190 | // /home/dfc/src/github.com/pkg/errors/example_test.go:47 191 | // github.com/pkg/errors_test.Example_stackTrace 192 | // /home/dfc/src/github.com/pkg/errors/example_test.go:127 193 | } 194 | 195 | func ExampleCause_printf() { 196 | err := errors.Wrap(func() error { 197 | return func() error { 198 | return errors.New("hello world") 199 | }() 200 | }(), "failed") 201 | 202 | fmt.Printf("%v", err) 203 | 204 | // Output: failed: hello world 205 | } 206 | -------------------------------------------------------------------------------- /errors_with_stack_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWithStack(t *testing.T) { 8 | origErr := NewPlain("something went wrong") 9 | err := WithStack(origErr) 10 | err2 := WithStack(err) 11 | 12 | t.Parallel() 13 | 14 | t.Run("error_message", func(t *testing.T) { 15 | checkErrorMessage(t, err, "something went wrong") 16 | }) 17 | 18 | t.Run("unwrap", func(t *testing.T) { 19 | checkUnwrap(t, err, origErr) 20 | }) 21 | 22 | t.Run("format", func(t *testing.T) { 23 | checkFormat(t, err, map[string][]string{ 24 | "%s": {"something went wrong"}, 25 | "%q": {`"something went wrong"`}, 26 | "%v": {"something went wrong"}, 27 | "%+v": {"something went wrong", "emperror.dev/errors.TestWithStack\n\t.+/errors_with_stack_test.go:9"}, 28 | }) 29 | 30 | checkFormat(t, err2, map[string][]string{ 31 | "%s": {"something went wrong"}, 32 | "%q": {`"something went wrong"`}, 33 | "%v": {"something went wrong"}, 34 | "%+v": { 35 | "something went wrong", 36 | "emperror.dev/errors.TestWithStack\n\t.+/errors_with_stack_test.go:9", 37 | "emperror.dev/errors.TestWithStack\n\t.+/errors_with_stack_test.go:10", 38 | }, 39 | }) 40 | }) 41 | 42 | t.Run("nil", func(t *testing.T) { 43 | checkErrorNil(t, WithStack(nil)) 44 | }) 45 | } 46 | 47 | func TestWithStackDepth(t *testing.T) { 48 | origErr := NewPlain("something went wrong") 49 | err := WithStackDepth(origErr, 0) 50 | err2 := WithStackDepth(err, 0) 51 | 52 | t.Parallel() 53 | 54 | t.Run("error_message", func(t *testing.T) { 55 | checkErrorMessage(t, err, "something went wrong") 56 | }) 57 | 58 | t.Run("unwrap", func(t *testing.T) { 59 | checkUnwrap(t, err, origErr) 60 | }) 61 | 62 | t.Run("format", func(t *testing.T) { 63 | checkFormat(t, err, map[string][]string{ 64 | "%s": {"something went wrong"}, 65 | "%q": {`"something went wrong"`}, 66 | "%v": {"something went wrong"}, 67 | "%+v": {"something went wrong", "emperror.dev/errors.TestWithStackDepth\n\t.+/errors_with_stack_test.go:49"}, 68 | }) 69 | 70 | checkFormat(t, err2, map[string][]string{ 71 | "%s": {"something went wrong"}, 72 | "%q": {`"something went wrong"`}, 73 | "%v": {"something went wrong"}, 74 | "%+v": { 75 | "something went wrong", 76 | "emperror.dev/errors.TestWithStackDepth\n\t.+/errors_with_stack_test.go:49", 77 | "emperror.dev/errors.TestWithStackDepth\n\t.+/errors_with_stack_test.go:50", 78 | }, 79 | }) 80 | }) 81 | 82 | t.Run("nil", func(t *testing.T) { 83 | checkErrorNil(t, WithStackDepth(nil, 0)) 84 | }) 85 | } 86 | 87 | func TestWithStackDepth_CustomDepth(t *testing.T) { 88 | origErr := NewPlain("something went wrong") 89 | 90 | var err, err2 error 91 | 92 | func() { 93 | err = WithStackDepth(origErr, 1) 94 | err2 = WithStackDepth(err, 1) 95 | }() 96 | 97 | t.Parallel() 98 | 99 | t.Run("error_message", func(t *testing.T) { 100 | checkErrorMessage(t, err, "something went wrong") 101 | }) 102 | 103 | t.Run("unwrap", func(t *testing.T) { 104 | checkUnwrap(t, err, origErr) 105 | }) 106 | 107 | t.Run("format", func(t *testing.T) { 108 | checkFormat(t, err, map[string][]string{ 109 | "%s": {"something went wrong"}, 110 | "%q": {`"something went wrong"`}, 111 | "%v": {"something went wrong"}, 112 | "%+v": { 113 | "something went wrong", 114 | "emperror.dev/errors.TestWithStackDepth_CustomDepth\n\t.+/errors_with_stack_test.go:95", 115 | }, 116 | }) 117 | 118 | checkFormat(t, err2, map[string][]string{ 119 | "%s": {"something went wrong"}, 120 | "%q": {`"something went wrong"`}, 121 | "%v": {"something went wrong"}, 122 | "%+v": { 123 | "something went wrong", 124 | "emperror.dev/errors.TestWithStackDepth_CustomDepth\n\t.+/errors_with_stack_test.go:95", 125 | "emperror.dev/errors.TestWithStackDepth_CustomDepth\n\t.+/errors_with_stack_test.go:95", 126 | }, 127 | }) 128 | }) 129 | 130 | t.Run("nil", func(t *testing.T) { 131 | checkErrorNil(t, WithStackDepth(nil, 0)) 132 | }) 133 | } 134 | 135 | func TestWithStackIf(t *testing.T) { 136 | origErr := NewPlain("something went wrong") 137 | err := WithStackIf(origErr) 138 | err2 := WithStackIf(err) 139 | 140 | t.Parallel() 141 | 142 | t.Run("error_message", func(t *testing.T) { 143 | checkErrorMessage(t, err, "something went wrong") 144 | }) 145 | 146 | t.Run("unwrap", func(t *testing.T) { 147 | checkUnwrap(t, err, origErr) 148 | }) 149 | 150 | t.Run("format", func(t *testing.T) { 151 | checkFormat(t, err, map[string][]string{ 152 | "%s": {"something went wrong"}, 153 | "%q": {`"something went wrong"`}, 154 | "%v": {"something went wrong"}, 155 | "%+v": {"something went wrong", "emperror.dev/errors.TestWithStackIf\n\t.+/errors_with_stack_test.go:137"}, 156 | }) 157 | 158 | checkFormat(t, err2, map[string][]string{ 159 | "%s": {"something went wrong"}, 160 | "%q": {`"something went wrong"`}, 161 | "%v": {"something went wrong"}, 162 | "%+v": {"something went wrong", "emperror.dev/errors.TestWithStackIf\n\t.+/errors_with_stack_test.go:137"}, 163 | }) 164 | }) 165 | 166 | t.Run("nil", func(t *testing.T) { 167 | checkErrorNil(t, WithStackIf(nil)) 168 | }) 169 | } 170 | 171 | func TestWithStackDepthIf(t *testing.T) { 172 | origErr := NewPlain("something went wrong") 173 | err := WithStackDepthIf(origErr, 0) 174 | err2 := WithStackDepthIf(err, 0) 175 | 176 | t.Parallel() 177 | 178 | t.Run("error_message", func(t *testing.T) { 179 | checkErrorMessage(t, err, "something went wrong") 180 | }) 181 | 182 | t.Run("unwrap", func(t *testing.T) { 183 | checkUnwrap(t, err, origErr) 184 | }) 185 | 186 | t.Run("format", func(t *testing.T) { 187 | checkFormat(t, err, map[string][]string{ 188 | "%s": {"something went wrong"}, 189 | "%q": {`"something went wrong"`}, 190 | "%v": {"something went wrong"}, 191 | "%+v": {"something went wrong", "emperror.dev/errors.TestWithStackDepthIf\n\t.+/errors_with_stack_test.go:173"}, 192 | }) 193 | 194 | checkFormat(t, err2, map[string][]string{ 195 | "%s": {"something went wrong"}, 196 | "%q": {`"something went wrong"`}, 197 | "%v": {"something went wrong"}, 198 | "%+v": {"something went wrong", "emperror.dev/errors.TestWithStackDepthIf\n\t.+/errors_with_stack_test.go:173"}, 199 | }) 200 | }) 201 | 202 | t.Run("nil", func(t *testing.T) { 203 | checkErrorNil(t, WithStack(nil)) 204 | }) 205 | } 206 | -------------------------------------------------------------------------------- /tests/wrap_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.13 2 | 3 | // Copyright 2018 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | package errors_test 8 | 9 | import ( 10 | "emperror.dev/errors" 11 | "fmt" 12 | "os" 13 | "reflect" 14 | "testing" 15 | ) 16 | 17 | func TestIs(t *testing.T) { 18 | err1 := errors.NewPlain("1") 19 | erra := wrapped{"wrap 2", err1} 20 | errb := wrapped{"wrap 3", erra} 21 | 22 | err3 := errors.NewPlain("3") 23 | 24 | poser := &poser{"either 1 or 3", func(err error) bool { 25 | return err == err1 || err == err3 26 | }} 27 | 28 | testCases := []struct { 29 | err error 30 | target error 31 | match bool 32 | }{ 33 | {nil, nil, true}, 34 | {err1, nil, false}, 35 | {err1, err1, true}, 36 | {erra, err1, true}, 37 | {errb, err1, true}, 38 | {err1, err3, false}, 39 | {erra, err3, false}, 40 | {errb, err3, false}, 41 | {poser, err1, true}, 42 | {poser, err3, true}, 43 | {poser, erra, false}, 44 | {poser, errb, false}, 45 | {errorUncomparable{}, errorUncomparable{}, true}, 46 | {errorUncomparable{}, &errorUncomparable{}, false}, 47 | {&errorUncomparable{}, errorUncomparable{}, true}, 48 | {&errorUncomparable{}, &errorUncomparable{}, false}, 49 | {errorUncomparable{}, err1, false}, 50 | {&errorUncomparable{}, err1, false}, 51 | } 52 | for _, tc := range testCases { 53 | t.Run("", func(t *testing.T) { 54 | if got := errors.Is(tc.err, tc.target); got != tc.match { 55 | t.Errorf("Is(%v, %v) = %v, want %v", tc.err, tc.target, got, tc.match) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | type poser struct { 62 | msg string 63 | f func(error) bool 64 | } 65 | 66 | var poserPathErr = &os.PathError{Op: "poser"} 67 | 68 | func (p *poser) Error() string { return p.msg } 69 | func (p *poser) Is(err error) bool { return p.f(err) } 70 | func (p *poser) As(err interface{}) bool { 71 | switch x := err.(type) { 72 | case **poser: 73 | *x = p 74 | case *errorT: 75 | *x = errorT{"poser"} 76 | case **os.PathError: 77 | *x = poserPathErr 78 | default: 79 | return false 80 | } 81 | return true 82 | } 83 | 84 | func TestAs(t *testing.T) { 85 | var errT errorT 86 | var errP *os.PathError 87 | var timeout interface{ Timeout() bool } 88 | var p *poser 89 | _, errF := os.Open("non-existing") 90 | poserErr := &poser{"oh no", nil} 91 | 92 | testCases := []struct { 93 | err error 94 | target interface{} 95 | match bool 96 | want interface{} // value of target on match 97 | }{{ 98 | nil, 99 | &errP, 100 | false, 101 | nil, 102 | }, { 103 | wrapped{"pitied the fool", errorT{"T"}}, 104 | &errT, 105 | true, 106 | errorT{"T"}, 107 | }, { 108 | errF, 109 | &errP, 110 | true, 111 | errF, 112 | }, { 113 | errorT{}, 114 | &errP, 115 | false, 116 | nil, 117 | }, { 118 | wrapped{"wrapped", nil}, 119 | &errT, 120 | false, 121 | nil, 122 | }, { 123 | &poser{"error", nil}, 124 | &errT, 125 | true, 126 | errorT{"poser"}, 127 | }, { 128 | &poser{"path", nil}, 129 | &errP, 130 | true, 131 | poserPathErr, 132 | }, { 133 | poserErr, 134 | &p, 135 | true, 136 | poserErr, 137 | }, { 138 | errors.NewPlain("err"), 139 | &timeout, 140 | false, 141 | nil, 142 | }, { 143 | errF, 144 | &timeout, 145 | true, 146 | errF, 147 | }, { 148 | wrapped{"path error", errF}, 149 | &timeout, 150 | true, 151 | errF, 152 | }} 153 | for i, tc := range testCases { 154 | name := fmt.Sprintf("%d:As(Errorf(..., %v), %v)", i, tc.err, tc.target) 155 | // Clear the target pointer, in case it was set in a previous test. 156 | rtarget := reflect.ValueOf(tc.target) 157 | rtarget.Elem().Set(reflect.Zero(reflect.TypeOf(tc.target).Elem())) 158 | t.Run(name, func(t *testing.T) { 159 | match := errors.As(tc.err, tc.target) 160 | if match != tc.match { 161 | t.Fatalf("match: got %v; want %v", match, tc.match) 162 | } 163 | if !match { 164 | return 165 | } 166 | if got := rtarget.Elem().Interface(); got != tc.want { 167 | t.Fatalf("got %#v, want %#v", got, tc.want) 168 | } 169 | }) 170 | } 171 | } 172 | 173 | func TestAsValidation(t *testing.T) { 174 | var s string 175 | testCases := []interface{}{ 176 | nil, 177 | (*int)(nil), 178 | "error", 179 | &s, 180 | } 181 | err := errors.NewPlain("error") 182 | for _, tc := range testCases { 183 | t.Run(fmt.Sprintf("%T(%v)", tc, tc), func(t *testing.T) { 184 | defer func() { 185 | recover() 186 | }() 187 | if errors.As(err, tc) { 188 | t.Errorf("As(err, %T(%v)) = true, want false", tc, tc) 189 | return 190 | } 191 | t.Errorf("As(err, %T(%v)) did not panic", tc, tc) 192 | }) 193 | } 194 | } 195 | 196 | func TestUnwrap(t *testing.T) { 197 | err1 := errors.NewPlain("1") 198 | erra := wrapped{"wrap 2", err1} 199 | 200 | testCases := []struct { 201 | err error 202 | want error 203 | }{ 204 | {nil, nil}, 205 | {wrapped{"wrapped", nil}, nil}, 206 | {err1, nil}, 207 | {erra, err1}, 208 | {wrapped{"wrap 3", erra}, erra}, 209 | } 210 | for _, tc := range testCases { 211 | if got := errors.Unwrap(tc.err); got != tc.want { 212 | t.Errorf("Unwrap(%v) = %v, want %v", tc.err, got, tc.want) 213 | } 214 | } 215 | } 216 | 217 | type errorT struct{ s string } 218 | 219 | func (e errorT) Error() string { return fmt.Sprintf("errorT(%s)", e.s) } 220 | 221 | type wrapped struct { 222 | msg string 223 | err error 224 | } 225 | 226 | func (e wrapped) Error() string { return e.msg } 227 | 228 | func (e wrapped) Unwrap() error { return e.err } 229 | 230 | type errorUncomparable struct { 231 | f []string 232 | } 233 | 234 | func (errorUncomparable) Error() string { 235 | return "uncomparable error" 236 | } 237 | 238 | func (errorUncomparable) Is(target error) bool { 239 | _, ok := target.(errorUncomparable) 240 | return ok 241 | } 242 | 243 | func ExampleIs() { 244 | if _, err := os.Open("non-existing"); err != nil { 245 | if errors.Is(err, os.ErrNotExist) { 246 | fmt.Println("file does not exist") 247 | } else { 248 | fmt.Println(err) 249 | } 250 | } 251 | 252 | // Output: 253 | // file does not exist 254 | } 255 | 256 | func ExampleAs() { 257 | if _, err := os.Open("non-existing"); err != nil { 258 | var pathError *os.PathError 259 | if errors.As(err, &pathError) { 260 | fmt.Println("Failed at path:", pathError.Path) 261 | } else { 262 | fmt.Println(err) 263 | } 264 | } 265 | 266 | // Output: 267 | // Failed at path: non-existing 268 | } 269 | -------------------------------------------------------------------------------- /tests/errors_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | . "emperror.dev/errors" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestNew(t *testing.T) { 12 | tests := []struct { 13 | err string 14 | want error 15 | }{ 16 | {"", fmt.Errorf("")}, 17 | {"foo", fmt.Errorf("foo")}, 18 | {"foo", New("foo")}, 19 | {"string with format specifiers: %v", NewPlain("string with format specifiers: %v")}, 20 | } 21 | 22 | for _, tt := range tests { 23 | got := New(tt.err) 24 | if got.Error() != tt.want.Error() { 25 | t.Errorf("New.Error(): got: %q, want %q", got, tt.want) 26 | } 27 | } 28 | } 29 | 30 | func TestWrapNil(t *testing.T) { 31 | got := Wrap(nil, "no error") 32 | if got != nil { 33 | t.Errorf("Wrap(nil, \"no error\"): got %#v, expected nil", got) 34 | } 35 | } 36 | 37 | func TestWrap(t *testing.T) { 38 | tests := []struct { 39 | err error 40 | message string 41 | want string 42 | }{ 43 | {io.EOF, "read error", "read error: EOF"}, 44 | {Wrap(io.EOF, "read error"), "client error", "client error: read error: EOF"}, 45 | } 46 | 47 | for _, tt := range tests { 48 | got := Wrap(tt.err, tt.message).Error() 49 | if got != tt.want { 50 | t.Errorf("Wrap(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) 51 | } 52 | } 53 | } 54 | 55 | type nilError struct{} 56 | 57 | func (nilError) Error() string { return "nil error" } 58 | 59 | func TestCause(t *testing.T) { 60 | x := NewPlain("error") 61 | tests := []struct { 62 | err error 63 | want error 64 | }{{ 65 | // nil error is nil 66 | err: nil, 67 | want: nil, 68 | }, { 69 | // explicit nil error is nil 70 | err: (error)(nil), 71 | want: nil, 72 | }, { 73 | // typed nil is nil 74 | err: (*nilError)(nil), 75 | want: (*nilError)(nil), 76 | }, { 77 | // uncaused error is unaffected 78 | err: io.EOF, 79 | want: io.EOF, 80 | }, { 81 | // caused error returns cause 82 | err: Wrap(io.EOF, "ignored"), 83 | want: io.EOF, 84 | }, { 85 | err: x, // return from NewPlain 86 | want: x, 87 | }, { 88 | WithMessage(nil, "whoops"), 89 | nil, 90 | }, { 91 | WithMessage(io.EOF, "whoops"), 92 | io.EOF, 93 | }, { 94 | WithStack(nil), 95 | nil, 96 | }, { 97 | WithStack(io.EOF), 98 | io.EOF, 99 | }} 100 | 101 | for i, tt := range tests { 102 | got := Cause(tt.err) 103 | if !reflect.DeepEqual(got, tt.want) { 104 | t.Errorf("test %d: got %#v, want %#v", i+1, got, tt.want) 105 | } 106 | } 107 | } 108 | 109 | func TestWrapfNil(t *testing.T) { 110 | got := Wrapf(nil, "no error") 111 | if got != nil { 112 | t.Errorf("Wrapf(nil, \"no error\"): got %#v, expected nil", got) 113 | } 114 | } 115 | 116 | func TestWrapf(t *testing.T) { 117 | tests := []struct { 118 | err error 119 | message string 120 | want string 121 | }{ 122 | {io.EOF, "read error", "read error: EOF"}, 123 | {Wrapf(io.EOF, "read error without format specifiers"), "client error", "client error: read error without format specifiers: EOF"}, 124 | {Wrapf(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"}, 125 | } 126 | 127 | for _, tt := range tests { 128 | got := Wrapf(tt.err, tt.message).Error() 129 | if got != tt.want { 130 | t.Errorf("Wrapf(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) 131 | } 132 | } 133 | } 134 | 135 | func TestErrorf(t *testing.T) { 136 | tests := []struct { 137 | err error 138 | want string 139 | }{ 140 | {Errorf("read error without format specifiers"), "read error without format specifiers"}, 141 | {Errorf("read error with %d format specifier", 1), "read error with 1 format specifier"}, 142 | } 143 | 144 | for _, tt := range tests { 145 | got := tt.err.Error() 146 | if got != tt.want { 147 | t.Errorf("Errorf(%v): got: %q, want %q", tt.err, got, tt.want) 148 | } 149 | } 150 | } 151 | 152 | func TestWithStackNil(t *testing.T) { 153 | got := WithStack(nil) 154 | if got != nil { 155 | t.Errorf("WithStack(nil): got %#v, expected nil", got) 156 | } 157 | } 158 | 159 | func TestWithStack(t *testing.T) { 160 | tests := []struct { 161 | err error 162 | want string 163 | }{ 164 | {io.EOF, "EOF"}, 165 | {WithStack(io.EOF), "EOF"}, 166 | } 167 | 168 | for _, tt := range tests { 169 | got := WithStack(tt.err).Error() 170 | if got != tt.want { 171 | t.Errorf("WithStack(%v): got: %v, want %v", tt.err, got, tt.want) 172 | } 173 | } 174 | } 175 | 176 | func TestWithMessageNil(t *testing.T) { 177 | got := WithMessage(nil, "no error") 178 | if got != nil { 179 | t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got) 180 | } 181 | } 182 | 183 | func TestWithMessage(t *testing.T) { 184 | tests := []struct { 185 | err error 186 | message string 187 | want string 188 | }{ 189 | {io.EOF, "read error", "read error: EOF"}, 190 | {WithMessage(io.EOF, "read error"), "client error", "client error: read error: EOF"}, 191 | } 192 | 193 | for _, tt := range tests { 194 | got := WithMessage(tt.err, tt.message).Error() 195 | if got != tt.want { 196 | t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want) 197 | } 198 | } 199 | } 200 | 201 | func TestWithMessagefNil(t *testing.T) { 202 | got := WithMessagef(nil, "no error") 203 | if got != nil { 204 | t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got) 205 | } 206 | } 207 | 208 | func TestWithMessagef(t *testing.T) { 209 | tests := []struct { 210 | err error 211 | message string 212 | want string 213 | }{ 214 | {io.EOF, "read error", "read error: EOF"}, 215 | {WithMessagef(io.EOF, "read error without format specifier"), "client error", "client error: read error without format specifier: EOF"}, 216 | {WithMessagef(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"}, 217 | } 218 | 219 | for _, tt := range tests { 220 | got := WithMessagef(tt.err, tt.message).Error() 221 | if got != tt.want { 222 | t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want) 223 | } 224 | } 225 | } 226 | 227 | // NewPlain, etc values are not expected to be compared by value 228 | // but the change in errors#27 made them incomparable. Assert that 229 | // various kinds of errors have a functional equality operator, even 230 | // if the result of that equality is always false. 231 | func TestErrorEquality(t *testing.T) { 232 | vals := []error{ 233 | nil, 234 | io.EOF, 235 | NewPlain("EOF"), 236 | New("EOF"), 237 | Errorf("EOF"), 238 | Wrap(io.EOF, "EOF"), 239 | Wrapf(io.EOF, "EOF%d", 2), 240 | WithMessage(nil, "whoops"), 241 | WithMessage(io.EOF, "whoops"), 242 | WithStack(io.EOF), 243 | WithStack(nil), 244 | } 245 | 246 | for i := range vals { 247 | for j := range vals { 248 | _ = vals[i] == vals[j] // mustn't panic 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /errors_wrap_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWrap(t *testing.T) { 8 | origErr := NewPlain("something went wrong") 9 | err := Wrap(origErr, "error") 10 | err2 := Wrap(err, "panic") 11 | 12 | t.Parallel() 13 | 14 | t.Run("error_message", func(t *testing.T) { 15 | checkErrorMessage(t, err, "error: something went wrong") 16 | }) 17 | 18 | t.Run("unwrap", func(t *testing.T) { 19 | checkUnwrap(t, err, err.(*withStack).error) 20 | }) 21 | 22 | t.Run("format", func(t *testing.T) { 23 | checkFormat(t, err, map[string][]string{ 24 | "%s": {"error: something went wrong"}, 25 | "%q": {`"error: something went wrong"`}, 26 | "%v": {"error: something went wrong"}, 27 | "%+v": {"something went wrong", "error", "emperror.dev/errors.TestWrap\n\t.+/errors_wrap_test.go:9"}, 28 | }) 29 | 30 | checkFormat(t, err2, map[string][]string{ 31 | "%s": {"panic: error: something went wrong"}, 32 | "%q": {`"panic: error: something went wrong"`}, 33 | "%v": {"panic: error: something went wrong"}, 34 | "%+v": { 35 | "something went wrong", 36 | "error", 37 | "emperror.dev/errors.TestWrap\n\t.+/errors_wrap_test.go:9", 38 | "panic", 39 | "emperror.dev/errors.TestWrap\n\t.+/errors_wrap_test.go:10", 40 | }, 41 | }) 42 | }) 43 | 44 | t.Run("nil", func(t *testing.T) { 45 | checkErrorNil(t, Wrap(nil, "error")) 46 | }) 47 | } 48 | 49 | func TestWrapf(t *testing.T) { 50 | origErr := NewPlain("something went wrong") 51 | err := Wrapf(origErr, "%s", "error") 52 | err2 := Wrapf(err, "%s", "panic") 53 | 54 | t.Parallel() 55 | 56 | t.Run("error_message", func(t *testing.T) { 57 | checkErrorMessage(t, err, "error: something went wrong") 58 | }) 59 | 60 | t.Run("unwrap", func(t *testing.T) { 61 | checkUnwrap(t, err, err.(*withStack).error) 62 | }) 63 | 64 | t.Run("format", func(t *testing.T) { 65 | checkFormat(t, err, map[string][]string{ 66 | "%s": {"error: something went wrong"}, 67 | "%q": {`"error: something went wrong"`}, 68 | "%v": {"error: something went wrong"}, 69 | "%+v": {"something went wrong", "error", "emperror.dev/errors.TestWrapf\n\t.+/errors_wrap_test.go:51"}, 70 | }) 71 | 72 | checkFormat(t, err2, map[string][]string{ 73 | "%s": {"panic: error: something went wrong"}, 74 | "%q": {`"panic: error: something went wrong"`}, 75 | "%v": {"panic: error: something went wrong"}, 76 | "%+v": { 77 | "something went wrong", 78 | "error", 79 | "emperror.dev/errors.TestWrapf\n\t.+/errors_wrap_test.go:51", 80 | "panic", 81 | "emperror.dev/errors.TestWrapf\n\t.+/errors_wrap_test.go:52", 82 | }, 83 | }) 84 | }) 85 | 86 | t.Run("nil", func(t *testing.T) { 87 | checkErrorNil(t, Wrapf(nil, "%s", "error")) 88 | }) 89 | } 90 | 91 | func TestWrapIf(t *testing.T) { 92 | origErr := NewPlain("something went wrong") 93 | err := WrapIf(origErr, "error") 94 | err2 := WrapIf(err, "panic") 95 | 96 | t.Parallel() 97 | 98 | t.Run("error_message", func(t *testing.T) { 99 | checkErrorMessage(t, err, "error: something went wrong") 100 | }) 101 | 102 | t.Run("unwrap", func(t *testing.T) { 103 | checkUnwrap(t, err, err.(*withStack).error) 104 | }) 105 | 106 | t.Run("format", func(t *testing.T) { 107 | checkFormat(t, err, map[string][]string{ 108 | "%s": {"error: something went wrong"}, 109 | "%q": {`"error: something went wrong"`}, 110 | "%v": {"error: something went wrong"}, 111 | "%+v": {"something went wrong", "error", "emperror.dev/errors.TestWrapIf\n\t.+/errors_wrap_test.go:93"}, 112 | }) 113 | 114 | checkFormat(t, err2, map[string][]string{ 115 | "%s": {"panic: error: something went wrong"}, 116 | "%q": {`panic: error: something went wrong`}, // TODO: quotes? 117 | "%v": {"panic: error: something went wrong"}, 118 | "%+v": { 119 | "something went wrong", 120 | "error", 121 | "emperror.dev/errors.TestWrapIf\n\t.+/errors_wrap_test.go:93", 122 | "panic", 123 | }, 124 | }) 125 | }) 126 | 127 | t.Run("nil", func(t *testing.T) { 128 | checkErrorNil(t, WrapIf(nil, "error")) 129 | }) 130 | } 131 | 132 | func TestWrapIff(t *testing.T) { 133 | origErr := NewPlain("something went wrong") 134 | err := WrapIff(origErr, "%s", "error") 135 | err2 := WrapIff(err, "%s", "panic") 136 | 137 | t.Parallel() 138 | 139 | t.Run("error_message", func(t *testing.T) { 140 | checkErrorMessage(t, err, "error: something went wrong") 141 | }) 142 | 143 | t.Run("unwrap", func(t *testing.T) { 144 | checkUnwrap(t, err, err.(*withStack).error) 145 | }) 146 | 147 | t.Run("format", func(t *testing.T) { 148 | checkFormat(t, err, map[string][]string{ 149 | "%s": {"error: something went wrong"}, 150 | "%q": {`"error: something went wrong"`}, 151 | "%v": {"error: something went wrong"}, 152 | "%+v": {"something went wrong", "error", "emperror.dev/errors.TestWrapIff\n\t.+/errors_wrap_test.go:134"}, 153 | }) 154 | 155 | checkFormat(t, err2, map[string][]string{ 156 | "%s": {"panic: error: something went wrong"}, 157 | "%q": {`panic: error: something went wrong`}, // TODO: quotes? 158 | "%v": {"panic: error: something went wrong"}, 159 | "%+v": { 160 | "something went wrong", 161 | "error", 162 | "emperror.dev/errors.TestWrapIff\n\t.+/errors_wrap_test.go:134", 163 | "panic", 164 | }, 165 | }) 166 | }) 167 | 168 | t.Run("nil", func(t *testing.T) { 169 | checkErrorNil(t, WrapIff(nil, "%s", "error")) 170 | }) 171 | } 172 | 173 | func TestWrapWithDetails(t *testing.T) { 174 | origErr := NewPlain("something went wrong") 175 | details := []interface{}{"key", "value"} 176 | err := WrapWithDetails(origErr, "error", details...) 177 | 178 | t.Parallel() 179 | 180 | t.Run("error_message", func(t *testing.T) { 181 | checkErrorMessage(t, err, "error: something went wrong") 182 | }) 183 | 184 | t.Run("unwrap", func(t *testing.T) { 185 | checkUnwrap(t, err, err.(*withDetails).error) 186 | }) 187 | 188 | t.Run("format", func(t *testing.T) { 189 | checkFormat(t, err, map[string][]string{ 190 | "%s": {"error: something went wrong"}, 191 | "%q": {`"error: something went wrong"`}, 192 | "%v": {"error: something went wrong"}, 193 | "%+v": {"something went wrong", "error", "emperror.dev/errors.TestWrapWithDetails\n\t.+/errors_wrap_test.go:176"}, 194 | }) 195 | }) 196 | 197 | t.Run("nil", func(t *testing.T) { 198 | checkErrorNil(t, WrapWithDetails(nil, "error", "key", "value")) 199 | }) 200 | 201 | t.Run("details", func(t *testing.T) { 202 | d := GetDetails(err) 203 | 204 | for i, detail := range d { 205 | if got, want := detail, details[i]; got != want { 206 | t.Errorf("error detail does not match the expected one\nactual: %+v\nexpected: %+v", got, want) 207 | } 208 | } 209 | }) 210 | } 211 | 212 | func TestWrapIfWithDetails(t *testing.T) { 213 | origErr := NewPlain("something went wrong") 214 | details := []interface{}{"key", "value"} 215 | err := WrapIfWithDetails(WithStack(origErr), "error", details...) 216 | 217 | t.Parallel() 218 | 219 | t.Run("error_message", func(t *testing.T) { 220 | checkErrorMessage(t, err, "error: something went wrong") 221 | }) 222 | 223 | t.Run("unwrap", func(t *testing.T) { 224 | checkUnwrap(t, err, err.(*withDetails).error) 225 | }) 226 | 227 | t.Run("format", func(t *testing.T) { 228 | checkFormat(t, err, map[string][]string{ 229 | "%s": {"error: something went wrong"}, 230 | "%q": {`error: something went wrong`}, // TODO: quotes? 231 | "%v": {"error: something went wrong"}, 232 | // "%+v": {"something went wrong", "error", "emperror.dev/errors.TestWrapIfWithDetails\n\t.+/errors_wrap_test.go:215"}, 233 | }) 234 | }) 235 | 236 | t.Run("nil", func(t *testing.T) { 237 | checkErrorNil(t, WrapIfWithDetails(nil, "error", "key", "value")) 238 | }) 239 | 240 | t.Run("details", func(t *testing.T) { 241 | d := GetDetails(err) 242 | 243 | for i, detail := range d { 244 | if got, want := detail, details[i]; got != want { 245 | t.Errorf("error detail does not match the expected one\nactual: %+v\nexpected: %+v", got, want) 246 | } 247 | } 248 | }) 249 | } 250 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package errors is a drop-in replacement for the standard errors package and github.com/pkg/errors. 3 | 4 | 5 | Overview 6 | 7 | This is a single, lightweight library merging the features of standard library `errors` package 8 | and https://github.com/pkg/errors. It also backports a few features 9 | (like Go 1.13 error handling related features). 10 | 11 | 12 | Printing errors 13 | 14 | If not stated otherwise, errors can be formatted with the following specifiers: 15 | %s error message 16 | %q double-quoted error message 17 | %v error message in default format 18 | %+v error message and stack trace 19 | */ 20 | package errors // import "emperror.dev/errors" 21 | 22 | import ( 23 | "fmt" 24 | "io" 25 | 26 | "github.com/pkg/errors" 27 | ) 28 | 29 | // NewPlain returns a simple error without any annotated context, like stack trace. 30 | // Useful for creating sentinel errors and in testing. 31 | // 32 | // var ErrSomething = errors.NewPlain("something went wrong") 33 | func NewPlain(message string) error { 34 | return &plainError{message} 35 | } 36 | 37 | // plainError is a trivial implementation of error. 38 | type plainError struct { 39 | msg string 40 | } 41 | 42 | func (e *plainError) Error() string { 43 | return e.msg 44 | } 45 | 46 | // Sentinel is a simple error without any annotated context, like stack trace. 47 | // Useful for creating sentinel errors. 48 | // 49 | // const ErrSomething = errors.Sentinel("something went wrong") 50 | // 51 | // See https://dave.cheney.net/2016/04/07/constant-errors 52 | type Sentinel string 53 | 54 | func (e Sentinel) Error() string { 55 | return string(e) 56 | } 57 | 58 | // New returns a new error annotated with stack trace at the point New is called. 59 | // 60 | // New is a shorthand for: 61 | // WithStack(NewPlain(message)) 62 | func New(message string) error { 63 | return WithStackDepth(NewPlain(message), 1) 64 | } 65 | 66 | // NewWithDetails returns a new error annotated with stack trace at the point NewWithDetails is called, 67 | // and the supplied details. 68 | func NewWithDetails(message string, details ...interface{}) error { 69 | return WithDetails(WithStackDepth(NewPlain(message), 1), details...) 70 | } 71 | 72 | // Errorf returns a new error with a formatted message and annotated with stack trace at the point Errorf is called. 73 | // 74 | // err := errors.Errorf("something went %s", "wrong") 75 | func Errorf(format string, a ...interface{}) error { 76 | return WithStackDepth(NewPlain(fmt.Sprintf(format, a...)), 1) 77 | } 78 | 79 | // WithStack annotates err with a stack trace at the point WithStack was called. 80 | // If err is nil, WithStack returns nil. 81 | // 82 | // WithStack is commonly used with sentinel errors and errors returned from libraries 83 | // not annotating errors with stack trace: 84 | // 85 | // var ErrSomething = errors.NewPlain("something went wrong") 86 | // 87 | // func doSomething() error { 88 | // return errors.WithStack(ErrSomething) 89 | // } 90 | func WithStack(err error) error { 91 | return WithStackDepth(err, 1) 92 | } 93 | 94 | // WithStackDepth annotates err with a stack trace at the given call depth. 95 | // Zero identifies the caller of WithStackDepth itself. 96 | // If err is nil, WithStackDepth returns nil. 97 | // 98 | // WithStackDepth is generally used in other error constructors: 99 | // 100 | // func MyWrapper(err error) error { 101 | // return WithStackDepth(err, 1) 102 | // } 103 | func WithStackDepth(err error, depth int) error { 104 | if err == nil { 105 | return nil 106 | } 107 | 108 | return &withStack{ 109 | error: err, 110 | stack: callers(depth + 1), 111 | } 112 | } 113 | 114 | // WithStackIf behaves the same way as WithStack except it does not annotate the error with a stack trace 115 | // if there is already one in err's chain. 116 | func WithStackIf(err error) error { 117 | return WithStackDepthIf(err, 1) 118 | } 119 | 120 | // WithStackDepthIf behaves the same way as WithStackDepth except it does not annotate the error with a stack trace 121 | // if there is already one in err's chain. 122 | func WithStackDepthIf(err error, depth int) error { 123 | if err == nil { 124 | return nil 125 | } 126 | 127 | var st interface{ StackTrace() errors.StackTrace } 128 | if !As(err, &st) { 129 | return &withStack{ 130 | error: err, 131 | stack: callers(depth + 1), 132 | } 133 | } 134 | 135 | return err 136 | } 137 | 138 | type withStack struct { 139 | error 140 | *stack 141 | } 142 | 143 | func (w *withStack) Cause() error { return w.error } 144 | func (w *withStack) Unwrap() error { return w.error } 145 | 146 | // nolint: errcheck 147 | func (w *withStack) Format(s fmt.State, verb rune) { 148 | switch verb { 149 | case 'v': 150 | if s.Flag('+') { 151 | fmt.Fprintf(s, "%+v", w.error) 152 | w.stack.Format(s, verb) 153 | 154 | return 155 | } 156 | 157 | fallthrough 158 | case 's': 159 | io.WriteString(s, w.Error()) 160 | case 'q': 161 | fmt.Fprintf(s, "%q", w.Error()) 162 | } 163 | } 164 | 165 | // WithMessage annotates err with a new message. 166 | // If err is nil, WithMessage returns nil. 167 | // 168 | // WithMessage is useful when the error already contains a stack trace, but adding additional info to the message 169 | // helps in debugging. 170 | // 171 | // Errors returned by WithMessage are formatted slightly differently: 172 | // %s error messages separated by a colon and a space (": ") 173 | // %q double-quoted error messages separated by a colon and a space (": ") 174 | // %v one error message per line 175 | // %+v one error message per line and stack trace (if any) 176 | func WithMessage(err error, message string) error { 177 | return errors.WithMessage(err, message) 178 | } 179 | 180 | // WithMessagef annotates err with the format specifier. 181 | // If err is nil, WithMessagef returns nil. 182 | // 183 | // WithMessagef is useful when the error already contains a stack trace, but adding additional info to the message 184 | // helps in debugging. 185 | // 186 | // The same formatting rules apply as in case of WithMessage. 187 | func WithMessagef(err error, format string, a ...interface{}) error { 188 | return errors.WithMessagef(err, format, a...) 189 | } 190 | 191 | // Wrap returns an error annotating err with a stack trace 192 | // at the point Wrap is called, and the supplied message. 193 | // If err is nil, Wrap returns nil. 194 | // 195 | // Wrap is a shorthand for: 196 | // WithStack(WithMessage(err, message)) 197 | func Wrap(err error, message string) error { 198 | return WithStackDepth(WithMessage(err, message), 1) 199 | } 200 | 201 | // Wrapf returns an error annotating err with a stack trace 202 | // at the point Wrapf is called, and the format specifier. 203 | // If err is nil, Wrapf returns nil. 204 | // 205 | // Wrapf is a shorthand for: 206 | // WithStack(WithMessagef(err, format, a...)) 207 | func Wrapf(err error, format string, a ...interface{}) error { 208 | return WithStackDepth(WithMessagef(err, format, a...), 1) 209 | } 210 | 211 | // WrapIf behaves the same way as Wrap except it does not annotate the error with a stack trace 212 | // if there is already one in err's chain. 213 | // 214 | // If err is nil, WrapIf returns nil. 215 | func WrapIf(err error, message string) error { 216 | return WithStackDepthIf(WithMessage(err, message), 1) 217 | } 218 | 219 | // WrapIff behaves the same way as Wrapf except it does not annotate the error with a stack trace 220 | // if there is already one in err's chain. 221 | // 222 | // If err is nil, WrapIff returns nil. 223 | func WrapIff(err error, format string, a ...interface{}) error { 224 | return WithStackDepthIf(WithMessagef(err, format, a...), 1) 225 | } 226 | 227 | // WrapWithDetails returns an error annotating err with a stack trace 228 | // at the point WrapWithDetails is called, and the supplied message and details. 229 | // If err is nil, WrapWithDetails returns nil. 230 | // 231 | // WrapWithDetails is a shorthand for: 232 | // WithDetails(WithStack(WithMessage(err, message, details...)) 233 | func WrapWithDetails(err error, message string, details ...interface{}) error { 234 | return WithDetails(WithStackDepth(WithMessage(err, message), 1), details...) 235 | } 236 | 237 | // WrapIfWithDetails returns an error annotating err with a stack trace 238 | // at the point WrapIfWithDetails is called, and the supplied message and details. 239 | // If err is nil, WrapIfWithDetails returns nil. 240 | // 241 | // WrapIfWithDetails is a shorthand for: 242 | // WithDetails(WithStackIf(WithMessage(err, message, details...)) 243 | func WrapIfWithDetails(err error, message string, details ...interface{}) error { 244 | return WithDetails(WithStackDepthIf(WithMessage(err, message), 1), details...) 245 | } 246 | -------------------------------------------------------------------------------- /tests/format_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | . "emperror.dev/errors" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestFormatNew(t *testing.T) { 13 | tests := []struct { 14 | error 15 | format string 16 | want string 17 | }{{ 18 | New("error"), 19 | "%s", 20 | "error", 21 | }, { 22 | New("error"), 23 | "%v", 24 | "error", 25 | }, { 26 | New("error"), 27 | "%+v", 28 | "error\n" + 29 | "emperror.dev/errors/tests.TestFormatNew\n" + 30 | "\t.+/format_test.go:26", 31 | }, { 32 | New("error"), 33 | "%q", 34 | `"error"`, 35 | }} 36 | 37 | for i, tt := range tests { 38 | testFormatRegexp(t, i, tt.error, tt.format, tt.want) 39 | } 40 | } 41 | 42 | func TestFormatErrorf(t *testing.T) { 43 | tests := []struct { 44 | error 45 | format string 46 | want string 47 | }{{ 48 | Errorf("%s", "error"), 49 | "%s", 50 | "error", 51 | }, { 52 | Errorf("%s", "error"), 53 | "%v", 54 | "error", 55 | }, { 56 | Errorf("%s", "error"), 57 | "%+v", 58 | "error\n" + 59 | "emperror.dev/errors/tests.TestFormatErrorf\n" + 60 | "\t.+/format_test.go:56", 61 | }} 62 | 63 | for i, tt := range tests { 64 | testFormatRegexp(t, i, tt.error, tt.format, tt.want) 65 | } 66 | } 67 | 68 | func TestFormatWrap(t *testing.T) { 69 | tests := []struct { 70 | error 71 | format string 72 | want string 73 | }{{ 74 | Wrap(New("error"), "error2"), 75 | "%s", 76 | "error2: error", 77 | }, { 78 | Wrap(New("error"), "error2"), 79 | "%v", 80 | "error2: error", 81 | }, { 82 | Wrap(New("error"), "error2"), 83 | "%+v", 84 | "error\n" + 85 | "emperror.dev/errors/tests.TestFormatWrap\n" + 86 | "\t.+/format_test.go:82", 87 | }, { 88 | Wrap(io.EOF, "error"), 89 | "%s", 90 | "error: EOF", 91 | }, { 92 | Wrap(io.EOF, "error"), 93 | "%v", 94 | "error: EOF", 95 | }, { 96 | Wrap(io.EOF, "error"), 97 | "%+v", 98 | "EOF\n" + 99 | "error\n" + 100 | "emperror.dev/errors/tests.TestFormatWrap\n" + 101 | "\t.+/format_test.go:96", 102 | }, { 103 | Wrap(Wrap(io.EOF, "error1"), "error2"), 104 | "%+v", 105 | "EOF\n" + 106 | "error1\n" + 107 | "emperror.dev/errors/tests.TestFormatWrap\n" + 108 | "\t.+/format_test.go:103\n", 109 | }, { 110 | Wrap(New("error with space"), "context"), 111 | "%q", 112 | `"context: error with space"`, 113 | }} 114 | 115 | for i, tt := range tests { 116 | testFormatRegexp(t, i, tt.error, tt.format, tt.want) 117 | } 118 | } 119 | 120 | func TestFormatWrapf(t *testing.T) { 121 | tests := []struct { 122 | error 123 | format string 124 | want string 125 | }{{ 126 | Wrapf(io.EOF, "error%d", 2), 127 | "%s", 128 | "error2: EOF", 129 | }, { 130 | Wrapf(io.EOF, "error%d", 2), 131 | "%v", 132 | "error2: EOF", 133 | }, { 134 | Wrapf(io.EOF, "error%d", 2), 135 | "%+v", 136 | "EOF\n" + 137 | "error2\n" + 138 | "emperror.dev/errors/tests.TestFormatWrapf\n" + 139 | "\t.+/format_test.go:134", 140 | }, { 141 | Wrapf(New("error"), "error%d", 2), 142 | "%s", 143 | "error2: error", 144 | }, { 145 | Wrapf(New("error"), "error%d", 2), 146 | "%v", 147 | "error2: error", 148 | }, { 149 | Wrapf(New("error"), "error%d", 2), 150 | "%+v", 151 | "error\n" + 152 | "emperror.dev/errors/tests.TestFormatWrapf\n" + 153 | "\t.+/format_test.go:149", 154 | }} 155 | 156 | for i, tt := range tests { 157 | testFormatRegexp(t, i, tt.error, tt.format, tt.want) 158 | } 159 | } 160 | 161 | func TestFormatWithStack(t *testing.T) { 162 | tests := []struct { 163 | error 164 | format string 165 | want []string 166 | }{{ 167 | WithStack(io.EOF), 168 | "%s", 169 | []string{"EOF"}, 170 | }, { 171 | WithStack(io.EOF), 172 | "%v", 173 | []string{"EOF"}, 174 | }, { 175 | WithStack(io.EOF), 176 | "%+v", 177 | []string{"EOF", 178 | "emperror.dev/errors/tests.TestFormatWithStack\n" + 179 | "\t.+/format_test.go:175"}, 180 | }, { 181 | WithStack(New("error")), 182 | "%s", 183 | []string{"error"}, 184 | }, { 185 | WithStack(New("error")), 186 | "%v", 187 | []string{"error"}, 188 | }, { 189 | WithStack(New("error")), 190 | "%+v", 191 | []string{"error", 192 | "emperror.dev/errors/tests.TestFormatWithStack\n" + 193 | "\t.+/format_test.go:189", 194 | "emperror.dev/errors/tests.TestFormatWithStack\n" + 195 | "\t.+/format_test.go:189"}, 196 | }, { 197 | WithStack(WithStack(io.EOF)), 198 | "%+v", 199 | []string{"EOF", 200 | "emperror.dev/errors/tests.TestFormatWithStack\n" + 201 | "\t.+/format_test.go:197", 202 | "emperror.dev/errors/tests.TestFormatWithStack\n" + 203 | "\t.+/format_test.go:197"}, 204 | }, { 205 | WithStack(WithStack(Wrapf(io.EOF, "message"))), 206 | "%+v", 207 | []string{"EOF", 208 | "message", 209 | "emperror.dev/errors/tests.TestFormatWithStack\n" + 210 | "\t.+/format_test.go:205", 211 | "emperror.dev/errors/tests.TestFormatWithStack\n" + 212 | "\t.+/format_test.go:205", 213 | "emperror.dev/errors/tests.TestFormatWithStack\n" + 214 | "\t.+/format_test.go:205"}, 215 | }, { 216 | WithStack(Errorf("error%d", 1)), 217 | "%+v", 218 | []string{"error1", 219 | "emperror.dev/errors/tests.TestFormatWithStack\n" + 220 | "\t.+/format_test.go:216", 221 | "emperror.dev/errors/tests.TestFormatWithStack\n" + 222 | "\t.+/format_test.go:216"}, 223 | }} 224 | 225 | for i, tt := range tests { 226 | testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) 227 | } 228 | } 229 | 230 | func TestFormatWithMessage(t *testing.T) { 231 | tests := []struct { 232 | error 233 | format string 234 | want []string 235 | }{{ 236 | WithMessage(New("error"), "error2"), 237 | "%s", 238 | []string{"error2: error"}, 239 | }, { 240 | WithMessage(New("error"), "error2"), 241 | "%v", 242 | []string{"error2: error"}, 243 | }, { 244 | WithMessage(New("error"), "error2"), 245 | "%+v", 246 | []string{ 247 | "error", 248 | "emperror.dev/errors/tests.TestFormatWithMessage\n" + 249 | "\t.+/format_test.go:244", 250 | "error2"}, 251 | }, { 252 | WithMessage(io.EOF, "addition1"), 253 | "%s", 254 | []string{"addition1: EOF"}, 255 | }, { 256 | WithMessage(io.EOF, "addition1"), 257 | "%v", 258 | []string{"addition1: EOF"}, 259 | }, { 260 | WithMessage(io.EOF, "addition1"), 261 | "%+v", 262 | []string{"EOF", "addition1"}, 263 | }, { 264 | WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), 265 | "%v", 266 | []string{"addition2: addition1: EOF"}, 267 | }, { 268 | WithMessage(WithMessage(io.EOF, "addition1"), "addition2"), 269 | "%+v", 270 | []string{"EOF", "addition1", "addition2"}, 271 | }, { 272 | Wrap(WithMessage(io.EOF, "error1"), "error2"), 273 | "%+v", 274 | []string{"EOF", "error1", "error2", 275 | "emperror.dev/errors/tests.TestFormatWithMessage\n" + 276 | "\t.+/format_test.go:272"}, 277 | }, { 278 | WithMessage(Errorf("error%d", 1), "error2"), 279 | "%+v", 280 | []string{"error1", 281 | "emperror.dev/errors/tests.TestFormatWithMessage\n" + 282 | "\t.+/format_test.go:278", 283 | "error2"}, 284 | }, { 285 | WithMessage(WithStack(io.EOF), "error"), 286 | "%+v", 287 | []string{ 288 | "EOF", 289 | "emperror.dev/errors/tests.TestFormatWithMessage\n" + 290 | "\t.+/format_test.go:285", 291 | "error"}, 292 | }, { 293 | WithMessage(Wrap(WithStack(io.EOF), "inside-error"), "outside-error"), 294 | "%+v", 295 | []string{ 296 | "EOF", 297 | "emperror.dev/errors/tests.TestFormatWithMessage\n" + 298 | "\t.+/format_test.go:293", 299 | "inside-error", 300 | "emperror.dev/errors/tests.TestFormatWithMessage\n" + 301 | "\t.+/format_test.go:293", 302 | "outside-error"}, 303 | }} 304 | 305 | for i, tt := range tests { 306 | testFormatCompleteCompare(t, i, tt.error, tt.format, tt.want, true) 307 | } 308 | } 309 | 310 | func TestFormatGeneric(t *testing.T) { 311 | starts := []struct { 312 | err error 313 | want []string 314 | }{ 315 | {New("new-error"), []string{ 316 | "new-error", 317 | "emperror.dev/errors/tests.TestFormatGeneric\n" + 318 | "\t.+/format_test.go:315"}, 319 | }, {Errorf("errorf-error"), []string{ 320 | "errorf-error", 321 | "emperror.dev/errors/tests.TestFormatGeneric\n" + 322 | "\t.+/format_test.go:319"}, 323 | }, {NewPlain("errors-new-error"), []string{ 324 | "errors-new-error"}, 325 | }, 326 | } 327 | 328 | wrappers := []wrapper{ 329 | { 330 | func(err error) error { return WithMessage(err, "with-message") }, 331 | []string{"with-message"}, 332 | }, { 333 | func(err error) error { return WithStack(err) }, 334 | []string{ 335 | "emperror.dev/errors/tests.(func·002|TestFormatGeneric.func2)\n\t" + 336 | ".+/format_test.go:333", 337 | }, 338 | }, { 339 | func(err error) error { return Wrap(err, "wrap-error") }, 340 | []string{ 341 | "wrap-error", 342 | "emperror.dev/errors/tests.(func·003|TestFormatGeneric.func3)\n\t" + 343 | ".+/format_test.go:339", 344 | }, 345 | }, { 346 | func(err error) error { return Wrapf(err, "wrapf-error%d", 1) }, 347 | []string{ 348 | "wrapf-error1", 349 | "emperror.dev/errors/tests.(func·004|TestFormatGeneric.func4)\n\t" + 350 | ".+/format_test.go:346", 351 | }, 352 | }, 353 | } 354 | 355 | for s := range starts { 356 | err := starts[s].err 357 | want := starts[s].want 358 | testFormatCompleteCompare(t, s, err, "%+v", want, false) 359 | testGenericRecursive(t, err, want, wrappers, 3) 360 | } 361 | } 362 | 363 | func wrappedNew(message string) error { // This function will be mid-stack inlined in go 1.12+ 364 | return New(message) 365 | } 366 | 367 | func TestFormatWrappedNew(t *testing.T) { 368 | tests := []struct { 369 | error 370 | format string 371 | want string 372 | }{{ 373 | wrappedNew("error"), 374 | "%+v", 375 | "error\n" + 376 | "emperror.dev/errors/tests.wrappedNew\n" + 377 | "\t.+/format_test.go:364\n" + 378 | "emperror.dev/errors/tests.TestFormatWrappedNew\n" + 379 | "\t.+/format_test.go:373", 380 | }} 381 | 382 | for i, tt := range tests { 383 | testFormatRegexp(t, i, tt.error, tt.format, tt.want) 384 | } 385 | } 386 | 387 | func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) { 388 | t.Helper() 389 | got := fmt.Sprintf(format, arg) 390 | gotLines := strings.SplitN(got, "\n", -1) 391 | wantLines := strings.SplitN(want, "\n", -1) 392 | 393 | if len(wantLines) > len(gotLines) { 394 | t.Errorf("test %d: wantLines(%d) > gotLines(%d):\n got: %q\nwant: %q", n+1, len(wantLines), len(gotLines), got, want) 395 | return 396 | } 397 | 398 | for i, w := range wantLines { 399 | match, err := regexp.MatchString(w, gotLines[i]) 400 | if err != nil { 401 | t.Fatal(err) 402 | } 403 | if !match { 404 | t.Errorf("test %d: line %d: fmt.Sprintf(%q, err):\n got: %q\nwant: %q", n+1, i+1, format, got, want) 405 | } 406 | } 407 | } 408 | 409 | var stackLineR = regexp.MustCompile(`\.`) 410 | 411 | // parseBlocks parses input into a slice, where: 412 | // - incase entry contains a newline, its a stacktrace 413 | // - incase entry contains no newline, its a solo line. 414 | // 415 | // Detecting stack boundaries only works incase the WithStack-calls are 416 | // to be found on the same line, thats why it is optionally here. 417 | // 418 | // Example use: 419 | // 420 | // for _, e := range blocks { 421 | // if strings.ContainsAny(e, "\n") { 422 | // // Match as stack 423 | // } else { 424 | // // Match as line 425 | // } 426 | // } 427 | // 428 | func parseBlocks(input string, detectStackboundaries bool) ([]string, error) { 429 | var blocks []string 430 | 431 | stack := "" 432 | wasStack := false 433 | lines := map[string]bool{} // already found lines 434 | 435 | for _, l := range strings.Split(input, "\n") { 436 | isStackLine := stackLineR.MatchString(l) 437 | 438 | switch { 439 | case !isStackLine && wasStack: 440 | blocks = append(blocks, stack, l) 441 | stack = "" 442 | lines = map[string]bool{} 443 | case isStackLine: 444 | if wasStack { 445 | // Detecting two stacks after another, possible cause lines match in 446 | // our tests due to WithStack(WithStack(io.EOF)) on same line. 447 | if detectStackboundaries { 448 | if lines[l] { 449 | if len(stack) == 0 { 450 | return nil, NewPlain("len of block must not be zero here") 451 | } 452 | 453 | blocks = append(blocks, stack) 454 | stack = l 455 | lines = map[string]bool{l: true} 456 | continue 457 | } 458 | } 459 | 460 | stack = stack + "\n" + l 461 | } else { 462 | stack = l 463 | } 464 | lines[l] = true 465 | case !isStackLine && !wasStack: 466 | blocks = append(blocks, l) 467 | default: 468 | return nil, NewPlain("must not happen") 469 | } 470 | 471 | wasStack = isStackLine 472 | } 473 | 474 | // Use up stack 475 | if stack != "" { 476 | blocks = append(blocks, stack) 477 | } 478 | return blocks, nil 479 | } 480 | 481 | func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) { 482 | gotStr := fmt.Sprintf(format, arg) 483 | 484 | got, err := parseBlocks(gotStr, detectStackBoundaries) 485 | if err != nil { 486 | t.Fatal(err) 487 | } 488 | 489 | if len(got) != len(want) { 490 | t.Fatalf("test %d: fmt.Sprintf(%s, err) -> wrong number of blocks: got(%d) want(%d)\n got: %s\nwant: %s\ngotStr: %q", 491 | n+1, format, len(got), len(want), prettyBlocks(got), prettyBlocks(want), gotStr) 492 | } 493 | 494 | for i := range got { 495 | if strings.ContainsAny(want[i], "\n") { 496 | // Match as stack 497 | match, err := regexp.MatchString(want[i], got[i]) 498 | if err != nil { 499 | t.Fatal(err) 500 | } 501 | if !match { 502 | t.Fatalf("test %d: block %d: fmt.Sprintf(%q, err):\ngot:\n%q\nwant:\n%q\nall-got:\n%s\nall-want:\n%s\n", 503 | n+1, i+1, format, got[i], want[i], prettyBlocks(got), prettyBlocks(want)) 504 | } 505 | } else { 506 | // Match as message 507 | if got[i] != want[i] { 508 | t.Fatalf("test %d: fmt.Sprintf(%s, err) at block %d got != want:\n got: %q\nwant: %q", n+1, format, i+1, got[i], want[i]) 509 | } 510 | } 511 | } 512 | } 513 | 514 | type wrapper struct { 515 | wrap func(err error) error 516 | want []string 517 | } 518 | 519 | func prettyBlocks(blocks []string) string { 520 | var out []string 521 | 522 | for _, b := range blocks { 523 | out = append(out, fmt.Sprintf("%v", b)) 524 | } 525 | 526 | return " " + strings.Join(out, "\n ") 527 | } 528 | 529 | func testGenericRecursive(t *testing.T, beforeErr error, beforeWant []string, list []wrapper, maxDepth int) { 530 | if len(beforeWant) == 0 { 531 | panic("beforeWant must not be empty") 532 | } 533 | for _, w := range list { 534 | if len(w.want) == 0 { 535 | panic("want must not be empty") 536 | } 537 | 538 | err := w.wrap(beforeErr) 539 | 540 | // Copy required cause append(beforeWant, ..) modified beforeWant subtly. 541 | beforeCopy := make([]string, len(beforeWant)) 542 | copy(beforeCopy, beforeWant) 543 | 544 | beforeWant := beforeCopy 545 | last := len(beforeWant) - 1 546 | var want []string 547 | 548 | // Merge two stacks behind each other. 549 | if strings.ContainsAny(beforeWant[last], "\n") && strings.ContainsAny(w.want[0], "\n") { 550 | want = append(beforeWant[:last], append([]string{beforeWant[last] + "((?s).*)" + w.want[0]}, w.want[1:]...)...) 551 | } else { 552 | want = append(beforeWant, w.want...) 553 | } 554 | 555 | testFormatCompleteCompare(t, maxDepth, err, "%+v", want, false) 556 | if maxDepth > 0 { 557 | testGenericRecursive(t, err, want, list, maxDepth-1) 558 | } 559 | } 560 | } 561 | --------------------------------------------------------------------------------