diff --git a/.travis.yml b/.travis.yml index 15e5a192..eaaabc55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,11 @@ +arch: + - amd64 + language: go -go_import_path: github.com/pkg/errors +go_import_path: github.com/pingcap/errors go: - - 1.4.x - - 1.5.x - - 1.6.x - - 1.7.x - - 1.8.x - - 1.9.x - - 1.10.x - - tip + - 1.13.x + - stable script: - go test -v ./... diff --git a/README.md b/README.md index 6483ba2a..b97656ac 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Package errors provides simple error handling primitives. -`go get github.com/pkg/errors` +`go get github.com/pingcap/errors` The traditional error handling idiom in Go is roughly akin to ```go diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index a932eade..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: build-{build}.{branch} - -clone_folder: C:\gopath\src\github.com\pkg\errors -shallow_clone: true # for startup speed - -environment: - GOPATH: C:\gopath - -platform: - - x64 - -# http://www.appveyor.com/docs/installed-software -install: - # some helpful output for debugging builds - - go version - - go env - # pre-installed MinGW at C:\MinGW is 32bit only - # but MSYS2 at C:\msys64 has mingw64 - - set PATH=C:\msys64\mingw64\bin;%PATH% - - gcc --version - - g++ --version - -build_script: - - go install -v ./... - -test_script: - - set PATH=C:\gopath\bin;%PATH% - - go test -v ./... - -#artifacts: -# - path: '%GOPATH%\bin\*.exe' -deploy: off diff --git a/bench_test.go b/bench_test.go index c906870e..b8ad7a02 100644 --- a/bench_test.go +++ b/bench_test.go @@ -1,12 +1,10 @@ -// +build go1.7 - package errors import ( + stderrors "errors" "fmt" + "strings" "testing" - - stderrors "errors" ) func noErrors(at, depth int) error { @@ -25,7 +23,7 @@ func yesErrors(at, depth int) error { // GlobalE is an exported global to store the result of benchmark results, // preventing the compiler from optimising the benchmark functions away. -var GlobalE interface{} +var GlobalE any func BenchmarkErrors(b *testing.B) { type run struct { @@ -108,3 +106,94 @@ func BenchmarkStackFormatting(b *testing.B) { } GlobalE = stackStr } + +type argsProfile struct { + name string + containsString bool + build func(count, stringLen int) []any +} + +type benchmarkHackedStr string + +func (s benchmarkHackedStr) FreezeStr() string { + return string(append([]byte(nil), s...)) +} + +func buildHackedStringArgs(count, stringLen int) []any { + arg := benchmarkHackedStr(strings.Repeat("x", stringLen)) + args := make([]any, count) + for i := range args { + args[i] = arg + } + return args +} + +func buildPlainStringArgs(count, stringLen int) []any { + arg := strings.Repeat("x", stringLen) + args := make([]any, count) + for i := range args { + args[i] = arg + } + return args +} + +func buildIntArgs(count, _ int) []any { + args := make([]any, count) + for i := range args { + args[i] = i + } + return args +} + +func BenchmarkByArgsHackedStrFreeze(b *testing.B) { + errPrototype := Normalize("bench", RFCCodeText("Internal:Bench")) + + apiCases := []struct { + name string + call func(errPrototype *Error, args []any) error + }{ + { + name: "FastGenByArgs", + call: func(errPrototype *Error, args []any) error { + return errPrototype.FastGenByArgs(args...) + }, + }, + } + profiles := []argsProfile{ + {name: "plain", containsString: true, build: buildPlainStringArgs}, + {name: "hacked", containsString: true, build: buildHackedStringArgs}, + } + argCounts := []int{1, 4, 8} + stringLens := []int{16, 1024} + + for _, apiCase := range apiCases { + b.Run(apiCase.name, func(b *testing.B) { + for _, profile := range profiles { + lens := []int{0} + if profile.containsString { + lens = stringLens + } + for _, argCount := range argCounts { + for _, strLen := range lens { + templateArgs := profile.build(argCount, strLen) + caseName := fmt.Sprintf("type-%s/count-%d", profile.name, argCount) + if profile.containsString { + caseName = fmt.Sprintf("%s/strlen-%d", caseName, strLen) + } + + b.Run(caseName, func(b *testing.B) { + var err error + args := make([]any, len(templateArgs)) + b.ReportAllocs() + for i := 0; i < b.N; i++ { + copy(args, templateArgs) + err = apiCase.call(errPrototype, args) + } + GlobalE = err + }) + } + } + } + }) + } +} diff --git a/compatible_shim.go b/compatible_shim.go index ba446e08..0835a0cd 100644 --- a/compatible_shim.go +++ b/compatible_shim.go @@ -63,7 +63,6 @@ func init() { } } - // MarshalJSON implements json.Marshaler interface. // aware that this function cannot save a 'registered' status, // since we cannot access the registry when unmarshaling, diff --git a/errdoc-gen/main.go b/errdoc-gen/main.go index 21f17084..a5e4df33 100644 --- a/errdoc-gen/main.go +++ b/errdoc-gen/main.go @@ -22,6 +22,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "text/template" ) @@ -30,6 +31,7 @@ var opt struct { source string module string output string + ignore string retainCode bool } @@ -37,14 +39,15 @@ func init() { flag.StringVar(&opt.source, "source", "", "The source directory of error documentation") flag.StringVar(&opt.module, "module", "", "The module name of target repository") flag.StringVar(&opt.output, "output", "", "The output path of error documentation file") + flag.StringVar(&opt.ignore, "ignore", "", "Directories to ignore, splitted by comma") flag.BoolVar(&opt.retainCode, "retain-code", false, "Retain the generated code when generator exit") } -func log(format string, args ...interface{}) { +func log(format string, args ...any) { fmt.Println(fmt.Sprintf(format, args...)) } -func fatal(format string, args ...interface{}) { +func fatal(format string, args ...any) { log(format, args...) os.Exit(1) } @@ -186,7 +189,7 @@ func main() { } buffer.WriteString("\n") } - if err := ioutil.WriteFile(outpath, buffer.Bytes(), os.ModePerm); err != nil { + if err := ioutil.WriteFile(outpath, buffer.Bytes(), 0644); err != nil { panic(err) } } @@ -203,7 +206,7 @@ func main() { } outFile := filepath.Join(targetDir, entryFileName) - out, err := os.OpenFile(outFile, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, os.ModePerm) + out, err := os.OpenFile(outFile, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644) if err != nil { fatal("Open %s failed: %+v", outFile, err) } @@ -242,11 +245,18 @@ func errdoc(source, module string) ([]*errDecl, error) { dedup := map[string]*errDecl{} + ignored := strings.Split(opt.ignore, ",") + for i := range ignored { + ignored[i] = filepath.Join(source, ignored[i]) + } err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { + if slices.Contains(ignored, path) { + return filepath.SkipDir + } return nil } if !strings.HasSuffix(path, ".go") { diff --git a/errors.go b/errors.go index 2e1d3f62..2ac469b4 100644 --- a/errors.go +++ b/errors.go @@ -2,84 +2,84 @@ // // The traditional error handling idiom in Go is roughly akin to // -// if err != nil { -// return err -// } +// if err != nil { +// return err +// } // // which applied recursively up the call stack results in error reports // without context or debugging information. The errors package allows // programmers to add context to the failure path in their code in a way // that does not destroy the original value of the error. // -// Adding context to an error +// # Adding context to an error // // The errors.Annotate function returns a new error that adds context to the // original error by recording a stack trace at the point Annotate is called, // and the supplied message. For example // -// _, err := ioutil.ReadAll(r) -// if err != nil { -// return errors.Annotate(err, "read failed") -// } +// _, err := ioutil.ReadAll(r) +// if err != nil { +// return errors.Annotate(err, "read failed") +// } // // If additional control is required the errors.AddStack and errors.WithMessage // functions destructure errors.Annotate into its component operations of annotating // an error with a stack trace and an a message, respectively. // -// Retrieving the cause of an error +// # Retrieving the cause of an error // // Using errors.Annotate constructs a stack of errors, adding context to the // preceding error. Depending on the nature of the error it may be necessary // to reverse the operation of errors.Annotate to retrieve the original error // for inspection. Any error value which implements this interface // -// type causer interface { -// Cause() error -// } +// type causer interface { +// Cause() error +// } // // can be inspected by errors.Cause. errors.Cause will recursively retrieve // the topmost error which does not implement causer, which is assumed to be // the original cause. For example: // -// switch err := errors.Cause(err).(type) { -// case *MyError: -// // handle specifically -// default: -// // unknown error -// } +// switch err := errors.Cause(err).(type) { +// case *MyError: +// // handle specifically +// default: +// // unknown error +// } // // causer interface is not exported by this package, but is considered a part // of stable public API. // errors.Unwrap is also available: this will retrieve the next error in the chain. // -// Formatted printing of errors +// # Formatted printing of errors // // All error values returned from this package implement fmt.Formatter and can // be formatted by the fmt package. The following verbs are supported // -// %s print the error. If the error has a Cause it will be -// printed recursively -// %v see %s -// %+v extended format. Each Frame of the error's StackTrace will -// be printed in detail. +// %s print the error. If the error has a Cause it will be +// printed recursively +// %v see %s +// %+v extended format. Each Frame of the error's StackTrace will +// be printed in detail. // -// Retrieving the stack trace of an error or wrapper +// # Retrieving the stack trace of an error or wrapper // // New, Errorf, Annotate, and Annotatef record a stack trace at the point they are invoked. // This information can be retrieved with the StackTracer interface that returns // a StackTrace. Where errors.StackTrace is defined as // -// type StackTrace []Frame +// type StackTrace []Frame // // The Frame type represents a call site in the stack trace. Frame supports // the fmt.Formatter interface that can be used for printing information about // the stack trace of this error. For example: // -// if stacked := errors.GetStackTracer(err); stacked != nil { -// for _, f := range stacked.StackTrace() { -// fmt.Printf("%+s:%d", f) -// } -// } +// if stacked := errors.GetStackTracer(err); stacked != nil { +// for _, f := range stacked.StackTrace() { +// fmt.Printf("%+s:%d\n", f, f) +// } +// } // // See the documentation for Frame.Format for more details. // @@ -91,6 +91,12 @@ import ( "io" ) +// represent an error carries with message +type messenger interface { + // GetSelfMsg get its own message, the message of its cause error is NOT included. + GetSelfMsg() string +} + // New returns an error with the supplied message. // New also records the stack trace at the point it was called. func New(message string) error { @@ -103,7 +109,7 @@ func New(message string) error { // Errorf formats according to a format specifier and returns the string // as a value that satisfies error. // Errorf also records the stack trace at the point it was called. -func Errorf(format string, args ...interface{}) error { +func Errorf(format string, args ...any) error { return &fundamental{ msg: fmt.Sprintf(format, args...), stack: callers(), @@ -122,7 +128,11 @@ func HasStack(err error) bool { if errWithStack, ok := err.(StackTraceAware); ok { return errWithStack.HasStack() } - return GetStackTracer(err) != nil + // Error.FastGenXXX or call SuspendStack directly will make an empty stack trace, + // which should be considered as no stack trace, to allow upper layer code to + // add stack trace with Trace. + stackTracer := GetStackTracer(err) + return stackTracer != nil && !stackTracer.Empty() } // fundamental is an error that has a message and a stack, but no caller. @@ -131,8 +141,12 @@ type fundamental struct { *stack } +var _ messenger = (*fundamental)(nil) + func (f *fundamental) Error() string { return f.msg } +func (f *fundamental) GetSelfMsg() string { return f.msg } + func (f *fundamental) Format(s fmt.State, verb rune) { switch verb { case 'v': @@ -168,10 +182,14 @@ func WithStack(err error) error { // AddStack is similar to WithStack. // However, it will first check with HasStack to see if a stack trace already exists in the causer chain before creating another one. func AddStack(err error) error { - if HasStack(err) { + if err == nil || HasStack(err) { return err } - return WithStack(err) + + return &withStack{ + err, + callers(), + } } type withStack struct { @@ -179,8 +197,19 @@ type withStack struct { *stack } +var _ messenger = (*withStack)(nil) + func (w *withStack) Cause() error { return w.error } +func (w *withStack) GetSelfMsg() string { + // it doesn't have its own message, but we still need impl it to avoid calling + // err.Error() for its cause + return "" +} + +// Unwrap provides compatibility for Go 1.13 error chains. +func (w *withStack) Unwrap() error { return w.error } + func (w *withStack) Format(s fmt.State, verb rune) { switch verb { case 'v': @@ -225,7 +254,7 @@ func Wrap(err error, message string) error { // // For most use cases this is deprecated in favor of Annotatef. // Annotatef avoids creating duplicate stack traces. -func Wrapf(err error, format string, args ...interface{}) error { +func Wrapf(err error, format string, args ...any) error { if err == nil { return nil } @@ -260,8 +289,15 @@ type withMessage struct { causeHasStack bool } -func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } -func (w *withMessage) Cause() error { return w.cause } +var _ messenger = (*withMessage)(nil) + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } + +func (w *withMessage) GetSelfMsg() string { return w.msg } + +// Unwrap provides compatibility for Go 1.13 error chains. +func (w *withMessage) Unwrap() error { return w.cause } func (w *withMessage) HasStack() bool { return w.causeHasStack } func (w *withMessage) Format(s fmt.State, verb rune) { @@ -273,8 +309,10 @@ func (w *withMessage) Format(s fmt.State, verb rune) { return } fallthrough - case 's', 'q': + case 's': io.WriteString(s, w.Error()) + case 'q': + fmt.Fprintf(s, "%q", w.Error()) } } @@ -282,9 +320,9 @@ func (w *withMessage) Format(s fmt.State, verb rune) { // An error value has a cause if it implements the following // interface: // -// type causer interface { -// Cause() error -// } +// type causer interface { +// Cause() error +// } // // If the error does not implement Cause, the original error will // be returned. If the error is nil, nil will be returned without further @@ -322,3 +360,33 @@ func Find(origErr error, test func(error) bool) error { }) return foundErr } + +// GetErrStackMsg get the concat error message the whole error stack. +// it's different from err.Error(), as pingcap/errors.Error will prepend the error +// code in the result of err.Error(), like below: +// +// [types:1292]Truncated incorrect +// +// and when there are multiple errors.Error in the chain, the err.Error() will +// return like this: +// +// [Lightning:Restore:ErrEncodeKV]encode kv error ... : [types:1292]Truncated incorrect DOUBLE value: 'a'" +// +// But sometimes we only want a single error code with pure message part. +func GetErrStackMsg(err error) string { + if err == nil { + return "" + } + m, ok := err.(messenger) + if ok { + msg := m.GetSelfMsg() + causeMsg := GetErrStackMsg(Unwrap(err)) + if msg == "" { + msg = causeMsg + } else if causeMsg != "" { + msg = msg + ": " + causeMsg + } + return msg + } + return err.Error() +} diff --git a/errors_test.go b/errors_test.go index 7f5e225f..7a26dc8f 100644 --- a/errors_test.go +++ b/errors_test.go @@ -4,9 +4,12 @@ import ( "errors" "fmt" "io" + "net/url" "reflect" "strconv" "testing" + + "github.com/stretchr/testify/require" ) func TestNew(t *testing.T) { @@ -132,7 +135,7 @@ func TestWrapf(t *testing.T) { } for _, tt := range tests { - got := Annotatef(tt.err, tt.message).Error() + got := Annotatef(tt.err, "%s", tt.message).Error() if got != tt.want { t.Errorf("Wrapf(%v, %q): got: %v, want %v", tt.err, tt.message, got, tt.want) } @@ -369,3 +372,153 @@ func TestWalkDeep(t *testing.T) { t.Errorf("found not exists") } } + +func TestWalkDeepNil(t *testing.T) { + require.False(t, WalkDeep(nil, func(err error) bool { return true })) +} + +func TestWalkDeepComplexTree(t *testing.T) { + err := &errWalkTest{v: 1, cause: &errWalkTest{ + sub: []error{ + &errWalkTest{ + v: 10, + cause: &errWalkTest{v: 11}, + }, + &errWalkTest{ + v: 20, + sub: []error{ + &errWalkTest{v: 21}, + &errWalkTest{v: 22}, + }, + }, + &errWalkTest{ + v: 30, + cause: &errWalkTest{v: 31}, + }, + }, + }} + + assertFind := func(v int, comment string) { + if !testFind(err, v) { + t.Errorf("%d not found in the error: %s", v, comment) + } + } + assertNotFind := func(v int, comment string) { + if testFind(err, v) { + t.Errorf("%d found in the error, but not expected: %s", v, comment) + } + } + + assertFind(1, "shallow search") + assertFind(11, "deep search A1") + assertFind(21, "deep search A2") + assertFind(22, "deep search B1") + assertNotFind(23, "deep search Neg") + assertFind(31, "deep search B2") + assertNotFind(32, "deep search Neg") + assertFind(30, "Tree node A") + assertFind(20, "Tree node with many children") +} + +type fooError int + +func (fooError) Error() string { + return "foo" +} + +func TestWorkWithStdErrors(t *testing.T) { + e1 := fooError(100) + e2 := Normalize("e2", RFCCodeText("e2")) + e3 := Normalize("e3", RFCCodeText("e3")) + e21 := e2.Wrap(e1) + e31 := e3.Wrap(e1) + e32 := e3.Wrap(e2) + e321 := e3.Wrap(e21) + + unwrapTbl := []struct { + x *Error // x.Unwrap() == y + y error + }{{e2, nil}, {e3, nil}, {e21, e1}, {e31, e1}, {e32, e2}, {e321, e21}} + for _, c := range unwrapTbl { + if c.x.Unwrap() != c.y { + t.Errorf("`%s`.Unwrap() != `%s`", c.x, c.y) + } + } + + isTbl := []struct { + x, y error // errors.Is(x, y) == b + b bool + }{ + {e1, e1, true}, {e2, e1, false}, {e3, e1, false}, {e21, e1, true}, {e321, e1, true}, + {e1, e2, false}, {e2, e2, true}, {e3, e2, false}, {e21, e2, true}, {e31, e2, false}, {e321, e2, true}, + {e2, e21, true}, {e21, e21, true}, {e31, e21, false}, {e321, e21, true}, + {e321, e321, true}, {e3, e321, true}, {e21, e321, false}, + } + for _, c := range isTbl { + if c.b && !errors.Is(c.x, c.y) { + t.Errorf("`%s` is not `%s`", c.x, c.y) + } + if !c.b && errors.Is(c.x, c.y) { + t.Errorf("`%s` is `%s`", c.x, c.y) + } + } + + var e1x fooError + if ok := errors.As(e21, &e1x); !ok { + t.Error("e21 cannot convert to e1") + } + if int(e1x) != 100 { + t.Error("e1x is not 100") + } + + var e2x *Error + if ok := errors.As(e21, &e2x); !ok { + t.Error("e21 cannot convert to e2") + } + if e2x.ID() != "e2" { + t.Error("err is not e2") + } + + e3x := e3.Wrap(e1) + if ok := errors.As(e21, &e3x); !ok { + t.Error("e21 cannot convert to e3") + } + if e3x.ID() != "e2" { + t.Error("err is not e2") + } +} + +func TestHasTrace(t *testing.T) { + targetErr := Normalize("test err") + require.False(t, HasStack(targetErr)) + require.False(t, HasStack(targetErr.FastGen("fast gen"))) + require.False(t, HasStack(targetErr.FastGenByArgs("fast gen arg"))) + require.True(t, HasStack(Trace(targetErr.FastGen("fast gen")))) + require.True(t, HasStack(targetErr.GenWithStack("gen"))) +} + +func TestGetErrStackMsg(t *testing.T) { + require.Equal(t, "", GetErrStackMsg(nil)) + + namedErr := Normalize("named err message", RFCCodeText("NamedError")) + require.False(t, HasStack(namedErr)) + require.Equal(t, "named err message", GetErrStackMsg(namedErr)) + tracedErr := Trace(namedErr) + require.Equal(t, "named err message", GetErrStackMsg(tracedErr)) + + annotatedErr := Annotate(tracedErr, "annotated message") + require.Equal(t, "annotated message: named err message", GetErrStackMsg(annotatedErr)) + + annotatedErr = Annotate(annotatedErr, "annotated message 2") + require.Equal(t, "annotated message 2: annotated message: named err message", GetErrStackMsg(annotatedErr)) + + fundErr := New("new fundamental error") + wrappedErr := namedErr.Wrap(fundErr) + require.Equal(t, "named err message: new fundamental error", GetErrStackMsg(wrappedErr)) + fastGen := wrappedErr.FastGen("fast gen") + require.Equal(t, "fast gen: new fundamental error", GetErrStackMsg(fastGen)) + + urlErr := &url.Error{Op: "GET", URL: "/url", Err: errors.New("internal golang err")} + fastGen = namedErr.Wrap(urlErr).FastGen("fast gen") + require.Equal(t, `fast gen: GET "/url": internal golang err`, GetErrStackMsg(fastGen)) +} diff --git a/example_test.go b/example_test.go index c1fc13e3..cc4edaa2 100644 --- a/example_test.go +++ b/example_test.go @@ -2,8 +2,7 @@ package errors_test import ( "fmt" - - "github.com/pkg/errors" + "github.com/pingcap/errors" ) func ExampleNew() { diff --git a/format_test.go b/format_test.go index 30275a3f..10c9b155 100644 --- a/format_test.go +++ b/format_test.go @@ -26,8 +26,8 @@ func TestFormatNew(t *testing.T) { New("error"), "%+v", "error\n" + - "github.com/pkg/errors.TestFormatNew\n" + - "\t.+/github.com/pkg/errors/format_test.go:26", + "github.com/pingcap/errors.TestFormatNew\n" + + "\t.+/pingcap/errors/format_test.go:26", }, { New("error"), "%q", @@ -56,8 +56,8 @@ func TestFormatErrorf(t *testing.T) { Errorf("%s", "error"), "%+v", "error\n" + - "github.com/pkg/errors.TestFormatErrorf\n" + - "\t.+/github.com/pkg/errors/format_test.go:56", + "github.com/pingcap/errors.TestFormatErrorf\n" + + "\t.+/pingcap/errors/format_test.go:56", }} for i, tt := range tests { @@ -82,8 +82,8 @@ func TestFormatWrap(t *testing.T) { Annotate(New("error"), "error2"), "%+v", "error\n" + - "github.com/pkg/errors.TestFormatWrap\n" + - "\t.+/github.com/pkg/errors/format_test.go:82", + "github.com/pingcap/errors.TestFormatWrap\n" + + "\t.+/pingcap/errors/format_test.go:82", }, { Annotate(io.EOF, "error"), "%s", @@ -97,15 +97,15 @@ func TestFormatWrap(t *testing.T) { "%+v", "EOF\n" + "error\n" + - "github.com/pkg/errors.TestFormatWrap\n" + - "\t.+/github.com/pkg/errors/format_test.go:96", + "github.com/pingcap/errors.TestFormatWrap\n" + + "\t.+/pingcap/errors/format_test.go:96", }, { Annotate(Annotate(io.EOF, "error1"), "error2"), "%+v", "EOF\n" + "error1\n" + - "github.com/pkg/errors.TestFormatWrap\n" + - "\t.+/github.com/pkg/errors/format_test.go:103\n", + "github.com/pingcap/errors.TestFormatWrap\n" + + "\t.+/pingcap/errors/format_test.go:103\n", }, { Annotate(New("error with space"), "context"), "%q", @@ -135,8 +135,8 @@ func TestFormatWrapf(t *testing.T) { "%+v", "EOF\n" + "error2\n" + - "github.com/pkg/errors.TestFormatWrapf\n" + - "\t.+/github.com/pkg/errors/format_test.go:134", + "github.com/pingcap/errors.TestFormatWrapf\n" + + "\t.+/pingcap/errors/format_test.go:134", }, { Annotatef(New("error"), "error%d", 2), "%s", @@ -149,8 +149,8 @@ func TestFormatWrapf(t *testing.T) { Annotatef(New("error"), "error%d", 2), "%+v", "error\n" + - "github.com/pkg/errors.TestFormatWrapf\n" + - "\t.+/github.com/pkg/errors/format_test.go:149", + "github.com/pingcap/errors.TestFormatWrapf\n" + + "\t.+/pingcap/errors/format_test.go:149", }} for i, tt := range tests { @@ -175,8 +175,8 @@ func TestFormatWithStack(t *testing.T) { WithStack(io.EOF), "%+v", []string{"EOF", - "github.com/pkg/errors.TestFormatWithStack\n" + - "\t.+/github.com/pkg/errors/format_test.go:175"}, + "github.com/pingcap/errors.TestFormatWithStack\n" + + "\t.+/pingcap/errors/format_test.go:175"}, }, { WithStack(New("error")), "%s", @@ -189,37 +189,37 @@ func TestFormatWithStack(t *testing.T) { WithStack(New("error")), "%+v", []string{"error", - "github.com/pkg/errors.TestFormatWithStack\n" + - "\t.+/github.com/pkg/errors/format_test.go:189", - "github.com/pkg/errors.TestFormatWithStack\n" + - "\t.+/github.com/pkg/errors/format_test.go:189"}, + "github.com/pingcap/errors.TestFormatWithStack\n" + + "\t.+/pingcap/errors/format_test.go:189", + "github.com/pingcap/errors.TestFormatWithStack\n" + + "\t.+/pingcap/errors/format_test.go:189"}, }, { WithStack(WithStack(io.EOF)), "%+v", []string{"EOF", - "github.com/pkg/errors.TestFormatWithStack\n" + - "\t.+/github.com/pkg/errors/format_test.go:197", - "github.com/pkg/errors.TestFormatWithStack\n" + - "\t.+/github.com/pkg/errors/format_test.go:197"}, + "github.com/pingcap/errors.TestFormatWithStack\n" + + "\t.+/pingcap/errors/format_test.go:197", + "github.com/pingcap/errors.TestFormatWithStack\n" + + "\t.+/pingcap/errors/format_test.go:197"}, }, { WithStack(WithStack(Annotatef(io.EOF, "message"))), "%+v", []string{"EOF", "message", - "github.com/pkg/errors.TestFormatWithStack\n" + - "\t.+/github.com/pkg/errors/format_test.go:205", - "github.com/pkg/errors.TestFormatWithStack\n" + - "\t.+/github.com/pkg/errors/format_test.go:205", - "github.com/pkg/errors.TestFormatWithStack\n" + - "\t.+/github.com/pkg/errors/format_test.go:205"}, + "github.com/pingcap/errors.TestFormatWithStack\n" + + "\t.+/pingcap/errors/format_test.go:205", + "github.com/pingcap/errors.TestFormatWithStack\n" + + "\t.+/pingcap/errors/format_test.go:205", + "github.com/pingcap/errors.TestFormatWithStack\n" + + "\t.+/pingcap/errors/format_test.go:205"}, }, { WithStack(Errorf("error%d", 1)), "%+v", []string{"error1", - "github.com/pkg/errors.TestFormatWithStack\n" + - "\t.+/github.com/pkg/errors/format_test.go:216", - "github.com/pkg/errors.TestFormatWithStack\n" + - "\t.+/github.com/pkg/errors/format_test.go:216"}, + "github.com/pingcap/errors.TestFormatWithStack\n" + + "\t.+/pingcap/errors/format_test.go:216", + "github.com/pingcap/errors.TestFormatWithStack\n" + + "\t.+/pingcap/errors/format_test.go:216"}, }} for i, tt := range tests { @@ -245,8 +245,8 @@ func TestFormatWithMessage(t *testing.T) { "%+v", []string{ "error", - "github.com/pkg/errors.TestFormatWithMessage\n" + - "\t.+/github.com/pkg/errors/format_test.go:244", + "github.com/pingcap/errors.TestFormatWithMessage\n" + + "\t.+/pingcap/errors/format_test.go:244", "error2"}, }, { WithMessage(io.EOF, "addition1"), @@ -272,30 +272,30 @@ func TestFormatWithMessage(t *testing.T) { Annotate(WithMessage(io.EOF, "error1"), "error2"), "%+v", []string{"EOF", "error1", "error2", - "github.com/pkg/errors.TestFormatWithMessage\n" + - "\t.+/github.com/pkg/errors/format_test.go:272"}, + "github.com/pingcap/errors.TestFormatWithMessage\n" + + "\t.+/pingcap/errors/format_test.go:272"}, }, { WithMessage(Errorf("error%d", 1), "error2"), "%+v", []string{"error1", - "github.com/pkg/errors.TestFormatWithMessage\n" + - "\t.+/github.com/pkg/errors/format_test.go:278", + "github.com/pingcap/errors.TestFormatWithMessage\n" + + "\t.+/pingcap/errors/format_test.go:278", "error2"}, }, { WithMessage(WithStack(io.EOF), "error"), "%+v", []string{ "EOF", - "github.com/pkg/errors.TestFormatWithMessage\n" + - "\t.+/github.com/pkg/errors/format_test.go:285", + "github.com/pingcap/errors.TestFormatWithMessage\n" + + "\t.+/pingcap/errors/format_test.go:285", "error"}, }, { WithMessage(Annotate(WithStack(io.EOF), "inside-error"), "outside-error"), "%+v", []string{ "EOF", - "github.com/pkg/errors.TestFormatWithMessage\n" + - "\t.+/github.com/pkg/errors/format_test.go:293", + "github.com/pingcap/errors.TestFormatWithMessage\n" + + "\t.+/pingcap/errors/format_test.go:293", "inside-error", "outside-error"}, }} @@ -312,12 +312,12 @@ func TestFormatWithMessage(t *testing.T) { }{ {New("new-error"), []string{ "new-error", - "github.com/pkg/errors.TestFormatGeneric\n" + - "\t.+/github.com/pkg/errors/format_test.go:313"}, + "github.com/pingcap/errors.TestFormatGeneric\n" + + "\t.+/github.com/pingcap/errors/format_test.go:313"}, }, {Errorf("errorf-error"), []string{ "errorf-error", - "github.com/pkg/errors.TestFormatGeneric\n" + - "\t.+/github.com/pkg/errors/format_test.go:317"}, + "github.com/pingcap/errors.TestFormatGeneric\n" + + "\t.+/github.com/pingcap/errors/format_test.go:317"}, }, {errors.New("errors-new-error"), []string{ "errors-new-error"}, }, @@ -330,22 +330,22 @@ func TestFormatWithMessage(t *testing.T) { }, { func(err error) error { return WithStack(err) }, []string{ - "github.com/pkg/errors.(func·002|TestFormatGeneric.func2)\n\t" + - ".+/github.com/pkg/errors/format_test.go:331", + "github.com/pingcap/errors.(func·002|TestFormatGeneric.func2)\n\t" + + ".+/github.com/pingcap/errors/format_test.go:331", }, }, { func(err error) error { return Annotate(err, "wrap-error") }, []string{ "wrap-error", - "github.com/pkg/errors.(func·003|TestFormatGeneric.func3)\n\t" + - ".+/github.com/pkg/errors/format_test.go:337", + "github.com/pingcap/errors.(func·003|TestFormatGeneric.func3)\n\t" + + ".+/github.com/pingcap/errors/format_test.go:337", }, }, { func(err error) error { return Annotatef(err, "wrapf-error%d", 1) }, []string{ "wrapf-error1", - "github.com/pkg/errors.(func·004|TestFormatGeneric.func4)\n\t" + - ".+/github.com/pkg/errors/format_test.go:346", + "github.com/pingcap/errors.(func·004|TestFormatGeneric.func4)\n\t" + + ".+/github.com/pingcap/errors/format_test.go:346", }, }, } @@ -358,7 +358,7 @@ func TestFormatWithMessage(t *testing.T) { } }*/ -func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) { +func testFormatRegexp(t *testing.T, n int, arg any, format, want string) { t.Helper() got := fmt.Sprintf(format, arg) gotLines := strings.SplitN(got, "\n", -1) @@ -383,22 +383,21 @@ func testFormatRegexp(t *testing.T, n int, arg interface{}, format, want string) var stackLineR = regexp.MustCompile(`\.`) // parseBlocks parses input into a slice, where: -// - incase entry contains a newline, its a stacktrace -// - incase entry contains no newline, its a solo line. +// - incase entry contains a newline, its a stacktrace +// - incase entry contains no newline, its a solo line. // // Detecting stack boundaries only works incase the WithStack-calls are // to be found on the same line, thats why it is optionally here. // // Example use: // -// for _, e := range blocks { -// if strings.ContainsAny(e, "\n") { -// // Match as stack -// } else { -// // Match as line -// } -// } -// +// for _, e := range blocks { +// if strings.ContainsAny(e, "\n") { +// // Match as stack +// } else { +// // Match as line +// } +// } func parseBlocks(input string, detectStackboundaries bool) ([]string, error) { var blocks []string @@ -406,7 +405,7 @@ func parseBlocks(input string, detectStackboundaries bool) ([]string, error) { wasStack := false lines := map[string]bool{} // already found lines - for _, l := range strings.Split(input, "\n") { + for l := range strings.SplitSeq(input, "\n") { isStackLine := stackLineR.MatchString(l) switch { @@ -452,7 +451,7 @@ func parseBlocks(input string, detectStackboundaries bool) ([]string, error) { return blocks, nil } -func testFormatCompleteCompare(t *testing.T, n int, arg interface{}, format string, want []string, detectStackBoundaries bool) { +func testFormatCompleteCompare(t *testing.T, n int, arg any, format string, want []string, detectStackBoundaries bool) { gotStr := fmt.Sprintf(format, arg) got, err := parseBlocks(gotStr, detectStackBoundaries) diff --git a/go.mod b/go.mod index f5a4166f..8423b18f 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,14 @@ module github.com/pingcap/errors -go 1.14 +go 1.25 require ( - github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8 - github.com/pingcap/log v0.0.0-20200511115504-543df19646ad - github.com/pkg/errors v0.9.1 - go.uber.org/atomic v1.6.0 - go.uber.org/zap v1.15.0 + github.com/stretchr/testify v1.11.1 + go.uber.org/atomic v1.11.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f2f8f423..8d2f3332 100644 --- a/go.sum +++ b/go.sum @@ -1,66 +1,12 @@ -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8 h1:USx2/E1bX46VG32FIw034Au6seQ2fY9NEILmNh/UlQg= -github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ= -github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= -github.com/pingcap/log v0.0.0-20200511115504-543df19646ad h1:SveG82rmu/GFxYanffxsSF503SiQV+2JLnWEiGiF+Tc= -github.com/pingcap/log v0.0.0-20200511115504-543df19646ad/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= -go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/group.go b/group.go index e5a969ab..f07083bc 100644 --- a/group.go +++ b/group.go @@ -20,13 +20,20 @@ func Errors(err error) []error { // The visitor function can return true to end the traversal early // In that case, WalkDeep will return true, otherwise false. func WalkDeep(err error, visitor func(err error) bool) bool { + if err == nil { + return false + } + + if visitor(err) { + return true + } + // Go deep - unErr := err - for unErr != nil { - if done := visitor(unErr); done { + unErr := Unwrap(err) + if unErr != nil { + if WalkDeep(unErr, visitor) { return true } - unErr = Unwrap(unErr) } // Go wide diff --git a/join.go b/join.go new file mode 100644 index 00000000..af587b1e --- /dev/null +++ b/join.go @@ -0,0 +1,62 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package errors + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. +// Join returns nil if every value in errs is nil. +// The error formats as the concatenation of the strings obtained +// by calling the Error method of each element of errs, with a newline +// between each string. +// +// A non-nil error returned by Join implements the Unwrap() []error method. +func Join(errs ...error) error { + n := 0 + for _, err := range errs { + if err != nil { + n++ + } + } + if n == 0 { + return nil + } + e := &joinError{ + errs: make([]error, 0, n), + } + for _, err := range errs { + if err != nil { + e.errs = append(e.errs, err) + } + } + return e +} + +type joinError struct { + errs []error +} + +func (e *joinError) Error() string { + var b []byte + for i, err := range e.errs { + if i > 0 { + b = append(b, '\n') + } + b = append(b, err.Error()...) + } + return string(b) +} + +func (e *joinError) Unwrap() []error { + return e.errs +} diff --git a/join_test.go b/join_test.go new file mode 100644 index 00000000..56a44160 --- /dev/null +++ b/join_test.go @@ -0,0 +1,80 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package errors + +import ( + "reflect" + "testing" +) + +func TestJoinReturnsNil(t *testing.T) { + if err := Join(); err != nil { + t.Errorf("Join() = %v, want nil", err) + } + if err := Join(nil); err != nil { + t.Errorf("Join(nil) = %v, want nil", err) + } + if err := Join(nil, nil); err != nil { + t.Errorf("Join(nil, nil) = %v, want nil", err) + } +} + +func TestJoin(t *testing.T) { + err1 := New("err1") + err2 := New("err2") + for _, test := range []struct { + errs []error + want []error + }{{ + errs: []error{err1}, + want: []error{err1}, + }, { + errs: []error{err1, err2}, + want: []error{err1, err2}, + }, { + errs: []error{err1, nil, err2}, + want: []error{err1, err2}, + }} { + got := Join(test.errs...).(interface{ Unwrap() []error }).Unwrap() + if !reflect.DeepEqual(got, test.want) { + t.Errorf("Join(%v) = %v; want %v", test.errs, got, test.want) + } + if len(got) != cap(got) { + t.Errorf("Join(%v) returns errors with len=%v, cap=%v; want len==cap", test.errs, len(got), cap(got)) + } + } +} + +func TestJoinErrorMethod(t *testing.T) { + err1 := New("err1") + err2 := New("err2") + for _, test := range []struct { + errs []error + want string + }{{ + errs: []error{err1}, + want: "err1", + }, { + errs: []error{err1, err2}, + want: "err1\nerr2", + }, { + errs: []error{err1, nil, err2}, + want: "err1\nerr2", + }} { + got := Join(test.errs...).Error() + if got != test.want { + t.Errorf("Join(%v).Error() = %q; want %q", test.errs, got, test.want) + } + } +} diff --git a/juju_adaptor.go b/juju_adaptor.go index ece86cd6..aa20182a 100644 --- a/juju_adaptor.go +++ b/juju_adaptor.go @@ -36,7 +36,7 @@ func Annotate(err error, message string) error { } // Annotatef adds a message and ensures there is a stack trace. -func Annotatef(err error, format string, args ...interface{}) error { +func Annotatef(err error, format string, args ...any) error { if err == nil { return nil } @@ -66,7 +66,21 @@ func NewNoStackError(msg string) error { } } +// NewNoStackErrorf creates error with error stack and formats according +// to a format specifier and returns the string as a value that satisfies error. +func NewNoStackErrorf(format string, args ...any) error { + return &fundamental{ + msg: fmt.Sprintf(format, args...), + stack: &emptyStack, + } +} + // SuspendStack suspends stack generate for error. +// Deprecated, it's semantic is to clear the stack inside, we still allow upper +// layer to add stack again by using Trace. +// Sometimes we have very deep calling stack, the lower layer calls SuspendStack, +// but the upper layer want to add stack to it, if we disable adding stack permanently +// for an error, it's very hard to diagnose certain issues. func SuspendStack(err error) error { if err == nil { return err @@ -113,22 +127,22 @@ func IsNotFound(err error) bool { } // NotFoundf represents an error with not found message. -func NotFoundf(format string, args ...interface{}) error { +func NotFoundf(format string, args ...any) error { return Errorf(format+" not found", args...) } // BadRequestf represents an error with bad request message. -func BadRequestf(format string, args ...interface{}) error { +func BadRequestf(format string, args ...any) error { return Errorf(format+" bad request", args...) } // NotSupportedf represents an error with not supported message. -func NotSupportedf(format string, args ...interface{}) error { +func NotSupportedf(format string, args ...any) error { return Errorf(format+" not supported", args...) } // NotValidf represents an error with not valid message. -func NotValidf(format string, args ...interface{}) error { +func NotValidf(format string, args ...any) error { return Errorf(format+" not valid", args...) } @@ -138,7 +152,7 @@ func IsAlreadyExists(err error) bool { } // AlreadyExistsf represents an error with already exists message. -func AlreadyExistsf(format string, args ...interface{}) error { +func AlreadyExistsf(format string, args ...any) error { return Errorf(format+" already exists", args...) } diff --git a/normalize.go b/normalize.go index 5712fe72..31f69426 100644 --- a/normalize.go +++ b/normalize.go @@ -21,8 +21,16 @@ import ( "go.uber.org/atomic" ) +var _ fmt.Formatter = (*redactFormatter)(nil) + // RedactLogEnabled defines whether the arguments of Error need to be redacted. -var RedactLogEnabled atomic.Bool +var RedactLogEnabled atomic.String + +const ( + RedactLogEnable string = "ON" + RedactLogDisable = "OFF" + RedactLogMarker = "MARKER" +) // ErrCode represents a specific error type in a error class. // Same error code can be used in different error classes. @@ -34,24 +42,35 @@ type ErrCodeText string type ErrorID string type RFCErrorCode string +// HackedStr can provide a stable string snapshot for unsafe/ephemeral string sources. +// The method name intentionally uses FreezeStr instead of Clone so normal clone-style +// types are not accidentally treated as error-format arguments. +// During error construction, arguments implementing this interface are replaced +// with FreezeStr() so deferred error formatting does not observe later mutations. +type HackedStr interface { + FreezeStr() string +} + // Error is the 'prototype' of a type of errors. // Use DefineError to make a *Error: // var ErrUnavailable = errors.Normalize("Region %d is unavailable", errors.RFCCodeText("Unavailable")) // // "throw" it at runtime: -// func Somewhat() error { -// ... -// if err != nil { -// // generate a stackful error use the message template at defining, -// // also see FastGen(it's stackless), GenWithStack(it uses custom message template). -// return ErrUnavailable.GenWithStackByArgs(region.ID) -// } -// } +// +// func Somewhat() error { +// ... +// if err != nil { +// // generate a stackful error use the message template at defining, +// // also see FastGen(it's stackless), GenWithStack(it uses custom message template). +// return ErrUnavailable.GenWithStackByArgs(region.ID) +// } +// } // // testing whether an error belongs to a prototype: -// if ErrUnavailable.Equal(err) { -// // handle this error. -// } +// +// if ErrUnavailable.Equal(err) { +// // handle this error. +// } type Error struct { code ErrCode // codeText is the textual describe of the error code @@ -63,14 +82,18 @@ type Error struct { // And it is controlled by the global var RedactLogEnabled. // For example, an original error is `Duplicate entry 'PRIMARY' for key 'key'`, // when RedactLogEnabled is ON and redactArgsPos is [0, 1], the error is `Duplicate entry '?' for key '?'`. + // when RedactLogEnabled is MARKER and redactArgsPos is [0, 1], the error is `Duplicate entry '‹..›' for key '‹..›'`. redactArgsPos []int // Cause is used to warp some third party error. cause error - args []interface{} + args []any file string line int } +var _ messenger = (*Error)(nil) +var _ fmt.Formatter = (*Error)(nil) + // Code returns the numeric code of this error. // ID() will return textual error if there it is, // when you just want to get the purely numeric error @@ -106,18 +129,42 @@ func (e *Error) MessageTemplate() string { return e.message } +// Args returns the message arguments of this error. +func (e *Error) Args() []any { + return e.args +} + // Error implements error interface. func (e *Error) Error() string { if e == nil { return "" } - describe := e.codeText - if len(describe) == 0 { - describe = ErrCodeText(strconv.Itoa(int(e.code))) + if e.cause != nil { + return fmt.Sprintf("[%s]%s: %s", e.RFCCode(), e.GetMsg(), e.cause.Error()) } return fmt.Sprintf("[%s]%s", e.RFCCode(), e.GetMsg()) } +func (e *Error) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + if e != nil && e.cause != nil { + fmt.Fprintf(s, "%+v\n", e.cause) + fmt.Fprintf(s, "[%s]%s", e.RFCCode(), e.GetMsg()) + return + } + fmt.Fprint(s, e.Error()) + return + } + fallthrough + case 's': + fmt.Fprint(s, e.Error()) + case 'q': + fmt.Fprintf(s, "%q", e.Error()) + } +} + func (e *Error) GetMsg() string { if len(e.args) > 0 { return fmt.Sprintf(e.message, e.args...) @@ -125,6 +172,28 @@ func (e *Error) GetMsg() string { return e.message } +func freezeHackedStringArgs(args []any) []any { + // This helper intentionally mutates the input slice in place. + // It is only used inside error-construction paths where args are internal and should + // not be reused by callers after passing into Gen*/FastGen* APIs. + for i := range args { + hackedArg, ok := args[i].(HackedStr) + if !ok { + continue + } + // TiDB may pass unsafe zero-copy strings (for example from chunk buffers) as error args. + // Error message rendering is deferred until GetMsg/Error, so without freezing here we may + // observe later writes and print a different value than the one used when creating the error. + // Freezing in this central path keeps the copy cost only on error construction. + args[i] = hackedArg.FreezeStr() + } + return args +} + +func (e *Error) GetSelfMsg() string { + return e.GetMsg() +} + func (e *Error) fillLineAndFile(skip int) { // skip this _, file, line, ok := runtime.Caller(skip + 1) @@ -138,40 +207,40 @@ func (e *Error) fillLineAndFile(skip int) { } // GenWithStack generates a new *Error with the same class and code, and a new formatted message. -func (e *Error) GenWithStack(format string, args ...interface{}) error { +func (e *Error) GenWithStack(format string, args ...any) error { // TODO: RedactErrorArg err := *e err.message = format - err.args = args + err.args = freezeHackedStringArgs(args) err.fillLineAndFile(1) return AddStack(&err) } // GenWithStackByArgs generates a new *Error with the same class and code, and new arguments. -func (e *Error) GenWithStackByArgs(args ...interface{}) error { +func (e *Error) GenWithStackByArgs(args ...any) error { RedactErrorArg(args, e.redactArgsPos) err := *e - err.args = args + err.args = freezeHackedStringArgs(args) err.fillLineAndFile(1) return AddStack(&err) } // FastGen generates a new *Error with the same class and code, and a new formatted message. // This will not call runtime.Caller to get file and line. -func (e *Error) FastGen(format string, args ...interface{}) error { +func (e *Error) FastGen(format string, args ...any) error { // TODO: RedactErrorArg err := *e err.message = format - err.args = args + err.args = freezeHackedStringArgs(args) return SuspendStack(&err) } // FastGen generates a new *Error with the same class and code, and a new arguments. // This will not call runtime.Caller to get file and line. -func (e *Error) FastGenByArgs(args ...interface{}) error { +func (e *Error) FastGenByArgs(args ...any) error { RedactErrorArg(args, e.redactArgsPos) err := *e - err.args = args + err.args = freezeHackedStringArgs(args) return SuspendStack(&err) } @@ -198,13 +267,20 @@ func (e *Error) NotEqual(err error) bool { } // RedactErrorArg redacts the args by position if RedactLogEnabled is enabled. -func RedactErrorArg(args []interface{}, position []int) { - if RedactLogEnabled.Load() { +func RedactErrorArg(args []any, position []int) { + switch RedactLogEnabled.Load() { + case RedactLogEnable: for _, pos := range position { if len(args) > pos { args[pos] = "?" } } + case RedactLogMarker: + for _, pos := range position { + if len(args) > pos { + args[pos] = &redactFormatter{args[pos]} + } + } } } @@ -252,6 +328,26 @@ func (e *Error) Wrap(err error) *Error { return nil } +// Unwrap returns cause of the error. +// It allows Error to work with errors.Is() and errors.As() from the Go +// standard package. +func (e *Error) Unwrap() error { + if e == nil { + return nil + } + return e.cause +} + +// Is checks if e has the same error ID with other. +// It allows Error to work with errors.Is() from the Go standard package. +func (e *Error) Is(other error) bool { + err, ok := other.(*Error) + if !ok { + return false + } + return (e == nil && err == nil) || (e != nil && err != nil && e.ID() == err.ID()) +} + func (e *Error) Cause() error { root := Unwrap(e.cause) if root == nil { @@ -260,21 +356,21 @@ func (e *Error) Cause() error { return root } -func (e *Error) FastGenWithCause(args ...interface{}) error { +func (e *Error) FastGenWithCause(args ...any) error { err := *e if e.cause != nil { err.message = e.cause.Error() } - err.args = args + err.args = freezeHackedStringArgs(args) return SuspendStack(&err) } -func (e *Error) GenWithStackByCause(args ...interface{}) error { +func (e *Error) GenWithStackByCause(args ...any) error { err := *e if e.cause != nil { err.message = e.cause.Error() } - err.args = args + err.args = freezeHackedStringArgs(args) err.fillLineAndFile(1) return AddStack(&err) } @@ -311,3 +407,21 @@ func Normalize(message string, opts ...NormalizeOption) *Error { } return e } + +type redactFormatter struct { + arg any +} + +func (e *redactFormatter) Format(f fmt.State, verb rune) { + origin := fmt.Sprintf(fmt.FormatString(f, verb), e.arg) + fmt.Fprintf(f, "‹") + for _, c := range origin { + if c == '‹' || c == '›' { + fmt.Fprintf(f, "%c", c) + fmt.Fprintf(f, "%c", c) + } else { + fmt.Fprintf(f, "%c", c) + } + } + fmt.Fprintf(f, "›") +} diff --git a/normalize_test.go b/normalize_test.go new file mode 100644 index 00000000..89557c1c --- /dev/null +++ b/normalize_test.go @@ -0,0 +1,200 @@ +package errors + +import ( + stderrors "errors" + "fmt" + "regexp" + "strings" + "testing" + "unsafe" +) + +type hackedStringArg struct { + raw []byte +} + +func (h hackedStringArg) FreezeStr() string { + return string(append([]byte(nil), h.raw...)) +} + +func errorMatches(t *testing.T, err error, re string) { + if err == nil && re != "" { + t.Errorf("nil error doesn't match %s", re) + return + } + match, reErr := regexp.MatchString(re, err.Error()) + if reErr != nil { + t.Errorf("invalid regexp %s (%s)", re, reErr.Error()) + return + } + if !match { + t.Errorf("error %s doesn't match %s", err.Error(), re) + return + } + t.Logf("passed: %s ~= %s", err.Error(), re) +} + +func TestCauseInErrorMessage(t *testing.T) { + errTest := Normalize("this error just for testing", RFCCodeText("Internal:Test")) + + wrapped := errTest.Wrap(New("everything is alright :)")) + errorMatches(t, wrapped, `\[Internal:Test\]this error just for testing: everything is alright :\)`) + + notWrapped := errTest.GenWithStack("everything is alright") + errorMatches(t, notWrapped, `^\[Internal:Test\]everything is alright$`) +} + +func TestWrappedNamedErrorGenWithStackByArgsFormatsCauseStack(t *testing.T) { + errTest := Normalize("named error: %s", RFCCodeText("Internal:Test")) + + err := errTest.Wrap(New("cause error")).GenWithStackByArgs("wrapped") + + if _, ok := err.(fmt.Formatter); !ok { + t.Fatalf("zap requires fmt.Formatter to emit errorVerbose for stackful errors, got %T", err) + } + + formatted := fmt.Sprintf("%+v", err) + if !strings.Contains(formatted, "github.com/pingcap/errors.TestWrappedNamedErrorGenWithStackByArgsFormatsCauseStack") { + t.Fatalf("formatted error does not contain the wrapped cause stack:\n%s", formatted) + } + if !strings.Contains(formatted, "[Internal:Test]named error: wrapped") { + t.Fatalf("formatted error does not contain named error context:\n%s", formatted) + } +} + +func TestWrappedNamedErrorFormatsStacklessCause(t *testing.T) { + errTest := Normalize("named error: %s", RFCCodeText("Internal:Test")) + + err := errTest.Wrap(stderrors.New("plain cause")).GenWithStackByArgs("wrapped") + + formatted := fmt.Sprintf("%+v", err) + wantPrefix := "plain cause\n[Internal:Test]named error: wrapped\n" + if !strings.HasPrefix(formatted, wantPrefix) { + t.Fatalf("unexpected formatted error prefix:\ngot: %q\nwant prefix: %q", formatted, wantPrefix) + } + if !strings.Contains(formatted, "github.com/pingcap/errors.TestWrappedNamedErrorFormatsStacklessCause") { + t.Fatalf("formatted error does not contain the generated stack:\n%s", formatted) + } +} + +func TestRedactFormatter(t *testing.T) { + rv := 34.03498 + v := &redactFormatter{rv} + for _, f := range []string{"%d", "%.2d"} { + a := fmt.Sprintf(f, v) + b := fmt.Sprintf("‹"+f+"›", rv) + if a != b { + t.Errorf("%s != %s", a, b) + } + } + + v = &redactFormatter{"‹"} + if a := fmt.Sprintf("%s", v); a != "‹‹‹›" { + t.Errorf("%s != <<<>", a) + } +} + +func TestGenWithStackByArgsNoCloneByDefault(t *testing.T) { + errTest := Normalize("Incorrect time value: '%s'", RFCCodeText("Internal:Test")) + + origin := []byte("120120519090607") + arg := *(*string)(unsafe.Pointer(&origin)) + err := errTest.GenWithStackByArgs(arg) + + copy(origin, "1 1:1:1.0000027") + got := err.(*withStack).error.(*Error).GetMsg() + want := "Incorrect time value: '1 1:1:1.0000027'" + if got != want { + t.Fatalf("message should track source bytes by default, got %q, want %q", got, want) + } +} + +func TestGenWithStackByArgsFreezeHackedStringArg(t *testing.T) { + errTest := Normalize("Incorrect time value: '%s'", RFCCodeText("Internal:Test")) + + origin := []byte("120120519090607") + arg := hackedStringArg{raw: origin} + err := errTest.GenWithStackByArgs(arg) + + copy(origin, "1 1:1:1.0000027") + got := err.(*withStack).error.(*Error).GetMsg() + want := "Incorrect time value: '120120519090607'" + if got != want { + t.Fatalf("message changed after source bytes mutated, got %q, want %q", got, want) + } +} + +func TestFastGenByArgsFreezeHackedStringArg(t *testing.T) { + errTest := Normalize("Incorrect time value: '%s'", RFCCodeText("Internal:Test")) + + origin := []byte("120120519090607") + arg := hackedStringArg{raw: origin} + err := errTest.FastGenByArgs(arg) + + copy(origin, "1 1:1:1.0000027") + got := err.(*withStack).error.(*Error).GetMsg() + want := "Incorrect time value: '120120519090607'" + if got != want { + t.Fatalf("message changed after source bytes mutated, got %q, want %q", got, want) + } +} + +func TestGenWithStackFreezeHackedStringArg(t *testing.T) { + errTest := Normalize("Incorrect time value: '%s'", RFCCodeText("Internal:Test")) + + origin := []byte("120120519090607") + arg := hackedStringArg{raw: origin} + err := errTest.GenWithStack("Incorrect time value: '%s'", arg) + + copy(origin, "1 1:1:1.0000027") + got := err.(*withStack).error.(*Error).GetMsg() + want := "Incorrect time value: '120120519090607'" + if got != want { + t.Fatalf("message changed after source bytes mutated, got %q, want %q", got, want) + } +} + +func TestFastGenFreezeHackedStringArg(t *testing.T) { + errTest := Normalize("Incorrect time value: '%s'", RFCCodeText("Internal:Test")) + + origin := []byte("120120519090607") + arg := hackedStringArg{raw: origin} + err := errTest.FastGen("Incorrect time value: '%s'", arg) + + copy(origin, "1 1:1:1.0000027") + got := err.(*withStack).error.(*Error).GetMsg() + want := "Incorrect time value: '120120519090607'" + if got != want { + t.Fatalf("message changed after source bytes mutated, got %q, want %q", got, want) + } +} + +func TestGenWithStackByCauseFreezeHackedStringArg(t *testing.T) { + errTest := Normalize("Incorrect time value: '%s'", RFCCodeText("Internal:Test")) + + origin := []byte("120120519090607") + arg := hackedStringArg{raw: origin} + err := errTest.GenWithStackByCause(arg) + + copy(origin, "1 1:1:1.0000027") + got := err.(*withStack).error.(*Error).GetMsg() + want := "Incorrect time value: '120120519090607'" + if got != want { + t.Fatalf("message changed after source bytes mutated, got %q, want %q", got, want) + } +} + +func TestFastGenWithCauseFreezeHackedStringArg(t *testing.T) { + errTest := Normalize("Incorrect time value: '%s'", RFCCodeText("Internal:Test")) + + origin := []byte("120120519090607") + arg := hackedStringArg{raw: origin} + err := errTest.FastGenWithCause(arg) + + copy(origin, "1 1:1:1.0000027") + got := err.(*withStack).error.(*Error).GetMsg() + want := "Incorrect time value: '120120519090607'" + if got != want { + t.Fatalf("message changed after source bytes mutated, got %q, want %q", got, want) + } +} diff --git a/stack.go b/stack.go index bb1e6a84..2263ba37 100644 --- a/stack.go +++ b/stack.go @@ -14,6 +14,9 @@ import ( // Generally you would want to use the GetStackTracer function to do that. type StackTracer interface { StackTrace() StackTrace + // Empty returns true if the stack trace is empty, StackTrace might clone the + // stack trace, add this method to avoid unnecessary clone. + Empty() bool } // GetStackTracer will return the first StackTracer in the causer chain. @@ -63,16 +66,16 @@ func (f Frame) line() int { // Format formats the frame according to the fmt.Formatter interface. // -// %s source file -// %d source line -// %n function name -// %v equivalent to %s:%d +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d // // Format accepts flags that alter the printing of some verbs, as follows: // -// %+s function name and path of source file relative to the compile time -// GOPATH separated by \n\t (\n\t) -// %+v equivalent to %+s:%d +// %+s function name and path of source file relative to the compile time +// GOPATH separated by \n\t (\n\t) +// %+v equivalent to %+s:%d func (f Frame) Format(s fmt.State, verb rune) { f.format(s, s, verb) } @@ -113,12 +116,12 @@ type StackTrace []Frame // Format formats the stack of Frames according to the fmt.Formatter interface. // -// %s lists source files for each Frame in the stack -// %v lists the source file and line number for each Frame in the stack +// %s lists source files for each Frame in the stack +// %v lists the source file and line number for each Frame in the stack // // Format accepts flags that alter the printing of some verbs, as follows: // -// %+v Prints filename, function, and line number for each Frame in the stack. +// %+v Prints filename, function, and line number for each Frame in the stack. func (st StackTrace) Format(s fmt.State, verb rune) { var b bytes.Buffer switch verb { @@ -186,12 +189,16 @@ func (s *stack) Format(st fmt.State, verb rune) { func (s *stack) StackTrace() StackTrace { f := make([]Frame, len(*s)) - for i := 0; i < len(f); i++ { + for i := range f { f[i] = Frame((*s)[i]) } return f } +func (s *stack) Empty() bool { + return len(*s) == 0 +} + func callers() *stack { return callersSkip(4) } diff --git a/stack_test.go b/stack_test.go index d581ad6e..d6d855ee 100644 --- a/stack_test.go +++ b/stack_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "runtime" + "strings" "testing" ) @@ -15,23 +16,23 @@ func TestFrameLine(t *testing.T) { want int }{{ Frame(initpc), - 10, + 11, }, { func() Frame { var pc, _, _, _ = runtime.Caller(0) return Frame(pc) }(), - 21, - }, { - func() Frame { - var pc, _, _, _ = runtime.Caller(1) - return Frame(pc) - }(), - 29, - }, { - Frame(0), // invalid PC - 0, - }} + 22, + }, /* { // TODO stdlib `runtime` Behavior changed between 1.13 and 1.14 + func() Frame { + var pc, _, _, _ = runtime.Caller(1) + return Frame(pc) + }(), + 24, + }, */{ + Frame(0), // invalid PC + 0, + }} for _, tt := range tests { got := tt.Frame.line() @@ -67,7 +68,7 @@ func TestFrameFormat(t *testing.T) { Frame(initpc), "%+s", "github.com/pingcap/errors.init\n" + - "\t.+/github.com/pingcap/errors/stack_test.go", + "\t.+/pingcap/errors/stack_test.go", }, { Frame(0), "%s", @@ -79,7 +80,7 @@ func TestFrameFormat(t *testing.T) { }, { Frame(initpc), "%d", - "10", + "11", }, { Frame(0), "%d", @@ -109,12 +110,12 @@ func TestFrameFormat(t *testing.T) { }, { Frame(initpc), "%v", - "stack_test.go:10", + "stack_test.go:11", }, { Frame(initpc), "%+v", "github.com/pingcap/errors.init\n" + - "\t.+/github.com/pingcap/errors/stack_test.go:10", + "\t.+/pingcap/errors/stack_test.go:11", }, { Frame(0), "%v", @@ -154,24 +155,24 @@ func TestStackTrace(t *testing.T) { }{{ New("ooh"), []string{ "github.com/pingcap/errors.TestStackTrace\n" + - "\t.+/github.com/pingcap/errors/stack_test.go:155", + "\t.+/pingcap/errors/stack_test.go", }, }, { Annotate(New("ooh"), "ahh"), []string{ "github.com/pingcap/errors.TestStackTrace\n" + - "\t.+/github.com/pingcap/errors/stack_test.go:160", // this is the stack of Wrap, not New + "\t.+/pingcap/errors/stack_test.go", // this is the stack of Wrap, not New }, }, { Cause(Annotate(New("ooh"), "ahh")), []string{ "github.com/pingcap/errors.TestStackTrace\n" + - "\t.+/github.com/pingcap/errors/stack_test.go:165", // this is the stack of New + "\t.+/pingcap/errors/stack_test.go", // this is the stack of New }, }, { func() error { return New("ooh") }(), []string{ `github.com/pingcap/errors.(func·009|TestStackTrace.func1)` + - "\n\t.+/github.com/pingcap/errors/stack_test.go:170", // this is the stack of New + "\n\t.+/pingcap/errors/stack_test.go", // this is the stack of New "github.com/pingcap/errors.TestStackTrace\n" + - "\t.+/github.com/pingcap/errors/stack_test.go:170", // this is the stack of New's caller + "\t.+/pingcap/errors/stack_test.go", // this is the stack of New's caller }, }, { Cause(func() error { @@ -179,12 +180,14 @@ func TestStackTrace(t *testing.T) { return Errorf("hello %s", fmt.Sprintf("world")) }() }()), []string{ - `github.com/pingcap/errors.(func·010|TestStackTrace.func2.1)` + - "\n\t.+/github.com/pingcap/errors/stack_test.go:179", // this is the stack of Errorf + // go 1.23 when Debug its suffix is "TestStackTrace.func2.1", it's + // "TestStackTrace.TestStackTrace.func2.func3" when Run + `github.com/pingcap/errors.(func·010|TestStackTrace.func2.1|TestStackTrace.TestStackTrace.func2.func3)` + + "\n\t.+/pingcap/errors/stack_test.go", // this is the stack of Errorf `github.com/pingcap/errors.(func·011|TestStackTrace.func2)` + - "\n\t.+/github.com/pingcap/errors/stack_test.go:180", // this is the stack of Errorf's caller + "\n\t.+/pingcap/errors/stack_test.go", // this is the stack of Errorf's caller "github.com/pingcap/errors.TestStackTrace\n" + - "\t.+/github.com/pingcap/errors/stack_test.go:181", // this is the stack of Errorf's caller's caller + "\t.+/pingcap/errors/stack_test.go", // this is the stack of Errorf's caller's caller }, }} for i, tt := range tests { @@ -263,13 +266,13 @@ func TestStackTraceFormat(t *testing.T) { "%+v", "\n" + "github.com/pingcap/errors.stackTrace\n" + - "\t.+/github.com/pingcap/errors/stack_test.go:211\n" + + "\t.+/pingcap/errors/stack_test.go:\\d+\n" + "github.com/pingcap/errors.TestStackTraceFormat\n" + - "\t.+/github.com/pingcap/errors/stack_test.go:262", + "\t.+/pingcap/errors/stack_test.go:\\d+", }, { stackTrace()[:2], "%#v", - `\[\]errors.Frame{stack_test.go:211, stack_test.go:270}`, + `\[\]errors.Frame{stack_test.go:\d+, stack_test.go:\d+}`, }} for i, tt := range tests { @@ -294,7 +297,19 @@ func TestNewNoStackError(t *testing.T) { err = Trace(err) err = Trace(err) result := fmt.Sprintf("%+v", err) - if result != "test error" { + if !strings.Contains(result, "test error") || + !strings.Contains(result, "pingcap/errors.TestNewNoStackError") { + t.Errorf("NewNoStackError(): want %s, got %v", "test error", result) + } +} + +func TestNewNoStackErrorf(t *testing.T) { + err := NewNoStackErrorf("test error %s", "yes") + err = Trace(err) + err = Trace(err) + result := fmt.Sprintf("%+v", err) + if !strings.Contains(result, "test error yes") || + !strings.Contains(result, "pingcap/errors.TestNewNoStackErrorf") { t.Errorf("NewNoStackError(): want %s, got %v", "test error", result) } } @@ -304,7 +319,8 @@ func TestSuspendError(t *testing.T) { err = SuspendStack(err) err = Trace(err) result := fmt.Sprintf("%+v", err) - if result != "EOF" { + if !strings.Contains(result, "EOF") || + !strings.Contains(result, "pingcap/errors.TestSuspendError") { t.Errorf("NewNoStackError(): want %s, got %v", "EOF", result) } if io.EOF != Cause(err) { diff --git a/terror_test/terror_test.go b/terror_test/terror_test.go index 7967412f..86272fff 100644 --- a/terror_test/terror_test.go +++ b/terror_test/terror_test.go @@ -21,29 +21,25 @@ import ( "testing" "time" - . "github.com/pingcap/check" + "github.com/stretchr/testify/suite" + "github.com/pingcap/errors" ) const ( - CodeExecResultIsEmpty errors.ErrCode = 3 CodeMissConnectionID errors.ErrCode = 1 CodeResultUndetermined errors.ErrCode = 2 + CodeExecResultIsEmpty errors.ErrCode = 3 ) -func TestT(t *testing.T) { - CustomVerboseFlag = true - TestingT(t) -} - -var _ = Suite(&testTErrorSuite{}) - -type testTErrorSuite struct { +type TErrorTestSuite struct { + suite.Suite } -func (s *testTErrorSuite) TestErrCode(c *C) { - c.Assert(CodeMissConnectionID, Equals, errors.ErrCode(1)) - c.Assert(CodeResultUndetermined, Equals, errors.ErrCode(2)) +func (s *TErrorTestSuite) TestErrCode() { + s.Equal(CodeMissConnectionID, errors.ErrCode(1)) + s.Equal(CodeResultUndetermined, errors.ErrCode(2)) + s.Equal(CodeExecResultIsEmpty, errors.ErrCode(3)) } var predefinedErr = errors.Normalize("predefiend error", errors.MySQLErrorCode(123)) @@ -58,19 +54,19 @@ func call() error { return predefinedErr.GenWithStack("error message:%s", "abc") } -func (s *testTErrorSuite) TestJson(c *C) { +func (s *TErrorTestSuite) TestJson() { tmpErr := errors.Normalize("this is a test error", errors.RFCCodeText("ddl:-1"), errors.MySQLErrorCode(-1)) buf, err := json.Marshal(tmpErr) - c.Assert(err, IsNil) + s.Nil(err) var curTErr errors.Error err = json.Unmarshal(buf, &curTErr) - c.Assert(err, IsNil) + s.Nil(err) isEqual := tmpErr.Equal(&curTErr) - c.Assert(curTErr.Error(), Equals, tmpErr.Error()) - c.Assert(isEqual, IsTrue) + s.Equal(curTErr.Error(), tmpErr.Error()) + s.True(isEqual) } -func (s *testTErrorSuite) TestTraceAndLocation(c *C) { +func (s *TErrorTestSuite) TestTraceAndLocation() { err := example() stack := errors.ErrorStack(err) lines := strings.Split(stack, "\n") @@ -81,7 +77,7 @@ func (s *testTErrorSuite) TestTraceAndLocation(c *C) { sysStack++ } } - c.Assert(len(lines)-(2*sysStack), Equals, 15, Commentf("stack =\n%s", stack)) + s.Equalf(11, len(lines)-(2*sysStack), "stack = \n%s", stack) var containTerr bool for _, v := range lines { if strings.Contains(v, "terror_test.go") { @@ -89,77 +85,87 @@ func (s *testTErrorSuite) TestTraceAndLocation(c *C) { break } } - c.Assert(containTerr, IsTrue) + s.True(containTerr) } -func (s *testTErrorSuite) TestErrorEqual(c *C) { +func (s *TErrorTestSuite) TestErrorEqual() { e1 := errors.New("test error") - c.Assert(e1, NotNil) + s.NotNil(e1) e2 := errors.Trace(e1) - c.Assert(e2, NotNil) + s.NotNil(e2) e3 := errors.Trace(e2) - c.Assert(e3, NotNil) + s.NotNil(e3) - c.Assert(errors.Cause(e2), Equals, e1) - c.Assert(errors.Cause(e3), Equals, e1) - c.Assert(errors.Cause(e2), Equals, errors.Cause(e3)) + s.Equal(e1, errors.Cause(e2)) + s.Equal(e1, errors.Cause(e3)) + s.Equal(errors.Cause(e3), errors.Cause(e2)) e4 := errors.New("test error") - c.Assert(errors.Cause(e4), Not(Equals), e1) + s.NotEqual(e1, errors.Cause(e4)) e5 := errors.Errorf("test error") - c.Assert(errors.Cause(e5), Not(Equals), e1) + s.NotEqual(e1, errors.Cause(e5)) - c.Assert(errors.ErrorEqual(e1, e2), IsTrue) - c.Assert(errors.ErrorEqual(e1, e3), IsTrue) - c.Assert(errors.ErrorEqual(e1, e4), IsTrue) - c.Assert(errors.ErrorEqual(e1, e5), IsTrue) + s.True(errors.ErrorEqual(e1, e2)) + s.True(errors.ErrorEqual(e1, e3)) + s.True(errors.ErrorEqual(e1, e4)) + s.True(errors.ErrorEqual(e1, e5)) var e6 error - c.Assert(errors.ErrorEqual(nil, nil), IsTrue) - c.Assert(errors.ErrorNotEqual(e1, e6), IsTrue) + s.True(errors.ErrorEqual(nil, nil)) + s.True(errors.ErrorNotEqual(e1, e6)) } -func (s *testTErrorSuite) TestNewError(c *C) { +func (s *TErrorTestSuite) TestNewError() { today := time.Now().Weekday().String() err := predefinedTextualErr.GenWithStackByArgs(today) - c.Assert(err, NotNil) - c.Assert(err.Error(), Equals, "[executor:ExecutorAbsent]executor is taking vacation at "+today) + s.NotNil(err) + s.Equal("[executor:ExecutorAbsent]executor is taking vacation at "+today, err.Error()) } -func (s *testTErrorSuite) TestRFCCode(c *C) { +func (s *TErrorTestSuite) TestRFCCode() { c1err1 := errors.Normalize("nothing", errors.RFCCodeText("TestErr1:Err1")) c2err2 := errors.Normalize("nothing", errors.RFCCodeText("TestErr2:Err2")) - c.Assert(c1err1.RFCCode(), Equals, errors.RFCErrorCode("TestErr1:Err1")) - c.Assert(c2err2.RFCCode(), Equals, errors.RFCErrorCode("TestErr2:Err2")) + s.Equal(errors.RFCErrorCode("TestErr1:Err1"), c1err1.RFCCode()) + s.Equal(errors.RFCErrorCode("TestErr2:Err2"), c2err2.RFCCode()) + berr := errors.Normalize("nothing", errors.RFCCodeText("Blank:B1")) - c.Assert(berr.RFCCode(), Equals, errors.RFCErrorCode("Blank:B1")) + s.Equal(errors.RFCErrorCode("Blank:B1"), berr.RFCCode()) } -func (*testTErrorSuite) TestLineAndFile(c *C) { +func (s *TErrorTestSuite) TestLineAndFile() { err := predefinedTextualErr.GenWithStackByArgs("everyday") _, f, l, _ := runtime.Caller(0) terr, ok := errors.Cause(err).(*errors.Error) - c.Assert(ok, IsTrue) + s.True(ok) + file, line := terr.Location() - c.Assert(file, Equals, f) - c.Assert(line, Equals, l-1) + s.Equal(f, file) + s.Equal(l-1, line) err2 := predefinedTextualErr.GenWithStackByArgs("everyday and everywhere") _, f2, l2, _ := runtime.Caller(0) terr2, ok2 := errors.Cause(err2).(*errors.Error) - c.Assert(ok2, IsTrue) + s.True(ok2) file2, line2 := terr2.Location() - c.Assert(file2, Equals, f2) - c.Assert(line2, Equals, l2-1) + s.Equal(f2, file2) + s.Equal(l2-1, line2) +} + +func (s *TErrorTestSuite) TestWarpAndField() { + cause := errors.New("load from etcd meet error") + s.NotNil(cause) + + err := errors.Normalize("fail to get leader", errors.RFCCodeText("member:ErrGetLeader")) + errWithCause := errors.Annotate(err, cause.Error()) + s.NotNil(errWithCause) + + s.Equal("load from etcd meet error: [member:ErrGetLeader]fail to get leader", errWithCause.Error()) } -func (*testTErrorSuite) TestWarpAndField(c *C) { - causeErr := errors.New("load from etcd meet error") - ErrGetLeader := errors.Normalize("fail to get leader", errors.RFCCodeText("member:ErrGetLeader")) - errWithWarpedCause := errors.Annotate(ErrGetLeader, causeErr.Error()) - c.Assert(errWithWarpedCause.Error(), Equals, "load from etcd meet error: [member:ErrGetLeader]fail to get leader") +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(TErrorTestSuite)) }